@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.
@@ -31,10 +31,13 @@ export interface CodeBlockProps {
31
31
  /** The code is still arriving: highlighting is not cached, and the block
32
32
  * keeps its end in view while it is scrolled there. */
33
33
  streaming?: boolean | undefined;
34
+ /** On a phone, reaches past the host's content padding (`p-4`) to the
35
+ * container's edges, without corners, so long lines get the screen's width. */
36
+ bleed?: boolean | undefined;
34
37
  className?: string | undefined;
35
38
  }
36
39
  /** Code to read on a page, such as a workflow's YAML: highlighted in the
37
40
  * theme's code colors, with numbered lines, a copy button and, given a
38
41
  * `filename`, a download button. Markdown's fenced code renders as one, so
39
42
  * both read the same. */
40
- export declare function CodeBlock({ code, language, title, label, filename, headerActions, copy, lineNumbers, startLine, wrap, fill, maxHeight, streaming, className, }: CodeBlockProps): import("react").JSX.Element;
43
+ export declare function CodeBlock({ code, language, title, label, filename, headerActions, copy, lineNumbers, startLine, wrap, fill, maxHeight, streaming, bleed, className, }: CodeBlockProps): import("react").JSX.Element;
@@ -66,7 +66,7 @@ function downloadText(text, name) {
66
66
  * theme's code colors, with numbered lines, a copy button and, given a
67
67
  * `filename`, a download button. Markdown's fenced code renders as one, so
68
68
  * both read the same. */
69
- function CodeBlock({ code, language = "", title, label, filename, headerActions, copy = true, lineNumbers, startLine = 1, wrap = false, fill = false, maxHeight, streaming = false, className }) {
69
+ function CodeBlock({ code, language = "", title, label, filename, headerActions, copy = true, lineNumbers, startLine = 1, wrap = false, fill = false, maxHeight, streaming = false, bleed = false, className }) {
70
70
  const { common: t } = useStrings();
71
71
  const source = code.endsWith("\n") ? code.slice(0, -1) : code;
72
72
  const lines = useMemo(() => source.split("\n"), [source]);
@@ -100,7 +100,7 @@ function CodeBlock({ code, language = "", title, label, filename, headerActions,
100
100
  };
101
101
  return /* @__PURE__ */ jsxs("div", {
102
102
  "data-code-block": "",
103
- className: cx("not-prose flex flex-col overflow-hidden rounded-xl bg-(--theme-code-bg) text-(--theme-code-text) shadow-[0_0_0_1px_color-mix(in_oklab,var(--theme-app)_8%,transparent)]", fill && "h-full min-h-0", className),
103
+ className: cx("not-prose flex flex-col overflow-hidden rounded-xl bg-(--theme-code-bg) text-(--theme-code-text) shadow-[0_0_0_1px_color-mix(in_oklab,var(--theme-app)_8%,transparent)]", fill && "h-full min-h-0", bleed && "max-sm:-mx-4 max-sm:rounded-none", className),
104
104
  children: [/* @__PURE__ */ jsxs("div", {
105
105
  className: "flex h-9 shrink-0 items-center gap-2 pr-1.5 pl-4",
106
106
  children: [/* @__PURE__ */ jsx("div", {
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode } from 'react';
2
2
  /** A bar's color: the theme's accent, a status, or muted for something stopped. */
3
3
  export type BarTone = 'accent' | 'info' | 'success' | 'warning' | 'error' | 'muted';
4
4
  declare const trackHeight: {
@@ -32,7 +32,7 @@ export declare function levelTone(value: number, warnAt: number, errorAt: number
32
32
  /** How full something is, such as storage used or a quota spent: a reading,
33
33
  * not a task under way (that's `ProgressBar`). The bar is blue, then turns
34
34
  * amber, then red, as it passes `warnAt` and `errorAt`; `tone` overrides that. */
35
- export declare function Meter({ value, min, max, warnAt, errorAt, tone, okTone, format, showValue, valueText, getAriaValueText, minFill, locale, label, 'aria-label': ariaLabel, detail, size, trackClassName, className, }: BarProps & {
35
+ export declare function Meter({ value, min, max, warnAt, errorAt, tone, okTone, format, showValue, valueText, getAriaValueText, minFill, animate, locale, label, 'aria-label': ariaLabel, detail, size, trackClassName, className, }: BarProps & {
36
36
  value: number;
37
37
  min?: number;
38
38
  max?: number;
@@ -54,6 +54,10 @@ export declare function Meter({ value, min, max, warnAt, errorAt, tone, okTone,
54
54
  /** The narrowest the fill draws for a value above `min`, as a percent of the
55
55
  * track, so a small reading still shows. */
56
56
  minFill?: number | undefined;
57
+ /** Grows the fill in from empty when the bar first shows, and eases it to each new value, for a
58
+ * reading that arrives with the page, such as a dashboard's. Still for anyone who asks for
59
+ * less motion. */
60
+ animate?: boolean | undefined;
57
61
  }): import("react").JSX.Element;
58
62
  /** A further stretch of a progress bar, drawn after its value, such as work
59
63
  * started but not yet done. */
@@ -1,6 +1,7 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { toneFill } from "./tone.js";
3
3
  import { Tooltip } from "./Tooltip.js";
4
+ import { useEffect, useState } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
  import { Meter } from "@base-ui/react/meter";
6
7
  import { Progress } from "@base-ui/react/progress";
@@ -26,6 +27,21 @@ var trackBaseClassName = "w-full overflow-hidden rounded-full bg-(--theme-border
26
27
  function trackClasses(size, end, own) {
27
28
  return cx(trackBaseClassName, trackHeight[size], end && (own ? "shrink-0" : "min-w-0 flex-1"), own);
28
29
  }
30
+ function useGrown(grow) {
31
+ const [grown, setGrown] = useState(!grow);
32
+ useEffect(() => {
33
+ if (grown) return;
34
+ let inner = 0;
35
+ const outer = requestAnimationFrame(() => {
36
+ inner = requestAnimationFrame(() => setGrown(true));
37
+ });
38
+ return () => {
39
+ cancelAnimationFrame(outer);
40
+ cancelAnimationFrame(inner);
41
+ };
42
+ }, [grown]);
43
+ return grown;
44
+ }
29
45
  /** The tone for `value` against a warning and an error level, for a measure
30
46
  * that's worse the higher it reads; `okTone` below the warning. */
31
47
  function levelTone(value, warnAt, errorAt, okTone = "info") {
@@ -34,7 +50,8 @@ function levelTone(value, warnAt, errorAt, okTone = "info") {
34
50
  /** How full something is, such as storage used or a quota spent: a reading,
35
51
  * not a task under way (that's `ProgressBar`). The bar is blue, then turns
36
52
  * amber, then red, as it passes `warnAt` and `errorAt`; `tone` overrides that. */
37
- function Meter$1({ value, min = 0, max = 100, warnAt = 80, errorAt = 90, tone, okTone = "info", format, showValue = "beside", valueText, getAriaValueText, minFill = 0, locale, label, "aria-label": ariaLabel, detail, size = "md", trackClassName, className }) {
53
+ function Meter$1({ value, min = 0, max = 100, warnAt = 80, errorAt = 90, tone, okTone = "info", format, showValue = "beside", valueText, getAriaValueText, minFill = 0, animate = false, locale, label, "aria-label": ariaLabel, detail, size = "md", trackClassName, className }) {
54
+ const grown = useGrown(animate);
38
55
  const clamped = Math.min(Math.max(value, min), max);
39
56
  const fill = fillClassName[tone ?? levelTone(clamped, warnAt, errorAt, okTone)];
40
57
  const end = showValue === "end";
@@ -45,8 +62,9 @@ function Meter$1({ value, min = 0, max = 100, warnAt = 80, errorAt = 90, tone, o
45
62
  const track = /* @__PURE__ */ jsx(Meter.Track, {
46
63
  className: trackClasses(size, end, trackClassName),
47
64
  children: /* @__PURE__ */ jsx(Meter.Indicator, {
48
- className: cx("h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none", fill),
49
- style: minFill > 0 && clamped > min ? { minWidth: `${minFill}%` } : void 0
65
+ "data-growing": grown ? void 0 : "",
66
+ className: cx("h-full rounded-full motion-reduce:transition-none", animate ? "transition-[width,min-width] delay-150 duration-500 ease-[cubic-bezier(0.25,0.1,0.25,1)]" : "transition-[width] duration-300", fill),
67
+ style: !grown ? { width: 0 } : minFill > 0 && clamped > min ? { minWidth: `${minFill}%` } : void 0
50
68
  })
51
69
  });
52
70
  const share = formatShare(clamped, min, max, format, locale);
@@ -59,7 +59,7 @@ function Pagination({ page, pageSize, total, hasNext, onPageChange, pageHref, pa
59
59
  const position = summary ?? (known ? t.pager(start, end, pastCap ? end : total, pastCap) : null);
60
60
  return /* @__PURE__ */ jsxs("nav", {
61
61
  "aria-label": t.pagination,
62
- className: cx("flex h-10 shrink-0 items-center justify-between gap-3 px-2.5", className),
62
+ className: cx("flex h-8 shrink-0 items-center justify-between gap-3 px-2.5", className),
63
63
  children: [/* @__PURE__ */ jsxs("div", {
64
64
  className: "flex min-w-0 flex-1 items-center gap-3 text-xs text-(--theme-muted-text-color)",
65
65
  children: [position && /* @__PURE__ */ jsx("span", {
@@ -118,22 +118,27 @@ function Pagination({ page, pageSize, total, hasNext, onPageChange, pageHref, pa
118
118
  function PageStep({ to, enabled, label, icon, onPageChange, pageHref }) {
119
119
  if (pageHref && enabled) return /* @__PURE__ */ jsx(IconButton, {
120
120
  variant: "outline",
121
+ size: "xs",
122
+ className: STEP_TOUCH,
121
123
  icon,
122
124
  label,
123
125
  href: pageHref(to)
124
126
  });
125
127
  return /* @__PURE__ */ jsx(IconButton, {
126
128
  variant: "outline",
129
+ size: "xs",
130
+ className: STEP_TOUCH,
127
131
  icon,
128
132
  label,
129
133
  disabled: !enabled,
130
134
  onClick: () => onPageChange?.(to)
131
135
  });
132
136
  }
137
+ var STEP_TOUCH = "pointer-coarse:size-8";
133
138
  var numberClassName = cx(buttonClasses({
134
139
  variant: "outline",
135
- size: "sm"
136
- }), "min-w-7 px-1.5 tabular-nums aria-[current=page]:border-(--theme-link) aria-[current=page]:bg-(--theme-accent-subtle) aria-[current=page]:text-(--theme-app)");
140
+ size: "xs"
141
+ }), "min-w-6 px-1 tabular-nums pointer-coarse:h-8 pointer-coarse:min-w-8 aria-[current=page]:border-(--theme-link) aria-[current=page]:bg-(--theme-accent-subtle) aria-[current=page]:text-(--theme-app)");
137
142
  function PageNumber({ to, current, label, onPageChange, pageHref }) {
138
143
  const number = formatInteger(to + 1, useLocale());
139
144
  const props = {
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
2
  /** Which way a column sorts, as `aria-sort` names it. */
3
3
  export type SortDirection = 'ascending' | 'descending' | 'none';
4
- /** `comfortable` gives each row room; `compact` fits a short key and value
5
- * list; `dense` sets small type in tight rows, for a table inside a card or
6
- * a panel of figures. */
4
+ /** `comfortable` gives each row room; `compact` fits many rows in little
5
+ * height, such as a key and value list or a dashboard card's runs; `dense`
6
+ * sets small type in tight rows, for a panel of figures. */
7
7
  export type TableDensity = 'comfortable' | 'compact' | 'dense';
8
8
  interface ItemProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
9
9
  /** Lets the text wrap; by default a cell keeps to one line. */
@@ -9,20 +9,25 @@ var DensityContext = createContext("comfortable");
9
9
  var CapsContext = createContext(true);
10
10
  var cellY = {
11
11
  comfortable: "py-4",
12
- compact: "py-1.5 text-sm",
12
+ compact: "h-8 py-0 text-sm",
13
13
  dense: "py-1 text-xs"
14
14
  };
15
+ var wrapY = {
16
+ comfortable: false,
17
+ compact: "py-1",
18
+ dense: false
19
+ };
15
20
  var headerY = {
16
21
  comfortable: "py-3",
17
- compact: "py-2",
18
- dense: "py-1.5"
22
+ compact: "py-1.5",
23
+ dense: "py-1"
19
24
  };
20
25
  var cellX = "px-3";
21
26
  function Item({ wrap = false, children, className, ...tdProps }) {
22
27
  const density = useContext(DensityContext);
23
28
  return /* @__PURE__ */ jsx("td", {
24
29
  ...tdProps,
25
- className: cx(cellX, cellY[density], !wrap && "whitespace-nowrap", className),
30
+ className: cx(cellX, cellY[density], wrap ? wrapY[density] : "whitespace-nowrap", className),
26
31
  children
27
32
  });
28
33
  }
@@ -61,10 +61,10 @@ function Chip({ value, label, icon, count, disabled, testId, size }) {
61
61
  value,
62
62
  disabled,
63
63
  "data-testid": testId,
64
- className: cx("group aria-pressed:bg-(--theme-accent-subtle)", buttonClasses({
64
+ className: cx(buttonClasses({
65
65
  variant: "outline",
66
66
  size
67
- })),
67
+ }), "group focus-visible:-outline-offset-2 aria-pressed:bg-(--theme-accent-subtle)"),
68
68
  children: [
69
69
  icon && /* @__PURE__ */ jsx("span", {
70
70
  className: "inline-flex shrink-0 transition-opacity group-not-aria-pressed:opacity-55",
@@ -28,4 +28,4 @@ export interface TooltipProps extends Omit<BaseTooltip.Trigger.Props, 'content'
28
28
  /** A visual hint for sighted pointer and keyboard users. The anchor still needs
29
29
  * its own accessible name; `describesAnchor` also gives assistive technology the
30
30
  * hint. Use InfoTip for content everyone must reach, such as on a touch screen. */
31
- export declare function Tooltip({ content, children, side, shortcut, disabled, overflowOnly, describesAnchor, ...anchorProps }: TooltipProps): import("react").JSX.Element;
31
+ export declare function Tooltip({ content, children, side, shortcut, disabled, overflowOnly, describesAnchor, ref, ...anchorProps }: TooltipProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { positionerClassName, tipClassName } from "./floating.js";
2
2
  import { usePopupContainer } from "./popupContainer.js";
3
3
  import { Kbd } from "./Kbd.js";
4
- import { cloneElement, createContext, use, useEffect, useId, useState } from "react";
4
+ import { cloneElement, createContext, use, useCallback, useEffect, useId, useState } from "react";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { Tooltip } from "@base-ui/react/tooltip";
7
7
  //#region src/components/Tooltip.tsx
@@ -82,13 +82,36 @@ function useRestAfterPopup(anchor) {
82
82
  }, [anchor]);
83
83
  return resting;
84
84
  }
85
+ /** Sets a ref and returns how to unset it: a callback ref's own cleanup, or a call with null. */
86
+ function assignRef(ref, node) {
87
+ if (typeof ref === "function") {
88
+ const cleanup = ref(node);
89
+ return typeof cleanup === "function" ? cleanup : () => ref(null);
90
+ }
91
+ if (ref) {
92
+ ref.current = node;
93
+ return () => {
94
+ ref.current = null;
95
+ };
96
+ }
97
+ return () => {};
98
+ }
85
99
  /** A visual hint for sighted pointer and keyboard users. The anchor still needs
86
100
  * its own accessible name; `describesAnchor` also gives assistive technology the
87
101
  * hint. Use InfoTip for content everyone must reach, such as on a touch screen. */
88
- function Tooltip$1({ content, children, side = "top", shortcut, disabled = false, overflowOnly = false, describesAnchor = false, ...anchorProps }) {
102
+ function Tooltip$1({ content, children, side = "top", shortcut, disabled = false, overflowOnly = false, describesAnchor = false, ref, ...anchorProps }) {
89
103
  const handle = use(TooltipHandleContext);
90
104
  const descriptionId = useId();
91
105
  const [anchorElement, setAnchorElement] = useState(null);
106
+ const anchorRef = useCallback((node) => {
107
+ if (!node) return;
108
+ setAnchorElement(node);
109
+ const unset = assignRef(ref, node);
110
+ return () => {
111
+ setAnchorElement(null);
112
+ unset();
113
+ };
114
+ }, [ref]);
92
115
  const resting = useRestAfterPopup(anchorElement);
93
116
  const payload = {
94
117
  content,
@@ -107,7 +130,7 @@ function Tooltip$1({ content, children, side = "top", shortcut, disabled = false
107
130
  ...!handle && anchorProps.delay === void 0 && { delay: OPEN_DELAY },
108
131
  handle: handle ?? void 0,
109
132
  payload,
110
- ref: setAnchorElement,
133
+ ref: anchorRef,
111
134
  disabled: disabled || !content || resting,
112
135
  "data-overflow-only": overflowOnly || void 0,
113
136
  render: described ? cloneElement(anchor, { "aria-describedby": describedBy }) : children
@@ -1,6 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  //#region src/components/buttonClasses.ts
3
- var base = "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md font-medium no-underline transition-[color,background-color,border-color,box-shadow,translate] duration-150 hover:not-disabled:not-aria-disabled:-translate-y-px hover:not-disabled:not-aria-disabled:shadow-(--theme-shadow-sm) active:not-disabled:not-aria-disabled:translate-y-0 active:not-disabled:not-aria-disabled:shadow-none motion-reduce:hover:not-disabled:not-aria-disabled:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-focus) disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50";
3
+ var base = "relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md font-medium no-underline transition-[color,background-color,border-color,box-shadow,translate] duration-150 hover:not-disabled:not-aria-disabled:not-[[aria-pressed]]:-translate-y-px hover:not-disabled:not-aria-disabled:not-[[aria-pressed]]:shadow-(--theme-shadow-sm) active:not-disabled:not-aria-disabled:translate-y-0 active:not-disabled:not-aria-disabled:shadow-none motion-reduce:hover:not-disabled:not-aria-disabled:translate-y-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-focus) disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50";
4
4
  var variants = {
5
5
  primary: "bg-(--theme-accent) text-(--theme-accent-text) hover:not-disabled:not-aria-disabled:bg-(--theme-accent-hover)",
6
6
  secondary: "border border-(--theme-border) bg-(--theme-panel-bg) text-(--theme-app) hover:not-disabled:not-aria-disabled:bg-(--theme-hover)",
@@ -21,7 +21,7 @@ interface GraphRun {
21
21
  * marked on each job and step they're about, which matters most for one skipped without a log. */
22
22
  warnings?: readonly RunWarning[] | null | undefined;
23
23
  }
24
- export default function DependencyGraph({ run, preview, removeBorder, viewMode, setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar, }: {
24
+ export default function DependencyGraph({ run, preview, removeBorder, viewMode, setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar, bleed, }: {
25
25
  run: GraphRun;
26
26
  preview?: boolean;
27
27
  removeBorder?: boolean | undefined;
@@ -44,6 +44,9 @@ export default function DependencyGraph({ run, preview, removeBorder, viewMode,
44
44
  onCloseLog?: (() => void) | undefined;
45
45
  /** Pins the job list as the page scrolls, under whatever the page already pins at its top. */
46
46
  stickySidebar?: boolean | undefined;
47
+ /** On a phone, the jobs' panel, graph or list, reaches past the host's content padding (`p-4`) to
48
+ * the container's edges, without side borders or corners, so a job's row has the screen's width. */
49
+ bleed?: boolean | undefined;
47
50
  }): import("react").JSX.Element | undefined;
48
51
  export declare function DependencyGraphPreview(inputs: {
49
52
  yml?: Record<string, unknown>;
@@ -26,7 +26,7 @@ import { WarningMarks, WarningMarksContext, locateWarnings, rowKey } from "./run
26
26
  import { Joblist } from "./JobSummary.js";
27
27
  import { Store, useStore } from "./store.js";
28
28
  import { Lineage, LineageContext } from "./lineage.js";
29
- import { DimRules, JobNode, setDimmed } from "./GraphNode.js";
29
+ import { DimRules, GraphDirectionContext, JobNode, setDimmed } from "./GraphNode.js";
30
30
  import { ConnectorLayers } from "./Connectors.js";
31
31
  import { routeConnectors, sameBoxes } from "./connectorGeometry.js";
32
32
  import { isJobRecord } from "./types.js";
@@ -35,7 +35,7 @@ import { MatrixGroupNode, MatrixGroupSummaryItem } from "./MatrixGroup.js";
35
35
  import { StepSummaries } from "./StepSummaries.js";
36
36
  import { StepSummary } from "./StepSummary.js";
37
37
  import TreeView from "./TreeView.js";
38
- import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoView, viewShowing, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
38
+ import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoReach, intoView, reachKept, viewShowing, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
39
39
  import { Fragment, Suspense, createContext, lazy, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
40
40
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
41
41
  import { flushSync } from "react-dom";
@@ -294,14 +294,20 @@ function sizingTransitions(el) {
294
294
  }
295
295
  var HOLD_MS = 2e3;
296
296
  var skippedBefore = (slots, i) => (slots?.[i] ?? i) - (i === 0 ? 0 : (slots?.[i - 1] ?? i - 1) + 1);
297
- function measureBoxes(cols, pathPrefix, wrapper) {
297
+ function measureBoxes(cols, pathPrefix, wrapper, down) {
298
298
  const boxes = /* @__PURE__ */ new Map();
299
299
  for (const col of cols) for (const [head] of col) {
300
300
  const el = head === void 0 ? null : document.getElementById(`node_${pathPrefix}${head}`);
301
- if (head !== void 0 && el) boxes.set(head, {
302
- ...offsetWithin(el, wrapper),
303
- width: el.offsetWidth
304
- });
301
+ if (head !== void 0 && el) {
302
+ const box = {
303
+ ...offsetWithin(el, wrapper),
304
+ width: el.offsetWidth
305
+ };
306
+ boxes.set(head, down ? {
307
+ ...box,
308
+ height: el.offsetHeight
309
+ } : box);
310
+ }
305
311
  }
306
312
  return boxes;
307
313
  }
@@ -313,6 +319,7 @@ var Subgraph = memo(function Subgraph({ jobs, pathPrefix, depth, preview, animT,
313
319
  const editorApi = useGraphEditor();
314
320
  const selectedEdges = useSelectedEdges(editable ? editorApi : null);
315
321
  const zBase = depth * SUBGRAPH_Z_STEP;
322
+ const down = useContext(GraphDirectionContext) === "down";
316
323
  const placed = useMemo(() => layout ?? (depth > 0 ? positionsOf(jobs) : void 0), [
317
324
  layout,
318
325
  depth,
@@ -459,9 +466,10 @@ var Subgraph = memo(function Subgraph({ jobs, pathPrefix, depth, preview, animT,
459
466
  toggleMatrix
460
467
  ]);
461
468
  const nodes = useMemo(() => /* @__PURE__ */ jsx("div", {
462
- className: "flex relative",
469
+ className: down ? "flex flex-col items-center relative" : "flex relative",
463
470
  children: dependencyCols.map((col, c) => /* @__PURE__ */ jsx("div", {
464
- style: emptyColumnsStyle(skippedBefore(colSlots, c)),
471
+ className: down ? "flex items-start justify-center" : void 0,
472
+ style: down ? void 0 : emptyColumnsStyle(skippedBefore(colSlots, c)),
465
473
  children: col.map((jobNames, i) => {
466
474
  const first = jobNames[0];
467
475
  if (first === void 0) return null;
@@ -504,16 +512,17 @@ var Subgraph = memo(function Subgraph({ jobs, pathPrefix, depth, preview, animT,
504
512
  pathPrefix,
505
513
  toggleMatrix,
506
514
  handlers,
507
- editable
515
+ editable,
516
+ down
508
517
  ]);
509
518
  if (!dependencyCols[0]?.length) return null;
510
519
  const wrapper = wrapperRef.current;
511
- const measured = wrapper ? measureBoxes(dependencyCols, pathPrefix, wrapper) : /* @__PURE__ */ new Map();
520
+ const measured = wrapper ? measureBoxes(dependencyCols, pathPrefix, wrapper, down) : /* @__PURE__ */ new Map();
512
521
  const last = routedRef.current;
513
522
  const routed = last && last.directDeps === directDeps && sameBoxes(last.boxes, measured) ? last : {
514
523
  boxes: measured,
515
524
  directDeps,
516
- edges: routeConnectors(dependencyCols, colSlots.some((column, c) => column !== c), directDeps, measured, 37)
525
+ edges: routeConnectors(dependencyCols, colSlots.some((column, c) => column !== c), directDeps, measured, 37, down ? "down" : "right")
517
526
  };
518
527
  routedRef.current = routed;
519
528
  const { boxes, edges } = routed;
@@ -704,6 +713,8 @@ function graphBounds(content) {
704
713
  }
705
714
  return bounds;
706
715
  }
716
+ var NARROW_PANEL_PX = 640;
717
+ var BLEED_CLASSES = "max-sm:-mx-4 max-sm:w-auto max-sm:rounded-none max-sm:border-x-0";
707
718
  var READABLE_SCALE = .5;
708
719
  /** Where a job or step a warning took the graph to shows: the panel below its view bar, which sits
709
720
  * over the graph. */
@@ -741,7 +752,7 @@ function flash(row) {
741
752
  easing: "ease-in"
742
753
  });
743
754
  }
744
- function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar }) {
755
+ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, stickySidebar, bleed = false }) {
745
756
  const engine = useWorkflowEngine();
746
757
  const animT = .125;
747
758
  const rootJobs = isJobRecord(run.executedJobs) ? run.executedJobs : {};
@@ -800,6 +811,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
800
811
  }
801
812
  }, [preview]);
802
813
  const containerHeight = topOffset !== null ? Math.max(windowSize.h - topOffset - 16, 160) : null;
814
+ const runsDown = !preview && !editor && windowSize.w < NARROW_PANEL_PX;
803
815
  const [fitHeight, setFitHeight] = useState(null);
804
816
  const fitViewRef = useRef(null);
805
817
  useEffect(() => {
@@ -1012,7 +1024,12 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1012
1024
  if (dx !== 0 || dy !== 0) requestAnimationFrame(() => transform.setTransform(x + dx, y + dy, scale, FIT_ANIMATION_MS, "easeOut"));
1013
1025
  return;
1014
1026
  }
1015
- const view = fitView(bounds, panel);
1027
+ const narrow = !preview && !editor && panel.width < NARROW_PANEL_PX;
1028
+ const view = fitView(bounds, panel, narrow || runsDown ? {
1029
+ minScale: READABLE_SCALE,
1030
+ top: viewBarRef.current?.offsetHeight ?? 0,
1031
+ width: runsDown
1032
+ } : void 0);
1016
1033
  if (!view) return;
1017
1034
  if (animationMs > 0) {
1018
1035
  transform.setTransform(view.x, view.y, view.scale, animationMs, "easeOut");
@@ -1024,7 +1041,9 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1024
1041
  }, [
1025
1042
  nodeOf,
1026
1043
  editor,
1027
- runRowOf
1044
+ runRowOf,
1045
+ preview,
1046
+ runsDown
1028
1047
  ]);
1029
1048
  const fitWhenSettled = useCallback(async () => {
1030
1049
  const container = containerRef.current;
@@ -1135,6 +1154,27 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1135
1154
  const moved = useCallback(() => {
1136
1155
  viewed.current = true;
1137
1156
  }, []);
1157
+ const keepInReach = useCallback(() => {
1158
+ const container = containerRef.current;
1159
+ const content = graphContentRef.current;
1160
+ const transform = transformRef.current;
1161
+ const bounds = content ? graphBounds(content) : null;
1162
+ if (!container || !transform || !bounds) return;
1163
+ const { positionX, positionY, scale } = transform.instance.state;
1164
+ const panel = {
1165
+ left: 0,
1166
+ top: 0,
1167
+ width: container.clientWidth,
1168
+ height: container.clientHeight
1169
+ };
1170
+ const { dx, dy } = intoReach({
1171
+ left: positionX + bounds.minX * scale,
1172
+ top: positionY + bounds.minY * scale,
1173
+ width: (bounds.maxX - bounds.minX) * scale,
1174
+ height: (bounds.maxY - bounds.minY) * scale
1175
+ }, panel, reachKept(panel));
1176
+ if (dx !== 0 || dy !== 0) transform.setTransform(positionX + dx, positionY + dy, scale, FIT_ANIMATION_MS, "easeOut");
1177
+ }, []);
1138
1178
  const zoomTo = useCallback((view, animationMs) => {
1139
1179
  viewed.current = true;
1140
1180
  transformRef.current?.setTransform(view.x, view.y, view.scale, animationMs, "easeOut");
@@ -1484,7 +1524,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1484
1524
  }),
1485
1525
  !preview && /* @__PURE__ */ jsx(AnnotationBanner, { executedJobs: jobs }),
1486
1526
  viewMode === "tree" && !preview ? /* @__PURE__ */ jsx("div", {
1487
- className: "panel w-full border",
1527
+ className: cx("panel w-full border", bleed && BLEED_CLASSES),
1488
1528
  children: /* @__PURE__ */ jsx(TreeView, {
1489
1529
  jobs,
1490
1530
  slug: runSlug,
@@ -1494,9 +1534,9 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1494
1534
  }) : /* @__PURE__ */ jsxs("div", {
1495
1535
  ref: setContainerElement,
1496
1536
  tabIndex: editor ? -1 : void 0,
1497
- className: cx("panel w-full overflow-hidden relative", removeBorder ? "border-none" : "border", editor && "outline-none"),
1537
+ className: cx("panel w-full overflow-hidden relative", removeBorder ? "border-none" : "border", bleed && BLEED_CLASSES, editor && "outline-none"),
1498
1538
  style: {
1499
- height: fixedHeight ?? (fitHeight !== null ? `${Math.min(fitHeight, containerHeight ?? windowSize.h * .62)}px` : containerHeight !== null ? `${containerHeight}px` : "62vh"),
1539
+ height: fixedHeight ?? (fitHeight !== null ? `${Math.min(fitHeight, runsDown ? Math.max(containerHeight ?? 0, windowSize.h * .75) : containerHeight ?? windowSize.h * .62)}px` : containerHeight !== null ? `${containerHeight}px` : "62vh"),
1500
1540
  minHeight: preview && fitHeight !== null && !fixedHeight ? 0 : 160
1501
1541
  },
1502
1542
  children: [/* @__PURE__ */ jsx(TransformWrapper, {
@@ -1511,8 +1551,11 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1511
1551
  onInit: () => {
1512
1552
  setTimeout(invalidateConnectors, 0);
1513
1553
  },
1554
+ velocityAnimation: { sensitivityTouch: 0 },
1514
1555
  onPanning: moved,
1556
+ onPanningStop: keepInReach,
1515
1557
  onPinch: moved,
1558
+ onPinchStop: keepInReach,
1516
1559
  onTransform,
1517
1560
  children: () => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1518
1561
  ref: viewBarRef,
@@ -1542,27 +1585,30 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1542
1585
  ref: graphContentRef,
1543
1586
  className: "flex relative mr-[8000px]",
1544
1587
  style: { minHeight: 4e3 },
1545
- children: /* @__PURE__ */ jsx(Subgraph, {
1546
- jobs,
1547
- pathPrefix: pathAppend,
1548
- depth: 0,
1549
- preview,
1550
- animT,
1551
- expandedSubworkflows,
1552
- toggleSubworkflow,
1553
- openInNewGraph,
1554
- setSublogOpen,
1555
- invalidateConnectors,
1556
- requestFit: fitWhenSettled,
1557
- stepsOpen,
1558
- toggleSteps,
1559
- matrixOpen,
1560
- toggleMatrix,
1561
- onReady: fitGraph,
1562
- layout: placed,
1563
- graph: rootGraph,
1564
- editable: !!editorApi && currentPath.length === 0
1565
- }, `sg-${executedJobsPathIdx}-${pathAppend}`)
1588
+ children: /* @__PURE__ */ jsx(GraphDirectionContext, {
1589
+ value: runsDown ? "down" : "right",
1590
+ children: /* @__PURE__ */ jsx(Subgraph, {
1591
+ jobs,
1592
+ pathPrefix: pathAppend,
1593
+ depth: 0,
1594
+ preview,
1595
+ animT,
1596
+ expandedSubworkflows,
1597
+ toggleSubworkflow,
1598
+ openInNewGraph,
1599
+ setSublogOpen,
1600
+ invalidateConnectors,
1601
+ requestFit: fitWhenSettled,
1602
+ stepsOpen,
1603
+ toggleSteps,
1604
+ matrixOpen,
1605
+ toggleMatrix,
1606
+ onReady: fitGraph,
1607
+ layout: placed,
1608
+ graph: rootGraph,
1609
+ editable: !!editorApi && currentPath.length === 0
1610
+ }, `sg-${executedJobsPathIdx}-${pathAppend}`)
1611
+ })
1566
1612
  })
1567
1613
  }) })] })
1568
1614
  }), editorOverlays]
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
+ import type { GraphDirection } from './connectorGeometry.js';
2
3
  import { type JobHandlers } from './JobSummary.js';
3
4
  import type { WorkflowJob } from './types.js';
4
5
  export declare const dimClasses: (depth: number) => {
@@ -10,6 +11,8 @@ export declare const dimClasses: (depth: number) => {
10
11
  export declare function DimRules({ depth }: {
11
12
  depth: number;
12
13
  }): import("react").JSX.Element;
14
+ /** Which way the graph around a node runs; a run's graph on a phone runs down. */
15
+ export declare const GraphDirectionContext: import("react").Context<GraphDirection>;
13
16
  /** Dims what of a graph level isn't in the hovered job's lineage, while one is hovered. */
14
17
  export declare function setDimmed(wrapper: HTMLElement, depth: number, hovering: boolean): void;
15
18
  interface NodeShell {
@@ -2,7 +2,7 @@ import { BoxGrip } from "./editorRows.js";
2
2
  import { emptyRowsStyle } from "./gridSpacing.js";
3
3
  import { Joblist } from "./JobSummary.js";
4
4
  import { useLineage, useLineageStore } from "./lineage.js";
5
- import { memo } from "react";
5
+ import { createContext, memo, useContext } from "react";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  //#region src/graph/GraphNode.tsx
8
8
  var dimClasses = (depth) => ({
@@ -19,6 +19,8 @@ function DimRules({ depth }) {
19
19
  children: [`.${level} .${node}{opacity:.5}.${level} .${edge}{opacity:.3}`, "@media (prefers-reduced-motion:reduce){.dag-fade{transition:none!important}}"]
20
20
  });
21
21
  }
22
+ /** Which way the graph around a node runs; a run's graph on a phone runs down. */
23
+ var GraphDirectionContext = createContext("right");
22
24
  /** Dims what of a graph level isn't in the hovered job's lineage, while one is hovered. */
23
25
  function setDimmed(wrapper, depth, hovering) {
24
26
  wrapper.classList.toggle(dimClasses(depth).level, hovering);
@@ -26,6 +28,7 @@ function setDimmed(wrapper, depth, hovering) {
26
28
  /** One node of the graph: its jobs' box, raised above connectors while its lineage is shown. */
27
29
  function GraphNode({ id, job, rowsAbove, zBase, children }) {
28
30
  const lineage = useLineageStore();
31
+ const down = useContext(GraphDirectionContext) === "down";
29
32
  const highlight = useLineage((l) => l.node(job), 0);
30
33
  const enter = () => lineage?.hover(job);
31
34
  const leave = () => lineage?.hover(null);
@@ -35,10 +38,10 @@ function GraphNode({ id, job, rowsAbove, zBase, children }) {
35
38
  return /* @__PURE__ */ jsxs("div", {
36
39
  id,
37
40
  role: "none",
38
- className: "relative m-24",
41
+ className: down ? "relative mx-8 my-16" : "relative m-24",
39
42
  style: {
40
43
  zIndex: (highlight > 0 ? 25 : 1) + zBase,
41
- ...emptyRowsStyle(rowsAbove)
44
+ ...down ? {} : emptyRowsStyle(rowsAbove)
42
45
  },
43
46
  onMouseEnter: enter,
44
47
  onMouseLeave: leave,
@@ -70,4 +73,4 @@ var JobNode = memo(function JobNode({ jobNames, jobs, handlers, preview, editabl
70
73
  });
71
74
  });
72
75
  //#endregion
73
- export { DimRules, GraphNode, JobNode, dimClasses, setDimmed };
76
+ export { DimRules, GraphDirectionContext, GraphNode, JobNode, dimClasses, setDimmed };
@@ -77,20 +77,20 @@ function JobRow({ jobName, job, isExpanded, onToggle, prefix, state }) {
77
77
  className: "flex min-w-0 items-center gap-1.5",
78
78
  children: [/* @__PURE__ */ jsx("span", {
79
79
  className: "font-mono text-sm text-[var(--theme-app)] truncate",
80
+ title: jobLabel(jobName, job),
80
81
  children: jobLabel(jobName, job)
81
82
  }), /* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName)) })]
82
83
  }), dependencies.length > 0 && /* @__PURE__ */ jsxs("div", {
83
- className: "flex items-center gap-1.5",
84
+ className: "flex flex-wrap items-center gap-x-1.5 gap-y-1",
84
85
  children: [/* @__PURE__ */ jsx("span", {
85
- className: "text-[10px] text-[var(--theme-muted-text-color)]",
86
+ className: "whitespace-nowrap text-xs text-[var(--theme-muted-text-color)]",
86
87
  children: t.dependsOn
87
88
  }), dependencies.map((dep) => /* @__PURE__ */ jsx("span", {
88
- className: "text-[10px] px-1.5 py-0.5 rounded bg-[var(--theme-muted-panel-bg)] text-[var(--theme-muted-text-color)]",
89
+ className: "whitespace-nowrap text-xs px-1.5 py-0.5 rounded bg-[var(--theme-muted-panel-bg)] text-[var(--theme-muted-text-color)]",
89
90
  children: jobLabel(dep)
90
91
  }, dep))]
91
92
  })]
92
93
  }),
93
- /* @__PURE__ */ jsx("div", { className: "flex-1" }),
94
94
  /* @__PURE__ */ jsx(Elapsed, {
95
95
  start: job.startedAt,
96
96
  end: job.completedAt,