diff --git a/apps/api/src/memo-list-service.ts b/apps/api/src/memo-list-service.ts
index 0332c6d7..88663c94 100644
--- a/apps/api/src/memo-list-service.ts
+++ b/apps/api/src/memo-list-service.ts
@@ -3,7 +3,7 @@ import {
DEFAULT_MEMO_TITLE,
docToText,
markdownToDoc,
- parseDiagramDocument,
+ getDiagramSummary,
type MemoSummary,
} from "@edgeever/shared";
import { parseJsonArray } from "./entity-utils";
@@ -69,7 +69,7 @@ export const mapMemoSummary = (row: MemoSummaryRow): MemoSummary => ({
row.excerpt ||
createExcerpt(row.content_text ?? "") ||
createExcerpt(docToText(markdownToDoc(row.content_markdown ?? ""))),
- diagramKind: parseDiagramDocument(row.content_markdown)?.kind ?? null,
+ ...getDiagramSummary(row.content_markdown),
tags: parseJsonArray(row.tags_json),
isPinned: Boolean(row.is_pinned),
isArchived: Boolean(row.is_archived),
diff --git a/apps/ios/EdgeEver/Resources/EditorBundle/index.html b/apps/ios/EdgeEver/Resources/EditorBundle/index.html
index 1b30cba3..ebff1f49 100644
--- a/apps/ios/EdgeEver/Resources/EditorBundle/index.html
+++ b/apps/ios/EdgeEver/Resources/EditorBundle/index.html
@@ -7,14 +7,14 @@
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
EdgeEver Editor
-
+`},"getStyles"),ET,eu=(ET=class{constructor(){this.d=""}moveTo(e,r){return this.d+=`M ${e} ${r} `,this}lineTo(e,r){return this.d+=`L ${e} ${r} `,this}horizontalTo(e){return this.d+=`H ${e} `,this}verticalTo(e){return this.d+=`V ${e} `,this}arcTo(e,r,n,i,s,a,o){return this.d+=`A ${e} ${r} ${n} ${i?1:0} ${s?1:0} ${a} ${o} `,this}build(){return this.d.trim()}},w(ET,"PathBuilder"),ET),AT,bvr=(AT=class{constructor(e,r=Ine()){this.textCache=new Map,this.svg=e,this.config=r}measureText(e){if(this.textCache.has(e))return this.textCache.get(e);const r=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(e),n=r.node().getBBox(),i={width:n.width,height:n.height};return r.remove(),this.textCache.set(e,i),i}renderTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-terminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s).attr("rx",10).attr("ry",10),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderNonTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-nonterminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderSequence(e,r){const n=r.map(u=>this.renderExpression(e,u));let i=0,s=0,a=0;for(const u of n)i+=u.dimensions.width,s=Math.max(s,u.dimensions.up),a=Math.max(a,u.dimensions.down);i+=(n.length-1)*this.config.horizontalSeparation;const o=e.append("g").attr("class","railroad-sequence");let l=0;for(let u=0;uthis.renderExpression(e,f));let i=0,s=0;for(const f of n)i=Math.max(i,f.dimensions.width),s+=f.dimensions.height;s+=(n.length-1)*this.config.verticalSeparation;const a=this.config.arcRadius,o=a*4,l=i+o,u=e.append("g").attr("class","railroad-choice");let h=0;const d=s/2;for(const f of n){const p=h,g=p+f.dimensions.up,m=a*2+(i-f.dimensions.width)/2;u.node().appendChild(f.element).setAttribute("transform",`translate(${m}, ${p})`);const v=new eu,x=g>d;g===d?v.moveTo(0,d).lineTo(m,g):v.moveTo(0,d).arcTo(a,a,0,!1,x,a,d+(x?a:-a)).lineTo(a,g-(x?a:-a)).arcTo(a,a,0,!1,!x,a*2,g).lineTo(m,g),u.append("path").attr("class","railroad-line").attr("d",v.build());const b=new eu,T=m+f.dimensions.width,C=l-a*2;g===d?b.moveTo(T,g).lineTo(l,d):b.moveTo(T,g).lineTo(C,g).arcTo(a,a,0,!1,!x,l-a,g+(x?-a:a)).lineTo(l-a,d+(x?a:-a)).arcTo(a,a,0,!1,x,l,d),u.append("path").attr("class","railroad-line").attr("d",b.build()),h+=f.dimensions.height+this.config.verticalSeparation}return{element:u.node(),dimensions:{width:l,height:s,up:d,down:s-d}}}renderOptional(e,r){const n=this.renderExpression(e,r),i=this.config.arcRadius,s=i*2,a=n.dimensions.width+i*4,o=n.dimensions.height+s,l=e.append("g").attr("class","railroad-optional"),u=i*2,h=s;l.node().appendChild(n.element).setAttribute("transform",`translate(${u}, ${h})`);const f=h+n.dimensions.up,p=new eu().moveTo(0,f).lineTo(i*2,f);l.append("path").attr("class","railroad-line").attr("d",p.build());const g=new eu().moveTo(u+n.dimensions.width,f).lineTo(a,f);l.append("path").attr("class","railroad-line").attr("d",g.build());const m=new eu().moveTo(0,f).arcTo(i,i,0,!1,!1,i,f-i).lineTo(i,i).arcTo(i,i,0,!1,!0,i*2,0).lineTo(a-i*2,0).arcTo(i,i,0,!1,!0,a-i,i).lineTo(a-i,f-i).arcTo(i,i,0,!1,!1,a,f);return l.append("path").attr("class","railroad-line").attr("d",m.build()),{element:l.node(),dimensions:{width:a,height:o,up:f,down:o-f}}}renderRepetition(e,r,n){const i=this.renderExpression(e,r),s=this.config.arcRadius,a=s*2,o=i.dimensions.width+s*4,l=n===0,u=i.dimensions.height+a+(l?a:0),h=e.append("g").attr("class","railroad-repetition"),d=s*2,f=l?a:0;h.node().appendChild(i.element).setAttribute("transform",`translate(${d}, ${f})`);const g=f+i.dimensions.up;h.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(0,g).lineTo(s*2,g).build()),h.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(d+i.dimensions.width,g).lineTo(o,g).build());const m=f+i.dimensions.height+s,y=new eu().moveTo(d+i.dimensions.width,g).arcTo(s,s,0,!1,!0,d+i.dimensions.width+s,g+s).lineTo(d+i.dimensions.width+s,m).arcTo(s,s,0,!1,!0,d+i.dimensions.width,m+s).lineTo(s*2,m+s).arcTo(s,s,0,!1,!0,s,m).lineTo(s,g+s).arcTo(s,s,0,!1,!0,s*2,g);if(h.append("path").attr("class","railroad-line").attr("d",y.build()),l){const v=new eu().moveTo(0,g).arcTo(s,s,0,!1,!1,s,g-s).lineTo(s,s).arcTo(s,s,0,!1,!0,s*2,0).lineTo(o-s*2,0).arcTo(s,s,0,!1,!0,o-s,s).lineTo(o-s,g-s).arcTo(s,s,0,!1,!1,o,g);h.append("path").attr("class","railroad-line").attr("d",v.build())}return{element:h.node(),dimensions:{width:o,height:u,up:g,down:u-g}}}renderSpecial(e,r){const n=this.measureText("? "+r+" ?"),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-special");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text("? "+r+" ?"),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderExpression(e,r){switch(r.type){case"terminal":return this.renderTerminal(e,r.value);case"nonterminal":return this.renderNonTerminal(e,r.name);case"sequence":return this.renderSequence(e,r.elements);case"choice":return this.renderChoice(e,r.alternatives);case"optional":return this.renderOptional(e,r.element);case"repetition":return this.renderRepetition(e,r.element,r.min);case"special":return this.renderSpecial(e,r.text);default:throw new Error(`Unknown node type: ${r.type}`)}}renderRule(e,r){const n=this.svg.append("g").attr("class","railroad-rule").attr("transform",`translate(0, ${r})`),i=e.name+" =",s=this.measureText(i).width+20,a=s+20,o=n.append("g"),l=this.renderExpression(o,e.definition),u=Math.max(20,l.dimensions.up),h=u-l.dimensions.up;return o.attr("transform",`translate(${a}, ${h})`),n.append("g").attr("class","railroad-rule-name-group").append("text").attr("class","railroad-rule-name").attr("x",0).attr("y",u).text(i),n.append("g").attr("class","railroad-start").append("circle").attr("cx",s).attr("cy",u).attr("r",this.config.markerRadius),n.append("g").attr("class","railroad-end").append("circle").attr("cx",a+l.dimensions.width+10).attr("cy",u).attr("r",this.config.markerRadius),n.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(s+this.config.markerRadius,u).lineTo(a,u).build()),n.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(a+l.dimensions.width,u).lineTo(a+l.dimensions.width+10-this.config.markerRadius,u).build()),{height:Math.max(40,h+l.dimensions.height+this.config.padding*2),width:a+l.dimensions.width+10+this.config.markerRadius}}renderDiagram(e){let r=this.config.padding,n=0;for(const i of e){const s=this.renderRule(i,r);r+=s.height+this.config.verticalSeparation,n=Math.max(n,s.width)}return{width:n+this.config.padding*2,height:r+this.config.padding}}},w(AT,"RailroadRenderer"),AT),eye=w((t,e,r)=>{Bs(t,e.height,e.width,r),t.attr("viewBox",`0 0 ${e.width} ${e.height}`)},"configureRailroadSvgSize"),xvr=w((t,e,r)=>{de.debug(`[Railroad] Rendering diagram
+`+t);try{const n=jo(e);n.attr("class","railroad-diagram");const i=mr().railroad,s=(i==null?void 0:i.useMaxWidth)??!0,a=Xi.getRules();if(de.debug(`[Railroad] Rendering ${a.length} rules`),a.length===0){de.warn("[Railroad] No rules to render"),eye(n,{height:100,width:200},s);return}const l=new bvr(n,Ine()).renderDiagram(a);eye(n,l,s),de.debug("[Railroad] Render complete")}catch(n){throw de.error("[Railroad] Render error:",n),n}},"draw"),SP={draw:xvr},wvr=Rre().Railroad.parser.LangiumParser,z2=w(t=>{switch(t.$type){case"RailroadTerminalExpr":return{type:"terminal",value:t.value};case"RailroadNonTerminalExpr":return{type:"nonterminal",name:t.name};case"RailroadSpecialExpr":return{type:"special",text:t.text};case"RailroadSequenceExpr":{const e=t.elements.map(z2);return e.length===1?e[0]:{type:"sequence",elements:e}}case"RailroadChoiceExpr":{const e=t.alternatives.map(z2);return e.length===1?e[0]:{type:"choice",alternatives:e}}case"RailroadOptionalExpr":return{type:"optional",element:z2(t.element)};case"RailroadOneOrMoreExpr":return{type:"repetition",element:z2(t.element),min:1,max:1/0};case"RailroadZeroOrMoreExpr":return{type:"repetition",element:z2(t.element),min:0,max:1/0};default:throw new Error(`Unsupported railroad expression: ${t.$type}`)}},"transformExpression"),Tvr=w(t=>({name:t.name,definition:z2(t.definition)}),"transformRule"),Cvr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Tvr(e)))},"populateDb"),kvr={parse:w(t=>{Xi.clear(),de.debug("[Railroad Parser] Starting Langium parse");const e=wvr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[Railroad Parser] Parsed rules:",r.rules.length),Cvr(r),de.debug("[Railroad Parser] Parse complete")},"parse"),parser:{yy:Xi}},Svr={parser:kvr,db:Xi,renderer:SP,styles:kP};const Evr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Svr},Symbol.toStringTag,{value:"Module"}));var Avr=_re().RailroadEbnf.parser.LangiumParser,VR=w(t=>{const e=t.alternatives.map(Mvr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformChoice"),Mvr=w(t=>{const e=t.elements.map(_vr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),lYe=w(t=>{switch(t.$type){case"EbnfTerminal":return{type:"terminal",value:t.value};case"EbnfNonTerminal":return{type:"nonterminal",name:t.name};case"EbnfSpecial":return{type:"special",text:t.text};case"EbnfGroup":return VR(t.element);case"EbnfOptional":return{type:"optional",element:VR(t.element)};case"EbnfRepetition":return{type:"repetition",element:VR(t.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${t.$type}`)}},"transformPrimary"),Rvr=w((t,e)=>{switch(e.$type){case"EbnfOptionalPostfix":return{type:"optional",element:t};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:t,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:t,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[t,{type:"terminal",value:"-"},lYe(e.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${e.$type}`)}},"transformPostfix"),_vr=w(t=>t.postfixes.reduce((e,r)=>Rvr(e,r),lYe(t.base)),"transformTerm"),Lvr=w(t=>({name:t.name,definition:VR(t.definition)}),"transformRule"),Ivr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Lvr(e)))},"populateDb"),Nvr={parse:w(t=>{Xi.clear(),de.debug("[EBNF Parser] Starting Langium parse");const e=Avr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[EBNF Parser] Parsed rules:",r.rules.length),Ivr(r),de.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:Xi}},Ovr={parser:Nvr,db:Xi,renderer:SP,styles:kP};const Dvr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Ovr},Symbol.toStringTag,{value:"Module"}));var Pvr=Lre().RailroadAbnf.parser.LangiumParser,Mj=w(t=>{const e=t.alternatives.map($vr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformAlternation"),$vr=w(t=>{const e=t.elements.map(Fvr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformConcatenation"),Bvr=w(t=>{if(t.includes("*")){const[r,n]=t.split("*"),i=r?parseInt(r,10):0,s=n?parseInt(n,10):1/0;return{min:i,max:s}}const e=parseInt(t,10);return{min:e,max:e}},"parseRepeat"),Fvr=w(t=>{const e=zvr(t.primary);if(!t.repeat)return e;const{min:r,max:n}=Bvr(t.repeat);return r===0&&n===1?{type:"optional",element:e}:{type:"repetition",element:e,min:r,max:n}},"transformElement"),zvr=w(t=>{switch(t.$type){case"AbnfStringLiteral":return{type:"terminal",value:t.value};case"AbnfNumVal":return{type:"terminal",value:t.value};case"AbnfRuleName":return{type:"nonterminal",name:t.name};case"AbnfGroup":return Mj(t.element);case"AbnfOptionalGroup":return{type:"optional",element:Mj(t.element)};default:throw new Error(`Unsupported ABNF primary node: ${t.$type}`)}},"transformPrimary"),Vvr=w(t=>({name:t.name,definition:Mj(t.definition)}),"transformRule"),Gvr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Vvr(e)))},"populateDb"),Hvr={parse:w(t=>{Xi.clear(),de.debug("[ABNF Parser] Starting Langium parse");const e=Pvr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[ABNF Parser] Parsed rules:",r.rules.length),Gvr(r),de.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:Xi}},Wvr={parser:Hvr,db:Xi,renderer:SP,styles:kP};const qvr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Wvr},Symbol.toStringTag,{value:"Module"}));var Uvr=Ire().RailroadPeg.parser.LangiumParser,cYe=w(t=>{const e=t.alternatives.map(Yvr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformOrderedChoice"),Yvr=w(t=>{const e=t.elements.map(jvr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),jvr=w(t=>{const e=Xvr(t.suffix);return t.operator?{type:"special",text:t.operator==="&"?`&${tye(e)}`:`!${tye(e)}`}:e},"transformPrefix"),tye=w(t=>{switch(t.type){case"terminal":return`"${t.value}"`;case"nonterminal":return t.name;case"special":return t.text;default:return"(...)"}},"nodeToLabel"),Xvr=w(t=>{const e=Kvr(t.primary);if(!t.operator)return e;switch(t.operator){case"?":return{type:"optional",element:e};case"*":return{type:"repetition",element:e,min:0,max:1/0};case"+":return{type:"repetition",element:e,min:1,max:1/0};default:throw new Error(`Unsupported PEG suffix operator: ${t.operator}`)}},"transformSuffix"),Kvr=w(t=>{switch(t.$type){case"PegLiteral":return{type:"terminal",value:t.value};case"PegIdentifier":return{type:"nonterminal",name:t.name};case"PegGroup":return cYe(t.element);case"PegAny":return{type:"special",text:t.dot};default:throw new Error(`Unsupported PEG primary node: ${t.$type}`)}},"transformPrimary"),Zvr=w(t=>({name:t.name,definition:cYe(t.definition)}),"transformRule"),Qvr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Zvr(e)))},"populateDb"),Jvr={parse:w(t=>{Xi.clear(),de.debug("[PEG Parser] Starting Langium parse");const e=Uvr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[PEG Parser] Parsed rules:",r.rules.length),Qvr(r),de.debug("[PEG Parser] Parse complete")},"parse"),parser:{yy:Xi}},e2r={parser:Jvr,db:Xi,renderer:SP,styles:kP};const t2r=Object.freeze(Object.defineProperty({__proto__:null,diagram:e2r},Symbol.toStringTag,{value:"Module"}));var r2r=1;function Nne(){if(!(typeof globalThis>"u"))return globalThis}w(Nne,"getCaptureGlobal");function uYe(){var t;return!!((t=Nne())!=null&&t.mermaidCaptureSizes)}w(uYe,"shouldCaptureSizes");function hYe(){return typeof location>"u"?"browser-dev":`${location.pathname}${location.search}`}w(hYe,"capturedFromLocation");function dYe(t,e){const r=Nne();if(!r)return;const n=e.node(),i=(n&&"ownerSVGElement"in n?n.ownerSVGElement:null)??n,s=(i==null?void 0:i.id)??"(unknown)";r.mermaidCapturedSizes??(r.mermaidCapturedSizes=[]);const a={svgId:s,sizes:t};r.mermaidCapturedSizes.push(a),r.mermaidLastCapturedSizes=a}w(dYe,"emitCapturedSizes");function fYe(t,e){const r=[];for(const n of e.nodes)n.isGroup||r.push({id:n.id,width:n.width??0,height:n.height??0});r.length!==0&&dYe({metadata:{captureVersion:r2r,capturedAt:new Date().toISOString(),capturedFrom:hYe()},nodes:r},t)}w(fYe,"captureNodeSizes");const n2r=Object.freeze(Object.defineProperty({__proto__:null,captureNodeSizes:fYe,shouldCaptureSizes:uYe},Symbol.toStringTag,{value:"Module"})),i2r=Object.freeze(Object.defineProperty({__proto__:null,InfoModule:AGe,createInfoServices:MGe},Symbol.toStringTag,{value:"Module"})),s2r=Object.freeze(Object.defineProperty({__proto__:null,PacketModule:RGe,createPacketServices:_Ge},Symbol.toStringTag,{value:"Module"})),a2r=Object.freeze(Object.defineProperty({__proto__:null,PieModule:LGe,createPieServices:IGe},Symbol.toStringTag,{value:"Module"})),o2r=Object.freeze(Object.defineProperty({__proto__:null,TreeViewModule:NGe,createTreeViewServices:OGe},Symbol.toStringTag,{value:"Module"})),l2r=Object.freeze(Object.defineProperty({__proto__:null,ArchitectureModule:DGe,createArchitectureServices:PGe},Symbol.toStringTag,{value:"Module"})),c2r=Object.freeze(Object.defineProperty({__proto__:null,GitGraphModule:SGe,createGitGraphServices:EGe},Symbol.toStringTag,{value:"Module"})),u2r=Object.freeze(Object.defineProperty({__proto__:null,EventModelingModule:BGe,createEventModelingServices:FGe},Symbol.toStringTag,{value:"Module"})),h2r=Object.freeze(Object.defineProperty({__proto__:null,RadarModule:dGe,createRadarServices:fGe},Symbol.toStringTag,{value:"Module"})),d2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadModule:pGe,createRailroadServices:Rre},Symbol.toStringTag,{value:"Module"})),f2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadEbnfModule:gGe,createRailroadEbnfServices:_re},Symbol.toStringTag,{value:"Module"})),p2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadAbnfModule:mGe,createRailroadAbnfServices:Lre},Symbol.toStringTag,{value:"Module"})),g2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadPegModule:yGe,createRailroadPegServices:Ire},Symbol.toStringTag,{value:"Module"})),m2r=Object.freeze(Object.defineProperty({__proto__:null,TreemapModule:bGe,createTreemapServices:xGe},Symbol.toStringTag,{value:"Module"})),y2r=Object.freeze(Object.defineProperty({__proto__:null,WardleyModule:wGe,createWardleyServices:TGe},Symbol.toStringTag,{value:"Module"})),v2r=Object.freeze(Object.defineProperty({__proto__:null,CynefinModule:CGe,createCynefinServices:kGe},Symbol.toStringTag,{value:"Module"}));
diff --git a/apps/ios/EditorBundle/index.html b/apps/ios/EditorBundle/index.html
index 1b30cba3..ebff1f49 100644
--- a/apps/ios/EditorBundle/index.html
+++ b/apps/ios/EditorBundle/index.html
@@ -7,14 +7,14 @@
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
EdgeEver Editor
-
+`},"getStyles"),ET,eu=(ET=class{constructor(){this.d=""}moveTo(e,r){return this.d+=`M ${e} ${r} `,this}lineTo(e,r){return this.d+=`L ${e} ${r} `,this}horizontalTo(e){return this.d+=`H ${e} `,this}verticalTo(e){return this.d+=`V ${e} `,this}arcTo(e,r,n,i,s,a,o){return this.d+=`A ${e} ${r} ${n} ${i?1:0} ${s?1:0} ${a} ${o} `,this}build(){return this.d.trim()}},w(ET,"PathBuilder"),ET),AT,bvr=(AT=class{constructor(e,r=Ine()){this.textCache=new Map,this.svg=e,this.config=r}measureText(e){if(this.textCache.has(e))return this.textCache.get(e);const r=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(e),n=r.node().getBBox(),i={width:n.width,height:n.height};return r.remove(),this.textCache.set(e,i),i}renderTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-terminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s).attr("rx",10).attr("ry",10),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderNonTerminal(e,r){const n=this.measureText(r),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-nonterminal");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text(r),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderSequence(e,r){const n=r.map(u=>this.renderExpression(e,u));let i=0,s=0,a=0;for(const u of n)i+=u.dimensions.width,s=Math.max(s,u.dimensions.up),a=Math.max(a,u.dimensions.down);i+=(n.length-1)*this.config.horizontalSeparation;const o=e.append("g").attr("class","railroad-sequence");let l=0;for(let u=0;uthis.renderExpression(e,f));let i=0,s=0;for(const f of n)i=Math.max(i,f.dimensions.width),s+=f.dimensions.height;s+=(n.length-1)*this.config.verticalSeparation;const a=this.config.arcRadius,o=a*4,l=i+o,u=e.append("g").attr("class","railroad-choice");let h=0;const d=s/2;for(const f of n){const p=h,g=p+f.dimensions.up,m=a*2+(i-f.dimensions.width)/2;u.node().appendChild(f.element).setAttribute("transform",`translate(${m}, ${p})`);const v=new eu,x=g>d;g===d?v.moveTo(0,d).lineTo(m,g):v.moveTo(0,d).arcTo(a,a,0,!1,x,a,d+(x?a:-a)).lineTo(a,g-(x?a:-a)).arcTo(a,a,0,!1,!x,a*2,g).lineTo(m,g),u.append("path").attr("class","railroad-line").attr("d",v.build());const b=new eu,T=m+f.dimensions.width,C=l-a*2;g===d?b.moveTo(T,g).lineTo(l,d):b.moveTo(T,g).lineTo(C,g).arcTo(a,a,0,!1,!x,l-a,g+(x?-a:a)).lineTo(l-a,d+(x?a:-a)).arcTo(a,a,0,!1,x,l,d),u.append("path").attr("class","railroad-line").attr("d",b.build()),h+=f.dimensions.height+this.config.verticalSeparation}return{element:u.node(),dimensions:{width:l,height:s,up:d,down:s-d}}}renderOptional(e,r){const n=this.renderExpression(e,r),i=this.config.arcRadius,s=i*2,a=n.dimensions.width+i*4,o=n.dimensions.height+s,l=e.append("g").attr("class","railroad-optional"),u=i*2,h=s;l.node().appendChild(n.element).setAttribute("transform",`translate(${u}, ${h})`);const f=h+n.dimensions.up,p=new eu().moveTo(0,f).lineTo(i*2,f);l.append("path").attr("class","railroad-line").attr("d",p.build());const g=new eu().moveTo(u+n.dimensions.width,f).lineTo(a,f);l.append("path").attr("class","railroad-line").attr("d",g.build());const m=new eu().moveTo(0,f).arcTo(i,i,0,!1,!1,i,f-i).lineTo(i,i).arcTo(i,i,0,!1,!0,i*2,0).lineTo(a-i*2,0).arcTo(i,i,0,!1,!0,a-i,i).lineTo(a-i,f-i).arcTo(i,i,0,!1,!1,a,f);return l.append("path").attr("class","railroad-line").attr("d",m.build()),{element:l.node(),dimensions:{width:a,height:o,up:f,down:o-f}}}renderRepetition(e,r,n){const i=this.renderExpression(e,r),s=this.config.arcRadius,a=s*2,o=i.dimensions.width+s*4,l=n===0,u=i.dimensions.height+a+(l?a:0),h=e.append("g").attr("class","railroad-repetition"),d=s*2,f=l?a:0;h.node().appendChild(i.element).setAttribute("transform",`translate(${d}, ${f})`);const g=f+i.dimensions.up;h.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(0,g).lineTo(s*2,g).build()),h.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(d+i.dimensions.width,g).lineTo(o,g).build());const m=f+i.dimensions.height+s,y=new eu().moveTo(d+i.dimensions.width,g).arcTo(s,s,0,!1,!0,d+i.dimensions.width+s,g+s).lineTo(d+i.dimensions.width+s,m).arcTo(s,s,0,!1,!0,d+i.dimensions.width,m+s).lineTo(s*2,m+s).arcTo(s,s,0,!1,!0,s,m).lineTo(s,g+s).arcTo(s,s,0,!1,!0,s*2,g);if(h.append("path").attr("class","railroad-line").attr("d",y.build()),l){const v=new eu().moveTo(0,g).arcTo(s,s,0,!1,!1,s,g-s).lineTo(s,s).arcTo(s,s,0,!1,!0,s*2,0).lineTo(o-s*2,0).arcTo(s,s,0,!1,!0,o-s,s).lineTo(o-s,g-s).arcTo(s,s,0,!1,!1,o,g);h.append("path").attr("class","railroad-line").attr("d",v.build())}return{element:h.node(),dimensions:{width:o,height:u,up:g,down:u-g}}}renderSpecial(e,r){const n=this.measureText("? "+r+" ?"),i=n.width+this.config.padding*2,s=n.height+this.config.padding*2,a=e.append("g").attr("class","railroad-special");return a.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",s),a.append("text").attr("x",i/2).attr("y",s/2).text("? "+r+" ?"),{element:a.node(),dimensions:{width:i,height:s,up:s/2,down:s/2}}}renderExpression(e,r){switch(r.type){case"terminal":return this.renderTerminal(e,r.value);case"nonterminal":return this.renderNonTerminal(e,r.name);case"sequence":return this.renderSequence(e,r.elements);case"choice":return this.renderChoice(e,r.alternatives);case"optional":return this.renderOptional(e,r.element);case"repetition":return this.renderRepetition(e,r.element,r.min);case"special":return this.renderSpecial(e,r.text);default:throw new Error(`Unknown node type: ${r.type}`)}}renderRule(e,r){const n=this.svg.append("g").attr("class","railroad-rule").attr("transform",`translate(0, ${r})`),i=e.name+" =",s=this.measureText(i).width+20,a=s+20,o=n.append("g"),l=this.renderExpression(o,e.definition),u=Math.max(20,l.dimensions.up),h=u-l.dimensions.up;return o.attr("transform",`translate(${a}, ${h})`),n.append("g").attr("class","railroad-rule-name-group").append("text").attr("class","railroad-rule-name").attr("x",0).attr("y",u).text(i),n.append("g").attr("class","railroad-start").append("circle").attr("cx",s).attr("cy",u).attr("r",this.config.markerRadius),n.append("g").attr("class","railroad-end").append("circle").attr("cx",a+l.dimensions.width+10).attr("cy",u).attr("r",this.config.markerRadius),n.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(s+this.config.markerRadius,u).lineTo(a,u).build()),n.append("path").attr("class","railroad-line").attr("d",new eu().moveTo(a+l.dimensions.width,u).lineTo(a+l.dimensions.width+10-this.config.markerRadius,u).build()),{height:Math.max(40,h+l.dimensions.height+this.config.padding*2),width:a+l.dimensions.width+10+this.config.markerRadius}}renderDiagram(e){let r=this.config.padding,n=0;for(const i of e){const s=this.renderRule(i,r);r+=s.height+this.config.verticalSeparation,n=Math.max(n,s.width)}return{width:n+this.config.padding*2,height:r+this.config.padding}}},w(AT,"RailroadRenderer"),AT),eye=w((t,e,r)=>{Bs(t,e.height,e.width,r),t.attr("viewBox",`0 0 ${e.width} ${e.height}`)},"configureRailroadSvgSize"),xvr=w((t,e,r)=>{de.debug(`[Railroad] Rendering diagram
+`+t);try{const n=jo(e);n.attr("class","railroad-diagram");const i=mr().railroad,s=(i==null?void 0:i.useMaxWidth)??!0,a=Xi.getRules();if(de.debug(`[Railroad] Rendering ${a.length} rules`),a.length===0){de.warn("[Railroad] No rules to render"),eye(n,{height:100,width:200},s);return}const l=new bvr(n,Ine()).renderDiagram(a);eye(n,l,s),de.debug("[Railroad] Render complete")}catch(n){throw de.error("[Railroad] Render error:",n),n}},"draw"),SP={draw:xvr},wvr=Rre().Railroad.parser.LangiumParser,z2=w(t=>{switch(t.$type){case"RailroadTerminalExpr":return{type:"terminal",value:t.value};case"RailroadNonTerminalExpr":return{type:"nonterminal",name:t.name};case"RailroadSpecialExpr":return{type:"special",text:t.text};case"RailroadSequenceExpr":{const e=t.elements.map(z2);return e.length===1?e[0]:{type:"sequence",elements:e}}case"RailroadChoiceExpr":{const e=t.alternatives.map(z2);return e.length===1?e[0]:{type:"choice",alternatives:e}}case"RailroadOptionalExpr":return{type:"optional",element:z2(t.element)};case"RailroadOneOrMoreExpr":return{type:"repetition",element:z2(t.element),min:1,max:1/0};case"RailroadZeroOrMoreExpr":return{type:"repetition",element:z2(t.element),min:0,max:1/0};default:throw new Error(`Unsupported railroad expression: ${t.$type}`)}},"transformExpression"),Tvr=w(t=>({name:t.name,definition:z2(t.definition)}),"transformRule"),Cvr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Tvr(e)))},"populateDb"),kvr={parse:w(t=>{Xi.clear(),de.debug("[Railroad Parser] Starting Langium parse");const e=wvr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[Railroad Parser] Parsed rules:",r.rules.length),Cvr(r),de.debug("[Railroad Parser] Parse complete")},"parse"),parser:{yy:Xi}},Svr={parser:kvr,db:Xi,renderer:SP,styles:kP};const Evr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Svr},Symbol.toStringTag,{value:"Module"}));var Avr=_re().RailroadEbnf.parser.LangiumParser,VR=w(t=>{const e=t.alternatives.map(Mvr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformChoice"),Mvr=w(t=>{const e=t.elements.map(_vr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),lYe=w(t=>{switch(t.$type){case"EbnfTerminal":return{type:"terminal",value:t.value};case"EbnfNonTerminal":return{type:"nonterminal",name:t.name};case"EbnfSpecial":return{type:"special",text:t.text};case"EbnfGroup":return VR(t.element);case"EbnfOptional":return{type:"optional",element:VR(t.element)};case"EbnfRepetition":return{type:"repetition",element:VR(t.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${t.$type}`)}},"transformPrimary"),Rvr=w((t,e)=>{switch(e.$type){case"EbnfOptionalPostfix":return{type:"optional",element:t};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:t,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:t,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[t,{type:"terminal",value:"-"},lYe(e.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${e.$type}`)}},"transformPostfix"),_vr=w(t=>t.postfixes.reduce((e,r)=>Rvr(e,r),lYe(t.base)),"transformTerm"),Lvr=w(t=>({name:t.name,definition:VR(t.definition)}),"transformRule"),Ivr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Lvr(e)))},"populateDb"),Nvr={parse:w(t=>{Xi.clear(),de.debug("[EBNF Parser] Starting Langium parse");const e=Avr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[EBNF Parser] Parsed rules:",r.rules.length),Ivr(r),de.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:Xi}},Ovr={parser:Nvr,db:Xi,renderer:SP,styles:kP};const Dvr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Ovr},Symbol.toStringTag,{value:"Module"}));var Pvr=Lre().RailroadAbnf.parser.LangiumParser,Mj=w(t=>{const e=t.alternatives.map($vr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformAlternation"),$vr=w(t=>{const e=t.elements.map(Fvr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformConcatenation"),Bvr=w(t=>{if(t.includes("*")){const[r,n]=t.split("*"),i=r?parseInt(r,10):0,s=n?parseInt(n,10):1/0;return{min:i,max:s}}const e=parseInt(t,10);return{min:e,max:e}},"parseRepeat"),Fvr=w(t=>{const e=zvr(t.primary);if(!t.repeat)return e;const{min:r,max:n}=Bvr(t.repeat);return r===0&&n===1?{type:"optional",element:e}:{type:"repetition",element:e,min:r,max:n}},"transformElement"),zvr=w(t=>{switch(t.$type){case"AbnfStringLiteral":return{type:"terminal",value:t.value};case"AbnfNumVal":return{type:"terminal",value:t.value};case"AbnfRuleName":return{type:"nonterminal",name:t.name};case"AbnfGroup":return Mj(t.element);case"AbnfOptionalGroup":return{type:"optional",element:Mj(t.element)};default:throw new Error(`Unsupported ABNF primary node: ${t.$type}`)}},"transformPrimary"),Vvr=w(t=>({name:t.name,definition:Mj(t.definition)}),"transformRule"),Gvr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Vvr(e)))},"populateDb"),Hvr={parse:w(t=>{Xi.clear(),de.debug("[ABNF Parser] Starting Langium parse");const e=Pvr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[ABNF Parser] Parsed rules:",r.rules.length),Gvr(r),de.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:Xi}},Wvr={parser:Hvr,db:Xi,renderer:SP,styles:kP};const qvr=Object.freeze(Object.defineProperty({__proto__:null,diagram:Wvr},Symbol.toStringTag,{value:"Module"}));var Uvr=Ire().RailroadPeg.parser.LangiumParser,cYe=w(t=>{const e=t.alternatives.map(Yvr);return e.length===1?e[0]:{type:"choice",alternatives:e}},"transformOrderedChoice"),Yvr=w(t=>{const e=t.elements.map(jvr);return e.length===1?e[0]:{type:"sequence",elements:e}},"transformSequence"),jvr=w(t=>{const e=Xvr(t.suffix);return t.operator?{type:"special",text:t.operator==="&"?`&${tye(e)}`:`!${tye(e)}`}:e},"transformPrefix"),tye=w(t=>{switch(t.type){case"terminal":return`"${t.value}"`;case"nonterminal":return t.name;case"special":return t.text;default:return"(...)"}},"nodeToLabel"),Xvr=w(t=>{const e=Kvr(t.primary);if(!t.operator)return e;switch(t.operator){case"?":return{type:"optional",element:e};case"*":return{type:"repetition",element:e,min:0,max:1/0};case"+":return{type:"repetition",element:e,min:1,max:1/0};default:throw new Error(`Unsupported PEG suffix operator: ${t.operator}`)}},"transformSuffix"),Kvr=w(t=>{switch(t.$type){case"PegLiteral":return{type:"terminal",value:t.value};case"PegIdentifier":return{type:"nonterminal",name:t.name};case"PegGroup":return cYe(t.element);case"PegAny":return{type:"special",text:t.dot};default:throw new Error(`Unsupported PEG primary node: ${t.$type}`)}},"transformPrimary"),Zvr=w(t=>({name:t.name,definition:cYe(t.definition)}),"transformRule"),Qvr=w(t=>{$l(t,Xi),t.title&&Xi.setTitle(t.title),t.rules.map(e=>Xi.addRule(Zvr(e)))},"populateDb"),Jvr={parse:w(t=>{Xi.clear(),de.debug("[PEG Parser] Starting Langium parse");const e=Uvr.parse(t);if(e.lexerErrors.length>0||e.parserErrors.length>0)throw new G7(e);const r=e.value;de.debug("[PEG Parser] Parsed rules:",r.rules.length),Qvr(r),de.debug("[PEG Parser] Parse complete")},"parse"),parser:{yy:Xi}},e2r={parser:Jvr,db:Xi,renderer:SP,styles:kP};const t2r=Object.freeze(Object.defineProperty({__proto__:null,diagram:e2r},Symbol.toStringTag,{value:"Module"}));var r2r=1;function Nne(){if(!(typeof globalThis>"u"))return globalThis}w(Nne,"getCaptureGlobal");function uYe(){var t;return!!((t=Nne())!=null&&t.mermaidCaptureSizes)}w(uYe,"shouldCaptureSizes");function hYe(){return typeof location>"u"?"browser-dev":`${location.pathname}${location.search}`}w(hYe,"capturedFromLocation");function dYe(t,e){const r=Nne();if(!r)return;const n=e.node(),i=(n&&"ownerSVGElement"in n?n.ownerSVGElement:null)??n,s=(i==null?void 0:i.id)??"(unknown)";r.mermaidCapturedSizes??(r.mermaidCapturedSizes=[]);const a={svgId:s,sizes:t};r.mermaidCapturedSizes.push(a),r.mermaidLastCapturedSizes=a}w(dYe,"emitCapturedSizes");function fYe(t,e){const r=[];for(const n of e.nodes)n.isGroup||r.push({id:n.id,width:n.width??0,height:n.height??0});r.length!==0&&dYe({metadata:{captureVersion:r2r,capturedAt:new Date().toISOString(),capturedFrom:hYe()},nodes:r},t)}w(fYe,"captureNodeSizes");const n2r=Object.freeze(Object.defineProperty({__proto__:null,captureNodeSizes:fYe,shouldCaptureSizes:uYe},Symbol.toStringTag,{value:"Module"})),i2r=Object.freeze(Object.defineProperty({__proto__:null,InfoModule:AGe,createInfoServices:MGe},Symbol.toStringTag,{value:"Module"})),s2r=Object.freeze(Object.defineProperty({__proto__:null,PacketModule:RGe,createPacketServices:_Ge},Symbol.toStringTag,{value:"Module"})),a2r=Object.freeze(Object.defineProperty({__proto__:null,PieModule:LGe,createPieServices:IGe},Symbol.toStringTag,{value:"Module"})),o2r=Object.freeze(Object.defineProperty({__proto__:null,TreeViewModule:NGe,createTreeViewServices:OGe},Symbol.toStringTag,{value:"Module"})),l2r=Object.freeze(Object.defineProperty({__proto__:null,ArchitectureModule:DGe,createArchitectureServices:PGe},Symbol.toStringTag,{value:"Module"})),c2r=Object.freeze(Object.defineProperty({__proto__:null,GitGraphModule:SGe,createGitGraphServices:EGe},Symbol.toStringTag,{value:"Module"})),u2r=Object.freeze(Object.defineProperty({__proto__:null,EventModelingModule:BGe,createEventModelingServices:FGe},Symbol.toStringTag,{value:"Module"})),h2r=Object.freeze(Object.defineProperty({__proto__:null,RadarModule:dGe,createRadarServices:fGe},Symbol.toStringTag,{value:"Module"})),d2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadModule:pGe,createRailroadServices:Rre},Symbol.toStringTag,{value:"Module"})),f2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadEbnfModule:gGe,createRailroadEbnfServices:_re},Symbol.toStringTag,{value:"Module"})),p2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadAbnfModule:mGe,createRailroadAbnfServices:Lre},Symbol.toStringTag,{value:"Module"})),g2r=Object.freeze(Object.defineProperty({__proto__:null,RailroadPegModule:yGe,createRailroadPegServices:Ire},Symbol.toStringTag,{value:"Module"})),m2r=Object.freeze(Object.defineProperty({__proto__:null,TreemapModule:bGe,createTreemapServices:xGe},Symbol.toStringTag,{value:"Module"})),y2r=Object.freeze(Object.defineProperty({__proto__:null,WardleyModule:wGe,createWardleyServices:TGe},Symbol.toStringTag,{value:"Module"})),v2r=Object.freeze(Object.defineProperty({__proto__:null,CynefinModule:CGe,createCynefinServices:kGe},Symbol.toStringTag,{value:"Module"}));
diff --git a/apps/ios/EditorSource/src/main.ts b/apps/ios/EditorSource/src/main.ts
index ea67dd82..5a5524de 100644
--- a/apps/ios/EditorSource/src/main.ts
+++ b/apps/ios/EditorSource/src/main.ts
@@ -17,6 +17,7 @@ import { toCanvas } from "html-to-image";
import {
createNativeUnsupportedContentExtensions,
diagramDocumentToX6Cells,
+ attachDiagramReader,
diagramFallbackMarkdown,
docToMarkdown,
NativeAttachmentMetadata,
@@ -372,12 +373,18 @@ async function renderMermaidBlocks(root: HTMLElement, theme: "light" | "dark") {
}
}
+let viewerDiagramReader: ReturnType | null = null;
+let viewerDiagramFrame: number | null = null;
let viewerDiagram: DiagramDocument | null = null;
let viewerDiagramGraph: Graph | null = null;
let viewerDiagramObserver: ResizeObserver | null = null;
let viewerDiagramContainer: HTMLElement | null = null;
function clearViewerDiagramGraph() {
+ if (viewerDiagramFrame !== null) cancelAnimationFrame(viewerDiagramFrame);
+ viewerDiagramFrame = null;
+ viewerDiagramReader?.dispose();
+ viewerDiagramReader = null;
viewerDiagramObserver?.disconnect();
viewerDiagramObserver = null;
viewerDiagramGraph?.dispose();
@@ -421,16 +428,14 @@ function renderViewerDiagram(root: HTMLElement, diagram: DiagramDocument, theme:
grid: false,
interacting: false,
panning: { enabled: true },
- mousewheel: { enabled: true, minScale: 0.35, maxScale: 2 },
+ mousewheel: { enabled: true, minScale: 0.1, maxScale: 2.5 },
});
graph.addNodes(cells.nodes);
graph.addEdges(cells.edges);
- const fit = () => {
- graph.resize(measureWidth(), Math.max(1, container.clientHeight));
- graph.zoomToFit({ maxScale: 1.05, padding: 28 });
- graph.centerContent();
- };
- requestAnimationFrame(fit);
+ const reader = attachDiagramReader(graph, container, { ...diagram, nodes: diagram.nodes.map((node, index) => ({ ...node, width: cells.nodes[index].width, height: cells.nodes[index].height })) }, locale, theme === "dark");
+ viewerDiagramReader = reader;
+ const fit = () => reader.resize(measureWidth(), Math.max(1, container.clientHeight));
+ viewerDiagramFrame = requestAnimationFrame(fit);
const observer = new ResizeObserver(fit);
observer.observe(parent ?? container);
viewerDiagramGraph = graph;
diff --git a/apps/ios/Tests/EdgeEverTests/TipTapContentSourceTests.swift b/apps/ios/Tests/EdgeEverTests/TipTapContentSourceTests.swift
index 4b5e16ca..8ee9151b 100644
--- a/apps/ios/Tests/EdgeEverTests/TipTapContentSourceTests.swift
+++ b/apps/ios/Tests/EdgeEverTests/TipTapContentSourceTests.swift
@@ -289,7 +289,7 @@ final class TipTapContentSourceTests: XCTestCase {
}
let samples = [
envelope(#"{"schemaVersion":1,"kind":"mind-map","nodes":[{"id":"a","label":"核心主题","x":0,"y":0,"width":100,"height":40,"shape":"topic"},{"id":"b","label":"分支主题","x":160,"y":0,"width":100,"height":40,"shape":"topic","parentId":"a"}],"edges":[{"id":"e","source":"a","target":"b"}]}"#),
- envelope(#"{"schemaVersion":1,"kind":"flowchart","nodes":[{"id":"a","label":"开始","x":0,"y":0,"width":100,"height":40,"shape":"terminator"},{"id":"b","label":"处理步骤","x":0,"y":100,"width":100,"height":40,"shape":"process"}],"edges":[{"id":"e","source":"a","target":"b"}]}"#),
+ envelope(#"{"schemaVersion":1,"kind":"flowchart","nodes":[{"id":"a","label":"开始","x":0,"y":0,"width":100,"height":40,"shape":"terminator"},{"id":"b","label":"Transformer 前向计算与因果注意力处理步骤","x":0,"y":1600,"width":100,"height":40,"shape":"process"}],"edges":[{"id":"e","source":"a","target":"b"}]}"#),
envelope(#"{"schemaVersion":2,"kind":"architecture","nodes":[{"id":"system","label":"应用系统","x":0,"y":0,"width":500,"height":300,"shape":"boundary"},{"id":"api","label":"API 服务","x":40,"y":40,"width":156,"height":64,"shape":"service","parentId":"system"},{"id":"db","label":"数据库","x":260,"y":40,"width":150,"height":72,"shape":"database","parentId":"system"}],"edges":[{"id":"query","source":"api","target":"db","label":"查询","kind":"data"}]}"#),
]
@@ -321,6 +321,21 @@ final class TipTapContentSourceTests: XCTestCase {
)
XCTAssertGreaterThan(graphWidth, 300, "X6 must occupy the viewer width instead of collapsing")
XCTAssertGreaterThan(nodeCount, 0, "X6 must materialize diagram nodes")
+ let controlsCount = try await evalInt(webView, "document.querySelectorAll('.edgeever-diagram-reader-controls').length")
+ XCTAssertEqual(controlsCount, 1, "reusing a viewer must not accumulate controls")
+ let hasReadingButton = try await evalBool(webView, "!!document.querySelector('button[aria-label=\"从起点阅读\"]')")
+ if hasReadingButton {
+ _ = try await eval(webView, "document.querySelector('button[aria-label=\"从起点阅读\"]').click()")
+ let readingScale = try await evalInt(webView, "parseInt(document.querySelector('button[aria-label=\"恢复 100%\"]').textContent)")
+ XCTAssertEqual(readingScale, 100)
+ _ = try await eval(webView, "document.querySelector('button[aria-label=\"放大\"]').click()")
+ let enlargedScale = try await evalInt(webView, "parseInt(document.querySelector('button[aria-label=\"恢复 100%\"]').textContent)")
+ XCTAssertEqual(enlargedScale, 125)
+ _ = try await eval(webView, "document.querySelector('button[aria-label=\"适应画布\"]').click()")
+ let overviewScale = try await evalInt(webView, "parseInt(document.querySelector('button[aria-label=\"恢复 100%\"]').textContent)")
+ XCTAssertLessThan(overviewScale, 80)
+ }
+
let leakedLegacyFallback = try await evalBool(webView, "document.body.innerText.includes('node list only')")
XCTAssertFalse(leakedLegacyFallback)
let leakedCodeAffordance = try await evalBool(webView, "document.body.innerText.includes('Copy code')")
diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx
index d2b0f716..65a0917f 100644
--- a/apps/mobile/src/components/LocalTiptapEditor.tsx
+++ b/apps/mobile/src/components/LocalTiptapEditor.tsx
@@ -35,6 +35,7 @@ import {
ImageGallery,
getResourceIdFromUrl,
diagramDocumentToX6Cells,
+ attachDiagramReader,
type AiAction,
type AiPromptParameterKind,
type AiPromptResultMode,
@@ -487,25 +488,23 @@ const ReadOnlyX6Diagram = ({
grid: false,
interacting: false,
panning: { enabled: true },
- mousewheel: { enabled: true, minScale: 0.35, maxScale: 2 },
+ mousewheel: { enabled: true, minScale: 0.1, maxScale: 2.5 },
});
graph.addNodes(cells.nodes);
graph.addEdges(cells.edges);
- const fit = () => {
- graph.resize(measureWidth(), Math.max(1, container.clientHeight));
- graph.zoomToFit({ maxScale: 1.05, padding: 28 });
- graph.centerContent();
- };
+ const reader = attachDiagramReader(graph, container, { ...diagram, nodes: diagram.nodes.map((node, index) => ({ ...node, width: cells.nodes[index].width, height: cells.nodes[index].height })) }, locale, theme === "dark");
+ const fit = () => reader.resize(measureWidth(), Math.max(1, container.clientHeight));
const frame = window.requestAnimationFrame(fit);
const observer = new ResizeObserver(fit);
observer.observe(parent ?? container);
return () => {
window.cancelAnimationFrame(frame);
observer.disconnect();
+ reader.dispose();
graph.dispose();
};
- }, [diagram, theme]);
+ }, [diagram, theme, locale]);
const title = locale === "en-US"
? diagram.kind === "mind-map" ? "Mind map" : diagram.kind === "architecture" ? "Architecture diagram" : "Flowchart"
diff --git a/apps/web/src/components/DiagramEditorPane.test.mjs b/apps/web/src/components/DiagramEditorPane.test.mjs
index 54783be2..d0fb3799 100644
--- a/apps/web/src/components/DiagramEditorPane.test.mjs
+++ b/apps/web/src/components/DiagramEditorPane.test.mjs
@@ -77,12 +77,13 @@ describe("diagram editor canvas surface", () => {
expect(source).not.toContain("graph.drawGrid");
});
- test("uses restrained rounded edges and presents connected diagrams smaller at the left", () => {
+ test("uses restrained rounded edges and fits the complete diagram without clipping", () => {
expect(source).toContain('connector: { name: kind === "mind-map" ? "smooth" : "rounded"');
- expect(source).toContain('router: "normal"');
- expect(source).not.toContain('name: "manhattan"');
+ expect(source).toContain('name: "manhattan"');
expect(source).toContain("maxScale: policy.maxScale");
- expect(source).toContain("policy.minScale ? { minScale: policy.minScale }");
+ expect(source).not.toContain("minScale: policy.minScale");
+ expect(source).toContain("graph.centerContent()");
+ expect(source).not.toContain("desiredLeft - contentLeft");
expect(source).toContain("getDiagramLayoutViewport(document.kind)");
expect(source).toContain("fitDiagramContent(graph, document, containerRef.current);");
});
@@ -94,16 +95,16 @@ describe("diagram editor canvas surface", () => {
expect(toolbarSource).not.toContain('