···55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7788+## [0.22.3] — 2026-04-04
99+1010+### Fixed
1111+- `setShapeRotation` now normalizes negative angles to 0-359 range (#351)
1212+- `duplicateShapes` deep-copies style and points (no shared references) (#351)
1313+- SVG/PNG export includes stroke dash patterns on all shape types (#351)
1414+- Arrow markers in export match arrow stroke color (per-color markers) (#351)
1515+- Arrow selection export includes arrows with at least one selected endpoint (#351)
1616+1717+### Added
1818+- 110 new polish tests covering export arrows, rotation, resize, groups, history edge cases
1919+820## [0.22.2] — 2026-04-03
9211022### Fixed
···11+# Diagrams Polish Report (Round 2)
22+33+## Test Report
44+- **Tests written**: 110 new in `tests/diagrams-polish.test.ts`
55+- **Tests passing**: 110/110
66+- **Full suite**: 5412/5412 (0 regressions)
77+- **Recommendation**: NEEDS WORK (3 bugs found, 2 known limitations documented)
88+99+---
1010+1111+## Polish Issues Found
1212+1313+### P2: `duplicateShapes` shallow-copies `style` object (BUG)
1414+- **File**: `src/diagrams/whiteboard.ts:804`
1515+- **Description**: `duplicateShapes` uses `{ ...shape }` spread to copy shapes, but this is a shallow copy. The `style` record object is shared by reference between the original and duplicate. Direct mutation of `shape.style` on either copy corrupts the other. The `points` array is also shared.
1616+- **Impact**: Low in practice because `setShapeStyle` always creates a new style object via spread. But copy/paste followed by direct `.style` mutation (e.g., from Yjs sync or manual state manipulation) would silently corrupt the other shape.
1717+- **Suggested fix**: Deep-copy style and points in `duplicateShapes`:
1818+ ```ts
1919+ shapes.set(newId, {
2020+ ...shape,
2121+ id: newId,
2222+ x: shape.x + offsetX,
2323+ y: shape.y + offsetY,
2424+ groupId: undefined,
2525+ style: { ...shape.style },
2626+ points: shape.points ? [...shape.points.map(p => ({ ...p }))] : undefined,
2727+ });
2828+ ```
2929+- **Test**: `diagrams-polish.test.ts` > "BUG: duplicated shapes share style reference"
3030+3131+### P2: `setShapeRotation` does not normalize negative values (BUG)
3232+- **File**: `src/diagrams/whiteboard.ts:710`
3333+- **Description**: `setShapeRotation` uses `rotation % 360` which produces negative results for negative input in JavaScript (e.g., `-90 % 360 = -90`). This is inconsistent with `rotateShape` (line 699) which correctly normalizes with `((val % 360) + 360) % 360`. During interactive rotation via mousemove, `setShapeRotation` is called with potentially negative angles, producing negative rotation values stored on the shape.
3434+- **Impact**: Negative rotation values work in SVG `rotate()` transforms, so rendering is correct. But it creates inconsistency: `rotateShape(-45)` gives 315, `setShapeRotation(-45)` gives -45.
3535+- **Suggested fix**: Use same normalization as `rotateShape`:
3636+ ```ts
3737+ shapes.set(shapeId, { ...shape, rotation: ((rotation % 360) + 360) % 360 });
3838+ ```
3939+- **Test**: `diagrams-polish.test.ts` > "setShapeRotation negative values"
4040+4141+### P2: Export SVG omits `stroke-dasharray` from shapes (KNOWN LIMITATION)
4242+- **File**: `src/diagrams/export.ts:112-253` (entire `renderShapeSvg` function)
4343+- **Description**: The `renderShapeSvg` function in export.ts reads `fill`, `stroke`, and `strokeWidth` from `shape.style` but never reads `strokeDasharray`. The UI (`main.ts`) supports dashed/dotted strokes via the style panel and correctly renders them on canvas using `shape.style.strokeDasharray`. But exported SVG always renders solid strokes.
4444+- **Impact**: Users who set dashed/dotted strokes will get solid strokes in their exported SVG/PNG files.
4545+- **Suggested fix**: In `renderShapeSvg`, read and apply the dash pattern:
4646+ ```ts
4747+ const strokeDasharray = shape.style.strokeDasharray || '';
4848+ // Then add to each element: ${strokeDasharray ? ` stroke-dasharray="${strokeDasharray}"` : ''}
4949+ ```
5050+- **Test**: `diagrams-polish.test.ts` > "does not export strokeDasharray from shape style (known limitation)"
5151+5252+### P3: Arrow marker color hardcoded in export (KNOWN LIMITATION)
5353+- **File**: `src/diagrams/export.ts:278-283`
5454+- **Description**: The `arrowMarkerDef()` function hardcodes `fill="${DEFAULT_STROKE}"` (#000000) for the arrowhead polygon. When an arrow has a custom stroke color (e.g., red), the line renders red but the arrowhead is always black.
5555+- **Impact**: Visual inconsistency in exported SVGs when arrows use non-black stroke colors.
5656+- **Suggested fix**: Generate per-arrow marker defs with unique IDs, or a single marker that inherits stroke color via `fill="currentColor"` or `fill="context-stroke"`.
5757+- **Test**: `diagrams-polish.test.ts` > "arrowhead marker uses default stroke color"
5858+5959+### P3: Freestanding arrows excluded from selection-filtered export (KNOWN LIMITATION)
6060+- **File**: `src/diagrams/export.ts:310-319`
6161+- **Description**: When `selectedIds` is provided to `exportToSVG`, the arrow filter only includes arrows where `arrowConnectsSelected` returns true (both endpoints must be selected shapes). Freestanding arrows (both endpoints are `{x,y}` coordinates) are always excluded because they have no `shapeId` to match.
6262+- **Impact**: If a user selects shapes and exports, any freestanding arrows drawn alongside those shapes are lost from the export.
6363+- **Test**: `diagrams-polish.test.ts` > "excludes freestanding arrows when selectedIds is provided"
6464+6565+---
6666+6767+## Test Coverage Gaps Filled
6868+6969+### Export (`export.ts`)
7070+- Arrow custom stroke color and strokeWidth rendering
7171+- Arrow default styles when no style specified
7272+- Arrow label rendering at midpoint with positioning
7373+- Arrow label + style combination
7474+- Arrowhead marker color behavior (documented limitation)
7575+- Freestanding arrow selection-filter behavior
7676+- Rotation + opacity combined on same shape
7777+- Grouped shapes export (groupId transparency)
7878+- Note shape default yellow fill vs custom fill
7979+- Text shape with custom fontFamily and fontSize
8080+- Non-text shape labels with custom font properties
8181+- ViewBox edge cases: zero-dimension, very large, negative coords, freehand overflow
8282+- XML escaping: `<`, `>`, `&`, `"`, `'` in shape and arrow labels
8383+- All 13 shape kinds produce valid SVG output (parametric test)
8484+- Line shape edge cases: no points, single point, with points
8585+- Freehand shape rendering with Catmull-Rom curves
8686+- Cylinder with rotation transform
8787+- Multiple shapes of same kind
8888+- Marker defs conditional inclusion (present with arrows, absent without)
8989+- Arrow with unresolvable endpoint shapes (graceful skip)
9090+- resolveEndpoint for all 5 anchor types (top, bottom, left, right, center)
9191+9292+### Whiteboard (`whiteboard.ts`)
9393+- `setShapeRotation` negative value behavior
9494+- `setShapeRotation` values above 360
9595+- `rotateShape` normalization confirmation
9696+- `duplicateShapes` shallow-copy bug documentation
9797+- `duplicateShapes` with non-existent IDs mixed in
9898+- `duplicateShapes` relative position preservation
9999+- `flipShapes` same-position edge case (no-op)
100100+- `flipShapes` axis isolation (vertical flip preserves x)
101101+- `alignShapes` already-aligned shapes
102102+- `alignShapes` with mixed valid/invalid IDs
103103+- `distributeShapes` overlapping shapes
104104+- `distributeShapes` vertically with varied sizes
105105+- `groupShapes` property preservation
106106+- `ungroupShapes` property preservation
107107+- `getGroupMembers` after ungroup returns empty
108108+- `applyResize` clamping for W, N handles with position correction
109109+- `applyResize` large expansion (no clamp)
110110+- `applyResize` zero delta identity
111111+- `getBoundingBox` single shape, negative coordinates
112112+- `nearestEdgeAnchor` center point, tall/wide rectangles
113113+- `shapesInRect` zero-area rect, touching-edge shapes
114114+- `removeShape` non-existent ID, cascade arrow cleanup
115115+- `removeArrow` non-existent ID
116116+- `pointsToCatmullRomPath` three points, collinear, duplicate points
117117+- `shapeAtPoint` z-order (topmost wins)
118118+- `moveShapes` isolation (unaffected shapes unchanged)
119119+- `setShapeStyle` overwrite and empty update
120120+- `setShapeFontSize` boundary at 8, large values
121121+- `setZoom` clamping at 0.1 and 5, exact boundaries, fractional
122122+- `pan` accumulation, very large values
123123+- `toggleSnap` round-trip
124124+- `addShape` / `addArrow` unique ID generation (100/50 items)
125125+126126+### History (`history.ts`)
127127+- Arrow style round-trip through undo/redo
128128+- Free-endpoint arrow preservation through undo/redo
129129+- Comprehensive shape with ALL optional properties (points, style with dasharray, opacity, rotation, fontFamily, fontSize, groupId)
130130+- Branching after multiple consecutive undos
131131+- Clear then push resumes normal operation
132132+133133+---
134134+135135+## Remaining Deferred Items
136136+137137+These require browser/DOM/E2E testing and cannot be unit tested:
138138+139139+1. **Inline text editing textarea positioning** -- foreignObject overlay alignment with rotated shapes
140140+2. **Cursor feedback accuracy** -- resize handle cursor changes, rotation grab cursor, eraser crosshair
141141+3. **Edge scrolling during drag** -- `startEdgeScroll`/`stopEdgeScroll` timing and speed
142142+4. **Snap guide rendering** -- visual alignment guides during shape drag
143143+5. **Creation preview shapes** -- ghost shape follows cursor during drag-to-create
144144+6. **Arrow hover target highlight** -- visual feedback when drawing arrow over potential target
145145+7. **Context menu positioning** -- off-screen adjustment logic
146146+8. **Touch pinch-to-zoom** -- two-finger gesture handling
147147+9. **Tool switching during active drawing** -- line/freehand interruption via keyboard shortcut
148148+10. **Focus mode** -- toolbar/panel hiding toggle
149149+11. **Keyboard shortcut conflicts** -- shortcut suppression during input focus and text editing
150150+12. **Style panel sync on selection change** -- color pickers, dropdowns reflect selected shape
151151+13. **Yjs sync round-trip** -- loadFromYjs/syncToYjs with encrypted provider
152152+14. **Highlighter stroke rendering** -- semi-transparent yellow wide stroke appearance
153153+15. **Grid pattern alignment with pan/zoom** -- grid pattern stays visually consistent