0065. Wrapped message labels, tags and activations on the canvas in sequence diagrams
Status: accepted (sequence-wrap-tags-and-activation change, 29 October 2026). Extends decisions 0019 and 0060, and wraps text with the measure of decision 0064.
Context
Section titled “Context”The owner reported three problems after drawing a sequence diagram on the canvas. First, a head
dragged towards its neighbour landed short: the status line said Gap before participant1: -300 written, 106 drawn: the labels need the room., because the label “Esto es una prueba de un texto
Largo”, sent across another lifeline, was measured and drawn on one line and its whole width held
the two columns apart; a \n written in a message label was drawn as a space. Second, sequence
diagrams had no way to mark a message as “PKCE”, “v2” or “idempotent”, or a participant as
“external”: tag was a flow declaration only, refused in a sequence diagram. Third, an activation
was code only: Alt+A and the context menu toggled one, but there was no button and no inspector
field, a bar was a rectangle inside its lifeline’s group that a press could not find, and removing
an activate left its deactivate unmatched, which the edit refused.
The base was ready for each: wrapLines breaks a text at a width with the layout’s measurer
(decision 0064), the canvas’s inline editor already wraps a sticky’s text, the flow tag’s pill,
colours and font exist, and moveEvent relocates an event statement with its comments.
Decision
Section titled “Decision”- A label keeps its line until its participants are pulled closer (D1). The label of a
message between two participants stays on one line and pushes its columns apart as before: that
is the pair’s natural distance. A pull, a negative sum of the
gapafter the left participant and thegapbefore the right one (what a head drag writes) or aspacingbelow the default by the difference, is taken from the natural distance, so a dragged head lands where it is dropped; the labels between the pair then wrap at the room left, the lifelines’ distance less 12 on each side and less the number’s disc (22) underautonumber, throughwrapLines: at each\n, then at spaces, a word wider than the room between its letters.\nalways breaks a line. A self-call’s label does not wrap: it stays beside the loop, broken at\nonly. The first version of this change wrapped every label at the heads’ distance; drawn, it made every diagram with long labels narrow and tall (the gallery’s “Asks for a fingerprint” on two lines with room to widen), and it was revised the same day. - A pull stops at the longest word (D2). A pair is drawn at the larger of its floor and its
natural distance plus its pull. The floor is half of each head plus 8, the longest word of every
label between them plus 24 (and the disc), the row of a message’s tags, self-calls and side
notes; a message spanning several columns pushes the natural columns with its whole label and
the pulled ones with its longest word.
layout.gap-floorkeeps its rule and message, the amount it names now measured from the natural distance. - Rows grow down (D3). A label of
nlines is a blockrelationLabelHeight + (n − 1) × stephigh,step1.25 times the label size; the arrow sits under the last line and the events below move down;label <message> at n% offset (dx, dy)moves the whole block without changing the room. The emitter draws one text item per line.LayoutResult.messageTextsgives each message’s room, lines, size, line step andwraps(true when a pull caps the room; false for a self-call and at the natural distance), for the editor. tag "Text"in the owner’s block (D4). A sequence message’s block (calls, replies, async and two-way messages, self-calls,return) and a participant’s block taketag "Text", repeated, in source order; in a call’s block it is the call’s, not an event. The model recordstags: readonly TagUse[]({ text, range, textRange }) on everyElementandRelation, empty when none and in every other profile. An empty text or a line break and any other form areprop.invalid-value; the same text twice on one owner isprop.duplicate;tagin a group block, a fragment or at the diagram level isprop.unknownwith the hint saying where tags go.tag <id> "Text"keeps its flow meaning.- Tags are annotations of their owner (D5). No id, no
style,class,rule,animate,linkor story target; coloured by the theme’stagandtagTextand sized bytagSize; drawn inside the owner’s scene group, so effects and stories take them with it. - A message’s tags sit on its arrow next to the source end (D6): a row of pills 4 apart, centred on the line, 6 from the arrow’s start (12 when numbered), the label lifted by half a pill and 2 and the row as much taller; the pair’s constraint holds the row plus 24, the disc and the arrowhead. A self-call’s row sits on the loop’s top segment and its label after it.
- A participant’s tags are a badge row over its head (D7): centred, their middle on the top
edge of a box kind, their bottom 4 above a figure; when any participant has tags, the head row
(and a head drawn by
create) starts lower by the room they need, and the column floor counts the wider of a head and its row. - Tags in the scene, the result and the JSON (D8). Each pill is a group of role
tagwith its owner’s id and its index (data-tagin the SVG), in the head group or after the message’s label group; the sketch passes take it; the label audit treats its rect as its text’s background and does not check its text against boxes.LayoutResult.tagsgives every owner’s pills. The model JSON writestagsafterclasseson every element and relation ([{ "text", "range" }],[]when none) and stays0.1; the model diff reports the parttags. - Activation bars are scene objects (D9). Each bar is its own group of role
eventinside its lifeline’s group, withbounds: a written bar has itsactivatekey as id, a call block’s bar the call’s key.LayoutResult.boxesholds the written bars, andLayoutResult.barsgives every bar{ participant, box, depth, from, to, block }. The drawing does not change. - Selecting, moving and removing an activation (D10). A press on a written bar selects its
activateand the status line names the participant and the messages it spans; a press on a call block’s bar selects the call. A selected written bar shows a handle above and below it; thebarEnddrag snaps to event rows and dropsmoveActivation { id, end, row }, which the session plans asmoveEventwith the new destination formto: { after | before: <event> }(exactly one ofdirectionandto, relocated and checked once), before the first event of a call’s block for a top handle on that call, and asinsertEventof adeactivatefor the bottom handle of an open bar.removeEventof anactivateor adeactivateremoves its pair in the same edit; Delete and Remove activation use it. A removal of several things reads every pair on the text first and removes each end by its key withalone: true, so no end is paired again with another activation’s after an earlier removal, and the other end of an activation a removed fragment or call cuts in two goes with it. - The session previews a bar’s end (D11).
moveActivationjoins the previewed actions on sequence diagrams (participant and event drags stay without previews); a refused move answers with itsreason, the handle turnsdanger, the status line says the reason and the drop writes nothing. - The Activate button and the inspector’s checkbox (D12). The sequence palette’s Activate
(“Activate the receiver (Alt+A)”) posts the same
toggleActivationas Alt+A, disabled without a message or participant selected and pressed whenRelationInfo.activationsays the receiver is activated after the message, written or by its block. The inspector of a message that is not areturnshows Activates the receiver, a new field typebooleandrawn as a checkbox, and on a call whose block activates the receiver it is checked, read-only, with a newInspectorField.notesaying why. - Typing a wrapped label (D13).
TextPlace.wrapisshrinkfor stickies andgrowfor a message label that wraps; withgrowthe inline editor is the room’s width at the zoom, wraps there, keeps its top on the label’s first line and grows down. Enter commits, Shift+Enter writes\n; a self-call’s label keeps the unwrapped editor. - Tags on the canvas (D14). T on the sequence profile and the Tag palette button post
addTag, which the session plans as the editaddTagwith the word “Tag” (“Marca”) for the selected message, else participant, and opens the editor on the new pill. A pill is not selectable on its own: a press selects its owner. A double click edits the pill (setTag, an empty textremoveTag); its context menu offers Edit tag and Remove tag; the inspector lists Tags astag.<n>text fields with Remove and an emptytag.newfield. The edit operationsaddTag,setTagandremoveTagwrite and remove the statements, creating a block (and declaring an implicit participant) or removing an emptied one, and refuse a diagram that is not a sequence diagram, a target that is no participant or message, a bad index, an empty or multi-line text and a repeated one. - Language services (D15). Completion offers
tagin message and participant blocks of sequence diagrams only; hover names a tag’s message or participant. - The sibling change (D16).
gantt-diagrams(decision 0066) touches the palette, the keys, the editor docs and the changelog too; the one applied second rebases on the first. - Motion (D17). The canvas’s motion tokens, zero under
prefers-reduced-motion: a new pill pops in with its editor, a removed one fades out, a hovered bar shows its outline, bar handles fade and scale in and slide from row to row, a refused row turnsdanger, the Activate button changes state over--mdg-motion-fast; the checkbox is the platform’s. - Packages (D18). The public surface above lives in
@meltemi/diagrams-model,@meltemi/diagrams-layout,@meltemi/diagrams-render,@meltemi/diagrams-edit,@meltemi/diagrams-editorand@meltemi/diagrams-canvas.
The owner took every recommendation on 29 October 2026, the first one revised once drawn: labels
keep their line and wrap when a gap or a spacing pulls their participants closer, badges sit
in a row over the head’s top edge, tags take the theme’s colours only, a message’s tags sit next
to the source end, a repeated tag is prop.duplicate, the end handle of an open bar writes a
deactivate, and a press on a call block’s bar selects the call.
Alternatives considered
Section titled “Alternatives considered”- Always wrapping at the heads’ distance (the first version of this change): drawn, every diagram with long labels became narrow and tall, though the owner’s complaint was the refused drag, not the width.
- One line, as before: a long label is a refused drag (
-300 written, 106 drawn). - Wrapping when pulled, the pull taken from the heads’ distance: the first drag of a pushed head would jump by the room the labels had taken.
- A maximum label width (a theme token or a
wrapproperty, as Mermaid’swrap): another number to tune that says nothing agapor aspacingdoes not. - Room for at most three lines, or no floor from labels at all: a label of sixty words would still hold its pair apart, or a single long word would be broken mid-word by a strong pull.
- Rows growing upward: the label would cover the previous row.
tags "A", "B"as one property: editing one tag would rewrite a list, and a comment could not sit beside a tag.tag <id> "Text" on <message>declarations, as flow’s: an id and a reference per word, and anonymous messages would need keys the text never writes.- Stereotypes in the label (
"«REST» Authorize"): a convention inside a string, which the wrap would break and the model could not tell from text. - Tags as elements with ids, or
rule tag { … }: styleable one by one at the price of ids, or one more rule kind where the theme already sets both colours. - Message tags above or below the label, or at the target end: they would read as part of the label, and the arrowhead is at the target.
- Participant badges at the top-right corner: they read as a count, and several would run off the head.
- Tags drawn with the role
label, or as top-level groups with synthetic ids: the canvas maps a label to its message’s editor, and effects and stories address groups by id and would leave the tags behind. - Hit-testing bars geometrically in the canvas, or a role
bar: a second hit path, or one more role for every consumer ofevent. - Moving a bar’s end as a batch of one-step moves: every intermediate text must compile, so a valid destination could be refused on the way, and a preview would compile once per step.
- A new edit operation
moveActivation, or tworemoveEvents from the session: a second relocation path that code actions and agents could not reuse, and the text’s own services would still meet the refusal. - No end handle on an open bar, or a press on a call block’s bar selecting the participant: closing a bar would need the text or Alt+A twice, and the bar is the call’s doing.
- An
enumoff/on for Activates the receiver, as effects are: a toggle button inside the Effects card, where a checkbox among the message’s fields says a fact of the message. - An unwrapped editor that the commit redraws wrapped, or an editor centred on the block: what is typed would not look like what is drawn, or the editor would move under the caret.
- A dedicated list field type for Tags, or a placing tool: a new value shape through the protocol and both inspectors, or one more armed state for an action that has a selection.
Consequences
Section titled “Consequences”- The language stays 0.2 and the model JSON 0.1:
tag "Text"in a block and thetagskey are additive. A\nin a message label, drawn as a space before, breaks the line. - Drawings without a pull do not change. A negative
gapis now measured from the natural distance, so a diagram with a negativegapand long labels between that pair draws it closer than before, its labels wrapped, andlayout.gap-floornames a different amount. The bars of sketched sequence diagrams jitter differently once regrouped: the corpus, acceptance, gallery and visual snapshots are re-recorded and looked at. Every model JSON gainstags. - New public surface:
TagUse,Element.tagsandRelation.tags,ElementJson.tagsandRelationJson.tags, the diff parttags;LayoutResult.messageTexts(MessageText),LayoutResult.bars(BarGeometry) andLayoutResult.tags, the scene roletagandGroupItem.tag;moveEvent.to,activationPair, and the operationsaddTag,setTagandremoveTag;TextPlace.wrap,RelationInfo.tagsandactivation,ElementInfo.tags,EventInfo.pair, the field typebooleanandInspectorField.note, and the canvas actionsaddTag,setTag,removeTagandmoveActivation. removeEventof anactivatenow removes a second statement, itsdeactivate, in the same undo step.- The corpus gains
valid/sequence/tags.meltemi, and the invalid cases ofprop.invalid-valueandprop.unknowngain the refused tags. - Not done: importing tags or wrapped labels from Mermaid (
%%{wrap}%%,<br/>) or PlantUML (stereotypes), tags with their own id, style or effect, and moving a call block’s bar by its ends, which the block decides.