@helping-ai-workflow/md2doc 3.5.0 → 3.6.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.
package/lib/md2doc.js CHANGED
@@ -1639,8 +1639,21 @@ ${itemsHtml}
1639
1639
  position: fixed; inset: 0; z-index: 998; background: rgba(0,0,0,.45);
1640
1640
  display: flex; align-items: center; justify-content: center;
1641
1641
  }
1642
+ /* v3.6.0 Task 13: width cap raised from 1200px to 1920px — MEASURED
1643
+ (createEditorServer, four real viewports): at 1200px the cap itself
1644
+ was the binding constraint at 1280/1600/1920 alike (94vw there is
1645
+ 1203/1504/1805px, all above the old 1200px cap), so every screen from a
1646
+ 1280 laptop up to a 1920 monitor got the IDENTICAL canvas width — the
1647
+ exact complaint that opened this version ("a bigger monitor buys the
1648
+ user nothing"). 1920px is chosen so 94vw is the actual governor at
1649
+ every one of those four measured sizes (94% of 1920 is 1804.8, still
1650
+ under the new cap) — this number is a ceiling for a screen wider than
1651
+ that, not a value that binds within the measured range. At 800x600,
1652
+ 94vw (752px) was already narrower than either cap, so that viewport is
1653
+ unchanged by this. See task-13-report.md for the re-measured
1654
+ toolbar/canvas table. */
1642
1655
  .ed-wave-panel {
1643
- background: #fff; border-radius: 6px; width: min(1200px, 94vw);
1656
+ background: #fff; border-radius: 6px; width: min(1920px, 94vw);
1644
1657
  height: min(760px, 90vh); display: flex; flex-direction: column;
1645
1658
  box-shadow: 0 6px 28px rgba(0,0,0,.35); overflow: hidden;
1646
1659
  }
@@ -1664,15 +1677,49 @@ ${itemsHtml}
1664
1677
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
1665
1678
  padding: 8px 12px; border-bottom: 1px solid #e2e2e2; background: #fafafa;
1666
1679
  }
1667
- .ed-wave-brushes, .ed-wave-cycleops, .ed-wave-meta {
1680
+ /* v3.6.0 Task 12: the eight captioned sections themselves. .ed-wave-tools
1681
+ is the row that holds them (the SAME element as .ed-wave-toolbar
1682
+ above -- that class still owns the bar's padding/border/background,
1683
+ this one owns only the group row's own gap). Each .ed-wave-tool-group
1684
+ is a bordered chip so eight adjacent sections read as eight things, not
1685
+ one longer wall of buttons; the caption sits to its buttons' LEFT (see
1686
+ wave-panels.js's toolGroup() for why not above). */
1687
+ .ed-wave-tools { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
1688
+ .ed-wave-tool-group {
1689
+ display: flex; align-items: center; gap: 4px;
1690
+ padding: 2px 6px; border: 1px solid #ececec; border-radius: 6px;
1691
+ }
1692
+ .ed-wave-tool-caption {
1693
+ font-size: 10px; color: #8a8a8a; letter-spacing: .5px;
1694
+ white-space: nowrap; margin-right: 2px;
1695
+ }
1696
+ .ed-wave-brushes, .ed-wave-meta {
1668
1697
  display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
1669
1698
  }
1670
- .ed-wave-brush {
1671
- width: 28px; height: 28px; border: 1px solid #c4c4c4; border-radius: 4px;
1672
- background: #fff; font-family: monospace; font-size: 14px; cursor: pointer;
1699
+ /* v3.6.0 Task 10: widened from a 28x28 bare-character square to a small
1700
+ column (glyph on top, the character kept as a caption underneath) so
1701
+ the 22-brush palette says what each brush paints instead of relying on
1702
+ the letter alone -- nine of those 22 (P, N, u, d, h, l, 2, 6-9) have no
1703
+ obvious on-canvas picture from the character by itself. */
1704
+ /* '.ed-wave-brushes .ed-wave-brush', not the bare class: '.ed-wave-toolbar
1705
+ button' below outranks a single class on specificity (class+type beats
1706
+ class alone) and would otherwise win the padding fight and squeeze the
1707
+ 24px-wide icon into a content box narrower than it is. */
1708
+ .ed-wave-brushes .ed-wave-brush {
1709
+ width: 34px; height: 40px; border: 1px solid #c4c4c4; border-radius: 4px;
1710
+ background: #fff; font-family: monospace; cursor: pointer;
1711
+ display: flex; flex-direction: column; align-items: center;
1712
+ justify-content: center; gap: 1px; padding: 2px 0; box-sizing: border-box;
1673
1713
  }
1674
1714
  .ed-wave-brush.is-on { background: #1a73e8; color: #fff; border-color: #1a73e8; }
1675
- .ed-wave-cycleops button, .ed-wave-toolbar button {
1715
+ .ed-wave-brush-char { font-size: 11px; line-height: 1; }
1716
+ /* The glyph itself. Its shapes are drawn by wave-draw.js's levelIcon,
1717
+ which reuses renderCanvas's own drawing functions (bandOf, brickPath,
1718
+ the bus/clock/gap helpers) -- never a second hand-drawn set, so a
1719
+ change to how a brush paints on the real canvas cannot leave the
1720
+ toolbar showing the old picture. */
1721
+ .ed-wave-level-icon { display: block; overflow: visible; }
1722
+ .ed-wave-toolbar button {
1676
1723
  border: 1px solid #c4c4c4; border-radius: 4px; background: #fff;
1677
1724
  padding: 3px 8px; font-size: 12px; cursor: pointer;
1678
1725
  }
@@ -1681,20 +1728,18 @@ ${itemsHtml}
1681
1728
  treatment as .ed-wave-brush.is-on above — a picked tool reads the same
1682
1729
  way whichever toolbar it lives in. */
1683
1730
  .ed-wave-edge-arm.is-on { background: #1a73e8; color: #fff; border-color: #1a73e8; }
1731
+ /* v3.6.0 Task 13: the fixed 110px input width that used to live here moved
1732
+ into .ed-wave-section-body input below — every .ed-wave-metafield
1733
+ now lives inside a right-hand panel (關聯線/文件/Lane), not the toolbar,
1734
+ and a panel field should fill the ~340px column rather than sit at a
1735
+ width chosen for the old horizontal toolbar row. */
1684
1736
  .ed-wave-metafield { display: flex; align-items: center; gap: 4px; font-size: 12px; }
1685
- .ed-wave-metafield input { width: 110px; font-size: 12px; padding: 2px 4px; }
1686
- /* v3.5.0 Task 6 fix round 2: with no rule at all this is a flex item inside
1687
- .ed-wave-toolbar (display:flex; flex-wrap:wrap) that sizes to content, so
1688
- its three controls squeeze against whichever control precedes them
1689
- instead of forming their own row. The two property groups below are not
1690
- new values invented for this rule: display/flex-wrap/gap/align-items are
1691
- copied from .ed-wave-brushes, .ed-wave-cycleops, .ed-wave-meta above (the
1692
- shared flex-row rule its own children need), and flex:1 1 100% is copied
1693
- from .ed-wave-unmodelled below (what makes THAT sibling claim its own row
1694
- instead of sitting beside the control before it) — this row needs both at
1695
- once: it lays out its own children AND has to stop sharing a line with
1696
- whatever toolbar control precedes it. */
1697
- .ed-wave-edgebar { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; flex: 1 1 100%; }
1737
+ /* v3.6.0 Task 13: the 關聯線 panel's own content, stacked vertically now
1738
+ that it lives in a narrow side column rather than a wide flex-wrap
1739
+ toolbar row (the old 'flex: 1 1 100%' here was what made it claim a
1740
+ full row of THAT row; a panel body has no siblings to share a row
1741
+ with). */
1742
+ .ed-wave-edgebar { display: flex; flex-direction: column; gap: 4px; }
1698
1743
  /* The "this drawing is not showing you everything" notice. period / phase /
1699
1744
  config.hscale all change what the ENGINE paints and none of them is
1700
1745
  modelled by the hand drawing, so a document carrying one says so instead
@@ -1709,14 +1754,33 @@ ${itemsHtml}
1709
1754
  Same shape as its neighbour so the two read as one family, one step
1710
1755
  louder because the information gap is bigger. */
1711
1756
  .ed-wave-nolanes { color: #7a1f00; background: #ffe9e0; border-color: #e8a987; }
1712
- .ed-wave-body { display: flex; flex: 1 1 auto; min-height: 0; }
1713
- /* Widths chosen so the DRAWING still gets room at the narrowest viewport this
1714
- repo's browser tests use (800x600 -> panel 752): 752 - 200 - 160 = 392px,
1715
- which is 8 cycles at .ed-wave-canvas's 48px pitch. The canvas is a scroll
1716
- container, so a wider diagram scrolls rather than pushing the preview off —
1717
- but a canvas column narrower than a couple of cycles would make the drag
1718
- gesture unusable, and that was the first thing measured to go wrong here
1719
- (a press landed on the preview column, 116px past the canvas's clip). */
1757
+ /* v3.6.0 Task 13 fix round 1 (Critical 2): position: relative added.
1758
+ .ed-wave-side[data-expanded] below is position: absolute, and neither
1759
+ .ed-wave-panel nor (until this line) this element had a position of
1760
+ their own — so its containing block was .ed-wave-overlay
1761
+ (position: fixed; inset: 0), the WHOLE viewport, not the dialog.
1762
+ .ed-wave-panel's own overflow: hidden did not clip it either: clipping
1763
+ follows the containing-block chain, not DOM nesting, so the rail
1764
+ rendered against viewport corners at a narrow window and covered
1765
+ .ed-wave-head's 關閉 button — the only discoverable way to close the
1766
+ dialog (a hit-test at 關閉's own rectangle returned the edge panel's
1767
+ delete button instead; Escape still worked but was not discoverable).
1768
+ Anchoring the rail to THIS element rather than to .ed-wave-panel itself
1769
+ is deliberate, not incidental: .ed-wave-body sits below .ed-wave-head
1770
+ and above .ed-wave-status, so top: 0 / bottom: 0 on the rail now lands
1771
+ at body's own top/bottom — never over the header (關閉) or the status
1772
+ line — without this rule having to know either one's height. */
1773
+ .ed-wave-body { display: flex; flex: 1 1 auto; min-height: 0; position: relative; }
1774
+ /* v3.6.0 Task 13: the old comment here hand-computed the canvas's leftover
1775
+ width from the (now gone) .ed-wave-preview column's own flex-basis;
1776
+ that arithmetic no longer applies — the third column is .ed-wave-side
1777
+ below, which collapses to a 28px rail under 1100px specifically so the
1778
+ canvas is not squeezed the way a fixed 160px minimum used to squeeze it
1779
+ at 800x600 (MEASURED before this task: canvas width there was only
1780
+ 282px). Re-measured after this task at four real viewports; see
1781
+ task-13-report.md rather than a formula here — a number derived by hand
1782
+ drifts the moment any sibling column's own width changes, which is
1783
+ exactly what this task did. */
1720
1784
  .ed-wave-lanes {
1721
1785
  flex: 0 0 200px; border-right: 1px solid #e2e2e2; overflow: auto;
1722
1786
  padding: 8px 0 8px 0;
@@ -1727,27 +1791,37 @@ ${itemsHtml}
1727
1791
  the rail spanning its lanes' rows instead, which is the shape the engine
1728
1792
  itself gives it (horizontal space in the name column, no vertical space). */
1729
1793
  .ed-wave-lane-rows { position: relative; }
1794
+ /* v3.6.0 Task 14: a row is a drag handle, a name field and (on a {}
1795
+ spacer) a badge. It used to be six buttons plus the field, and the six
1796
+ did not fit: flexed into what was left of 200px the field measured
1797
+ 45px on the most crowded row, and a browser given a 24px box and a
1798
+ three-character Chinese caption lays that caption out VERTICALLY — the
1799
+ editor shipped columns of stacked glyphs where labels were meant.
1800
+ Those six are toolbar buttons now (the 訊號 / 群組 sections), acting
1801
+ on the one selection. */
1730
1802
  .ed-wave-lane-row {
1731
1803
  position: absolute; left: 0; right: 0; display: flex; align-items: center;
1732
- gap: 3px; padding-right: 6px; box-sizing: border-box;
1804
+ gap: 6px; padding-right: 6px; box-sizing: border-box;
1733
1805
  }
1734
1806
  .ed-wave-lane-row.is-selected { background: #eaf2fe; }
1735
- /* v3.5.0 Task 12: Shift+click multi-select for group creation — a
1736
- distinct tint from .is-selected above (that one is the CYCLE selection
1737
- the brush/cycle-ops toolbar acts on; this one is a SET of whole lanes
1738
- the group buttons act on, and a user with both standing at once — a
1739
- cycle range selected on a lane that is also multi-selected for
1740
- grouping — needs to tell which is which at a glance). A lane can carry
1741
- both classes together, so this is additive (a left border) rather than
1742
- a second background that would just overwrite .is-selected's. */
1743
- .ed-wave-lane-row.is-multi-selected { border-left: 3px solid #7c3aed; }
1744
- .ed-wave-lane-name { flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 2px 4px; }
1745
- .ed-wave-lane-row button {
1746
- border: 1px solid #cfcfcf; border-radius: 3px; background: #fff;
1747
- font-size: 11px; line-height: 1; padding: 3px 4px; cursor: pointer;
1748
- }
1749
- .ed-wave-lane-row button:disabled { opacity: .4; cursor: default; }
1750
- .ed-wave-lane-add-tail { position: absolute; left: 6px; }
1807
+ .ed-wave-lane-handle {
1808
+ flex: 0 0 auto; cursor: grab; color: #aaa; user-select: none; font-size: 13px;
1809
+ }
1810
+ .ed-wave-lane-handle:active { cursor: grabbing; }
1811
+ /* min-width, not just flex-grow: min-width is what a flex item will not
1812
+ shrink past, and shrinking past it is the vertical-text failure above.
1813
+ MEASURED after this change (real page, 800x600, the journey suite's own
1814
+ wave fixture): rail 201px, handle 9.5px, badge 36px, and the field comes
1815
+ out 178.5px ungrouped, 162.5px one group deep, 136.5px on the {} spacer
1816
+ row that also carries the badge — writing-mode horizontal-tb on all six.
1817
+ So 120 is slack at every depth this fixture reaches, NOT a computed
1818
+ exact fit. Deeper nesting spends 16px a level and would eventually hit
1819
+ the floor; there the field stops shrinking and .ed-wave-lanes scrolls,
1820
+ which is the trade this rule chooses on purpose. */
1821
+ .ed-wave-lane-name {
1822
+ flex: 1 1 auto; min-width: 120px; font-size: 12px; padding: 2px 4px;
1823
+ }
1824
+ .ed-wave-lane-badge { flex: 0 0 auto; font-size: 10px; color: #888; }
1751
1825
  .ed-wave-group, .ed-wave-group-input {
1752
1826
  position: absolute; width: 14px; padding: 0; font-size: 11px;
1753
1827
  writing-mode: vertical-rl; text-orientation: mixed; overflow: hidden;
@@ -1775,6 +1849,14 @@ ${itemsHtml}
1775
1849
  }
1776
1850
  .ed-wave-grid { stroke: #ececec; stroke-width: 1; }
1777
1851
  .ed-wave-line, .ed-wave-edge { stroke: #111; stroke-width: 2; fill: none; }
1852
+ /* v3.6.0 Task 10: the toolbar glyph's plain-level backbone (0/1/h/l/z/u/d
1853
+ only -- bus/clock/x/gap glyphs draw their own shape with their own
1854
+ fixed canvas colour instead, further down). Declared after the rule
1855
+ above on purpose: both are single-class selectors, so source order is
1856
+ what lets currentColor win over the fixed #111 for this one case,
1857
+ turning the icon white together with the rest of the button's text
1858
+ under .ed-wave-brush.is-on. */
1859
+ .ed-wave-level-icon-path { stroke: currentColor; }
1778
1860
  .ed-wave-weak { stroke-dasharray: 4 3; }
1779
1861
  /* v3.5.0 Task 10: the P/N explicit-edge arrowhead — see wave-ui.js's
1780
1862
  drawLane isClock branch for the measurement behind its shape and
@@ -1885,6 +1967,38 @@ ${itemsHtml}
1885
1967
  untouched no matter what class it shares. */
1886
1968
  text.ed-wave-edge-label { pointer-events: none; }
1887
1969
  .ed-wave-edge-label { font-size: 11px; fill: #333; }
1970
+ /* v3.6.0 Task 8: the canvas's own name column — a mirror of the rail's
1971
+ .ed-wave-lane-name / .ed-wave-group (see the comment above
1972
+ .ed-wave-lane-rows). Unlike .ed-wave-edge-label these two class names
1973
+ are not shared with any non-SVG control, so pointer-events: none goes
1974
+ straight on the class rather than needing the element-scoped selector
1975
+ that rule uses. MEASURED (task-8-report.md): the longest lane name in
1976
+ this repo's fixtures is 12 chars ("he said \"hi\""), which at 11px in
1977
+ both the app's default sans-serif and the worst case (monospace)
1978
+ measures well under the 110px a top-level name has to fit in
1979
+ (nameColWidth 120 - NAME_GAP 10), so SIZES.nameColWidth stays 120. */
1980
+ .ed-wave-lane-label, .ed-wave-group-label { pointer-events: none; }
1981
+ .ed-wave-lane-label { font-size: 11px; fill: #333; }
1982
+ .ed-wave-group-label {
1983
+ font-size: 11px; fill: #555; writing-mode: vertical-rl; text-orientation: mixed;
1984
+ }
1985
+ /* v3.6.0 Task 9: the cycle ruler and the head/foot bands — see
1986
+ wave-draw.js's drawRuler()/drawBand() comment for the geometry (tick on
1987
+ boundaries, tock at centres, both filtered by every) and why this is
1988
+ read-only with respect to the document. Purely decorative like the name
1989
+ column above it, so pointer-events: none on the same principle. The
1990
+ ruler's own fill matches the pinned engine's own muted class (fill
1991
+ #aaa, node_modules/wavedrom's waveSkin CSS) so a tick/tock painted here
1992
+ reads the same shade as the identical mark in the WaveDrom preview
1993
+ beside it; the head/foot titles use the darker ed-wave-lane-label shade
1994
+ instead, matching the engine's own plain black fill for head.text /
1995
+ foot.text closely enough at this scale without introducing a third
1996
+ grey. */
1997
+ .ed-wave-ruler-tick, .ed-wave-ruler-tock, .ed-wave-head-text, .ed-wave-foot-text {
1998
+ pointer-events: none;
1999
+ }
2000
+ .ed-wave-ruler-tick, .ed-wave-ruler-tock { font-size: 11px; fill: #aaa; }
2001
+ .ed-wave-head-text, .ed-wave-foot-text { font-size: 11px; fill: #333; }
1888
2002
  .ed-wave-edge-handle { fill: #1a73e8; stroke: #ffffff; }
1889
2003
  .ed-wave-edge-arrow { fill: #0041c4; }
1890
2004
  .ed-wave-edge-tee { stroke: #0041c4; }
@@ -1897,11 +2011,113 @@ ${itemsHtml}
1897
2011
  stroke: #1a73e8; stroke-width: 2; stroke-dasharray: 5 4; fill: none;
1898
2012
  pointer-events: none;
1899
2013
  }
1900
- .ed-wave-preview {
1901
- flex: 0 1 340px; min-width: 160px; border-left: 1px solid #e2e2e2;
1902
- overflow: auto; padding: 8px;
1903
- }
2014
+ /* v3.6.0 Task 11: the transition dots wave-draw.js's drawTransitions()
2015
+ paints while edge-creation is armed -- the visual answer to "where can
2016
+ I aim". pointer-events: none for the same reason ed-wave-selection and
2017
+ the other UI-state marks above are: the actual hit-testing runs through
2018
+ geometry.boundaryAt against the pointer's raw coordinates
2019
+ (wave-ui.js's cellFromEvent/onCanvasHover), never through this circle
2020
+ as a DOM target, so it must never itself swallow the press it exists
2021
+ to help aim. The [data-hot] variant is the same dot, larger and
2022
+ filled solid, for the one nearest the pointer right now
2023
+ (state.hoverBoundary in drawTransitions). */
2024
+ .ed-wave-transition {
2025
+ fill: #ffffff; stroke: #1a73e8; stroke-width: 1.5; pointer-events: none;
2026
+ }
2027
+ .ed-wave-transition[data-hot] { fill: #1a73e8; stroke: #0041c4; }
2028
+ /* v3.6.0 Task 13: the old flex-basis/border/padding here were for
2029
+ .ed-wave-preview as a direct flex child of .ed-wave-body; it now
2030
+ lives inside .ed-wave-section-body (the 預覽 panel), which already
2031
+ supplies the column width and padding, so only its own scroll behaviour
2032
+ is left to set — a wide diagram's preview SVG must scroll within the
2033
+ panel rather than stretching the whole 340px column. */
2034
+ .ed-wave-preview { overflow: auto; max-height: 260px; }
1904
2035
  .ed-wave-preview-label { font-size: 11px; color: #666; margin-bottom: 6px; }
2036
+ /* v3.6.0 Task 13: the right-hand column — five collapsible panels
2037
+ (關聯線/文件/Lane/預覽/原始碼) replacing the old direct
2038
+ .ed-wave-preview column. 'flex: 0 0 340px' (not '0 1'): unlike the
2039
+ old preview column, this one does not shrink when the canvas wants
2040
+ more room — width-budget wins go through .ed-wave-panel's own cap
2041
+ above, not by starving this column, because starving it re-introduces
2042
+ the exact 160px-minimum squeeze this task's own .ed-wave-lanes
2043
+ comment measured as the 800x600 defect. Below 1100px it collapses to a
2044
+ 28px rail instead (media query below) for the identical reason. */
2045
+ /* v3.6.0 Task 13 fix round 4: box-sizing: border-box added. wave-ui.js's
2046
+ clearSelectedEdgeOfSide() sets an inline width (in border-box, i.e.
2047
+ rendered, pixels — the same unit getBoundingClientRect() reports
2048
+ everywhere else it measures this element) to keep the selected edge's
2049
+ own handle clear of this panel when expanded. Under the default
2050
+ content-box sizing, width excludes padding (16px) and the border
2051
+ (1px), so a JS-computed width landed 17px narrower on screen than
2052
+ intended — MEASURED, not assumed: an inline width of 310px rendered at
2053
+ 327px. border-box makes width mean the same thing in CSS and in
2054
+ getBoundingClientRect(), so the two agree without wave-ui.js having to
2055
+ hardcode this rule's own padding/border numbers into its arithmetic. */
2056
+ .ed-wave-side {
2057
+ box-sizing: border-box; flex: 0 0 340px; border-left: 1px solid #e2e2e2;
2058
+ overflow: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px;
2059
+ }
2060
+ .ed-wave-section { border: 1px solid #ececec; border-radius: 6px; }
2061
+ .ed-wave-section-title {
2062
+ display: block; width: 100%; text-align: left; border: 0; background: #fafafa;
2063
+ font: inherit; font-size: 11px; letter-spacing: .5px; color: #555;
2064
+ padding: 5px 8px; cursor: pointer; border-radius: 6px 6px 0 0;
2065
+ }
2066
+ .ed-wave-section-body { display: none; padding: 8px; }
2067
+ /* v3.6.0 Task 13 fix round 3: data-open is this SECTION's own toggle
2068
+ state (wave-panels.js's section()) and says NOTHING about whether the
2069
+ RAIL is currently showing it — those are two independent attributes on
2070
+ two different elements. A section can report data-open with a
2071
+ zero-size, unhittable body the whole time .ed-wave-side itself is
2072
+ collapsed to its <1100px 28px rail (below): 關聯線/文件/Lane all default
2073
+ data-open, and none of that changes just because the rail hides them.
2074
+ Left deliberately this way rather than making data-open track actual
2075
+ visibility — that would conflate "this section is open" with "the rail
2076
+ happens to be showing right now", and toggling ONE section would then
2077
+ have to silently relabel every OTHER already-open section's own state
2078
+ too. What actually closes the gap this could otherwise leave (a control
2079
+ that LOOKS reachable in the DOM but is not) is wave-ui.js's render()
2080
+ expanding the rail on the one transition that matters (selecting an
2081
+ edge) rather than this attribute pretending to mean something it does
2082
+ not. */
2083
+ .ed-wave-section[data-open] .ed-wave-section-body { display: block; }
2084
+ .ed-wave-section-body label { display: flex; align-items: center; gap: 6px; margin: 4px 0; }
2085
+ .ed-wave-section-body label span { flex: 0 0 96px; font-size: 12px; color: #555; }
2086
+ .ed-wave-section-body input {
2087
+ flex: 1 1 auto; min-width: 0; font-size: 12px; padding: 2px 4px;
2088
+ }
2089
+ .ed-wave-source {
2090
+ margin: 0; font-size: 11px; line-height: 1.45; white-space: pre;
2091
+ overflow: auto; max-height: 260px;
2092
+ }
2093
+ /* 視窗窄於這條線時右欄收成一條直立標籤,展開時覆蓋在畫布上 —— 否則
2094
+ 800x600 的測試視窗下畫布會被 340px 固定寬的側欄吃掉大半,拖曳手勢會
2095
+ 不可用(v3.6.0 Task 13 自己的 .ed-wave-lanes 註解量到的舊值:只剩
2096
+ 282px)。收起時仍是普通文件流寬度(28px),展開時疊在畫布上方而不擠
2097
+ 壓它。data-expanded 有三個來源:wave-panels.js 的 section() 點擊 title、
2098
+ 同檔案的 collapseBtn 反過來移除它、以及 fix round 3 新增的
2099
+ wave-panels.js 的 expandSide()(由 wave-ui.js 的 render() 在
2100
+ selectedEdge 從 null 變成有值的那一拍呼叫——選取一條 edge 之後,
2101
+ 整個回饋只長在這個側欄的關聯線面板裡,窄視窗下不展開就等於使用者
2102
+ 什麼都看不到、也按不到)。 */
2103
+ /* v3.6.0 Task 13 fix round 1: the collapse-back button. Hidden everywhere
2104
+ except the one state it exists for — the <1100px rail, expanded — so it
2105
+ never shows, and never sits in the tab order, at a wide viewport or in
2106
+ the rail's own default collapsed state. */
2107
+ .ed-wave-side-collapse { display: none; }
2108
+ @media (max-width: 1100px) {
2109
+ .ed-wave-side { flex: 0 0 28px; padding: 4px 2px; }
2110
+ .ed-wave-side:not([data-expanded]) .ed-wave-section-body { display: none; }
2111
+ .ed-wave-side[data-expanded] {
2112
+ position: absolute; right: 0; top: 0; bottom: 0; width: 340px;
2113
+ background: #ffffff; z-index: 3; flex: 0 0 340px; padding: 8px;
2114
+ }
2115
+ .ed-wave-side[data-expanded] .ed-wave-side-collapse {
2116
+ display: block; width: 100%; text-align: right; border: 0;
2117
+ background: transparent; font-size: 12px; color: #555; cursor: pointer;
2118
+ padding: 2px 4px 6px;
2119
+ }
2120
+ }
1905
2121
  .ed-wave-status {
1906
2122
  border-top: 1px solid #e2e2e2; padding: 6px 12px; font-size: 12px;
1907
2123
  color: #444; min-height: 18px;
@@ -2532,6 +2748,32 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
2532
2748
  height: auto;
2533
2749
  margin: 0 auto;
2534
2750
  }
2751
+ /* A diagram wide enough that fitting it makes its own labels unreadable is
2752
+ drawn at natural size in a scroll box instead. The verdict is never "is
2753
+ this a waveform" -- it is "how small does fitting make the smallest
2754
+ glyph", because the damage is proportional to the aspect ratio. MEASURED,
2755
+ 918px column: an 8-node flowchart fits at 0.71 and still draws 11.4px
2756
+ labels, while a 60-cycle waveform fits at 0.37 and draws 5.4px ones.
2757
+ applyDiagramScale() in the reader runtime below measures that and is the
2758
+ only thing that adds this class. The margin override is there because the
2759
+ ".content svg" rule centres with auto margins, which has nothing to
2760
+ centre once the svg is wider than the box.
2761
+ Source order is load-bearing: ".diagram-scroll > svg" and the
2762
+ ".content svg" rule above are both (0,1,1), so this wins only by coming
2763
+ after it. The child combinator matches the JS, which looks for the host's
2764
+ own ":scope > svg" -- one rule, stated twice, deliberately identically.
2765
+ One thing this rule does that it does not say: setting ONE axis to
2766
+ something other than visible promotes the OTHER axis to auto as well, per
2767
+ the CSS overflow spec, so every host here is a clipping block formatting
2768
+ context in BOTH directions. MEASURED on the three-engine fixture: all
2769
+ three scrolling hosts report a computed overflow-y of "auto" although only
2770
+ overflow-x is written, and all three have scrollHeight === clientHeight,
2771
+ so nothing is being clipped today. Recorded because a diagram that one day
2772
+ overflows VERTICALLY inside its host would be silently cut rather than
2773
+ overhanging, and neither this rule nor the tests would say why.
2774
+ (No backticks in this comment: it lives inside a JS template literal.) */
2775
+ .diagram-scroll { overflow-x: auto; }
2776
+ .diagram-scroll > svg { max-width: none; margin: 0; }
2535
2777
  /* Anything the reader can pop out. The click handler uses the same
2536
2778
  selectors, so this cursor and that behaviour cannot drift apart. */
2537
2779
  .content img,
@@ -2723,6 +2965,20 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
2723
2965
  }
2724
2966
  @media print {
2725
2967
  body { font-size: 11pt; }
2968
+ /* No scrollbars in a PDF, so an unscaled diagram there is not a
2969
+ scrollable one -- it is a clipped one, and half a waveform is worse
2970
+ than a small one. Hand it back to shrink-to-fit. Same (0,1,1) tie as
2971
+ the base rule, won the same way: source order.
2972
+ The !important is not decoration and was MEASURED: mermaid writes its
2973
+ own style="max-width: <natural>px" onto the svg element, and an inline
2974
+ declaration beats any rule in here regardless of specificity, so the
2975
+ plain version of this line clamped nothing and the first PDF built from
2976
+ it came out with a 16-stage flowchart cut off after Stage 3.
2977
+ applyDiagramScale's inline width is clamped by the same declaration --
2978
+ a max-width always beats a width -- so the runtime needs no print
2979
+ branch of its own. */
2980
+ .diagram-scroll { overflow-x: visible; }
2981
+ .diagram-scroll > svg { max-width: 100% !important; height: auto; }
2726
2982
  .page-layout {
2727
2983
  display: block;
2728
2984
  max-width: 100%;
@@ -4258,9 +4514,223 @@ ${usesDrawioMultiPage ? ` // v3.4.0 batch2 Task 6: wired through a NAMED, reusa
4258
4514
  ` : ''}
4259
4515
  // ── Diagram / image lightbox ──────────────────────────────────────────────
4260
4516
  // Specs are read at 100% but their block diagrams and waveforms are drawn far
4261
- // wider than the column, so the inline copy is unreadably small. Clicking one
4262
- // pops it into a modal stage that zooms and scrolls.
4517
+ // wider than the column, so the inline copy is shrunk to fit. Clicking one
4518
+ // pops it into a modal stage that zooms and scrolls. Past a point shrinking
4519
+ // stops being an acceptable inline copy at all -- that floor, and what the
4520
+ // inline copy does instead below it, is the block right after this one, and
4521
+ // the two share LIGHTBOX_TARGETS so they can never disagree about what
4522
+ // counts as a diagram.
4263
4523
  var LIGHTBOX_TARGETS = 'img, .mermaid, .graphviz, .drawio, [id^="WaveDrom_Display_"], .wavedrom-diagram';
4524
+
4525
+ // ── Readability floor ─────────────────────────────────────────────────────
4526
+ //
4527
+ // The lightbox above is the escape hatch for a diagram drawn wider than the
4528
+ // column. This is the other half: the inline copy has to be worth reading in
4529
+ // the first place. ".content svg { max-width: 100% }" shrinks to fit and
4530
+ // never looks at how small it is making the text, and the damage grows with
4531
+ // the aspect ratio, so past some width the reader is handed a picture of a
4532
+ // waveform rather than a waveform.
4533
+ //
4534
+ // MEASURED on this branch, 1280x900 viewport, .content clientWidth 918, the
4535
+ // waveform skin's own labels at 11pt = 14.667px: a 60-cycle waveform is
4536
+ // 2480px natural, fits at scale 0.370, and its labels land at 5.43px, where
4537
+ // "clk" and "data" are smudges. The identical expression leaves an 8-node
4538
+ // flowchart alone (1287px natural, fits at 0.714, labels at 11.41px), which
4539
+ // is the whole point -- the trigger is the measured glyph size, not which
4540
+ // engine drew the diagram.
4541
+ //
4542
+ // This block ships in EVERY document, including diagram-free ones, and
4543
+ // test/md2doc.test.js asserts that such a document carries no diagram-engine
4544
+ // runtime. So engines may be named here in prose only, never by the global,
4545
+ // skin or probe identifiers the engines themselves define -- that guard
4546
+ // greps for those, and it cannot tell a comment from code.
4547
+ //
4548
+ // The floor was measured, not estimated. Eight widths were rendered and
4549
+ // screenshotted one per row at deviceScaleFactor 1 and read: 9.35px was the
4550
+ // last comfortable row, 8.86px was where lowercase labels start breaking up.
4551
+ // The full table lives in test/reader-panels.test.js section 5, beside the
4552
+ // assertions that bracket this number from both sides.
4553
+ var DIAGRAM_FONT_FLOOR = 9;
4554
+
4555
+ // The width the diagram was drawn at, in the SAME units diagramMinFontPx
4556
+ // reports -- the svg's own user units. The pairing is the whole point: the
4557
+ // rendered size of a glyph is its computed font-size times (rendered width /
4558
+ // this), so the two must not be measured in different coordinate systems.
4559
+ //
4560
+ // That is why the viewBox comes first and the width attribute is only the
4561
+ // fallback for an svg that has none (where user units are CSS px anyway).
4562
+ // MEASURED, three engines, three different width attributes for the same
4563
+ // idea: the waveform engine writes width="2480" (equal to its viewBox, so
4564
+ // the order does not matter), mermaid writes width="100%" and keeps the real
4565
+ // number in the viewBox ("0 0 6838.25 70" for a 40-node flowchart), and the
4566
+ // graph engine writes width="4069pt" against a 4069-unit viewBox. Reading
4567
+ // the attribute first gives 100 for every mermaid diagram -- which reads as
4568
+ // "already fits", so the floor silently never applies to any of them -- and
4569
+ // mixes points into a ratio of pixels for the graph one.
4570
+ function diagramNaturalWidth(svg) {
4571
+ var viewBox = svg.getAttribute('viewBox');
4572
+ if (viewBox) {
4573
+ // The doubled backslashes are not a typo and node --check will not miss
4574
+ // them for you: this whole runtime is emitted from a JS template
4575
+ // literal, where "\\s" is an unrecognised escape that collapses to a
4576
+ // bare "s". This split really did ship as /[s,]+/ once, which parsed
4577
+ // "0 0 6838.25 70" as a single token, returned 0, and turned the
4578
+ // non-waveform half of this feature into a silent no-op. Precedent: the
4579
+ // normaliseText helper above spells the same thing /\\s+/g.
4580
+ var vw = parseFloat(viewBox.split(/[\\s,]+/)[2]);
4581
+ if (vw > 0) return vw;
4582
+ }
4583
+ var attr = svg.getAttribute('width');
4584
+ if (attr && /^\\s*[0-9.]+(px)?\\s*$/.test(attr)) {
4585
+ var px = parseFloat(attr);
4586
+ if (px > 0) return px;
4587
+ }
4588
+ return 0;
4589
+ }
4590
+
4591
+ // The smallest glyph the diagram draws, in those same user units -- an svg's
4592
+ // computed font-size does not move when its viewBox is scaled, so the size
4593
+ // on screen is this times the fit scale.
4594
+ //
4595
+ // Also MEASURED rather than assumed: mermaid's default label is not an svg
4596
+ // <text> at all, it is an html <span> inside a <foreignObject> (79 of them
4597
+ // in the flowchart above, and zero <text> nodes), so a text-only query
4598
+ // returns Infinity for a mermaid diagram and hands it the same free pass.
4599
+ // Only a node that really carries a text node is counted; the font-size of
4600
+ // an empty wrapper is a number nobody can see.
4601
+ function diagramMinFontPx(svg) {
4602
+ var min = Infinity;
4603
+ var nodes = svg.querySelectorAll('text, tspan, foreignObject *');
4604
+ for (var i = 0; i < nodes.length; i++) {
4605
+ var node = nodes[i];
4606
+ var carriesText = false;
4607
+ for (var child = node.firstChild; child; child = child.nextSibling) {
4608
+ // .trim() rather than a /\\S/ test: one less escape to get wrong in a
4609
+ // template literal, and it says the same thing.
4610
+ if (child.nodeType === 3 && child.nodeValue.trim()) { carriesText = true; break; }
4611
+ }
4612
+ if (!carriesText) continue;
4613
+ var size = parseFloat(window.getComputedStyle(node).fontSize);
4614
+ if (isFinite(size) && size > 0 && size < min) min = size;
4615
+ }
4616
+ return min;
4617
+ }
4618
+
4619
+ function applyDiagramScale() {
4620
+ var nodes = document.querySelectorAll(LIGHTBOX_TARGETS);
4621
+ for (var i = 0; i < nodes.length; i++) {
4622
+ var host = nodes[i];
4623
+ // ":scope > svg" is the same relationship ".diagram-scroll > svg" in the
4624
+ // stylesheet asks for -- one rule, written twice, deliberately
4625
+ // identically. An <img> target has no svg and no text to measure, and
4626
+ // neither do the empty duplicate display divs the waveform engine's
4627
+ // repeated render passes leave behind.
4628
+ var svg = host.querySelector ? host.querySelector(':scope > svg') : null;
4629
+ if (!svg) continue;
4630
+ var natural = diagramNaturalWidth(svg);
4631
+ if (!natural) continue;
4632
+ // Always measure the FITTED geometry, never geometry a previous pass
4633
+ // produced. Clearing the inline width is half of that and matters on its
4634
+ // own: a mermaid svg is width="100%", so lifting max-width would leave it
4635
+ // exactly as wide as the column it is supposed to be escaping.
4636
+ host.classList.remove('diagram-scroll');
4637
+ svg.style.width = '';
4638
+ var avail = host.clientWidth;
4639
+ if (!avail) continue;
4640
+ var scale = avail / natural;
4641
+ if (scale >= 1) continue;
4642
+ var minFont = diagramMinFontPx(svg);
4643
+ if (!isFinite(minFont)) continue;
4644
+ if (minFont * scale >= DIAGRAM_FONT_FLOOR) continue;
4645
+ host.classList.add('diagram-scroll');
4646
+ // max-width alone is enough for a diagram that states its width in px
4647
+ // and not enough for one that states it in percent, so both get the
4648
+ // number explicitly. @media print is unaffected: a max-width always
4649
+ // beats a width, which is what lets the print rule hand these back to
4650
+ // shrink-to-fit without this line having to know about it.
4651
+ svg.style.width = natural + 'px';
4652
+ }
4653
+ }
4654
+
4655
+ // When to re-run. Two inputs move after this script executes: whether the
4656
+ // diagram exists yet, and how wide the column is.
4657
+ //
4658
+ // The waveform engine's own render pass is scheduled four times
4659
+ // (DOMContentLoaded, load, +250ms, +1000ms) and mermaid renders on its own
4660
+ // clock, so an svg can appear long after this line. The MutationObserver is
4661
+ // deliberately not a copy of those timings -- a copy goes stale the first
4662
+ // time either engine changes its schedule, and silently: a pass that runs
4663
+ // before the svg exists looks exactly like a pass that decided to do
4664
+ // nothing. It only wakes for an
4665
+ // insertion that actually brought an svg, so edit-mode typing does not pay
4666
+ // for it, and it cannot self-trigger because adding the class is an
4667
+ // attribute mutation, not a childList one.
4668
+ //
4669
+ // The column width moves on a window resize, on crossing the 1080px drawer
4670
+ // breakpoint and on a sidebar-splitter drag. A ResizeObserver on .content
4671
+ // sees all three; a window resize listener sees only the first, which is why
4672
+ // there is no hand-placed call at the end of the sidebar handlers. Toggling
4673
+ // the class changes the host's HEIGHT (a scrollbar appears) and that feeds
4674
+ // back into the same observer, so a pass only runs when the width moved.
4675
+ var diagramScaleFrame = 0;
4676
+ function scheduleDiagramScale() {
4677
+ if (diagramScaleFrame) return;
4678
+ diagramScaleFrame = window.requestAnimationFrame(function () {
4679
+ diagramScaleFrame = 0;
4680
+ applyDiagramScale();
4681
+ });
4682
+ }
4683
+ applyDiagramScale();
4684
+ window.addEventListener('DOMContentLoaded', scheduleDiagramScale);
4685
+ window.addEventListener('load', scheduleDiagramScale);
4686
+ if (contentEl && typeof MutationObserver === 'function') {
4687
+ new MutationObserver(function (records) {
4688
+ for (var r = 0; r < records.length; r++) {
4689
+ var added = records[r].addedNodes;
4690
+ for (var a = 0; a < added.length; a++) {
4691
+ var node = added[a];
4692
+ if (!node || node.nodeType !== 1) continue;
4693
+ if (node.tagName.toLowerCase() === 'svg' ||
4694
+ (node.querySelector && node.querySelector('svg'))) {
4695
+ scheduleDiagramScale();
4696
+ return;
4697
+ }
4698
+ }
4699
+ }
4700
+ }).observe(contentEl, { childList: true, subtree: true });
4701
+ }
4702
+ if (contentEl && typeof ResizeObserver === 'function') {
4703
+ // KNOWN RISK, and honestly not measurable in this environment.
4704
+ //
4705
+ // On a platform with CLASSIC (space-taking) scrollbars rather than overlay
4706
+ // ones, adding the class makes the page taller -- the svg goes to natural
4707
+ // size and keeps height: auto -- and a page that gains a vertical
4708
+ // scrollbar loses about 15px of column width to it. That re-runs the
4709
+ // verdict at a narrower width, which can flip a diagram sitting close to
4710
+ // the floor back the other way, which makes the page shorter again. The
4711
+ // guard below is "the width changed", and the width is exactly what moves
4712
+ // in that loop, so the guard does not damp it.
4713
+ //
4714
+ // Not reproducible here: this environment only ever produces overlay
4715
+ // scrollbars (clientHeight === offsetHeight on a scrolling host, and
4716
+ // launching with the overlay-scrollbar features disabled changes nothing),
4717
+ // so neither the oscillation nor a fix for it can be observed -- and an
4718
+ // unmeasurable fix is how a worse bug gets shipped. Left alone on purpose.
4719
+ // The worst case is a busy loop rather than a hang, because
4720
+ // scheduleDiagramScale coalesces onto one animation frame. If it is ever
4721
+ // reproduced, the damping to reach for is hysteresis on the decision (two
4722
+ // thresholds instead of one), not a looser equality test on the width.
4723
+ var lastDiagramColW = -1;
4724
+ new ResizeObserver(function () {
4725
+ var w = contentEl.clientWidth;
4726
+ if (w === lastDiagramColW) return;
4727
+ lastDiagramColW = w;
4728
+ scheduleDiagramScale();
4729
+ }).observe(contentEl);
4730
+ } else {
4731
+ window.addEventListener('resize', scheduleDiagramScale);
4732
+ }
4733
+
4264
4734
  var LIGHTBOX_MIN_ZOOM = 0.05;
4265
4735
  var LIGHTBOX_MAX_ZOOM = 8;
4266
4736
  var LIGHTBOX_STEP = 1.25;