loom-spec 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Tag-based filter for HTML exports. Used by `loom-spec export-html` to
3
+ * produce scoped bundles (public manual, ops runbook, etc.) from the same
4
+ * `.loom/` source.
5
+ *
6
+ * Semantics:
7
+ *
8
+ * - A node "matches" iff
9
+ * (includeTags is empty OR node has at least one matching include tag)
10
+ * AND
11
+ * (excludeTags is empty OR node has none of the exclude tags)
12
+ *
13
+ * If both lists are empty the filter is a no-op (all nodes match).
14
+ *
15
+ * - Cascade rules (applied in order, so later rules see post-filter state):
16
+ * 1. Drop nodes that don't match.
17
+ * 2. Drop edges whose source or target node was dropped.
18
+ * 3. Drop or shrink groups: if any group's `children` are all dropped,
19
+ * drop the group; otherwise keep with surviving children.
20
+ * 4. Null out `drill_down` chevrons that target diagrams with zero
21
+ * surviving nodes after filtering.
22
+ * 5. Drop timeline events whose referenced node was dropped.
23
+ * 6. Drop timelines that end up with zero events.
24
+ * 7. Null out `triggered_by` refs pointing at dropped events.
25
+ *
26
+ * - Diagrams that end up empty are NOT dropped automatically — the caller
27
+ * decides (often via --diagram for explicit single-diagram exports).
28
+ * Empty diagrams still render an empty canvas, which is visible to the
29
+ * reader and gives them a clue something was filtered out.
30
+ */
31
+ function nodeMatches(node, spec) {
32
+ const tags = node.tags ?? [];
33
+ const includes = spec.includeTags ?? [];
34
+ const excludes = spec.excludeTags ?? [];
35
+ if (includes.length > 0) {
36
+ if (!includes.some((t) => tags.includes(t)))
37
+ return false;
38
+ }
39
+ if (excludes.length > 0) {
40
+ if (excludes.some((t) => tags.includes(t)))
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+ /**
46
+ * Apply the filter cascade. Returns a deep-enough copy that mutating the
47
+ * result doesn't mutate the input.
48
+ */
49
+ export function applyFilter(payload, spec) {
50
+ const noFilter = (spec.includeTags?.length ?? 0) === 0 &&
51
+ (spec.excludeTags?.length ?? 0) === 0;
52
+ if (noFilter) {
53
+ return {
54
+ payload,
55
+ summary: {
56
+ nodesDropped: 0,
57
+ edgesDropped: 0,
58
+ groupsDropped: 0,
59
+ eventsDropped: 0,
60
+ timelinesDropped: 0,
61
+ drillDownsCleared: 0,
62
+ },
63
+ };
64
+ }
65
+ let nodesDropped = 0;
66
+ let edgesDropped = 0;
67
+ let groupsDropped = 0;
68
+ let eventsDropped = 0;
69
+ let timelinesDropped = 0;
70
+ let drillDownsCleared = 0;
71
+ // First pass: filter each diagram's nodes / edges / groups.
72
+ // Track surviving node ids per diagram so we can decide which timelines
73
+ // and drill_down references stay valid.
74
+ const filteredDiagrams = {};
75
+ const survivingNodeIdsByDiagram = {};
76
+ for (const [id, d] of Object.entries(payload.diagrams)) {
77
+ const beforeNodes = d.nodes.length;
78
+ const beforeEdges = d.edges.length;
79
+ const beforeGroups = d.groups?.length ?? 0;
80
+ const survivingNodes = d.nodes.filter((n) => nodeMatches(n, spec));
81
+ const survivingIds = new Set(survivingNodes.map((n) => n.id));
82
+ survivingNodeIdsByDiagram[id] = survivingIds;
83
+ const survivingEdges = d.edges.filter((e) => {
84
+ const fromId = e.from.split(":")[0];
85
+ const toId = e.to.split(":")[0];
86
+ return survivingIds.has(fromId) && survivingIds.has(toId);
87
+ });
88
+ const survivingGroups = (d.groups ?? [])
89
+ .map((g) => ({
90
+ ...g,
91
+ children: (g.children ?? []).filter((cid) => survivingIds.has(cid)),
92
+ }))
93
+ .filter((g) => g.children.length > 0);
94
+ nodesDropped += beforeNodes - survivingNodes.length;
95
+ edgesDropped += beforeEdges - survivingEdges.length;
96
+ groupsDropped += beforeGroups - survivingGroups.length;
97
+ filteredDiagrams[id] = {
98
+ ...d,
99
+ nodes: survivingNodes,
100
+ edges: survivingEdges,
101
+ groups: survivingGroups.length > 0 ? survivingGroups : undefined,
102
+ };
103
+ }
104
+ // Second pass: scrub drill_down refs pointing at fully-empty diagrams.
105
+ for (const d of Object.values(filteredDiagrams)) {
106
+ d.nodes = d.nodes.map((n) => {
107
+ if (!n.drill_down)
108
+ return n;
109
+ const targetSurvivors = survivingNodeIdsByDiagram[n.drill_down];
110
+ // Target diagram missing OR empty after filter → drop chevron.
111
+ if (!targetSurvivors || targetSurvivors.size === 0) {
112
+ drillDownsCleared++;
113
+ const { drill_down: _drop, ...rest } = n;
114
+ return rest;
115
+ }
116
+ return n;
117
+ });
118
+ if (d.groups) {
119
+ d.groups = d.groups.map((g) => {
120
+ if (!g.drill_down)
121
+ return g;
122
+ const targetSurvivors = survivingNodeIdsByDiagram[g.drill_down];
123
+ if (!targetSurvivors || targetSurvivors.size === 0) {
124
+ drillDownsCleared++;
125
+ const { drill_down: _drop, ...rest } = g;
126
+ return rest;
127
+ }
128
+ return g;
129
+ });
130
+ }
131
+ }
132
+ // Third pass: filter timelines. Drop events on filtered nodes; drop the
133
+ // whole timeline if it ends up empty; scrub dangling triggered_by refs.
134
+ const filteredTimelines = {};
135
+ for (const [id, tl] of Object.entries(payload.timelines)) {
136
+ const survivingIds = survivingNodeIdsByDiagram[tl.diagram];
137
+ if (!survivingIds || survivingIds.size === 0) {
138
+ // Underlying diagram fully filtered → drop timeline.
139
+ timelinesDropped++;
140
+ eventsDropped += tl.events.length;
141
+ continue;
142
+ }
143
+ const beforeEvents = tl.events.length;
144
+ const survivingEvents = tl.events.filter((e) => survivingIds.has(e.node));
145
+ eventsDropped += beforeEvents - survivingEvents.length;
146
+ if (survivingEvents.length === 0) {
147
+ timelinesDropped++;
148
+ continue;
149
+ }
150
+ const survivingEventIds = new Set(survivingEvents.map((e) => e.id));
151
+ const scrubbed = survivingEvents.map((e) => e.triggered_by && !survivingEventIds.has(e.triggered_by)
152
+ ? { ...e, triggered_by: undefined }
153
+ : e);
154
+ filteredTimelines[id] = { ...tl, events: scrubbed };
155
+ }
156
+ return {
157
+ payload: {
158
+ diagrams: filteredDiagrams,
159
+ timelines: filteredTimelines,
160
+ nodeTypes: payload.nodeTypes,
161
+ },
162
+ summary: {
163
+ nodesDropped,
164
+ edgesDropped,
165
+ groupsDropped,
166
+ eventsDropped,
167
+ timelinesDropped,
168
+ drillDownsCleared,
169
+ },
170
+ };
171
+ }
172
+ //# sourceMappingURL=exportFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exportFilter.js","sourceRoot":"","sources":["../../src/server/exportFilter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import{c as te,r as c,j as e,i as Q,s as de,a as se,l as ne,T as ue,P as me,D as fe}from"./index-CqF2JC9l.js";/**
2
+ * @license lucide-react v0.460.0 - ISC
3
+ *
4
+ * This source code is licensed under the ISC license.
5
+ * See the LICENSE file in the root directory of this source tree.
6
+ */const he=te("Pause",[["rect",{x:"14",y:"4",width:"4",height:"16",rx:"1",key:"zuxfzm"}],["rect",{x:"6",y:"4",width:"4",height:"16",rx:"1",key:"1okwgv"}]]);/**
7
+ * @license lucide-react v0.460.0 - ISC
8
+ *
9
+ * This source code is licensed under the ISC license.
10
+ * See the LICENSE file in the root directory of this source tree.
11
+ */const pe=te("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/**
12
+ * @license lucide-react v0.460.0 - ISC
13
+ *
14
+ * This source code is licensed under the ISC license.
15
+ * See the LICENSE file in the root directory of this source tree.
16
+ */const ve=te("SkipBack",[["polygon",{points:"19 20 9 12 19 4 19 20",key:"o2sva"}],["line",{x1:"5",x2:"5",y1:"19",y2:"5",key:"1ocqjk"}]]),M=110,S=30,R=52,K=24,ae=8,xe=4,G=8,ee=10;function ge(t){const o=Math.max(t/10,1),v=Math.floor(Math.log10(o)),p=Math.pow(10,v),m=o/p;let x;return m<1.5?x=p:m<3.5?x=2*p:m<7.5?x=5*p:x=10*p,Math.max(x,1)}function J(t){return t===0?"0":t<1e3?`${t}ms`:t%1e3===0?`${t/1e3}s`:`${(t/1e3).toFixed(2)}s`}function je(t){const j=[],o=new Set;for(const v of t.tracks??[])o.has(v.id)||(o.add(v.id),j.push(v));for(const v of t.events??[]){const p=v.track??`node:${v.node}`;o.has(p)||(o.add(p),j.push({id:p,label:p}))}return j.length===0&&j.push({id:"default",label:""}),j}function ie(t){return t.track??`node:${t.node}`}function re(t){return Math.round(t/ee)*ee}function ye({timeline:t,diagram:j,nodeTypes:o,selectedEventId:v,onSelectEvent:p,onUpdateEvent:m,playheadMs:x,onScrub:n,zoom:u=1}){const N=c.useRef(null),k=c.useRef(null),[P,z]=c.useState(800),[i,f]=c.useState(null),[d,h]=c.useState(!1);c.useEffect(()=>{const a=N.current;if(!a)return;const l=new ResizeObserver(s=>{const r=s[0];r&&z(r.contentRect.width)});return l.observe(a),z(a.getBoundingClientRect().width),()=>l.disconnect()},[]);const b=c.useMemo(()=>{const a=je(t),s=(t.events??[]).reduce((E,O)=>Math.max(E,O.start_ms+O.duration_ms),0),r=new Map;a.forEach((E,O)=>r.set(E.id,O));const y=Math.max(P-M-K,100)*u,_=s>0?y/s:1,Z=M+y+K,A=ge(s/u),Y=[];for(let E=0;E<=s+A*.001;E+=A)Y.push(E);const q=new Map;j.nodes.forEach(E=>q.set(E.id,E.type));const B=E=>{const O=q.get(E.node),U=O?o.types[O]:void 0;return(U==null?void 0:U.color)??"#71717a"},oe=S+a.length*R+8;return{tracks:a,trackOf:r,totalMs:s,pixelsPerMs:_,svgWidth:Z,tickStep:A,ticks:Y,colorOf:B,svgHeight:oe}},[t,j,o,P,u]),{tracks:T,trackOf:W,ticks:H,pixelsPerMs:C,colorOf:I,svgHeight:D,svgWidth:w,totalMs:$}=b,V=a=>{var s;if(!i||i.eventId!==a.id)return{event:a};const l=i.originalEvent;if(i.mode==="move"){const r=Math.max(0,re(l.start_ms+i.deltaMs)),g=W.get(ie(l))??0,y=Math.min(Math.max(0,g+i.deltaTracks),T.length-1),_=((s=T[y])==null?void 0:s.id)??l.track;return{event:{...l,start_ms:r,track:_}}}else{const r=Math.max(ee,re(l.duration_ms+i.deltaMs));return{event:{...l,duration_ms:r}}}};c.useEffect(()=>{if(!i)return;const a=s=>{const r=s.clientX-i.startX,g=s.clientY-i.startY,y=r/i.pixelsPerMs,_=Math.round(g/R);f({...i,deltaMs:y,deltaTracks:_})},l=()=>{const s=V(i.originalEvent);m&&(s.event.start_ms!==i.originalEvent.start_ms||s.event.duration_ms!==i.originalEvent.duration_ms||s.event.track!==i.originalEvent.track)&&m(i.eventId,()=>s.event),f(null)};return document.addEventListener("pointermove",a),document.addEventListener("pointerup",l),()=>{document.removeEventListener("pointermove",a),document.removeEventListener("pointerup",l)}},[i,m]),c.useEffect(()=>{if(!d||!n)return;const a=k.current;if(!a)return;const l=r=>{const g=a.getBoundingClientRect(),y=r.clientX-g.left-M,_=Math.max(0,Math.min($,y/C));n(_)},s=()=>h(!1);return document.addEventListener("pointermove",l),document.addEventListener("pointerup",s),()=>{document.removeEventListener("pointermove",l),document.removeEventListener("pointerup",s)}},[d,n,$,C]);const X=(a,l,s)=>{m&&(a.stopPropagation(),f({mode:s,eventId:l.id,startX:a.clientX,startY:a.clientY,originalEvent:l,pixelsPerMs:C,deltaMs:0,deltaTracks:0}))},F=a=>x===void 0?!1:x>=a.start_ms&&x<=a.start_ms+a.duration_ms,L=x!==void 0?M+x*C:null;return e.jsx("div",{className:"timeline-wrap",ref:N,children:e.jsxs("svg",{ref:k,className:"timeline-svg",width:w,height:D,onClick:a=>{a.target.tagName==="svg"&&p(null)},children:[T.map((a,l)=>{const s=S+l*R;return e.jsxs("g",{children:[a.color&&e.jsx("rect",{x:M,y:s,width:w-M-K,height:R,fill:a.color,opacity:.4}),e.jsx("line",{x1:0,x2:w,y1:s,y2:s,className:"timeline-lane-sep"}),e.jsx("text",{x:12,y:s+R/2,className:"timeline-track-label",dominantBaseline:"middle",children:a.label})]},`lane-${a.id}`)}),e.jsx("line",{x1:0,x2:w,y1:S+T.length*R,y2:S+T.length*R,className:"timeline-lane-sep"}),e.jsx("rect",{x:M,y:0,width:w-M-K,height:S,fill:"transparent",style:{cursor:n?"ew-resize":"default"},onPointerDown:a=>{var g;if(!n)return;a.preventDefault();const l=(g=k.current)==null?void 0:g.getBoundingClientRect();if(!l)return;const s=a.clientX-l.left-M,r=Math.max(0,Math.min($,s/C));n(r),h(!0)}}),e.jsxs("g",{className:"timeline-axis",children:[e.jsx("line",{x1:M,x2:w-K,y1:S-1,y2:S-1,className:"timeline-axis-line"}),H.map(a=>{const l=M+a*C;return e.jsxs("g",{children:[e.jsx("line",{x1:l,x2:l,y1:S-6,y2:S,className:"timeline-tick-mark"}),e.jsx("line",{x1:l,x2:l,y1:S,y2:D,className:"timeline-tick-grid"}),e.jsx("text",{x:l,y:S-9,className:"timeline-tick-label",textAnchor:"middle",children:J(a)})]},`tick-${a}`)})]}),(t.events??[]).map(a=>{const{event:l}=V(a),s=W.get(ie(l))??0,r=M+l.start_ms*C,g=Math.max(l.duration_ms*C,xe),y=S+s*R+ae,_=R-ae*2,Z=I(l),A=a.id===v,Y=F(l),q=(i==null?void 0:i.eventId)===a.id;return e.jsxs("g",{className:`timeline-clip kind-${l.kind??"compute"}${A?" selected":""}${Y?" active":""}${q?" ghost":""}`,onClick:B=>{B.stopPropagation(),p(a.id)},children:[e.jsx("rect",{x:r,y,width:g,height:_,rx:4,ry:4,fill:Z,fillOpacity:Y?1:.85,stroke:A?"var(--accent)":Z,strokeWidth:A?2:1,style:{cursor:m?"grab":"pointer"},onPointerDown:B=>X(B,a,"move")}),m&&g>G&&e.jsx("rect",{x:r+g-G,y,width:G,height:_,fill:"transparent",style:{cursor:"ew-resize"},onPointerDown:B=>X(B,a,"resize")}),g>40&&l.label&&e.jsx("text",{x:r+6,y:y+_/2,className:"timeline-clip-label",dominantBaseline:"middle",pointerEvents:"none",children:l.label}),e.jsx("title",{children:(l.label??l.node)+` — ${J(l.start_ms)}, ${J(l.duration_ms)} long`})]},a.id)}),L!==null&&e.jsxs("g",{className:"timeline-playhead",pointerEvents:"none",children:[e.jsx("line",{x1:L,x2:L,y1:4,y2:D,className:"timeline-playhead-line"}),e.jsx("polygon",{points:`${L-5},4 ${L+5},4 ${L},14`,className:"timeline-playhead-handle"})]})]})})}function le(t){return t<1e3?`${t} ms`:`${(t/1e3).toFixed(2)} s`}function Ne({selectedEvent:t,diagram:j}){if(!t)return e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"Click a clip to inspect"})});const o=j.nodes.find(m=>m.id===t.node),v=t.code_refs??[],p=t.tags??[];return e.jsxs("div",{className:"inspector",children:[e.jsx("span",{className:"type-tag",style:{background:"#71717a",color:"#fff"},children:"EVENT"}),t.label&&e.jsx("h2",{children:t.label}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Node"}),e.jsxs("div",{className:"field-value",children:[e.jsx("code",{children:t.node}),o&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · ",o.label]})]})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Timing"}),e.jsxs("div",{className:"field-value",children:["starts ",le(t.start_ms)," ·"," ","lasts ",le(t.duration_ms)]})]}),t.kind&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Kind"}),e.jsx("div",{className:"field-value",children:t.kind})]}),t.track&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Track"}),e.jsx("div",{className:"field-value",children:e.jsx("code",{children:t.track})})]}),t.triggered_by&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Triggered by"}),e.jsx("div",{className:"field-value",children:e.jsx("code",{children:t.triggered_by})})]}),t.description&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Description"}),e.jsx("div",{className:"field-value",children:t.description})]}),v.length>0&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Code refs"}),v.map((m,x)=>e.jsxs("div",{className:"code-ref",children:[m.path,m.symbol&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · ",m.symbol]}),m.lines&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · L",m.lines]})]},x))]}),p.length>0&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Tags"}),e.jsx("div",{children:p.map(m=>e.jsx("span",{className:"tag",children:m},m))})]}),e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"ID"}),e.jsx("div",{className:"field-value",children:e.jsx("code",{children:t.id})})]})]})}const ke=[.25,.5,1,2,4];function ce(t){return t<1e3?`${Math.round(t)} ms`:`${(t/1e3).toFixed(2)} s`}function be({playing:t,positionMs:j,totalMs:o,speed:v,onPlayPause:p,onReset:m,onSpeed:x,actions:n}){return e.jsxs("div",{className:"transport-bar",children:[e.jsx("button",{className:"transport-btn",onClick:p,title:t?"Pause (space)":"Play (space)","aria-label":t?"Pause":"Play",children:t?e.jsx(he,{size:14}):e.jsx(pe,{size:14})}),e.jsx("button",{className:"transport-btn",onClick:m,title:"Reset to 0 (home)","aria-label":"Reset",children:e.jsx(ve,{size:14})}),e.jsxs("div",{className:"transport-position",children:[e.jsx("code",{children:ce(j)}),e.jsxs("span",{className:"muted",children:[" / ",ce(o)]})]}),n,e.jsx("div",{className:"transport-spacer"}),e.jsxs("label",{className:"transport-speed",children:[e.jsx("span",{className:"muted",children:"Speed"}),e.jsx("select",{value:v,onChange:u=>x(Number(u.target.value)),children:ke.map(u=>e.jsxs("option",{value:u,children:[u,"×"]},u))})]})]})}function we({diagram:t,nodeTypes:j,anchorRef:o,onPick:v,onClose:p}){const m=c.useRef(null),[x,n]=c.useState(null);return c.useLayoutEffect(()=>{function u(){const N=o.current;if(!N)return;const k=N.getBoundingClientRect();n({top:k.bottom+6,right:Math.max(8,window.innerWidth-k.right)})}return u(),window.addEventListener("resize",u),()=>window.removeEventListener("resize",u)},[o]),c.useEffect(()=>{function u(N){var k;if(m.current&&!m.current.contains(N.target)){if((k=o.current)!=null&&k.contains(N.target))return;p()}}return document.addEventListener("mousedown",u),()=>document.removeEventListener("mousedown",u)},[p,o]),x?e.jsxs("div",{className:"add-node-menu",ref:m,style:{top:x.top,right:x.right},children:[e.jsx("div",{className:"add-node-menu-title",children:"Add event on…"}),t.nodes.length===0?e.jsx("div",{className:"switcher-empty",children:"No nodes in this diagram"}):t.nodes.map(u=>{const N=j.types[u.type],k=(N==null?void 0:N.color)??"#71717a";return e.jsxs("button",{className:"add-node-item",onClick:()=>v(u.id),style:{"--node-color":k},children:[e.jsx("span",{className:"add-node-color"}),e.jsxs("div",{className:"add-node-text",children:[e.jsx("div",{className:"add-node-label",children:u.label}),e.jsx("code",{className:"add-node-key",children:u.id})]})]},u.id)})]}):null}const Ee=500;function Me(t){const[j,o]=c.useState({timeline:null,diagram:null,nodeTypes:null,loadError:null,saveStatus:"idle",saveError:null,connectionStatus:"connecting"}),v=c.useRef(null),p=c.useRef(null),m=c.useRef(j);m.current=j;const x=c.useRef(!1);c.useEffect(()=>{let i=!1;x.current=!1;async function f(){try{const d=await se(t);if(i)return;const h=await ne(d.diagram);if(i)return;p.current=d,x.current=!0,o(b=>({...b,timeline:d,diagram:h.diagram,nodeTypes:h.nodeTypes,loadError:null}))}catch(d){if(i)return;o(h=>({...h,loadError:d instanceof Error?d.message:String(d)}))}}return f(),()=>{i=!0}},[t]);const n=c.useCallback(()=>{Q()||(v.current&&clearTimeout(v.current),o(i=>({...i,saveStatus:"dirty",saveError:null})),v.current=setTimeout(async()=>{const i=p.current;if(i){o(f=>({...f,saveStatus:"saving"}));try{await de(i),o(f=>({...f,saveStatus:"saved"}))}catch(f){o(d=>({...d,saveStatus:"error",saveError:f instanceof Error?f.message:String(f)}))}}},Ee))},[]),u=c.useCallback(i=>{x.current&&(o(f=>{if(!f.timeline)return f;const d=i(f.timeline);return p.current=d,{...f,timeline:d}}),n())},[n]),N=c.useCallback((i,f)=>{u(d=>({...d,events:d.events.map(h=>h.id===i?f(h):h)}))},[u]),k=c.useCallback(i=>{u(f=>({...f,events:[...f.events,i]}))},[u]),P=c.useCallback(i=>{u(f=>({...f,events:f.events.filter(d=>d.id!==i)}))},[u]);return c.useEffect(()=>{if(Q()){o(d=>({...d,connectionStatus:"connected"}));return}o(d=>({...d,connectionStatus:"connecting"}));const i=new EventSource("/api/events");i.onopen=()=>o(d=>({...d,connectionStatus:"connected"})),i.onerror=()=>o(d=>({...d,connectionStatus:"disconnected"}));const f=async()=>{const d=m.current.saveStatus;if(!(d==="dirty"||d==="saving"))try{const h=await se(t),b=await ne(h.diagram);p.current=h,o(T=>({...T,timeline:h,diagram:b.diagram,nodeTypes:b.nodeTypes,saveStatus:"idle",saveError:null}))}catch{}};return i.addEventListener("change",d=>{try{const h=JSON.parse(d.data),b=m.current.timeline;(h.type==="timeline-changed"&&h.id===t||h.type==="diagram-changed"&&b&&h.id===b.diagram||h.type==="node-types-changed")&&f()}catch{}}),()=>{i.close()}},[t]),c.useEffect(()=>()=>{v.current&&clearTimeout(v.current)},[]),c.useMemo(()=>({...j,updateEvent:N,addEvent:k,deleteEvent:P}),[j,N,k,P])}function Se(t){const j=new Set(t.events.map(v=>v.id));let o=t.events.length+1;for(;j.has(`ev${o}`);)o++;return`ev${o}`}function _e({id:t,diagrams:j,timelines:o,isDefault:v,onClickHome:p,onNavigate:m,onCreateDiagram:x}){const n=Me(t),[u,N]=c.useState(null),[k,P]=c.useState(!1),z=c.useRef(null),[i,f]=c.useState(1),d=Q(),[h,b]=c.useState(0),[T,W]=c.useState(!1),[H,C]=c.useState(1),I=c.useRef(null),D=c.useRef(0),w=c.useMemo(()=>n.timeline?n.timeline.events.reduce((s,r)=>Math.max(s,r.start_ms+r.duration_ms),0):0,[n.timeline]),$=c.useMemo(()=>{const s=new Set;if(!n.timeline)return s;for(const r of n.timeline.events)h>=r.start_ms&&h<=r.start_ms+r.duration_ms&&s.add(r.node);return s},[n.timeline,h]),V=c.useMemo(()=>{const s=new Set;if(!n.diagram)return s;for(const r of n.diagram.edges){const g=r.from.indexOf(":"),y=g===-1?r.from:r.from.slice(0,g);$.has(y)&&s.add(r.id)}return s},[n.diagram,$]);c.useEffect(()=>{if(!T){I.current!==null&&(clearInterval(I.current),I.current=null);return}return D.current=performance.now(),I.current=setInterval(()=>{const s=performance.now(),r=s-D.current;D.current=s,b(g=>{const y=g+r*H;return y>=w?(W(!1),w):y})},16),()=>{I.current!==null&&clearInterval(I.current)}},[T,H,w]);const X=c.useCallback(()=>{w!==0&&W(s=>(!s&&h>=w&&b(0),!s))},[h,w]),F=c.useCallback(()=>{b(0),W(!1)},[]),L=c.useCallback(s=>{b(s)},[]),a=c.useCallback(s=>{const r=n.timeline;if(!r)return;const g=Math.max(0,Math.round(h)),y={id:Se(r),node:s,start_ms:g,duration_ms:200,kind:"compute"};n.addEvent(y),N(y.id),P(!1)},[n,h]);if(c.useEffect(()=>{const s=r=>{const g=r.target;g.tagName==="INPUT"||g.tagName==="TEXTAREA"||g.tagName==="SELECT"||(r.key===" "||r.code==="Space"?(r.preventDefault(),X()):r.key==="Home"?(r.preventDefault(),F()):(r.key==="Backspace"||r.key==="Delete")&&u&&!d&&(r.preventDefault(),n.deleteEvent(u),N(null)))};return document.addEventListener("keydown",s),()=>document.removeEventListener("keydown",s)},[X,F,u,n,d]),n.loadError)return e.jsxs("div",{className:"app timeline-app",children:[e.jsx("div",{className:"topbar",children:e.jsx("div",{className:"title",children:"loom-spec"})}),e.jsx("div",{className:"canvas-wrap",style:{padding:24},children:e.jsxs("code",{style:{color:"var(--status-stale)"},children:["Failed to load: ",n.loadError]})}),e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"—"})})]});if(!n.timeline||!n.diagram||!n.nodeTypes)return e.jsxs("div",{className:"app timeline-app",children:[e.jsx("div",{className:"topbar",children:e.jsx("div",{className:"title",children:"loom-spec"})}),e.jsx("div",{className:"canvas-wrap",style:{padding:24,color:"var(--text-muted)"},children:"Loading timeline…"}),e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"—"})})]});const l=u?n.timeline.events.find(s=>s.id===u)??null:null;return e.jsxs("div",{className:"app timeline-app",children:[e.jsx(ue,{viewKind:"timeline",viewId:t,title:n.timeline.title,subtitle:n.timeline.description,diagrams:j,timelines:o,saveStatus:n.saveStatus,saveError:n.saveError,connectionStatus:n.connectionStatus,onClickAdd:()=>{},addMenuOpen:!1,isDefault:v,onClickHome:p,onNavigate:m,onCreateDiagram:x,addButtonRef:null,hideAddButton:!0}),e.jsx(be,{playing:T,positionMs:h,totalMs:w,speed:H,onPlayPause:X,onReset:F,onSpeed:C,actions:e.jsxs(e.Fragment,{children:[!d&&e.jsxs("button",{ref:z,className:"transport-add-event",onClick:()=>P(s=>!s),title:"Add event at playhead",children:[e.jsx(me,{size:14})," Event"]}),e.jsxs("label",{className:"transport-zoom",children:[e.jsx("span",{className:"muted",children:"Zoom"}),e.jsxs("select",{value:i,onChange:s=>f(Number(s.target.value)),children:[e.jsx("option",{value:1,children:"1× (fit)"}),e.jsx("option",{value:2,children:"2×"}),e.jsx("option",{value:5,children:"5×"}),e.jsx("option",{value:10,children:"10×"}),e.jsx("option",{value:20,children:"20×"})]})]})]})}),k&&!d&&e.jsx(we,{diagram:n.diagram,nodeTypes:n.nodeTypes,anchorRef:z,onPick:a,onClose:()=>P(!1)}),e.jsx("div",{className:"canvas-wrap timeline-canvas-wrap",children:e.jsxs("div",{className:"timeline-split",children:[e.jsx("div",{className:"timeline-split-pane timeline-split-left",children:e.jsx(ye,{timeline:n.timeline,diagram:n.diagram,nodeTypes:n.nodeTypes,selectedEventId:u,onSelectEvent:N,onUpdateEvent:d?void 0:n.updateEvent,playheadMs:h,onScrub:L,zoom:i})}),e.jsx("div",{className:"timeline-split-pane timeline-split-right",children:e.jsx(fe,{diagram:n.diagram,nodeTypesConfig:n.nodeTypes,interactive:!1,activeNodeIds:$,pulsingEdgeIds:V})})]})}),e.jsx(Ne,{selectedEvent:l,diagram:n.diagram})]})}export{_e as TimelineView};