feat(diagram-engine): corners, borderless fills, shadows and strikethrough

Four more Tailwind classes, all four verified against draw.io's own source in
public/drawio rather than against a prose reference — which is how three earlier
exclusions turned out to be wrong:

  rounded-*      mxShape.js:1172-1189 — absoluteArcSize=1 switches arcSize to
                 absolute pixels and halves it, so the same class is the same
                 corner on every box. Previously excluded as 'percentage only'
  shadow-sm..xl  mxShape.js:505-535 — getShadowStyle reads five independent
                 params, not one flag, so Tailwind's offset+blur rungs map one
                 to one. Previously excluded as 'six sizes collapse to one'
  line-through   mxConstants.js:2054 FONT_STRIKETHROUGH: 8, read at both
                 mxText.js:723 and :1040. The bitmask has four bits, not three
  border-none    the only one of the four that adds something previously
                 inexpressible: a fill with no outline

Also fixes an edge style growing 76 characters per re-layout, without bound. The
router recomputes ports on every pass, and appending them to a style recovered
from the canvas — which already carried the previous pass's eight port keys —
grew the string forever. draw.io resolves duplicates last-wins so the arrow
always looked right; a byte-identity check is what caught it.

Two traps found while wiring the readback, both the same shape: a value the
THEME emits being recorded as one the model asked for. strokeColor=none from a
filled or ghost role, and rounded=0 from the fallback style. Either one would
outlive a set_role, since that clears style but keeps text.

Deliberately not included, with reasons in tw.ts: per-side borders and per-corner
radius (both would take the shape slot, and what a node IS matters more than
which of its edges show), per-side padding (draw.io's keys pad the label, not the
room left for children), text-shadow (a bare flag with no offset or blur),
opacity (Tailwind's is any integer, not a scale), tracking/uppercase/leading
(absent from draw.io — zero grep hits, not merely coarse).

615 tests, 90 new. Browser-verified: four shadow rungs visibly differ, radius is
real pixels, terminator + rounded-lg becomes a small-cornered rounded rect while
an untouched terminator stays a stadium.
This commit is contained in:
dayuan.jiang
2026-08-11 09:09:45 +09:00
parent 8687e8f04b
commit 0b03e15336
25 changed files with 3669 additions and 1234 deletions

View File

@@ -90,11 +90,62 @@ describe("grow", () => {
const flat = make(false)
const grown = make(true)
const extraMain = rectOf(grown, "main").w - rectOf(flat, "main").w
const extraSide = rectOf(grown, "side").w - rectOf(flat, "side").w
// The slack lands on the columns instead of the gaps, split 2:1.
expect(extraMain).toBeGreaterThan(0)
expect(extraMain / extraSide).toBeCloseTo(2, 0)
// The weights make main wider than it would be on its own content...
expect(rectOf(grown, "main").w).toBeGreaterThan(rectOf(flat, "main").w)
// ...but NOT the full 2:1, and that is correct rather than a shortfall. `side`
// will not shrink below the width of its own text, so the ratio settles wherever
// that floor allows. A browser does exactly the same: `min-width` defaults to
// `auto`, so a `flex: 2` column stops shrinking at its content too.
const ratio = rectOf(grown, "main").w / rectOf(grown, "side").w
expect(ratio).toBeGreaterThan(1.3)
expect(ratio).toBeLessThan(2.1)
})
it("min-w-0 lets the weights win over the content width", () => {
// The CSS escape hatch, same spelling: with min-w-0 the narrow column may be
// squeezed under its own text, so a declared 2:1 really comes out 2:1.
const r = restructureDiagram("", [
{ op: "add_container", id: "page", label: "", dir: "col", gap: 16 },
{
op: "add_box",
id: "mast",
parent: "page",
label: "A very wide masthead banner that stretches the page out",
role: "banner",
},
{
op: "add_container",
id: "cols",
parent: "page",
label: "",
dir: "row",
gap: 16,
},
{
op: "add_container",
id: "main",
parent: "cols",
label: "",
dir: "col",
class: "grow-2 min-w-0",
},
{
op: "add_container",
id: "side",
parent: "cols",
label: "",
dir: "col",
class: "grow-1 min-w-0",
},
{ op: "add_box", id: "a", parent: "main", label: "main content" },
{ op: "add_box", id: "b", parent: "side", label: "aside" },
])
expect(r.errors).toEqual([])
const xml = r.xml as string
const ratio = rectOf(xml, "main").w / rectOf(xml, "side").w
expect(ratio).toBeCloseTo(2, 0)
})
})