loom-spec 0.3.0 → 0.5.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.
Files changed (44) hide show
  1. package/dist/cli/exportHtml.d.ts +22 -0
  2. package/dist/cli/exportHtml.js +182 -0
  3. package/dist/cli/exportHtml.js.map +1 -0
  4. package/dist/cli/index.js +40 -34
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/mcp/server.js +3 -207
  7. package/dist/mcp/server.js.map +1 -1
  8. package/dist/server/app.js +2 -50
  9. package/dist/server/app.js.map +1 -1
  10. package/dist/server/exportConfig.d.ts +40 -0
  11. package/dist/server/exportConfig.js +78 -0
  12. package/dist/server/exportConfig.js.map +1 -0
  13. package/dist/server/exportFilter.d.ts +52 -0
  14. package/dist/server/exportFilter.js +138 -0
  15. package/dist/server/exportFilter.js.map +1 -0
  16. package/dist/server/fileOps.d.ts +0 -12
  17. package/dist/server/fileOps.js +0 -51
  18. package/dist/server/fileOps.js.map +1 -1
  19. package/dist/server/watch.d.ts +1 -1
  20. package/dist/server/watch.js +0 -5
  21. package/dist/server/watch.js.map +1 -1
  22. package/dist/validate.d.ts +1 -3
  23. package/dist/validate.js +0 -15
  24. package/dist/validate.js.map +1 -1
  25. package/dist/view/assets/index-D8qr-jiw.css +1 -0
  26. package/dist/view/assets/index-DI0VS0HQ.js +205 -0
  27. package/dist/view/index.html +2 -2
  28. package/dist/{view/assets/index-CvyHnPjR.css → view-export/assets/bundle.css} +1 -1
  29. package/dist/{view/assets/index-Du05xzao.js → view-export/assets/bundle.js} +44 -49
  30. package/dist/view-export/index.html +24 -0
  31. package/package.json +3 -2
  32. package/templates/.claude/skills/loom-spec/SKILL.md +91 -76
  33. package/templates/.loom/README.md +1 -1
  34. package/dist/cli/importTrace.d.ts +0 -15
  35. package/dist/cli/importTrace.js +0 -188
  36. package/dist/cli/importTrace.js.map +0 -1
  37. package/dist/server/otel.d.ts +0 -32
  38. package/dist/server/otel.js +0 -98
  39. package/dist/server/otel.js.map +0 -1
  40. package/dist/types/timeline.d.ts +0 -97
  41. package/dist/types/timeline.js +0 -7
  42. package/dist/types/timeline.js.map +0 -1
  43. package/dist/view/assets/TimelineView-DEfpV9mL.js +0 -16
  44. package/schema/timeline.schema.json +0 -135
@@ -1,97 +0,0 @@
1
- /**
2
- * AUTOGENERATED — do not edit.
3
- * Source: schema/timeline.schema.json
4
- */
5
- /**
6
- * A time-axis overlay describing when events happen on the nodes of a diagram. Same node universe, different render.
7
- */
8
- export interface LoomTimeline {
9
- $schema?: string;
10
- version: "1";
11
- /**
12
- * Timeline identifier. Should match the filename without extension.
13
- */
14
- id: string;
15
- title: string;
16
- description?: string;
17
- /**
18
- * Id of the diagram whose nodes this timeline overlays.
19
- */
20
- diagram: string;
21
- events: TimelineEvent[];
22
- /**
23
- * Optional explicit track definitions (label, color). If omitted, tracks are inferred from events[].track values.
24
- */
25
- tracks?: TimelineTrack[];
26
- }
27
- export interface TimelineEvent {
28
- /**
29
- * Unique within this timeline.
30
- */
31
- id: string;
32
- /**
33
- * Node id from the referenced diagram.
34
- */
35
- node: string;
36
- /**
37
- * Track label. Events on the same track are interpreted as sequential; events on different tracks at overlapping times are concurrent. If omitted, the renderer auto-assigns one track per node.
38
- */
39
- track?: string;
40
- /**
41
- * Start time in milliseconds from t=0.
42
- */
43
- start_ms: number;
44
- /**
45
- * Duration in milliseconds. May be 0 for instantaneous events.
46
- */
47
- duration_ms: number;
48
- /**
49
- * Short text rendered inside the clip.
50
- */
51
- label?: string;
52
- description?: string;
53
- /**
54
- * Optional category for clip styling. Distinct from edge.kind in the diagram — this describes what the node is doing during this clip, not what flows between nodes.
55
- */
56
- kind?: "compute" | "io" | "wait" | "error";
57
- /**
58
- * Optional anchors to the specific function(s) running during this clip. Like node-level code_refs but scoped to a moment in time. Drift detection checks these too. Use for function-level granularity inside a node (e.g. 'validate' vs. 'issue jwt' both happen on the auth-service node).
59
- */
60
- code_refs?: CodeRef[];
61
- /**
62
- * Optional id of another event that caused this one. Used for explicit causation chains and to preserve OTel span.parent_id when importing traces. The renderer can draw a connecting arrow between the two clips.
63
- */
64
- triggered_by?: string;
65
- /**
66
- * Free-form labels for filtering or grouping clips. Examples: 'critical-path', 'billable-time', 'background'.
67
- */
68
- tags?: string[];
69
- }
70
- /**
71
- * Same shape as the CodeRef in diagram.schema.json. Inlined here to keep the schema self-contained — no cross-file $ref required to validate.
72
- */
73
- export interface CodeRef {
74
- /**
75
- * Repo-relative file path.
76
- */
77
- path: string;
78
- /**
79
- * Function, class, or component name. Preferred over lines because it survives refactors.
80
- */
81
- symbol?: string;
82
- /**
83
- * Line range(s) like '1-80' or '12,45-50'. Use only when no symbol applies.
84
- */
85
- lines?: string;
86
- }
87
- export interface TimelineTrack {
88
- /**
89
- * Track id, matches event.track values.
90
- */
91
- id: string;
92
- label: string;
93
- /**
94
- * Optional background tint for the track lane.
95
- */
96
- color?: string;
97
- }
@@ -1,7 +0,0 @@
1
- /* eslint-disable */
2
- /**
3
- * AUTOGENERATED — do not edit.
4
- * Source: schema/timeline.schema.json
5
- */
6
- export {};
7
- //# sourceMappingURL=timeline.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"timeline.js","sourceRoot":"","sources":["../../src/types/timeline.ts"],"names":[],"mappings":"AAAA,oBAAoB;AACpB;;;GAGG"}
@@ -1,16 +0,0 @@
1
- import{c as ee,r as l,j as e,s as oe,a as te,l as se,T as de,P as ue,D as me}from"./index-Du05xzao.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 fe=ee("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 he=ee("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 pe=ee("SkipBack",[["polygon",{points:"19 20 9 12 19 4 19 20",key:"o2sva"}],["line",{x1:"5",x2:"5",y1:"19",y2:"5",key:"1ocqjk"}]]),S=110,M=30,I=52,Y=24,ne=8,ve=4,G=8,Q=10;function xe(s){const o=Math.max(s/10,1),p=Math.floor(Math.log10(o)),h=Math.pow(10,p),u=o/h;let x;return u<1.5?x=h:u<3.5?x=2*h:u<7.5?x=5*h:x=10*h,Math.max(x,1)}function J(s){return s===0?"0":s<1e3?`${s}ms`:s%1e3===0?`${s/1e3}s`:`${(s/1e3).toFixed(2)}s`}function ge(s){const g=[],o=new Set;for(const p of s.tracks??[])o.has(p.id)||(o.add(p.id),g.push(p));for(const p of s.events??[]){const h=p.track??`node:${p.node}`;o.has(h)||(o.add(h),g.push({id:h,label:h}))}return g.length===0&&g.push({id:"default",label:""}),g}function ae(s){return s.track??`node:${s.node}`}function ie(s){return Math.round(s/Q)*Q}function je({timeline:s,diagram:g,nodeTypes:o,selectedEventId:p,onSelectEvent:h,onUpdateEvent:u,playheadMs:x,onScrub:a,zoom:d=1}){const y=l.useRef(null),N=l.useRef(null),[R,B]=l.useState(800),[r,m]=l.useState(null),[c,v]=l.useState(!1);l.useEffect(()=>{const i=y.current;if(!i)return;const t=new ResizeObserver(n=>{const f=n[0];f&&B(f.contentRect.width)});return t.observe(i),B(i.getBoundingClientRect().width),()=>t.disconnect()},[]);const b=l.useMemo(()=>{const i=ge(s),n=(s.events??[]).reduce((E,A)=>Math.max(E,A.start_ms+A.duration_ms),0),f=new Map;i.forEach((E,A)=>f.set(E.id,A));const w=Math.max(R-S-Y,100)*d,C=n>0?w/n:1,K=S+w+Y,D=xe(n/d),F=[];for(let E=0;E<=n+D*.001;E+=D)F.push(E);const V=new Map;g.nodes.forEach(E=>V.set(E.id,E.type));const $=E=>{const A=V.get(E.node),U=A?o.types[A]:void 0;return(U==null?void 0:U.color)??"#71717a"},ce=M+i.length*I+8;return{tracks:i,trackOf:f,totalMs:n,pixelsPerMs:C,svgWidth:K,tickStep:D,ticks:F,colorOf:$,svgHeight:ce}},[s,g,o,R,d]),{tracks:T,trackOf:O,ticks:Z,pixelsPerMs:k,colorOf:X,svgHeight:_,svgWidth:P,totalMs:H}=b,z=i=>{var n;if(!r||r.eventId!==i.id)return{event:i};const t=r.originalEvent;if(r.mode==="move"){const f=Math.max(0,ie(t.start_ms+r.deltaMs)),j=O.get(ae(t))??0,w=Math.min(Math.max(0,j+r.deltaTracks),T.length-1),C=((n=T[w])==null?void 0:n.id)??t.track;return{event:{...t,start_ms:f,track:C}}}else{const f=Math.max(Q,ie(t.duration_ms+r.deltaMs));return{event:{...t,duration_ms:f}}}};l.useEffect(()=>{if(!r)return;const i=n=>{const f=n.clientX-r.startX,j=n.clientY-r.startY,w=f/r.pixelsPerMs,C=Math.round(j/I);m({...r,deltaMs:w,deltaTracks:C})},t=()=>{const n=z(r.originalEvent);u&&(n.event.start_ms!==r.originalEvent.start_ms||n.event.duration_ms!==r.originalEvent.duration_ms||n.event.track!==r.originalEvent.track)&&u(r.eventId,()=>n.event),m(null)};return document.addEventListener("pointermove",i),document.addEventListener("pointerup",t),()=>{document.removeEventListener("pointermove",i),document.removeEventListener("pointerup",t)}},[r,u]),l.useEffect(()=>{if(!c||!a)return;const i=N.current;if(!i)return;const t=f=>{const j=i.getBoundingClientRect(),w=f.clientX-j.left-S,C=Math.max(0,Math.min(H,w/k));a(C)},n=()=>v(!1);return document.addEventListener("pointermove",t),document.addEventListener("pointerup",n),()=>{document.removeEventListener("pointermove",t),document.removeEventListener("pointerup",n)}},[c,a,H,k]);const W=(i,t,n)=>{u&&(i.stopPropagation(),m({mode:n,eventId:t.id,startX:i.clientX,startY:i.clientY,originalEvent:t,pixelsPerMs:k,deltaMs:0,deltaTracks:0}))},q=i=>x===void 0?!1:x>=i.start_ms&&x<=i.start_ms+i.duration_ms,L=x!==void 0?S+x*k:null;return e.jsx("div",{className:"timeline-wrap",ref:y,children:e.jsxs("svg",{ref:N,className:"timeline-svg",width:P,height:_,onClick:i=>{i.target.tagName==="svg"&&h(null)},children:[T.map((i,t)=>{const n=M+t*I;return e.jsxs("g",{children:[i.color&&e.jsx("rect",{x:S,y:n,width:P-S-Y,height:I,fill:i.color,opacity:.4}),e.jsx("line",{x1:0,x2:P,y1:n,y2:n,className:"timeline-lane-sep"}),e.jsx("text",{x:12,y:n+I/2,className:"timeline-track-label",dominantBaseline:"middle",children:i.label})]},`lane-${i.id}`)}),e.jsx("line",{x1:0,x2:P,y1:M+T.length*I,y2:M+T.length*I,className:"timeline-lane-sep"}),e.jsx("rect",{x:S,y:0,width:P-S-Y,height:M,fill:"transparent",style:{cursor:a?"ew-resize":"default"},onPointerDown:i=>{var j;if(!a)return;i.preventDefault();const t=(j=N.current)==null?void 0:j.getBoundingClientRect();if(!t)return;const n=i.clientX-t.left-S,f=Math.max(0,Math.min(H,n/k));a(f),v(!0)}}),e.jsxs("g",{className:"timeline-axis",children:[e.jsx("line",{x1:S,x2:P-Y,y1:M-1,y2:M-1,className:"timeline-axis-line"}),Z.map(i=>{const t=S+i*k;return e.jsxs("g",{children:[e.jsx("line",{x1:t,x2:t,y1:M-6,y2:M,className:"timeline-tick-mark"}),e.jsx("line",{x1:t,x2:t,y1:M,y2:_,className:"timeline-tick-grid"}),e.jsx("text",{x:t,y:M-9,className:"timeline-tick-label",textAnchor:"middle",children:J(i)})]},`tick-${i}`)})]}),(s.events??[]).map(i=>{const{event:t}=z(i),n=O.get(ae(t))??0,f=S+t.start_ms*k,j=Math.max(t.duration_ms*k,ve),w=M+n*I+ne,C=I-ne*2,K=X(t),D=i.id===p,F=q(t),V=(r==null?void 0:r.eventId)===i.id;return e.jsxs("g",{className:`timeline-clip kind-${t.kind??"compute"}${D?" selected":""}${F?" active":""}${V?" ghost":""}`,onClick:$=>{$.stopPropagation(),h(i.id)},children:[e.jsx("rect",{x:f,y:w,width:j,height:C,rx:4,ry:4,fill:K,fillOpacity:F?1:.85,stroke:D?"var(--accent)":K,strokeWidth:D?2:1,style:{cursor:u?"grab":"pointer"},onPointerDown:$=>W($,i,"move")}),u&&j>G&&e.jsx("rect",{x:f+j-G,y:w,width:G,height:C,fill:"transparent",style:{cursor:"ew-resize"},onPointerDown:$=>W($,i,"resize")}),j>40&&t.label&&e.jsx("text",{x:f+6,y:w+C/2,className:"timeline-clip-label",dominantBaseline:"middle",pointerEvents:"none",children:t.label}),e.jsx("title",{children:(t.label??t.node)+` — ${J(t.start_ms)}, ${J(t.duration_ms)} long`})]},i.id)}),L!==null&&e.jsxs("g",{className:"timeline-playhead",pointerEvents:"none",children:[e.jsx("line",{x1:L,x2:L,y1:4,y2:_,className:"timeline-playhead-line"}),e.jsx("polygon",{points:`${L-5},4 ${L+5},4 ${L},14`,className:"timeline-playhead-handle"})]})]})})}function re(s){return s<1e3?`${s} ms`:`${(s/1e3).toFixed(2)} s`}function ye({selectedEvent:s,diagram:g}){if(!s)return e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"Click a clip to inspect"})});const o=g.nodes.find(u=>u.id===s.node),p=s.code_refs??[],h=s.tags??[];return e.jsxs("div",{className:"inspector",children:[e.jsx("span",{className:"type-tag",style:{background:"#71717a",color:"#fff"},children:"EVENT"}),s.label&&e.jsx("h2",{children:s.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:s.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 ",re(s.start_ms)," ·"," ","lasts ",re(s.duration_ms)]})]}),s.kind&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Kind"}),e.jsx("div",{className:"field-value",children:s.kind})]}),s.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:s.track})})]}),s.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:s.triggered_by})})]}),s.description&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Description"}),e.jsx("div",{className:"field-value",children:s.description})]}),p.length>0&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Code refs"}),p.map((u,x)=>e.jsxs("div",{className:"code-ref",children:[u.path,u.symbol&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · ",u.symbol]}),u.lines&&e.jsxs("span",{style:{color:"var(--text-muted)"},children:[" · L",u.lines]})]},x))]}),h.length>0&&e.jsxs("div",{className:"field",children:[e.jsx("div",{className:"field-label",children:"Tags"}),e.jsx("div",{children:h.map(u=>e.jsx("span",{className:"tag",children:u},u))})]}),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:s.id})})]})]})}const Ne=[.25,.5,1,2,4];function le(s){return s<1e3?`${Math.round(s)} ms`:`${(s/1e3).toFixed(2)} s`}function ke({playing:s,positionMs:g,totalMs:o,speed:p,onPlayPause:h,onReset:u,onSpeed:x,actions:a}){return e.jsxs("div",{className:"transport-bar",children:[e.jsx("button",{className:"transport-btn",onClick:h,title:s?"Pause (space)":"Play (space)","aria-label":s?"Pause":"Play",children:s?e.jsx(fe,{size:14}):e.jsx(he,{size:14})}),e.jsx("button",{className:"transport-btn",onClick:u,title:"Reset to 0 (home)","aria-label":"Reset",children:e.jsx(pe,{size:14})}),e.jsxs("div",{className:"transport-position",children:[e.jsx("code",{children:le(g)}),e.jsxs("span",{className:"muted",children:[" / ",le(o)]})]}),a,e.jsx("div",{className:"transport-spacer"}),e.jsxs("label",{className:"transport-speed",children:[e.jsx("span",{className:"muted",children:"Speed"}),e.jsx("select",{value:p,onChange:d=>x(Number(d.target.value)),children:Ne.map(d=>e.jsxs("option",{value:d,children:[d,"×"]},d))})]})]})}function be({diagram:s,nodeTypes:g,anchorRef:o,onPick:p,onClose:h}){const u=l.useRef(null),[x,a]=l.useState(null);return l.useLayoutEffect(()=>{function d(){const y=o.current;if(!y)return;const N=y.getBoundingClientRect();a({top:N.bottom+6,right:Math.max(8,window.innerWidth-N.right)})}return d(),window.addEventListener("resize",d),()=>window.removeEventListener("resize",d)},[o]),l.useEffect(()=>{function d(y){var N;if(u.current&&!u.current.contains(y.target)){if((N=o.current)!=null&&N.contains(y.target))return;h()}}return document.addEventListener("mousedown",d),()=>document.removeEventListener("mousedown",d)},[h,o]),x?e.jsxs("div",{className:"add-node-menu",ref:u,style:{top:x.top,right:x.right},children:[e.jsx("div",{className:"add-node-menu-title",children:"Add event on…"}),s.nodes.length===0?e.jsx("div",{className:"switcher-empty",children:"No nodes in this diagram"}):s.nodes.map(d=>{const y=g.types[d.type],N=(y==null?void 0:y.color)??"#71717a";return e.jsxs("button",{className:"add-node-item",onClick:()=>p(d.id),style:{"--node-color":N},children:[e.jsx("span",{className:"add-node-color"}),e.jsxs("div",{className:"add-node-text",children:[e.jsx("div",{className:"add-node-label",children:d.label}),e.jsx("code",{className:"add-node-key",children:d.id})]})]},d.id)})]}):null}const we=500;function Ee(s){const[g,o]=l.useState({timeline:null,diagram:null,nodeTypes:null,loadError:null,saveStatus:"idle",saveError:null,connectionStatus:"connecting"}),p=l.useRef(null),h=l.useRef(null),u=l.useRef(g);u.current=g;const x=l.useRef(!1);l.useEffect(()=>{let r=!1;x.current=!1;async function m(){try{const c=await te(s);if(r)return;const v=await se(c.diagram);if(r)return;h.current=c,x.current=!0,o(b=>({...b,timeline:c,diagram:v.diagram,nodeTypes:v.nodeTypes,loadError:null}))}catch(c){if(r)return;o(v=>({...v,loadError:c instanceof Error?c.message:String(c)}))}}return m(),()=>{r=!0}},[s]);const a=l.useCallback(()=>{p.current&&clearTimeout(p.current),o(r=>({...r,saveStatus:"dirty",saveError:null})),p.current=setTimeout(async()=>{const r=h.current;if(r){o(m=>({...m,saveStatus:"saving"}));try{await oe(r),o(m=>({...m,saveStatus:"saved"}))}catch(m){o(c=>({...c,saveStatus:"error",saveError:m instanceof Error?m.message:String(m)}))}}},we)},[]),d=l.useCallback(r=>{x.current&&(o(m=>{if(!m.timeline)return m;const c=r(m.timeline);return h.current=c,{...m,timeline:c}}),a())},[a]),y=l.useCallback((r,m)=>{d(c=>({...c,events:c.events.map(v=>v.id===r?m(v):v)}))},[d]),N=l.useCallback(r=>{d(m=>({...m,events:[...m.events,r]}))},[d]),R=l.useCallback(r=>{d(m=>({...m,events:m.events.filter(c=>c.id!==r)}))},[d]);return l.useEffect(()=>{o(c=>({...c,connectionStatus:"connecting"}));const r=new EventSource("/api/events");r.onopen=()=>o(c=>({...c,connectionStatus:"connected"})),r.onerror=()=>o(c=>({...c,connectionStatus:"disconnected"}));const m=async()=>{const c=u.current.saveStatus;if(!(c==="dirty"||c==="saving"))try{const v=await te(s),b=await se(v.diagram);h.current=v,o(T=>({...T,timeline:v,diagram:b.diagram,nodeTypes:b.nodeTypes,saveStatus:"idle",saveError:null}))}catch{}};return r.addEventListener("change",c=>{try{const v=JSON.parse(c.data),b=u.current.timeline;(v.type==="timeline-changed"&&v.id===s||v.type==="diagram-changed"&&b&&v.id===b.diagram||v.type==="node-types-changed")&&m()}catch{}}),()=>{r.close()}},[s]),l.useEffect(()=>()=>{p.current&&clearTimeout(p.current)},[]),l.useMemo(()=>({...g,updateEvent:y,addEvent:N,deleteEvent:R}),[g,y,N,R])}function Se(s){const g=new Set(s.events.map(p=>p.id));let o=s.events.length+1;for(;g.has(`ev${o}`);)o++;return`ev${o}`}function Te({id:s,diagrams:g,timelines:o,isDefault:p,onClickHome:h,onNavigate:u,onCreateDiagram:x}){const a=Ee(s),[d,y]=l.useState(null),[N,R]=l.useState(!1),B=l.useRef(null),[r,m]=l.useState(1),[c,v]=l.useState(0),[b,T]=l.useState(!1),[O,Z]=l.useState(1),k=l.useRef(null),X=l.useRef(0),_=l.useMemo(()=>a.timeline?a.timeline.events.reduce((t,n)=>Math.max(t,n.start_ms+n.duration_ms),0):0,[a.timeline]),P=l.useMemo(()=>{const t=new Set;if(!a.timeline)return t;for(const n of a.timeline.events)c>=n.start_ms&&c<=n.start_ms+n.duration_ms&&t.add(n.node);return t},[a.timeline,c]),H=l.useMemo(()=>{const t=new Set;if(!a.diagram)return t;for(const n of a.diagram.edges){const f=n.from.indexOf(":"),j=f===-1?n.from:n.from.slice(0,f);P.has(j)&&t.add(n.id)}return t},[a.diagram,P]);l.useEffect(()=>{if(!b){k.current!==null&&(clearInterval(k.current),k.current=null);return}return X.current=performance.now(),k.current=setInterval(()=>{const t=performance.now(),n=t-X.current;X.current=t,v(f=>{const j=f+n*O;return j>=_?(T(!1),_):j})},16),()=>{k.current!==null&&clearInterval(k.current)}},[b,O,_]);const z=l.useCallback(()=>{_!==0&&T(t=>(!t&&c>=_&&v(0),!t))},[c,_]),W=l.useCallback(()=>{v(0),T(!1)},[]),q=l.useCallback(t=>{v(t)},[]),L=l.useCallback(t=>{const n=a.timeline;if(!n)return;const f=Math.max(0,Math.round(c)),j={id:Se(n),node:t,start_ms:f,duration_ms:200,kind:"compute"};a.addEvent(j),y(j.id),R(!1)},[a,c]);if(l.useEffect(()=>{const t=n=>{const f=n.target;f.tagName==="INPUT"||f.tagName==="TEXTAREA"||f.tagName==="SELECT"||(n.key===" "||n.code==="Space"?(n.preventDefault(),z()):n.key==="Home"?(n.preventDefault(),W()):(n.key==="Backspace"||n.key==="Delete")&&d&&(n.preventDefault(),a.deleteEvent(d),y(null)))};return document.addEventListener("keydown",t),()=>document.removeEventListener("keydown",t)},[z,W,d,a]),a.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: ",a.loadError]})}),e.jsx("div",{className:"inspector",children:e.jsx("div",{className:"empty",children:"—"})})]});if(!a.timeline||!a.diagram||!a.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 i=d?a.timeline.events.find(t=>t.id===d)??null:null;return e.jsxs("div",{className:"app timeline-app",children:[e.jsx(de,{viewKind:"timeline",viewId:s,title:a.timeline.title,subtitle:a.timeline.description,diagrams:g,timelines:o,saveStatus:a.saveStatus,saveError:a.saveError,connectionStatus:a.connectionStatus,onClickAdd:()=>{},addMenuOpen:!1,isDefault:p,onClickHome:h,onNavigate:u,onCreateDiagram:x,addButtonRef:null,hideAddButton:!0}),e.jsx(ke,{playing:b,positionMs:c,totalMs:_,speed:O,onPlayPause:z,onReset:W,onSpeed:Z,actions:e.jsxs(e.Fragment,{children:[e.jsxs("button",{ref:B,className:"transport-add-event",onClick:()=>R(t=>!t),title:"Add event at playhead",children:[e.jsx(ue,{size:14})," Event"]}),e.jsxs("label",{className:"transport-zoom",children:[e.jsx("span",{className:"muted",children:"Zoom"}),e.jsxs("select",{value:r,onChange:t=>m(Number(t.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×"})]})]})]})}),N&&e.jsx(be,{diagram:a.diagram,nodeTypes:a.nodeTypes,anchorRef:B,onPick:L,onClose:()=>R(!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(je,{timeline:a.timeline,diagram:a.diagram,nodeTypes:a.nodeTypes,selectedEventId:d,onSelectEvent:y,onUpdateEvent:a.updateEvent,playheadMs:c,onScrub:q,zoom:r})}),e.jsx("div",{className:"timeline-split-pane timeline-split-right",children:e.jsx(me,{diagram:a.diagram,nodeTypesConfig:a.nodeTypes,interactive:!1,activeNodeIds:P,pulsingEdgeIds:H})})]})}),e.jsx(ye,{selectedEvent:i,diagram:a.diagram})]})}export{Te as TimelineView};
@@ -1,135 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://loom-spec.dev/schema/timeline-v1.json",
4
- "title": "Loom Timeline",
5
- "description": "A time-axis overlay describing when events happen on the nodes of a diagram. Same node universe, different render.",
6
- "type": "object",
7
- "required": ["version", "id", "title", "diagram", "events"],
8
- "additionalProperties": false,
9
- "properties": {
10
- "$schema": { "type": "string" },
11
- "version": { "const": "1" },
12
- "id": {
13
- "type": "string",
14
- "pattern": "^[a-z0-9-]+$",
15
- "description": "Timeline identifier. Should match the filename without extension."
16
- },
17
- "title": { "type": "string", "minLength": 1 },
18
- "description": { "type": "string" },
19
- "diagram": {
20
- "type": "string",
21
- "pattern": "^[a-z0-9-]+$",
22
- "description": "Id of the diagram whose nodes this timeline overlays."
23
- },
24
- "events": {
25
- "type": "array",
26
- "items": { "$ref": "#/$defs/TimelineEvent" }
27
- },
28
- "tracks": {
29
- "type": "array",
30
- "items": { "$ref": "#/$defs/TimelineTrack" },
31
- "description": "Optional explicit track definitions (label, color). If omitted, tracks are inferred from events[].track values."
32
- }
33
- },
34
-
35
- "$defs": {
36
- "TimelineEvent": {
37
- "type": "object",
38
- "required": ["id", "node", "start_ms", "duration_ms"],
39
- "additionalProperties": false,
40
- "properties": {
41
- "id": {
42
- "type": "string",
43
- "pattern": "^[a-z0-9-]+$",
44
- "description": "Unique within this timeline."
45
- },
46
- "node": {
47
- "type": "string",
48
- "pattern": "^[a-z0-9-]+$",
49
- "description": "Node id from the referenced diagram."
50
- },
51
- "track": {
52
- "type": "string",
53
- "description": "Track label. Events on the same track are interpreted as sequential; events on different tracks at overlapping times are concurrent. If omitted, the renderer auto-assigns one track per node."
54
- },
55
- "start_ms": {
56
- "type": "number",
57
- "minimum": 0,
58
- "description": "Start time in milliseconds from t=0."
59
- },
60
- "duration_ms": {
61
- "type": "number",
62
- "minimum": 0,
63
- "description": "Duration in milliseconds. May be 0 for instantaneous events."
64
- },
65
- "label": {
66
- "type": "string",
67
- "maxLength": 60,
68
- "description": "Short text rendered inside the clip."
69
- },
70
- "description": { "type": "string" },
71
- "kind": {
72
- "type": "string",
73
- "enum": ["compute", "io", "wait", "error"],
74
- "description": "Optional category for clip styling. Distinct from edge.kind in the diagram — this describes what the node is doing during this clip, not what flows between nodes."
75
- },
76
- "code_refs": {
77
- "type": "array",
78
- "items": { "$ref": "#/$defs/CodeRef" },
79
- "description": "Optional anchors to the specific function(s) running during this clip. Like node-level code_refs but scoped to a moment in time. Drift detection checks these too. Use for function-level granularity inside a node (e.g. 'validate' vs. 'issue jwt' both happen on the auth-service node)."
80
- },
81
- "triggered_by": {
82
- "type": "string",
83
- "pattern": "^[a-z0-9-]+$",
84
- "description": "Optional id of another event that caused this one. Used for explicit causation chains and to preserve OTel span.parent_id when importing traces. The renderer can draw a connecting arrow between the two clips."
85
- },
86
- "tags": {
87
- "type": "array",
88
- "items": { "type": "string" },
89
- "description": "Free-form labels for filtering or grouping clips. Examples: 'critical-path', 'billable-time', 'background'."
90
- }
91
- }
92
- },
93
-
94
- "CodeRef": {
95
- "type": "object",
96
- "required": ["path"],
97
- "additionalProperties": false,
98
- "description": "Same shape as the CodeRef in diagram.schema.json. Inlined here to keep the schema self-contained — no cross-file $ref required to validate.",
99
- "properties": {
100
- "path": {
101
- "type": "string",
102
- "description": "Repo-relative file path."
103
- },
104
- "symbol": {
105
- "type": "string",
106
- "description": "Function, class, or component name. Preferred over lines because it survives refactors."
107
- },
108
- "lines": {
109
- "type": "string",
110
- "pattern": "^[0-9]+(-[0-9]+)?(,[0-9]+(-[0-9]+)?)*$",
111
- "description": "Line range(s) like '1-80' or '12,45-50'. Use only when no symbol applies."
112
- }
113
- }
114
- },
115
-
116
- "TimelineTrack": {
117
- "type": "object",
118
- "required": ["id", "label"],
119
- "additionalProperties": false,
120
- "properties": {
121
- "id": {
122
- "type": "string",
123
- "pattern": "^[a-z0-9-]+$",
124
- "description": "Track id, matches event.track values."
125
- },
126
- "label": { "type": "string", "minLength": 1 },
127
- "color": {
128
- "type": "string",
129
- "pattern": "^#[0-9a-fA-F]{6}$",
130
- "description": "Optional background tint for the track lane."
131
- }
132
- }
133
- }
134
- }
135
- }