Compiled from the project's commit history and grouped into rough phases; entries are organized by theme rather than by release number.
Unreleased (post-4.1.0): Raster export paints connections above pasted images, and zone labels above connections - the order the canvas has always drawn them. A fibre run traced over a floor-plan image exported as the floor plan alone in PNG, JPEG and PDF; SVG and draw.io were fine. The rasterizer is the one export path that draws by hand, and it had a race: pasted images decode asynchronously, and it drew every connection synchronously before waiting for them, so each image landed on top of the lines crossing it - no matter the layer order, and no matter that the app's own comment says a floor plan MUST be crossed. Connections now draw after the images have landed, and zone labels after connections, matching the live layer stack (zones, images, connections, zone labels, devices). tools/tests.html pins it by pixel: a thick blue line across a solid red image must come out blue where they cross. Also worth knowing if you hit the snapping that led here: holding Ctrl (or Cmd) when you release a connection ends it as a free point instead of snapping to the nearest attachment point - image edges included - so the image layer need not be locked to draw over it.
Unreleased (post-4.1.0): Hit tolerance is a screen budget, and the nearest thing wins. Double-click targeting on densely packed connections was erratic, and two separate faults were doing it. The tolerances were raw CONTENT units - a 30 radius around an annotation, 15 perpendicular to a line - so they never shrank as you zoomed in. At 400% that 30 becomes 120 SCREEN pixels around a label a few pixels wide, which means zooming IN to click more precisely made the target relatively larger, not smaller: on six parallel fibre runs the neighbouring strand's annotation sat comfortably inside the target of the one being aimed at. Both are now quoted in screen pixels and divided by the zoom, so a hit target stays the size of a fingertip on the glass at any magnification. The second fault is why it felt random rather than merely loose: both searches took the FIRST candidate inside the tolerance and stopped, so among overlapping candidates the winner was decided by the order objects happen to sit in the file. Both now keep the closest. The double-click travel allowance was content units too, letting the two clicks land 20 screen pixels apart at 400% - enough to start on one strand and finish on its neighbour before any tolerance got a say. Annotations still outrank lines whenever one is in range, and that ordering is deliberate rather than a tie-break: an annotation sits ON its line, so the line is always nearer, and pure nearest-wins across both kinds would make editing an existing annotation impossible.
Unreleased (post-4.1.0): Clicking the canvas takes the keyboard back.
The properties panel is full of form controls - the AP slider, the label fields,
the dropdowns - and both the keydown and paste handlers open by returning when
the event target is one of them. So while any held focus, EVERY canvas shortcut
was dead: copy, paste, delete, undo. What made that read as a broken app rather
than as focus being somewhere reasonable is that nothing said so - the object
stayed drawn as selected and the panel kept showing its properties, so the app
looked responsive while ignoring the keyboard. Reported from the field exactly
that way ("Ctrl+V stopped working, only the right-click menu works") after a
stretch of dragging the AP slider and renaming zones, with a fresh tab working
fine - the signature of per-tab focus rather than anything about the build. A
canvas mousedown now blurs whatever is holding the keyboard, so the gesture
everyone already makes to get back to the diagram actually works, instead of
depending on the browser's own blur behaviour (which differs by engine, and did
not reproduce in Chromium at all). The same click also clears a STALE
inline-edit flag: state.inlineEditing makes the keydown handler return
unconditionally, whatever has focus, so a cleanup path that ever failed to
clear it left the tab ignoring every shortcut until a reload, with no way back.
Cleanup removes the editor from the document, so a flag whose element is no
longer connected is by definition a leftover. Only controls that actually block
the shortcuts are blurred, and never a LIVE inline editor - that one owns a blur
handler which commits the edit, so reaching for it here would commit twice.
Pinned by a suite that plants both mistakes: dropping the call from the
mousedown handler, and blurring unconditionally.
Unreleased (post-4.1.0): Add Space moves the whole diagram at once. A
diagram that grows by discovery keeps finding sections that belong above or left
of what is drawn, and the canvas only ever grows right and down - updateCanvasSize
pins the viewBox origin at 0 0, so a negative coordinate is simply off it. The
only way to open room on those sides was select-all and drag, a chore on a
sprawling document and one that can nudge a waypoint nobody meant to touch.
Edit > Add Space asks how far and performs the same whole-document translation
Merge already used, so manual bends travel correctly - a bend stores one number
whose axis is re-derived from the route rather than stored, so it has to move
along the axis its segment rides, not the one that happens to be handy. Imported
waypoints and free-floating ends move too, the view scrolls to follow so the
room simply appears (where the canvas overflows that axis, which is the only
place scrolling can help), and the whole shift is one undo step. Negative numbers
reclaim space and are REFUSED rather than clamped when they would push content
past the origin: quietly trimming the number somebody typed is how they stop
trusting the next one. Pinned by a suite that translates by an ASYMMETRIC 37x91
and asserts every routed polyline is the old one rigidly translated - under a
square shift a bend written to the wrong axis still lands on a plausible value,
and the round-trip check alone cannot see it either, because a consistently
wrong axis cancels itself out on the way back.
Unreleased (post-4.1.0): The minimap viewport follows the theme. Its
rectangle was a fixed blue in a 29-theme app; it now derives from --se-tint,
the same accent selection handles and attachment points already use. The raw
tint does not clear the 3:1 non-text contrast bar in every palette - measured
across all 29 in both modes, it bottoms out at 2.79 (Sakura on white) and 1.89
(Mono in dark mode, where an all-grey palette all but vanishes against the dark
canvas) - so it is nudged toward the surface's opposite, taking the worst cases
to 3.78 and 5.02 while keeping each theme's hue. Derived at paint time, so the
theme picker, the custom device-tint control and the dark-mode toggle all carry
it. Classic is visually unchanged. Exact stencil names now win imports - a
stencil column naming a real roster stencil ("Cloud Router", "ServerCluster")
used to lose to whichever substring relative (Cloud, Server) sat earlier in
the roster, because the fuzzy pass ran first. Both import resolvers now try
an exact, space-blind name match before any substring. Found by a downstream
consumer importing our own stencil vocabulary back at us and reading what it
became. Connection Labels joins Default Settings - a
per-board choice of how connection labels and annotations dress. The classic
translucent white box stays the default (old boards render byte-identically);
Rounded chip keeps the box but rounds it honestly and lets the sides
breathe, matching the kiosk's own latency and bandwidth chips; Halo only
drops the box entirely and gives the text the same background-color stroke
device labels get, so it sits directly on the artwork and stays readable
where its own line runs behind it (obeys Label Halos; travels in the file
for kiosk parity, and the raster export carries all three styles). Which
reads best depends on the layout - that is why it is a choice, not a new
default.
v4.1.0 (tagged August 2026) is the readability-and-routing arc, born from studying how each new AI model generation attacks the same "build a network diagram editor" prompt and adopting the mechanisms that fit - then testing against real boards. Connections got smarter without getting louder: auto-routes now step around devices that sit in their way (candidate scoring, not pathfinding - hand-shaped routes are never second-guessed, and a tunable Route Clearance travels with the board); line jumps hop the top connection over crossings at any angle (a board setting, on for new boards, never retrofitted onto boards saved before it - taps that end ON a line stay junctions); and endpoints can now float: drop a connection on a device body and it re-lands on the facing side whenever anything moves, while attachment-dot drops stay pinned exactly as before. Text got legible everywhere: every label strokes a halo in its background color, so lines passing behind text cut around the glyphs (scoped per board - all labels, devices & text only, or off), and zone titles now paint above connections. Navigation and capacity: a live minimap (on by default) that can never lose your viewport, and attachment-point counts that scale with object size - up to 64 on large zones and images for patch-panel-grade landings. Imports got more native: hand-routed Gliffy/draw.io connector paths convert to real routed connections wherever the drawn shape is exactly reproducible (verified per connection by a dry-run through the router), so they keep their shape and follow device moves; Recolor to Theme learned to keep meaningful colors - legend reds survive while grey default ink follows the theme. Under it all, 100+ new harness checks (230 total).
v4.0.0 (tagged July 2026) is the public-debut arc: the project becomes
CrossCanvas (and its monitoring sibling PingCanvas), with the
.xcanvas extension and permanent legacy support - .netdraw files open
forever, the window.NetDraw embed hook and NETDRAW_EMBED flag still work,
and saved netdraw-* preferences migrate automatically. The material theme
family arrives (Canvas, Blueprint, Ink, and Gesso - named for what diagrams
are made of) alongside an alphabetized picker and theme-aware brand colors;
Classic remains the fresh-install default. The mark
family becomes the easel of zones: a favicon and menu-bar logo showing a
six-zone board on an easel (the menu-bar version recolors with the active
theme). The stencil library grows from 24 to 55 categorized stencils
(Affinity expansion: Security, Telecom, and General join the existing
groups), the Devices palette gains collapsible category groups, the Icon
dropdowns gain matching optgroups, and the import stencil maps pick up the
new vocabulary - hubs, multilayer switches, wireless controllers, LDAP
directories, and more now land on dedicated icons.
v3.1.0 (tagged July 2026) covers the arc after v3.0.0: the draw.io importer (compressed + raw saves, exact anchors, multi-page picker, and lossless round-trip of CrossCanvas's own exports - Device Details included) proven against four corpora; the batch converter (File → Convert Diagrams…) for Gliffy and draw.io with an optional per-file Recolor All to Theme; finer 10px snapping; connect-mode click-vs-drag; menu-clarity renames (Open / Import Diagram…, Bulk Actions); a third review pass (export grid density, batch autosave, localized/dedup-hardened inventory text parsers, Kea append-log folding); The Unlicense; Affinity icon-set credits; and the in-file table of contents + "Why one file?" rationale.
v2.0.0 (tagged July 2026) covers the arc after v1.0.0: the .xcanvas
format with slimmed saves (v6: @name library refs + deduplicated image
table) and the Save with Embedded Images escape hatch, the team stencil layer
(customdevices.js) with Import/Export Device Library, three Visio import
quality passes against ground-truth corpora, the generic Template icon for
unmatched imports, canvas tier visibility/lock, the stacking overhaul,
compact Font blocks with horizontal alignment and font families, native
<dialog> modals, the consolidated Default Settings panel, and the
post-review correctness/performance/housekeeping passes.
v1.0.0 (tagged July 2026) was the first tagged release and covers everything below not listed above.
-
The Complex sample gained a Rack UPS. It is the one device the rest of the suite's demo estate already assumed:
ups-01appears in SNMPCanvas's device list and is three rows of AlertCanvas's Watching view (battery, runtime, on-mains), but the sample had no UPS, so the diagram and the wall showed boards that differed by a device. Placed beside the virtualization cluster with no network link drawn - which is how most people diagram one, it belongs to the power side of the picture - but carrying an address, because a managed UPS is among the more useful things to monitor. -
The built-in samples now carry hidden addressing. Every device in the Simple and Complex samples gained
HostnameandIP-AddressDevice Details fields on one coherent scheme (10.20.0.x HQ core, .10.x servers, .20.x users, .21.x/.22.x branches, .30.x cloud). Nothing about either diagram changed - no label, colour, zone tint or coordinate - because Details fields are never drawn on the canvas. What it buys: the samples now export a real inventory CSV, Ctrl+F finds a device by address, and the Complex sample can be taken straight into PingCanvas or SNMPCanvas the way the quickstart describes. Internet, Remote Staff and Satellite are deliberately left blank - a cloud, a roaming person and a bird are not things you poll. -
The role-label-plus-hidden-field split is also a sanitization property worth knowing about: a diagram of a sensitive estate can be screen-shared showing nothing but roles ("Core Switch", "Branch 1 Router") while the real names and addresses sit in fields nobody on the call sees.
-
tests.htmlmoved totools/tests.html. It sat directly besideindex.htmlin a repo that tracks thirteen files, so someone arriving for a diagram editor had to work out which HTML file was the product. It was never served to users - it is absent from the IIS package and the PingCanvas web build - so this only changes the repo's front door. The suite itself is unchanged and still runs the same way, now from one directory down. -
Initial editor - SVG canvas with grid, device/zone/connection management, drag-and-drop with grid snap, attachment points, orthogonal routing, multi-select/drag/copy-paste, resize handles, labels with font/color controls, connection labels, auto tool-switching, JSON save/load, JPEG export.
-
Finer 10px snapping - the snap step halved from 20px (matching the draw.io/Visio norm) so imported Gliffy/Visio positions barely move on first touch; the visible grid stays 20px, so objects snap to half-lines Visio-style without a busier canvas. Alt fine-snap and Shift-arrow nudge stay 5px. Alignment guides already carry the "line things up" job a coarse grid used to.
-
Undo/redo - state snapshots before each mutation (devices, connections, zones, nextId); pre-drag snapshots committed only on real change; 500ms debounce on text/color inputs; 50-level stack with toolbar buttons and keyboard shortcuts.
-
Dynamic scrollbars - canvas auto-expands as objects near the edges; scrollbars appear only when content exceeds the viewport.
-
Prevent text selection on canvas during drags.
- Draggable bend handles on orthogonal/rounded connections; generalized to every inner segment (keyed by segment index); bends persist through save/load/export and snap back to natural position to remove themselves.
- Bend dragging snaps relative to the natural rail - rails sit off-grid by the 30px attachment stub, so absolute grid snapping could never land back on the natural position and the release cleanup swallowed the first grid stop on either side ("drag one interval, it snaps back"). Offsets now step from the rail itself, one-interval moves stick, and dropping the handle back on its natural spot removes the bend exactly.
- Removed obstacle-avoidance routing that could infinite-loop and lock the page; connections now route directly.
- Rounded routing fixes - strip duplicate/collinear waypoints that produced NaN paths; new connections default to rounded.
- Arrow heads - independent start/end markers (arrow, open arrow, diamond, circle), lazily created per color; fixed markers being hidden behind devices (userSpaceOnUse, endpoint shortening, refX=0); settled on 14px size after iteration.
- Arrow overhaul - wider equilateral arrowheads that scale with line thickness by default (22px at the default 4px thickness) plus a per-connection Arrow Size setting (Small/Normal/Large/XL, also in Batch Edit); endpoint trimming walks the whole polyline so short end segments no longer leave the marker overshooting into the device (markers shrink to fit cramped connections); arrowheads are now drawn in PNG/JPEG/PDF exports, which previously omitted them entirely.
- Imported routes convert to native connections when lossless - after a Gliffy import, each hand-routed (waypoint) line is test-fitted against the app's natural route plus per-segment bends and converted only when the route's shape matches the natural template exactly (same segment count and orientations, and the result reproduces the identical polyline - verified through the router itself, so conversion can never change the drawn path). Converted connections behave like hand-drawn ones: bend handles on every inner segment, orthogonal re-routing when a device moves, bends that snap away when returned to the natural rail. Routes that don't fit (different corner topology, extra detours, non-orthogonal hand paths) keep their exact waypoints and waypoint handles - the honest representation. An earlier version force-fitted shorter routes onto longer templates by collapsing leftover segments to zero length: the path drew correctly, but the model carried phantom segments - stacked bend handles on one corner, and hidden zero-length legs that unfolded into jogs when dragged. Also fixed a latent bend bug the converter exposed: adjacent bends could misread a collapsed segment's orientation and apply an x value to the y axis - orientations now come from the pristine natural route.
- Waypoint drag handles - connections carrying imported (or otherwise explicit) waypoints show a square handle on every waypoint when selected; drag to reroute with fine-grid snapping. Previously imported bends were frozen - the only fix was deleting and redrawing the line.
- Inline connection annotations - double-click anywhere on a line to drop editable text; drag annotations along the path (projected via
getNearestT). - Annotation hit-testing fixes - short connectors between stacked devices and free-ended (device-less) lines are now annotatable on double-click; previously the device label-padding or a missing-endpoint guard swallowed the click.
- Label-aware double-click dispatch - when a connection line is under the cursor, a device's label padding or a zone's area only claims the double-click if it lands on the object's rendered label text; otherwise the line stays annotatable. Fixes vertical connections near their endpoints (which exit through the label-padding band) and connections crossing zones (which previously required moving the zone away to annotate). New annotations also clamp off the exact endpoints (2-98% along the path), matching the drag clamp.
- Routing determinism fix - resolve endpoints from the connection itself, not transient state; translate manual bends when both endpoints move together.
- Draggable connection endpoints - drag an end to re-attach it to any attachment point.
- Free-floating connection endpoints - ends can anchor to a node or a free point in empty space; draw from/to empty canvas; detach/reattach by dragging; resolved uniformly via
resolveConnEndpoint. - Close-proximity routing fixes - facing attachment points closer than the two 30px exit stubs no longer overshoot past each other and double back through both devices (single mid-gap rail; straight when aligned - also removes the rounded-mode curl artifacts); same-direction exits on a shared axis now bulge around the side instead of running through the far endpoint. Routes at normal spacing are unchanged.
- Drop-on-body connection targeting - ending a connection drag (or an endpoint re-attach drag) on a node's body attaches to its nearest attachment point; no need to land exactly on the small AP dots.
- Corner attachment points exit toward the target - corner APs previously always launched vertically; they now pick the outward axis (horizontal or vertical) that faces the other endpoint.
- Fixed: raster exports drew a 10px grid - halving the snap step (GRID_SIZE) silently doubled the grid density in PNG/JPEG/PDF exports; they now use a dedicated
VISIBLE_GRID(20px) matching the canvas and SVG export. Batch-Edit's typed W/H floor had the same fallout (dropped to 10) and is back at the 20px minimum every other path enforces. - Fixed: batch conversion left a bogus autosave - every import arms the 4s autosave debounce, and finishing the batch didn't cancel it, so the next launch offered to "Restore unsaved work" that was already converted to disk. The batch now clears dirty+autosave exactly when the board on canvas was successfully written (a failed write still keeps the protective autosave).
- Fixed:
arp -aimport on non-English Windows - the Type column word (динамический, 动态) is matched as\S+; JS\wis ASCII-only, so those locales previously fell through to a confusing CSV error. - Text parsers dedupe by IP - multi-interface
arp -adumps (Wi-Fi + Ethernet, vSwitches) no longer import the same host twice; repeats count as skipped. Octet-range validation added (999.888.777.666 can't become a device); Ciscoshow ip arpaccepts SNAP/SAP encapsulation rows. - Kea leases: fold the append-log -
kea-leases4.csvappends a row per renewal and avalid_lifetime=0row on release; rows are now folded newest-wins per address before filtering, so a live (uncompacted) file imports one device per host and released leases stay out. - dnsmasq dual-stack files import - the "most lines must be leases" check now judges only v4-shaped lines, so IPv6 leases and the
duidline no longer outvote perfectly good v4 leases. - "Keyboard Matrix" versions again - the stable-filename heuristic for boards matches the standalone word or known compounds (whiteboard, dashboard, NOC/status/wall board) instead of any title containing the substring "board".
- Fixed: resizing tore imported connections off their anchors - resize regenerated a node's attachment points as an even distribution, replacing the exact-anchor APs injected by the importers (connections jumped to wrong spots on the first resize). Attachment points now scale with the node - identical results for standard layouts (the corner-anchored walk is proportional), and imported contact fractions survive; the per-node AP slider remains the explicit regenerate path. Applies to device/zone/image resize, the W×H inputs, and Batch size changes.
- Fixed: exports faded zone borders - the canvas fades only a zone's fill at its opacity, but raster exports applied the opacity to fill and border alike, so the newly common low-opacity imported zones (Azure region bands) exported with near-invisible outlines. Export now scopes the alpha to the fill.
- Load Complex Sample (File menu) - a second built-in showcase: multi-site enterprise network with an HQ campus (VLAN zones for servers, users, voice/IoT), two branch offices, a cloud VPC, WAN/VPN links with colored dashed lines and labels, a wireless hop, a non-square device, and a per-span-colored legend. Constructed at runtime from the stencil library like the small sample (no embedded images, works on file://).
- Switch to Select on blank-canvas click in connect mode; resize handles offset off the corner attachment point.
- Auto-select objects after drop/paste; auto-select connections after placement.
- Include connections in copy/paste when both endpoints are selected (with ID remapping).
- Ctrl/Shift-click multi-selection.
- New Diagram button with save prompt; later changed to an unsaved-changes discard warning matching Load.
- Diagram Title/Version fields - title drives the save filename, version auto-increments and is appended to the filename; unsaved-changes warning on load.
.xcanvasfile extension - saved diagrams use a dedicated extension (content remains plain JSON, like.gliffy); legacy.jsonfiles load unchanged. Saved files carrysavedAtandappVersionprovenance stamps. On load, the filename wins over the embedded title (mirroring how Save back-parses the chosen name), so files renamed in the OS keep their new names.- File → Save with Embedded Images - self-contained archival save: library icons embed (deduplicated) instead of saving as
@namereferences, so the file opens with full icons on any CrossCanvas instance regardless of its stencil library. Larger (a 14-device test file: 291 KB vs 20 KB), same format, loads identically. - Saved files stop embedding library icons (format v6) - every device used to carry its icon as base64 twice, so 30 identical switches stored the same icon 60 times. Icons that came from the stencil library (devices.js / customdevices.js, loaded fresh every startup) now save as
@<name>references and aren't embedded at all; everything else (personal imports, tinted icons, pasted images) content-deduplicates into animageTablewith#keyreferences. A 14-device test file shrank from 1.26 MB to 10 KB; the localStorage autosave (5 MB browser cap) shrinks equally. Older files load unchanged. A@reference whose library entry is missing (e.g. a file from a site with a different team layer) degrades to the generic template icon - readable and fixable via the Icon dropdown. - Imports use a generic Template icon for unmatched stencils - instead of per-type gray dashed placeholders that also joined the device palette (and got saved into diagrams as
Gliffy_*/Visio_*templates), unknown types now render with a clean framed blank matching the bundled set's look, carried inline on the device only. The palette stays untouched; the import summary still names the unmatched types and points at the Icon dropdown for swapping. - Fixed: native Save dropped
groups- Save had a private copy of the serializer that missed the groups field (autosave had it); Save now delegates to the singleserializeDiagramso the payloads can never drift again. - Selection and property panels survive undo/redo - the restore path re-selects the previously selected object (when it still exists) through the normal selection functions, so an open panel repopulates with the restored values instead of keeping stale pre-undo numbers; panels for objects removed by the undo now close (previously only the device and zone panels were hidden, and image/text/batch panels lingered).
- Canvas tier visibility & lock - the Layers menu gains an eye and padlock per render tier (Devices & Text, Connections, Images, Zones). Hiding a tier removes it from view, selection (marquee/Ctrl+A), connection targeting, alignment guides and raster exports; locking keeps it visible but inert, so clicks and marquees pass through - lock Zones to rubber-band devices inside a zone without grabbing the zone itself. Tier state is view-only: never saved, reset on load/new/import; creating an object in a hidden tier auto-shows it. Export draws devices and text boxes in their shared stacking order, matching the canvas.
- Load closes stale property panels - loading a diagram while an object was selected left that object's panel open with the old values; the load path now deselects through the normal path (same class of fix as the undo/redo panel refresh).
- Arrange/stacking overhaul - the per-panel "Layer N (front/back)" dropdown is removed (it renumbered itself as objects came and went and read as layers when it meant z-order; the four arrange verbs in the Layers menu/toolbar/context menu express the same intent better). Arrange now works on multi-selections, preserving the selection's relative order (front/back move the set as a block; forward/backward step each object past its unselected neighbor). Devices and text boxes now truly interleave in their shared render layer via a persisted per-object stacking key - previously all text boxes drew above all devices, so Bring to Front on a device under a text box silently did nothing. Untouched diagrams keep their historical stacking; the key materializes on first arrange and new objects land on top.
- Unsaved-changes indicator in the toolbar via a centralized
setDirty(). - Native save dialog (File System Access API) with download fallback and a dirty-flag tab-close warning; filename prompt; title synced from the name chosen in the Save dialog.
- Typeable device Size field (exact pixel sizes); +/- step buttons.
- Free device resize - devices resize like zones: five handles (corner plus four edges) stretch width and height independently, and the panel's Size field became editable W × H inputs (the × buttons still scale proportionally). Previously any resize snapped the device back to a square, mangling imported non-square devices and their connection landings; with app-drawn frames the stretched shapes look native.
- Selection indicator reworked for app-drawn frames - selecting a device used to restyle its border stroke, which visually erased the new stencil frame (glaring on scaled-up devices); selection now draws a separate dashed outline around the device and leaves the frame untouched.
- Compact standardized Font blocks - every property panel (device, zone, image, text box, connection, annotation) shares one layout: font size + color swatch on one row, segmented icon groups for horizontal and vertical alignment below, and a compact Position row. Replaces four full-width labeled dropdowns per panel (~200px → ~90px). The alignment segments are generated from one icon set at startup.
- Horizontal text alignment everywhere - devices, zones, images, connection labels and annotations gain multi-line justification (previously implied silently by the label position: side labels justified toward the node, the rest centered). The new control defaults to auto (the position-implied value, shown as the active segment), so existing diagrams render unchanged; clicking the active value returns to auto. Justification re-anchors lines inside the label block without moving the block. Text boxes' existing alignment moved into the same segmented control. Vertical alignment on text boxes deliberately omitted (they auto-size - nothing to align within).
- Export label drawing consolidated - devices, zones and images now share
drawObjLabelToCanvas(the device and zone export paths had duplicated it), and all export label paths honor the new justification. - Font family selection - every text-bearing object (devices, zones, images, text boxes, connection labels, annotations) can pick from a curated list of system font stacks (Segoe UI, Arial, Verdana, Georgia, Times New Roman, Consolas, Courier New; webfonts are out - the zero-outbound-network posture). The family follows the text into the inline canvas editor, SVG measurement, and PNG/JPEG/PDF exports; each dropdown option previews its own face. Also in Batch Edit (with "Default" to reset). Default keeps the historical look.
- Draggable text box tool with multi-line support; text alignment and per-line bold/italic.
- Multi-line labels with per-line bold/italic on devices, zones, and connections (migrated from the old global bold/italic model).
- Inline canvas text editing - double-click any label for a contenteditable overlay with character-level bold/italic (spans model); sidebar fields became rich-text editors that mirror the canvas; type-to-replace restored.
- Vertical text alignment (top/center/bottom) for multi-line labels.
- Labels grow away from the node by default - a Top-positioned label now defaults to Bottom vertical layout and vice versa, so multi-line labels stack outward instead of spilling into the device (imported DSL/cable-modem labels used to overlap their icons). A Center-positioned label defaults to Center vertical layout (it used to top-align, throwing imported labeled boxes visually off). An explicit vertical alignment still wins, and existing files keep their stored value.
- Per-span text colors - a single label or text box can mix colors character-by-character (Gliffy legends do this; imports now keep it). Colors survive save/load and draw in PNG/JPEG/PDF exports. Setting the whole-label Font Color deliberately snaps everything to that color, so the color control never appears to do nothing.
- Default font-size slider.
- The sidebar "Defaults" section became "Default Settings" (collapsed by default - it's a set-and-forget panel). The Font Size slider is replaced with the same compact Font row the property panels use (family + size + color), and two new defaults join it: Device Color (a tint applied to newly placed devices; Reset = keep stencil colors) and Zone Color (fill for new zones, with the border derived by shading the fill; Reset = classic blue pair). All defaults apply to interactively created objects only - devices, zones, text boxes, connections, pasted images; imports keep their own conventions.
- Gliffy import fidelity pass (vs draw.io ground truth) - imported devices keep their original dimensions instead of inflating to max(w,h) squares (a 120×66 switch used to become 120×120, displacing neighbors and every connection landing spot); connection endpoints resolve Gliffy's exact fractional anchors, reusing an attachment point within 2px or injecting one at the precise contact point (nearest-AP snapping used to collapse parallel links onto one AP - the bowtie/dogleg artifacts); imported devices and zones default to 16 APs (the quarter grid Gliffy diagrams are authored against). Unlike draw.io's frozen edge points, connections stay attached - moving a device keeps its links live.
- Borderless stencils: the app draws the frames now - the two frame paths (white base + blue border ring) were stripped programmatically from every bundled icon (they share one structural marker), and
renderDevice/export draw the set's frame themselves at the device's real dimensions. Stretched imported devices get properly proportioned frames instead of a floating square; Device Color tints the frame directly (plus the glyph, as before); the palette thumbnails draw matching frames; devices.js shrank ~75% (the frame was most of every icon). New Device Background control in Device Properties shades the face inside the frame (white default), with a matching Default Settings entry for newly placed devices. The generic import icon is now an empty glyph - the app frame is the visual. Old files referencing icons by name pick up the new art automatically; diagrams with embedded old framed icons keep them (a harmless double frame in the same colors). - Gliffy labels import their font and justification - the dominant
font-familyin a label's HTML maps onto the app's font stacks (Arial/Helvetica, Times, Georgia, Courier, Verdana, Segoe) and the dominant per-paragraphtext-alignbecomes the label's justification, for device/zone labels, standalone text, and line annotations. A mixed block like a centered heading over a left-aligned body imports with the majority alignment. - Gliffy label positions import faithfully - the Text child's
hposition/vposition/valignfields map onto our position vocabulary (left→Left, right→Right, above→Top, below→Bottom, inside+top→Top (Inside), etc.), so a label that sat above or beside its switch stays there and zone titles stay inside their zones (they used to hardcode Bottom for devices and Top for zones). Orthogonal/hand-routed Gliffy lines import as Rounded (the house routing style; the corner-rounding path builder is generic over arbitrary polylines), straight lines stay Straight. - File → Open Recent - the last 10 saved/opened diagrams, stored whole in localStorage as slim v6 snapshots (10-20KB typical; oversized image-heavy diagrams are skipped for quota safety). Recorded on Save and on file open, deduped by title, newest first; one click reloads. A Clear Recent entry at the end of the list empties it.
- The toolbar Text button is a one-shot placement tool - clicking it arms the tool (text cursor, button highlighted); the next canvas click drops a ready-to-type text box at that spot and returns to Select, Gliffy-style. Esc disarms. Replaces the old behavior of dropping the box at the view center.
- Zones default to the device-frame blue border - new zones draw their border in the same blue the app-drawn stencil frames use (
#2d67b9), so devices and zones read as one family; the sidebar zone/flowchart thumbnails match. A new Zone Border Color default sits below Zone Color: explicit color wins; on auto the swatch tracks the effective value (frame blue, or the shade derived from a custom Zone Color, preserving the old derivation). - Bulk Select → Map Details to Label - stack chosen Device Details into every device's label, one field per line. A small picker offers the fields actually present (Hostname + IP-Address checked by default; Hostname falls back to the current label line when the field is empty, so a device named by its label keeps its name). The Hostname line is shortened to match the import's clean-label convention (
server.corp.example.com→server) - the full FQDN stays in the Hostname field, this only affects the canvas label. Undoable; a device with none of the chosen fields keeps its existing label. Note on spacing: hostname + IP (2 lines) fits cleanly under the import auto-layout, but 3+ lines begins to clip into the row below - the layout row-height is tuned for short labels. - Bulk Select → Recolor All to Theme - themes normally only seed new objects; this snaps every existing device and zone to the current Default Settings colors (Device Color, Zone Color, Zone Border, and the zone/device label font colors) in one click - so an imported diagram can be recolored into any theme wholesale. (Earlier versions recolored fills and borders but not the label font color, so zone labels stayed a fixed blue no matter the theme - fixed in 3.0.x.) Undoable; connection colors are left alone (they carry semantic meaning), as are Device Background and zone opacity. Raster/pasted icons that can't be tinted are skipped.
- The palette live-previews the color defaults - setting Device Color re-tints every device thumbnail in the sidebar (cached per stencil, debounced against color-picker drags) and setting Zone Color restyles the zone/flowchart shape thumbnails with the derived border. The stencil set itself is never modified: Reset instantly restores the original colors, so an org palette can be the working default without permanently recoloring the library.
- ~110 lines of dead code removed: seven never-called functions from the pre-rich-editor era (
zoomToFit, textarea line helpers, per-line bold/italic togglers) and dead CSS for removed features (.drag-preview,.font-toggle/.font-style-controls, paneltextarearules and their dark-mode companions). - Export function cleaned up:
exportJPEG→exportRaster(it exports PNG/JPEG/PDF); its unreachable un-cropped branch and dead grid-confirm()fallback removed. - Undo drag-commit deduplicated: the 7-line commit-pre-drag-snapshot block pasted four times in the mouseup handler is one
commitPreDragUndo(). - Connection-label and annotation backgrounds are measured, not estimated - the canvas used a chars×size×0.6 guess while exports measured real text, so the white box behind a label differed between editor and export; both now measure (cached).
- Legacy device-panel IDs renamed to the typed convention (
device-font-size,device-label-position,device-ap-count).
- Drags move nodes instead of rebuilding them - device/zone/image drags (single and multi) now update the group's
translateper mousemove; previously each mousemove tore down and recreated the whole group, including its multi-KB data-URI icon. - Marquee selection stopped rebuilding the canvas - rubber-banding toggles selection classes on existing elements instead of clearing and re-rendering all four layers per mousemove (thousands of elements at imported-diagram scale).
- Undo snapshots intern image data - snapshots stringified inline icon data URIs (multi-MB per drag-start, ×50 retained on the stack); distinct images are now swapped for short session-scoped keys and restored on undo, dropping snapshots to KBs.
- Text measurement is cached - the SVG measurement helper (forced layout per call) now caches by content/size/family, and
renderTextBoxuses it instead of a private copy, cutting per-keystroke and per-drag layout thrash. - Arrow-marker creation uses a registry - a Set of created marker ids replaces two DOM queries per connection render (a linear scan of the growing defs list that ran hundreds of times per marquee mousemove).
- Fixed: File → New left pasted images on the canvas while resetting the ID counter under them (a later object could mint a duplicate ID and corrupt saved references). All diagram-replacing paths (New, sample, both importers) now share one
resetDocumentState()- the six hand-rolled reset lists had drifted, also leaking stale groups (phantom group selection after Load Sample) and connection multi-selections. - Fixed: loading a malformed file destroyed the current diagram -
applyDiagramDatanow validates the file's shape before touching state, so a corrupt/hand-edited file errors out with the existing diagram intact. - Fixed: deleting a connected zone or image stranded its connections invisibly in the file forever; they now cascade like device connections (all three delete paths).
- Fixed: Gliffy import crashed on an unlabeled unknown stencil (null-template dereference introduced with the generic-icon change; the Visio path had the guard, the Gliffy copy didn't).
- Fixed three undo races: a pending debounced edit (label typing) could land on the stack after later actions - it's now flushed ahead of any push/undo/redo, so typing-then-quick-Ctrl+Z undoes the typing and redo survives correctly; undo mid-resize/bend/connect no longer leaves the gesture mutating an orphaned object (all interaction states cancel on restore); an open inline editor cancels on undo instead of resurrecting the undone text via its deferred blur-commit.
- Fixed: cropped exports clipped label overhang - content bounds now measure label widths (left/right positions overhang sideways; wide centered labels overhang both ways; text-box width was a fixed 200px guess), and the zone bounds' 13px font fallback drifted from the renderer's 20px.
- Fixed: a loaded file with non-hex connection colors (e.g.
rgb(51,51,51)) crashed the render pass via an invalid CSS selector in arrow-marker creation; marker ids are now sanitized. - Fixed: saving or loading within the 4s autosave debounce re-autosaved the clean diagram, causing a false "Restore unsaved work?" prompt next launch.
- Fixed: pasting an extreme-aspect image could snap a side to 0 (invisible object); sides clamp to grid size.
- Ctrl+O now opens a diagram (the menu advertised it but the browser's dialog opened instead); the impossible Ctrl+N hint was removed.
- Ctrl+A now matches marquee semantics - internal connections join the selection and the Batch Edit panel opens.
- Color themes - a Theme picker next to the Dark toggle offers nine looks: Classic, Garnet (a deep #A52238 red), Evergreen, Lagoon, Midnight, Ember, Crimson Navy, Slate, and Glacier (a light-chrome theme - a pale silver top bar, the only light one in the set). A theme is two coordinated layers: the top-chrome palette (menu bar, toolbar, the picker itself - all CSS variables, including the previously hardcoded active-tool blue) and a seed for the color defaults, applied through the same Default Settings controls the user can reach by hand - so every per-control Reset still restores classic values, imports keep their source colors, and existing objects are never touched; only newly created objects arrive themed. The zone/flowchart/text thumbnail button borders now follow the zone-border default too, so a theme switch repaints the whole sidebar family. The choice persists; Classic clears everything. Chrome themes are independent of dark mode.
- Dark mode shades in the theme's hue - the dark sidebar, panels, context menus and canvas previously used one fixed purple-navy palette under every theme; they now draw from the theme's own chrome variables (one source of truth - the sidebar matches the menu bar exactly), and each theme carries its own dark canvas + grid shade (
--se-canvas-dark/--se-grid-dark): Garnet gets a wine-black canvas, Evergreen a pine-black, and so on. Light mode is untouched - themes there color only the chrome and the object defaults. Dark-surface colors decouple from the chrome via optional--se-dk-*overrides (the dark-mode CSS readsvar(--se-dk-panel, var(--se-panel))etc.): the dark themes define none and fall back to their chrome palette (unchanged), while the light-chrome Glacier theme sets them so it keeps a genuinely dark workspace in dark mode while its top bar stays light. The two remaining hardcoded toolbar colors (button hover, separator) became variables in the same pass, so the whole top bar is now theme-driven. - Dark mode became surface-aware - the old adaptive flip turned every near-black label and line white in dark mode, blind to what sat behind it: imported diagrams lay their own light zone fills over the dark canvas, so flipped labels and lines vanished into the pale zones (and text boxes never flipped at all - invisible on the dark canvas). Adaptive colors now sample the actual surface behind them (
surfaceIsDarkAt: the canvas blended with every containing zone by its fill-opacity): labels and text boxes flip only over dark surface, and connection lines vote across their two ends and midpoint. Toggling dark mode re-renders all affected layers. - Native
<dialog>modals replace alert()/prompt() for imports - the Visio/Gliffy import summaries, the device-library import summary, and the Visio page picker (now a clickable list of pages with shape counts and a "largest" hint, instead of typing a page number into a prompt). The modal is themed with the app's chrome palette (dark-mode consistent), promise-based (showDialog), and Esc/Cancel-friendly. Destructive confirms (unsaved changes, deletes) deliberately stay native. - Menu-clarity renames - Open… → Open / Import Diagram… (surfaces that Gliffy/Visio import lives behind Open, and reads distinctly from Import Inventory CSV - mis-picks between the two were real); Bulk Select → Bulk Actions, with "Select" prefixed onto the selection entries (Select All Devices…, Select Everything…), since the menu had grown non-selection operations (Recolor All to Theme, Map Details/Label).
- Persist imported devices in localStorage; search box; delete with confirmation.
Default_prefix on built-ins + "Hide default devices" checkbox; alphabetical device list.- SVG icon tint/recolor - chromatic-aware tinting that preserves backgrounds/neutrals; handles both XML attributes and CSS inline styles; reset button.
- Imported icon-set SVGs normalize to the stencil blue - monochrome line art (Lucide, Tabler:
stroke="currentColor", plain black) used to import black and ignore Device Color, becausecurrentColorresolves to black inside an<image>and the tint pass deliberately preserves neutrals. Import Device Image / SVG now recolors currentColor and dark neutrals to the frame blue when the art carries no chromatic color of its own - the icon matches the bundled set on arrival and becomes tintable. Multi-color artwork imports untouched. - Default device-size slider; default size reduced to 60px.
- Clipboard image paste - paste images onto the canvas as selectable, resizable elements.
- Device library sharing - Export Device Library →
devices.js; bundled set loads at startup (script-tag approach forfile://support); bundled templates are non-deletable. - Team stencil layer + Import Device Library - an optional
customdevices.jsnext toindex.htmlloads at startup as a site/team stencil layer (same script-tag convention; name collisions keep the base bundle's icon). Export Device Library now writes thatcustomdevices.jsformat containing just your imported stencils, and a new Import Device Library button consumes it (or a legacydevices.jsexport, or a raw JSON array) into your personal palette with a per-name dedupe summary. - Device search now matches synonyms - the stencil set uses terse canonical names (Client, VOIPPhone, WifiAP, EthernetRJ45, Globe), so searching the palette for "pc", "computer", "phone", "wifi", "access point", "scanner" or "cctv" previously found nothing. The search box now also matches an alias vocabulary per stencil, built by inverting the Visio import map (which already knew
pc → Client,gateway → Router,nsg → Firewall,vnet → Cloud, …), folding in the consumer keywords from the client-stencil guesser (macbook, ipad, windows), and a small curated top-up (wifi, cctv, mfp, rj45, cluster…). Own-name hits rank above alias-only hits ("ethernet" lists EthernetRJ45 before Switch). One source of truth with the importers; canonical names stay terse, and no stencil had to be renamed (which would break saved diagrams'@namereferences). Documented under Help → User Guide → Devices & stencils. - Icon swap dropdown in Device Properties (alphabetized); clarified "+ Import Device Image" label.
- Collapsible sidebar sections; 3-column icon grid; flex-wrap zone/text thumbnails.
- Library actions moved to a collapsed "Device Library" section - the three rarely-used buttons (Import Device Image / SVG, Import/Export Device Library) left the always-open Devices section for their own collapsible section below the grid (collapsed by default), reclaiming sidebar height for the stencil palette. Same collapse mechanism as the other sections; a sibling section rather than nested, so the parent's max-height can't clip it.
- Pasted images became first-class nodes - labels, 8 attachment points, connectable, with their own properties panel.
- Bundled
devices.jsas 24 permanent base stencils; defaults auto-hidden when a bundle loads, defaults sorted last. - VRF replaced by a "Blank" stencil (VRF later returned in the v4.0 stencil expansion - today the library ships both) - an empty glyph, so the app-drawn frame is the whole visual: a clean framed box for anything without a good icon. Gliffy imports whose label sits inside the shape (center / top-inside / bottom-inside - the source drew a labeled box, and stencil art would clash with the text) now match Blank instead of a specific icon; the Icon dropdown upgrades one to a real stencil in a click. In practice this catches Gliffy's cloud-with-internal-label idiom ("Internet", "ISP").
- More homelab sources - building on the DNS/DHCP/arp set: Cisco
show ip arp(dotted-quad0050.56a9.dc34MACs normalized,ARPArows only), Linuxip neigh(iproute2, the modern replacement forarp;FAILED/INCOMPLETEand IPv6 skipped), dnsmasq lease files (/var/lib/misc/dnsmasq.leases- Pi-hole, OpenWrt, most home routers;*hostname → labelled by IP), ISC Keakea-leases4.csv(active leases only - declined/expired dropped), nmap ping scan (nmap -sn, multi-line records; the MAC-vendor name rides along as Description), and macOSarp -a((incomplete)entries filtered). Thearp -atext parser is now dialect-aware (Windows / Cisco / ip-neigh / net-tools+macOS in one pass). MAC normalization covers dotted-quad, bare-hex, dashed, colon, and single-digit-octet forms across every source. - Homelab quick-onboarding formats - four more auto-detected inventory sources aimed at seeding a board from what a home network already knows about itself: a Windows DNS zone Export List CSV (Host (A) records become devices - hostname + IP; zone plumbing like
_msdcs/_tcpsubfolders, NS/SOA/CNAME/AAAA records,(same as parent folder)apex rows, and exact duplicates are skipped), a Windows DHCP Address Leases export (IP + hostname with the trailing dot stripped + the Unique ID as MAC; leases with no name label by IP), and rawarp -aoutput pasted into a.txt- both the Windows (192.168.4.1 00-50-56-a9-dc-34 dynamic) and Linux net-tools (_gateway (192.168.4.1) at 00:50:56:a9:dc:34 [ether]) dialects, with multicast/broadcast rows and<incomplete>entries filtered. MAC addresses from every source normalize toAA:BB:CC:DD:EE:FF(bare hex, dashed, and single-digit-octet forms all handled). The Import Inventory picker now accepts.txt/.log, and the skip-count reason in the summary reads "blank, duplicate, or non-device rows" instead of the misleading "no label or Hostname". Documented in Help → Inventory CSV Format. - Import Sort - a Default Settings dropdown orders the devices within each zone on inventory-CSV import: File order (default, CSV order), Device type (groups cameras/printers/PCs by their resolved stencil), MAC address (groups by vendor OUI - same-make devices cluster), or Label. Stable sort keeps CSV order within a group; a synthetic switch/WLC (no MAC) sorts to the top of its zone.
- Import Spacing controls - two Default Settings sliders, "Import Spacing - Wider" and "- Taller" (100-250%, default 100%), scale the inventory-CSV layout grid horizontally and independently vertically. Widen to keep long hostnames from clipping into their neighbors; heighten to fit multi-line labels (e.g. after Map Details to Label stacks hostname + IP + port). Applies to the next import; 100% is byte-for-byte the previous packing.
- Gap-filling zone layout (bottom-left skyline packing) - the auto-layout used to shelf-pack zones into rows, so a row's height was set by its tallest zone and short zones sharing that row floated at the top with dead space beneath them (a 150-device site next to a few 20-device sites pushed the rest far down the page). Zones now drop into the lowest gap their width fits, so short zones tuck under other short zones beside a tall one instead of starting a fresh row below it. Verified: a 4×40 / 1×150 / 3×20 tier drops from ~4940px tall to ~4300 with the small zones nested up under the medium ones; 30 sibling zones / 1,247 devices pack with zero overlaps in ~0.4s. Uniform-width tiers (the common case) and small imports are unaffected in feel.
- Even nested-zone header spacing - the auto-layout drew each zone at its grid-snapped position but laid its children out from the un-snapped point, so the two snap errors compounded and the label-to-child gap came out uneven between nesting levels (e.g. 40px under one zone, a cramped 20px under the next, with the label almost touching the child border). Layout now recurses from the snapped origin and
HEADis a grid multiple, so every level gets a consistent 40px header. - Standalone ISE import groups clients under their NAD - an ISE endpoint export imported on its own now nests each client in a zone named for the switch/WLC it authenticated through (
NetworkDeviceName, withNADAddressas that device's IP), exactly like the Catalyst Center wired-client import - no second file needed. A NAD whose name says WLC (wlc/9800/wism) gets the WifiAP glyph; the rest get the Switch glyph. FQDN and short NAD names merge; clients with no NAD fall back to their own ISE location. The grouping logic is now a sharedgroupBySwitchused by both the ISE and CC-wired-client profiles. The CC+ISE hybrid is unchanged - it still does its own CC-device matching and never double-groups. (All the ISE fields, including the NAD name and IP, are apostrophe-stripped first.) - Cisco Catalyst Center wired-client import (single-source switch grouping) - a CC wired-client export (detected by Switch IP Address + Endpoint Type) groups clients under their switch from one file - the same switch-zone layout as the CC+ISE hybrid, but with no second export and no reconciling ISE's and Catalyst Center's differing Location syntaxes (the pain point that motivated it). Each unique switch (from the Switch / Switch IP Address columns) becomes a synthetic Switch device inside a zone named for it, carrying its IP; its clients nest in that zone; the client's CC site path supplies the Building/Floor tiers above. FQDN and short forms of the same switch name are merged. Stencils come from Device/Endpoint Type; MAC, OS, switch port, VLAN, and SGT ride along as fields. Implemented as a
postProcesshook on the vendor profile; the ISE/CC-client stencil guesser was factored into a sharedguessClientStencil. - Help → Inventory CSV Format - a Help dialog documenting the generic import columns (the stable spec external formatters target), with a Download example CSV button that saves a ready-to-fill
crosscanvas-inventory-template.csv(embedded, so it works offline). - Hybrid import: Catalyst Center + ISE selected together - the Import Inventory picker now accepts multi-select; one CC device export + one ISE endpoint export triggers a merged import. CC provides the authoritative location tree and infrastructure; ISE provides clients, each nesting inside a zone named for the switch it authenticated through (NetworkDeviceName ↔ Device Name, matched on short hostname so FQDN/short mismatches don't matter). Containment, not topology - no connections drawn. Switches on the same floor become sibling zones; a switch with no matched clients stays a plain device (no one-device zones); wireless clients (ISE reports the WLC as the NAD) group under their own ISE building in a "Wireless" sub-zone instead of piling into the controller's building; unmatched clients fall back to plain ISE-Location grouping. Matched clients inherit the switch's CC location, so the ISE and CC location hierarchies don't need to agree. The summary reports the detected hybrid plus matched/wireless/unmatched counts; ISE's
NetworkDeviceNameis kept as a "Network Device" custom field for auditability. Any other multi-file combination gets a clear guidance dialog; single-file imports are unchanged. - Vendor imports strip reverse-lookup domains from labels - an ISE/Catalyst Center hostname that came in as an FQDN (
finance-pc-042.corp.example.com) shows the short name on the canvas while the full FQDN is retained in the Hostname field. Guarded so an IPv4 or MAC that landed in the hostname slot isn't butchered. ISE stencil keywords also broadened:computer/win10/win11/pc→ Client,dock→ Laptop. Keyword matching now accepts a RegExp as well as a substring, sopcmatches as a whole token (PC,PC-01,corp_pc,pc01) without firing inside larger words (pcoip,epcot). - Auto-detected vendor CSV formats (Cisco Catalyst Center + ISE) - the two management-platform exports import as-is with no manual column cleanup. Detection keys on column combinations unique to each product (CC: Device Name + Device Family + Reachability; ISE: MACAddress + EndPointPolicy), so generic CSVs can never false-positive; the import summary names the detected format. Each profile maps a curated subset of columns to Device Details and drops the rest (CC exports carry ~55 columns, ISE 100+ - importing them all would bury the panel in noise). CC: Device Name → label+Hostname, Platform → Description, Site → Location with the constant
Global/root stripped, Device Family/Role → stencil (switch/router/AP/firewall; Wireless Controllers reuse the WifiAP glyph since there's no dedicated WLC icon), plus MAC, Role, and Image Version as custom fields. A hostname that's blank or a placeholder (--/-, as Catalyst Center emits for MAB devices that never reported one over 802.1X) falls back to the MAC and stores no junk Hostname. ISE wraps most field values in single quotes ('192.168.1.1','policy','<switch>', and the whole Location string) - the ISE reader strips the surrounding pair before parsing, so IPs, the Location#-hierarchy, stencil keywords, and the hybrid NAD↔switch match all see clean values (internal apostrophes likeBob's iPhoneare kept). ISE: host-name → label (falls back to the MAC - always present),#-delimited Location hierarchies (Location#All Locations#…) strip their constant roots and nest as zones, EA-cmdb Asset Tag/Serial map to the standing fields, and EndPointPolicy keywords pick stencils (printer/camera/phone/laptop/client/…) - endpoints with unmatched policies stay Blank silently, so a thousand-endpoint import doesn't drown the summary. - File → Import Inventory CSV - turns a device inventory into a laid-out starting diagram, ready for manual arrangement (deliberately not auto-topology: no connections are drawn). Column spec (header row required, names case-insensitive):
label(falls back to Hostname),stencil(resolved with the Visio importer's vocabulary + fuzzy matching; unknown → Blank), the six standing Device Details fields, optionalx/yfor explicit placement, and any other column becomes a custom data field named by its header.Locationdrives layout:/or|delimited paths (Catalyst Center style -Campus/Building/Floor) nest, with every level becoming a zone wrapping its children via a recursive shelf-packing layout; parent zones go near-white so the nesting reads, leaf zones take the default fill; devices without a Location land in a loose grid outside any zone. New objects use the active theme's Default Settings - there's no source styling to be faithful to. RFC 4180 parsing (quoted commas/newlines, BOM), import summary lists unmatched stencils. Round-trip symmetry: the CSV export's device columns are directly re-importable.
- Gliffy importer with auto-detection - stencil→template mapping with fuzzy fallback and placeholder icons; rectangles→zones/text boxes; constraint-based connection resolution; HTML labels with bold/italic; post-import summary.
- Import all line labels as draggable annotations; use Gliffy metadata/filename as the diagram title.
- Ellipses → ellipse zones, standalone Text → text boxes.
- Keep lines whose endpoint devices were deleted (free endpoints from geometry).
- Recursive group flattening to absolute coordinates so grouped objects survive; expanded the stencil map (~60 aliases) to target the bundled device set.
- Import quality pass (tested against four real diagrams with PNG ground truth): hand-routed lines keep their exact paths - intermediate
controlPathpoints import as first-class connection waypoints instead of collapsing to straight diagonals; dashed/dotted lines render dashed -dashStyleis a dash-array string ("8,2", "4,4", "1,1", "8,8"), now mapped onto the app's dash vocabulary (previously emitted an invalid value that rendered solid); small unlabeled rectangles import as colored zones instead of invisible empty text boxes (port chips/markers); font sizes import from the label HTML instead of stamping everything with the app default. - Stencil resolution prefers bundled/imported stencils over the built-in
Default_fallbacks (both the name map and the fuzzy matcher now search non-default templates first). - Batch conversion (File → Convert Gliffy Files…) - multi-select
.gliffyfiles and each runs through the real importer and is written back out asname.xcanvas(same fidelity as a hand import). Chrome/Edge write into a picked folder via the File System Access API; other browsers fall back to one download per file. Roll-up dialog reports per-file device/connection counts and failures; the last converted diagram stays on the canvas for spot-checking. - Batch conversion can recolor to the active theme - a prompt offers "keep source colors" (imports preserve source styling, as ever) or per-file Recolor All to Theme, so an old Gliffy archive can be converted and unified into the house palette in one pass.
- Batch conversion generalized to draw.io (menu: Convert Diagrams…) - the batch accepts
.gliffyand.drawio/.xmltogether, routing each file to its importer by content. Multi-page draw.io files take their busiest page without prompting (the roll-up notes which page of how many); the recolor option applies to every format.
- draw.io (
.drawio) importer via File → Open / Import Diagram - completes the format triangle (the export landed earlier; now files come back too). Reads both diagrams.net save styles: raw mxGraph XML and the default compressed form (base64 + deflate-raw + URI-encoding, inflated with the nativeDecompressionStream- same zero-dependency story as.vsdx)..xmlsaves are detected by content sniffing. Multi-page files prompt with per-page cell counts (largest flagged), like the Visio picker. - Mapping follows the proven Gliffy philosophy: exact bounding boxes;
exitX/exitY/entryX/entryYfractions become exact-anchor attachment points (injected APs, no bowties);mxPointwaypoints convert to native bends; containers/swimlanes and plain rectangles become zones (ellipse/rhombus/parallelogram/cylinder map to the matching zone shapes; the big-or-unlabeled rule splits small labeled rects into text boxes); stencil shapes resolve to bundled devices via curated hints + fuzzy match, with unrecognized types imported as the generic icon and listed in the summary; HTML labels parse to spans (bold/italic/per-span color); edge labels import as draggable annotations. - CrossCanvas's own exports round-trip: the composited device icons are de-composited back to glyph + frame colors (tint/background restored, no double-drawn frames), and Device Details return from the
<object>Edit-Data attributes. Foreign SVG image nodes become tintable device icons (color-normalized); raster image nodes become pasted images.
- Foreign shapes import their embedded images - screenshots, logos and bitmap icons embedded in the file (
visio/media/PNG/JPEG/GIF/BMP) become first-class pasted images at the shape's true footprint, resolved through the page's relationship map; the in-browser unzipper now extracts renderable media parts as data URIs (EMF/WMF vector media can't render in a browser and stays skipped). The Azure VNet Manager icon, corner VNet diamonds, and the Microsoft Azure logo in the hub-spoke corpus were all invisible before. Imported images dedupe into the v6 image table like any pasted image. - Fixed: near-axis connectors imported a few pixels diagonal - a straight two-point connector that was axis-aligned in the source could resolve 7-12px off-axis (the pin-centered device resize slides each contact along its box edge independently, and free ends grid-snap). When the source line was straight, the endpoints re-align: free ends move outright; a device end re-anchors at the lined-up fraction via a freshly injected attachment point, so other connections sharing the old point are untouched. The AKS corpus's three drifting lines are now dead straight.
- Zone labels import where Visio drew them - zone label position now comes from the shape's text block (
TxtPinY/TxtLocPinY/TxtHeight+VerticalAlign, shape cells first with the master sheet as scaled fallback): Visio's default block spans the shape with middle alignment, so an unadorned container has a centered label - the importer used to hardcode Top. Top/bottom-aligned blocks map to Top (Inside)/Bottom (Inside), and a block anchored above or below the shape maps to the outside positions. - Pasted vector icons import as Blank devices - modern Azure exports carry their service icons as raw grouped geometry with no stencil master, so there is no name to map and the icons used to flatten into skipped fragments (the AI Foundry compute page lost ~50 icons). An icon-sized masterless group of multiple textless vector children now imports as one Blank device at its true footprint: unnameable but swappable via the Icon dropdown, and connectors get real nodes to attach to. The import summary counts them.
- Fourth quality pass (vector-art Azure exports: AKS multi-cluster, AI Foundry) - these files' icons are masterless vector groups, and their mastered shapes are primitive Rectangles/Circles/badges, which broke three assumptions at once. Fixed: geometry rows may omit a coordinate that hasn't changed from the previous point (omit-if-unchanged serialization; the parser bailed and ~two-thirds of routed connectors fell back to straight diagonals - the AKS file went from 5 to 11 recovered routes); zones import their source colors - fill from
FillForegnd, opacity fromFillForegndTrans(Azure region bands are strong colors at ~90% transparency), border fromLineColor, with borderless shapes (LinePattern 0) blending the border toward white, and unfilled boxes rendering as the white cards they are instead of uniform gray; the size median ignores sub-icon shapes (a swarm of 6px mastered decoration dots dragged the median to dot size, exploding sizeNorm and slamming every real shape into the 200px clamp - numbered step badges imported at 200×200); and a file whose median is still below stencil size has no icons to normalize against and imports at its own diagram scale. - Connectors import their routed geometry - the connector's own Geometry rows (the actual right-angle route Visio drew) become waypoints, so hand- and auto-routed paths import as the paths they are instead of collapsing to straight diagonals; node-to-node routes that fit the native rails convert to bent connections via the lossless converter, and free-ended routes keep waypoint handles. Cosmetic arcs (corner rounding, line-jump "hops" over crossing lines) flatten onto the route; genuinely curved connectors keep the straight-line import. End segments re-align with their attachment points when the pin-centered device resize slid the contact sideways (a resize artifact, not a reroute). Plain two-point lines stay straight - the faithful import for them.
- Fixed: one-end-glued connectors could vanish, and glued endpoints could swap - Visio
<Connect>rows say which end they glue (FromCell), but the importer assigned them to Begin/End in file order: a connector glued only at its End got that node applied to its Begin, resolved both ends onto one device and was dropped as degenerate (the AVD sample's missing connector), and a connector whose rows arrived End-first imported with swapped endpoints (invisible until arrowheads mattered). - Devices keep their source aspect; connectors resolve exact anchors (ported from the Gliffy fidelity pass) - per-axis median normalization replaces the everything-becomes-a-square rule, so the basic sample's Ethernet buses import as the ~200×15 bars they are (wearing stretched app-drawn frames) instead of Default Size squares, and the old aspect>2 special case is gone. Geometry connector ends now resolve the file's exact contact points - expressed as fractions of the source shape box (devices resize around their pins, so page coordinates don't map directly), then reusing an attachment point within 2px or injecting one at the precise spot, sharing the Gliffy importer's
mapToAP. Square Azure icons import pixel-identically to before. - Visio
.vsdx/.vsdmimporter via File → Open - dependency-free: the OPC package is unzipped in-browser (hand-parsed ZIP central directory + the nativeDecompressionStream) and parsed withDOMParser(inert, no network). - Shapes → devices with master-inherited geometry; inch→pixel conversion with Y-flip;
NameU→ bundled stencil via a Visio name map reusing the Gliffy fuzzy-match and placeholder fallback. - Containers → zones, standalone text → text boxes, and
<Connects>→ connections (hub-style links and 1-D connectors, de-duplicated, snapped to the nearest attachment points). - Multi-page documents prompt for which page to import (single canvas).
- Stencil resolution prefers bundled/imported stencils over the built-in
Default_fallbacks (the defaults load first, so the fuzzy matcher used to win ties withDefault_Switchover the devices.jsSwitch). - Unlabeled Visio shapes import with a blank label rather than inheriting the stencil name (which produced labels like "Switch"/"Default_Switch").
- Import quality pass - device sizes normalize so the file's median stencil lands at the app's Default Size (Visio's ~1in shapes previously imported at 94-110px and crowded the layout); elongated structural shapes (Ethernet buses, comm links) icon at Default Size instead of squaring up to their long axis; connections use the panel's default thickness instead of hardcoded 2px; hub-synthesized edges (bus taps) route straight for a clean star.
- Azure/architecture-file support (tested against four Azure Architecture Center diagrams): unglued 1-D connectors import from their Begin/End geometry - ends glue to a nearby device or zone border, else stay free-floating, with Visio Begin/EndArrow cells mapped to arrowheads (previously these files imported with zero connections); masterless layout groups flatten recursively so nested content survives (mastered shapes stay atomic - their children are stencil internals); oversized boxy masters (VNet-style background boxes) import as zones instead of placeholder devices; the multi-page picker shows per-page shape counts and defaults to the busiest page (architecture posters put a cover image on page 1).
- Multi-line Visio text imports as multi-line - the text extractor collapsed all whitespace including newlines, so e.g. a 20-line site list in a zone label flattened into one long line; line breaks (including Unicode line/paragraph separators) are now preserved and only horizontal whitespace is squeezed.
- Azure stencil vocabulary - ~30 cloud-architecture terms map to bundled stencils (Virtual Machine→VM, VPN/Virtual Network Gateway→Router, SQL Database→Storage, Application Gateway→LoadBalancer, Availability Set→ServerCluster, Network Security Group→Firewall, DNS/Traffic Manager→Globe, App/Function/Logic services→Server, …); master-name
.NNNdedup suffixes normalize away; the fuzzy matcher compares space-stripped names so multi-word shapes reach compound templates ("Azure Load Balancer"→LoadBalancer). Placeholders on the test corpus dropped from a page-full per file to 0-3, all genuinely unmappable decorations. - Ground-truth quality pass (four more Azure Architecture Center files, compared against their published SVG renders): connector styling imports - Visio
LinePatternmaps onto the app's dash styles andLineColorcarries over (shape cell, falling back to the connector's master sheet), so dotted peering lines, gray dashed diagnostics and green "forced tunnel" links survive; border/frame masters import as zones matched by name ("dash square", "Virtual Network Box", "VNet WhiteBCK2", "Subnets" - containers drawn as stencils that the masterless-box rule couldn't see); tiny unlabeled badge shapes are skipped (NSG shields, network-manager markers - size normalization used to inflate them into full-size clutter); background pages (Background="1") no longer appear in the page picker; ~15 more vocabulary terms (OS Images (Classic)→ClientVM, Monitor→Client, Azure NetApp Files→NAS, Route Tables→Router, Web Page/Browser→Globe, Wi-Fi variants→WifiAP, …). Placeholders on the new corpus dropped from 3-8 types per file to 0-1. - Out of scope by design: legacy binary
.vsd(save as.vsdx); Visio's embedded EMF/raster icons (mapped to bundled stencils instead).
- Menu bar + CrossCanvas brand + dark mode - File/Export/Edit menus in a Styling-Example dark palette; brand area with logo, typeable name field, version stepper, dirty dot; Export grid choice via submenus; dark-mode toggle for sidebar/canvas (persisted).
- Toolbar Undo/Redo/Copy/Paste/Delete buttons; Copy/Paste in the Edit menu.
- Dark-mode legibility - flip black device labels and black connection strokes/arrows to white (colored ones untouched); exports keep true colors.
- Movable Properties pane - toggle between left sidebar and a right pane that appears only when something is selected.
- Layers menu + per-object Layer dropdown (bring to front/forward, send backward/back).
- The material family (CrossCanvas debut) - four new themes named for what diagrams are made of: Canvas (raw-canvas ecru with umber accents), Blueprint (cyanotype drafting paper - Prussian-blue chrome, pale-cyan accents), Ink (india-ink warm near-black, Parchment's dark counterpart), Gesso (warm-white minimal; Glacier keeps the cool-silver slot). The picker is alphabetized and Classic stays the fresh-install default (an explicit choice always wins; embeds stay unthemed); the light-chrome members carry
--se-dk-*overrides so dark mode keeps a genuinely dark workspace under the light top bar. 17 themes total. - UI accent follows the theme - the selection chrome (resize handles, attachment points, selection outlines, marquee, connection endpoints), the range sliders, and the segmented alignment controls were all a fixed blue (
#0066cc, jarring against warm themes like Parchment). They now take a--se-tintvariable that tracks the Device Color - set by the active theme or a manual pick - so they fit the palette (bronze on Parchment, red on Garnet, …). Defaults to the classic blue, so Classic and any untinted state are unchanged; resetting Device Color reverts the accent to blue too. - Themed two-tone zone nesting - parent zones (those holding other zones) render a fair bit lighter than the leaf zones that hold devices, so the nesting reads and the innermost zones stay the fuller theme color. The parent shade is derived - a lighter tint of the theme's Zone Color (
lightenHex, blended toward white) - so it's automatically themed for every palette instead of the old hardcoded near-white. Applied on CSV import (keyed on has-children) and by Recolor All to Theme (parents detected by geometric containment, since imported zones carry no nesting metadata by then). - Themes now seed a label font color - a theme sets the default font color alongside device tint and zone colors, so new objects (and inventory-CSV imports) get labels that fit the palette instead of the fixed dark gray. Switching to Classic restores
#333333. - Inventory CSV imports fully inherit the active theme - previously the device tint and leaf-zone fill/border followed the theme but the parent zone fill stayed near-white and every zone label was hardcoded blue, so an import looked half-themed and needed a manual Recolor All to Theme. Now every zone takes the theme's Zone Color fill, and zone labels + borders take the Zone Border color; device labels take the theme font color. Matches the Recolor output at import time. (Imported diagrams still keep their own source styling - only CSV inventory, which has none, is themed.)
- In-app Help menu - Quick Start, the full User Guide, a Keyboard Shortcuts reference (also on
?), and an About box (version + privacy note), all embedded in the app so they travel with it to wherever it's hosted - no network, works fromfile://. Opens in a wide, scrollable, theme-aware modal. Content mirrorsUSER_GUIDE.md/README.md.
- Security: CSV export neutralizes spreadsheet formula injection - a cell that starts with a formula trigger (
= @ + -, tab, CR) and isn't a plain number now gets a leading apostrophe, so an imported device labelled=cmd|'/c calc'!A1can't execute when the exported CSV is opened in Excel/Sheets. Legit negative coordinates (-40.5) stay numeric;-2+3(a formula, not a number) is neutralized. - Security: draw.io export escapes style attribute values - colors from an untrusted loaded file are XML-escaped before going into
style="…", so a crafted fill can't break the XML or inject markup (device icons were already base64-safe). - Export as draw.io - writes uncompressed mxGraph XML that diagrams.net opens directly (and draw.io itself can re-export .vsdx, bridging to Visio). Fidelity choices: device icons composite the app-drawn frame + glyph into one per-device SVG data URI; connection endpoints pin to the exact attachment fractions (
exitX/entryX); our actual route points ride as edge waypoints; dashes, arrowheads, zone shapes/fills/opacity, label positions, and per-span bold/italic/color labels all map onto draw.io style vocabulary; free connection ends become explicit source/target points; annotations become relative edge labels; Device Details fields export as draw.io shape data (right-click → Edit Data). Deliberately one-way - CrossCanvas's own.xcanvasformat remains the working format. - Export as PNG (Transparent) - a variant beside the normal PNG that skips the white background fill, so the diagram exports with a transparent canvas and can be laid over any backdrop (slides, dark themes, documents). PNG-only (JPEG/PDF have no alpha and stay white); content and anti-aliased edges are preserved.
- Export as SVG (with/without grid) - the canvas is SVG, so the export clones it, strips the interactive chrome (handles, attachment points, selection outlines, overlay), crops the viewBox to content bounds, and ships a self-contained vector file (icons ride as embedded data URIs). Always exports the light look, matching the raster exports: if dark mode is on, the layers re-render light for the clone and dark is restored after. Visio and browsers open it directly.
- Device Details data fields - the Device Properties panel splits into two collapsible halves: DEVICE PROPERTIES (the everyday controls) and a new DEVICE DETAILS section, both wearing the sidebar's section-header look (left arrow, rotate on collapse). Details offers six standing inventory fields - Hostname, IP-Address, Serial-Number, Asset-Tag, Description, Location - plus free-form custom rows (+ Add Field), with field and value boxes sharing the row evenly. Hostname derives from the label's first line unless set explicitly (the derivation shows as the placeholder); values store only when filled, so files stay slim. Fields ride through save/load/copy untouched, and the CSV export grows a
hostnamecolumn plus one column per used field key (standing fields first, in panel order), placed right afterlabelin the readable zone. Collapse state persists; the section lives inside the device panel, so it moves with the Properties Left/Right toggle automatically. - Export as CSV (data) - one row per object (devices, zones, images, text, connections) with a shared column set: human-readable columns lead (type, label, stencil, connection endpoints by name), geometry follows (x/y/width/height), styling and internal ids trail. Multi-line labels stay intact (RFC 4180 quoting), free connection ends export as
point(x,y), and the file carries a UTF-8 BOM + CRLF so Excel opens it cleanly. The first piece of the inventory/CMDB interop direction. - Cropped-to-content JPEG; grid toggle on export; PDF export (hand-built, dependency-free); PNG export + lossless PDF via FlateDecode (sharper than JPEG, JPEG fallback).
- Connection annotations included in image/PDF export - the canvas re-draw path previously omitted them, so they appeared on-canvas and saved/loaded but were missing from exports.
- Save excludes default/bundled templates to keep files small (re-merged on load with ID remapping).
- Alt fine-snap (¼-grid / 5px) for zones, then extended to all object types, resizing, multi-drag, and connection bends/endpoints; smaller minimum zone size.
- Pan & Zoom - pan tool, viewBox-based zoom (Ctrl+wheel to cursor, +/- buttons, percentage resets to 100%, click-to-fit); drops and inline editor are zoom-aware.
- Grid show/hide toggle (persisted).
- Autosave to localStorage with restore-on-startup prompt; cleared on save/New.
- Keyboard niceties - Esc (cancel/deselect), Ctrl+A select-all, Ctrl+D duplicate, Ctrl+S save, arrow-key nudge (Shift = ¼-grid, Alt = 1px); Backspace no longer deletes.
- Align/Distribute menu - align left/center/right/top/middle/bottom + distribute horizontally/vertically.
- Alignment guides - on by default, with a toolbar toggle (next to the grid button, persisted): snaps a dragged object to nearby objects' edges/centers within 6px and draws dashed guide lines; holding Alt bypasses guides for precise placement.
- Default Attachment Points slider - choose 4/8/12/16 APs for newly placed devices (8 keeps the classic corners+midpoints layout); later extended to newly created zones and pasted images.
- Corner-anchored attachment points - the AP distribution always pins a point on each corner and spaces the rest evenly along each side between the corners. The old even perimeter walk only landed on corners for squares; on rectangles the points floated just off the corners. Count 8 reproduces the classic corners+midpoints layout index-for-index.
- AP sliders in the properties panels - the + Add / − Remove buttons on devices, zones and images are replaced by the same 4/8/12/16 slider used in Defaults; changing the count re-snaps attached connections to whichever new point is closest.
- Defaults section + toolbar Text button - the three global sliders (Font Size, Device Size, Attachment Points) moved into one "Defaults" section at the top of the sidebar, shortening the panel so the Text tools aren't buried; a new toolbar T button (left of Pan) drops a ready-to-type text box at the view center.
- Removed the built-in
Default_stencils and the "Hide default devices" checkbox - the bundleddevices.jsset made them redundant. Old diagrams that used them still render (devices carry their images inline).
- Multi-select property editing - marquee or Ctrl/Shift-click, then one Batch Edit panel shows a section per selected type (devices, connections, zones, text boxes) plus a shared Text & Labels section; each control applies to every element of that type at once. Marquee selection now also grabs connections whose both endpoints are inside it (new
selectedConnections). - Bulk Actions menu (né Bulk Select) - Select All Devices / Connections / Zones / Text Boxes / Images / Everything, opening the same panel scoped to all objects of a type for canvas-wide changes (e.g. set every device to 80px, recolor all connections, set font size/color across all labels, annotations and text boxes at once). Renamed once it grew non-selection operations (Recolor, Map Details/Label), with "Select" prefixed onto the selection entries to keep the two kinds distinct.
- Controls default to "keep"/blank so opening the panel changes nothing; each applied control is a single undo step. Device color reuses the SVG icon tint (raster icons are skipped).
- Ctrl/Shift-click multi-select for every type - connections, text boxes and images now join devices and zones in a multi-selection (Ctrl toggles, Shift adds), and selections accumulate across types so mixed sets batch-edit together.
- Ctrl/Cmd-click now removes an already-selected item - the toggle's "remove" half never actually fired: clicking any item already in the selection started a drag of the whole set before the modifier logic ran, so you could Ctrl-click items in but never back out (the exact snag when a marquee over child zones also grabbed two parents). A modifier-click now falls through to the per-object toggle - Ctrl/Cmd removes a selected item (or adds an unselected one); Shift only ever adds. A plain click on a selected item still begins the multi-drag, unchanged.
- Alt+drag on a zone forces a marquee - rubber-band the child zones and devices inside a parent zone without the parent hijacking the drag as a move. Scoped to zones, so Alt+drag on a device keeps its existing fine (¼-grid) move / bypass-guides behavior. Complements locking the Zones tier (Layers menu) for the same "select inside a zone" goal.
- Image size in batch - an Images section (and Bulk Actions → Select All Images) sets image width while scaling height to preserve each image's aspect ratio.
- Batch icon swap - a Swap Icon dropdown in the Devices section re-icons every selected device at once (sets template/image, clears tint), mirroring the single-device swap.
- Batch number fields (device size, zone opacity, image width) read "- keep -" instead of "Mixed", matching the select controls.
- Diamond (Decision), Parallelogram (Data), Pill (Start/End), Document and Cylinder (Database) zone shapes alongside rectangle and ellipse - resizable, connectable via attachment points, labels centered inside the shape by convention, drawn in exports. New sidebar thumbnails drop them with flowchart-friendly proportions and default names.
- Shape selector in Zone Properties - convert any existing zone between the five shapes; size, label and attachment points are kept.
- Group / Ungroup (Ctrl+G / Ctrl+Shift+G, also in the right-click menu) - grouped objects select and move as a unit: clicking any member selects the whole group (dashed outline), Ctrl+click drills into a single member, Shift+click adds the group to the current selection, and a marquee touching any member takes the whole group.
- Groups are persistent multi-selections under the hood, so align/distribute, batch edit, copy/paste (re-created with new ids), delete and undo all work on them with the existing machinery.
- Saved in the diagram file (older files load with none); members deleted from the canvas are pruned, and a group dissolves below two members. No nesting.
- Context menu on the canvas: Undo/Redo, Copy, Paste, Delete, layer order (bring to front/forward, send backward/back), and an Align / Distribute submenu when 2+ objects are selected; empty-canvas menu offers Paste and Select All. Right-clicking an unselected object selects it; right-clicking within a multi-selection keeps it.
- Shows keyboard shortcuts alongside items, disables unavailable actions (Undo/Redo/Paste), stays on-screen near edges, and is dark-mode styled.
isSafeImageURLrestricts all image ingestion todata:image/URLs (diagram JSON, localStorage, bundled library).- IIS
web.config- CSP, X-Content-Type-Options, X-Frame-Options, and MIME maps for.js/.json/.svg. - DOMParser label parsing so untrusted Gliffy label HTML is inert (no scripts/
onerror/subresource loads) regardless of CSP - closes thefile://gap. - Decompression-bomb guard on Visio import - the in-browser unzip caps decompressed output (40 MB per part, 100 MB total) and aborts mid-stream, so a malicious/malformed
.vsdxfails with an alert instead of exhausting memory. sanitizedTitle()applied to PNG/JPEG/PDF export filenames.
- Removed one-off sample files; ignore
Icons/and the reference example folders.