From 9cd8102f582cebe236bcfa0cd8d21c3f49cb402e Mon Sep 17 00:00:00 2001 From: Jan Librowski Date: Mon, 7 Sep 2026 11:52:14 +0200 Subject: [PATCH 1/3] fix(ui): zero inherited xyflow handle minimum for 8px default ports xyflow's 5px minimum applies to the content box and expands the bordered port to 9px. Reset both minimums so 4px content plus 2px borders remains 8px. --- .changeset/canvas-port-size.md | 5 +++++ packages/ui/built-css-pitfalls.md | 10 ++++++++++ packages/ui/scripts/check-built-css.ts | 20 +++++++++++++++++++ .../node/node-panel/handle.module.css | 2 ++ 4 files changed, 37 insertions(+) create mode 100644 .changeset/canvas-port-size.md diff --git a/.changeset/canvas-port-size.md b/.changeset/canvas-port-size.md new file mode 100644 index 000000000..a18364b96 --- /dev/null +++ b/.changeset/canvas-port-size.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/ui': patch +--- + +Default canvas ports now render at the designed 8px outer size. diff --git a/packages/ui/built-css-pitfalls.md b/packages/ui/built-css-pitfalls.md index 596b626ee..31cc95b17 100644 --- a/packages/ui/built-css-pitfalls.md +++ b/packages/ui/built-css-pitfalls.md @@ -99,6 +99,16 @@ legitimate pattern rather than a mis-scoped default. Every other check applies e **Automated?** - Yes. The `publint` package script validates export targets and the broader published package surface. +## Incorrect built handle geometry + +**What breaks** - The default canvas port renders larger than its designed 8px outer size when a third-party handle minimum overrides its 4px content size plus 2px border on each side. + +**Why it is silent** - With `box-sizing: content-box`, `min-width` and `min-height` constrain the content box, so xyflow's 5px minimum produces a 9px outer size even though the declared width and height remain 4px. + +**How to spot it** - Inspect the built base `.react-flow__handle` rule and confirm its width, height, border, and `box-sizing` geometry is accompanied by `min-width: 0` and `min-height: 0`. + +**Automated?** - Yes. `Incorrect built handle geometry` validates the complete base handle geometry in each checked dist stylesheet. + ## Missing entry-chunk font faces **What breaks** - Importing the root JavaScript barrel omits bundled font declarations, so text renders in fallback fonts unless a consumer separately imports `fonts.css`. diff --git a/packages/ui/scripts/check-built-css.ts b/packages/ui/scripts/check-built-css.ts index 8c90ef266..1a451428b 100644 --- a/packages/ui/scripts/check-built-css.ts +++ b/packages/ui/scripts/check-built-css.ts @@ -17,6 +17,14 @@ const knownLayers = new Set(layerNames); const problems: string[] = []; const urlPattern = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi; const sanctionedPrefixes = ['--wb-ds-', '--wb-sdk-', '--wb-public-']; +const handleGeometry = [ + ['box-sizing', 'content-box'], + ['width', 'var(--wb-public-node-port-size)'], + ['height', 'var(--wb-public-node-port-size)'], + ['min-width', '0'], + ['min-height', '0'], + ['border', 'var(--wb-public-node-port-border-size) solid var(--wb-public-node-port-border-color)'], +] as const; function isOrderStatement(node: ChildNode | undefined): boolean { return ( @@ -92,6 +100,18 @@ for (const { path: directory, ownsLayerContract } of directories) { } root.walkAtRules('import', (atRule) => add(file, atRule, 'Built CSS import')); + root.walkRules((rule) => { + if (!rule.selectors.some((selector) => selector.endsWith(' .react-flow__handle'))) return; + + const declarations = new Map(); + rule.walkDecls((declaration) => { + declarations.set(declaration.prop, declaration.value); + }); + if (handleGeometry.some(([property, value]) => declarations.get(property) !== value)) { + add(file, rule, 'Incorrect built handle geometry'); + } + }); + root.walkDecls((declaration) => { if (/var\((?!\s*--)/.test(declaration.value)) add(file, declaration, 'Malformed var() argument'); const invalidNamespace = diff --git a/packages/ui/src/components/node/node-panel/handle.module.css b/packages/ui/src/components/node/node-panel/handle.module.css index af6200deb..6628a02cb 100644 --- a/packages/ui/src/components/node/node-panel/handle.module.css +++ b/packages/ui/src/components/node/node-panel/handle.module.css @@ -46,6 +46,8 @@ box-sizing: content-box; width: var(--wb-public-node-port-size); height: var(--wb-public-node-port-size); + min-width: 0; + min-height: 0; background: var(--wb-public-node-port-background-color); border: var(--wb-public-node-port-border-size) solid var(--wb-public-node-port-border-color); From b72fec3385b54a1b64998f58cd6cbb7a8b89d683 Mon Sep 17 00:00:00 2001 From: Jan Librowski Date: Wed, 9 Sep 2026 23:38:12 +0200 Subject: [PATCH 2/3] fix(ui): grow the target port while a connection hovers it React Flow captures the pointer on the dragged handle, so the node under the cursor never receives :hover and its ports stayed at the 8px default during a connection. The connectingto state now shares the connectingfrom rule, so the target shows the same 16px active port as the source. --- .changeset/port-connecting-target.md | 5 +++++ .../ui/src/components/node/node-panel/handle.module.css | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 .changeset/port-connecting-target.md diff --git a/.changeset/port-connecting-target.md b/.changeset/port-connecting-target.md new file mode 100644 index 000000000..de8782c47 --- /dev/null +++ b/.changeset/port-connecting-target.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/ui': patch +--- + +The target port grows to its hover size while a connection is dragged over it, matching the dragged source port. diff --git a/packages/ui/src/components/node/node-panel/handle.module.css b/packages/ui/src/components/node/node-panel/handle.module.css index 6628a02cb..5c68827fd 100644 --- a/packages/ui/src/components/node/node-panel/handle.module.css +++ b/packages/ui/src/components/node/node-panel/handle.module.css @@ -73,8 +73,11 @@ /* State rules repeat the base rule's content-box opt-out: the build injects border-box per rule, so an undeclared box-sizing here would - shrink the handle in exactly these states. */ - :global(.connectingfrom) { + shrink the handle in exactly these states. The target port needs its + own rule: React Flow captures the pointer on the dragged handle, so + :hover never reaches the node under the cursor while connecting. */ + :global(.connectingfrom), + :global(.connectingto) { box-sizing: content-box; background: var(--wb-public-node-port-background-color-hover); border-color: var(--wb-public-node-port-border-color-hover); From a581878597c84ce2c1d59202174cf2197969a6fe Mon Sep 17 00:00:00 2001 From: Jan Librowski Date: Mon, 14 Sep 2026 21:51:50 +0200 Subject: [PATCH 3/3] fix(ui): grow only valid connection targets and drop the handle geometry check Review follow-up. The connecting-state rule now targets .connectingto.valid, so a port rejected by isValidConnection keeps its default size while the connection line reports invalid; the comment explains the real reason the target needs its own rule (xyflow assigns connectingto by proximity within connectionRadius, so :hover cannot express it). The built-CSS geometry check is removed: it matched every rule ending in .react-flow__handle and would have failed any future state rule on the bare handle, and its exact-string comparison against minified output was fragile. The pitfalls entry records the manual check instead. --- .changeset/port-connecting-target.md | 2 +- packages/ui/built-css-pitfalls.md | 4 ++-- packages/ui/scripts/check-built-css.ts | 20 ------------------- .../node/node-panel/handle.module.css | 7 +++---- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/.changeset/port-connecting-target.md b/.changeset/port-connecting-target.md index de8782c47..8ace56ca0 100644 --- a/.changeset/port-connecting-target.md +++ b/.changeset/port-connecting-target.md @@ -2,4 +2,4 @@ '@workflowbuilder/ui': patch --- -The target port grows to its hover size while a connection is dragged over it, matching the dragged source port. +A valid target port grows to its hover size while a connection is dragged near it, matching the dragged source port; a target rejected by `isValidConnection` keeps its default size. diff --git a/packages/ui/built-css-pitfalls.md b/packages/ui/built-css-pitfalls.md index 31cc95b17..e75e67fe0 100644 --- a/packages/ui/built-css-pitfalls.md +++ b/packages/ui/built-css-pitfalls.md @@ -105,9 +105,9 @@ legitimate pattern rather than a mis-scoped default. Every other check applies e **Why it is silent** - With `box-sizing: content-box`, `min-width` and `min-height` constrain the content box, so xyflow's 5px minimum produces a 9px outer size even though the declared width and height remain 4px. -**How to spot it** - Inspect the built base `.react-flow__handle` rule and confirm its width, height, border, and `box-sizing` geometry is accompanied by `min-width: 0` and `min-height: 0`. +**How to spot it** - Inspect the built base `.react-flow__handle` rule and confirm its width, height, border, and `box-sizing` geometry is accompanied by `min-width: 0` and `min-height: 0`. In a running app, `offsetWidth` of a resting `.react-flow__handle` must read 8 at any zoom. -**Automated?** - Yes. `Incorrect built handle geometry` validates the complete base handle geometry in each checked dist stylesheet. +**Automated?** - No. A dist check existed briefly and was dropped: it matched every rule ending in ` .react-flow__handle`, so any later state rule on the bare handle would have failed it without touching geometry, and exact-string matching against minified output was fragile. The source rule carries a comment; the geometry is covered by the manual pass. ## Missing entry-chunk font faces diff --git a/packages/ui/scripts/check-built-css.ts b/packages/ui/scripts/check-built-css.ts index 1a451428b..8c90ef266 100644 --- a/packages/ui/scripts/check-built-css.ts +++ b/packages/ui/scripts/check-built-css.ts @@ -17,14 +17,6 @@ const knownLayers = new Set(layerNames); const problems: string[] = []; const urlPattern = /url\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*))\s*\)/gi; const sanctionedPrefixes = ['--wb-ds-', '--wb-sdk-', '--wb-public-']; -const handleGeometry = [ - ['box-sizing', 'content-box'], - ['width', 'var(--wb-public-node-port-size)'], - ['height', 'var(--wb-public-node-port-size)'], - ['min-width', '0'], - ['min-height', '0'], - ['border', 'var(--wb-public-node-port-border-size) solid var(--wb-public-node-port-border-color)'], -] as const; function isOrderStatement(node: ChildNode | undefined): boolean { return ( @@ -100,18 +92,6 @@ for (const { path: directory, ownsLayerContract } of directories) { } root.walkAtRules('import', (atRule) => add(file, atRule, 'Built CSS import')); - root.walkRules((rule) => { - if (!rule.selectors.some((selector) => selector.endsWith(' .react-flow__handle'))) return; - - const declarations = new Map(); - rule.walkDecls((declaration) => { - declarations.set(declaration.prop, declaration.value); - }); - if (handleGeometry.some(([property, value]) => declarations.get(property) !== value)) { - add(file, rule, 'Incorrect built handle geometry'); - } - }); - root.walkDecls((declaration) => { if (/var\((?!\s*--)/.test(declaration.value)) add(file, declaration, 'Malformed var() argument'); const invalidNamespace = diff --git a/packages/ui/src/components/node/node-panel/handle.module.css b/packages/ui/src/components/node/node-panel/handle.module.css index 5c68827fd..eca3a41cf 100644 --- a/packages/ui/src/components/node/node-panel/handle.module.css +++ b/packages/ui/src/components/node/node-panel/handle.module.css @@ -73,11 +73,10 @@ /* State rules repeat the base rule's content-box opt-out: the build injects border-box per rule, so an undeclared box-sizing here would - shrink the handle in exactly these states. The target port needs its - own rule: React Flow captures the pointer on the dragged handle, so - :hover never reaches the node under the cursor while connecting. */ + shrink the handle in exactly these states. */ + /* .connectingto is assigned by proximity (connectionRadius), so :hover cannot cover the target. */ :global(.connectingfrom), - :global(.connectingto) { + :global(.connectingto.valid) { box-sizing: content-box; background: var(--wb-public-node-port-background-color-hover); border-color: var(--wb-public-node-port-border-color-hover);