Skip to content

Commit 309028d

Browse files
committed
feat: Refactor timeline to use precise target IDs and rect-based alignment (#8536)
1 parent 2248383 commit 309028d

3 files changed

Lines changed: 69 additions & 39 deletions

File tree

apps/portal/canvas/TicketCanvas.mjs

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,11 @@ class TicketCanvas extends Base {
126126
// 1. Clear
127127
ctx.clearRect(0, 0, width, height);
128128

129-
// 2. Draw Vertical Neural Line (The "Spine")
130-
// Positioned relative to the first node.
131-
// CSS: Timeline padding-left 60px. Line at 30px. Item content starts at 60px.
132-
// So line is 30px to the left of the item content.
133-
// We assume nodes[0].x is the item content left edge.
134-
let spineX = 38,
135-
startY = me.startY;
136-
137-
if (me.nodes.length > 0) {
138-
spineX = me.nodes[0].x - 30; // Shift left to center in the gutter
139-
}
140-
129+
// 2. Draw Neural Connections (The "Spine")
130+
// We connect each node to the next one.
131+
// For the first segment, we can just draw straight down from the first node?
132+
// Or strictly connect nodes. Let's connect nodes.
133+
141134
// Gradient for the spine
142135
const gradient = ctx.createLinearGradient(0, 0, 0, height);
143136
gradient.addColorStop(0, 'rgba(64, 196, 255, 0.1)');
@@ -147,18 +140,53 @@ class TicketCanvas extends Base {
147140
ctx.strokeStyle = gradient;
148141
ctx.lineWidth = 2;
149142
ctx.beginPath();
150-
ctx.moveTo(spineX, startY);
151-
ctx.lineTo(spineX, height);
143+
144+
if (me.nodes.length > 0) {
145+
let first = me.nodes[0];
146+
ctx.moveTo(first.x, first.y);
147+
148+
for (let i = 1; i < me.nodes.length; i++) {
149+
let node = me.nodes[i];
150+
ctx.lineTo(node.x, node.y);
151+
}
152+
// Extend to bottom from last node
153+
let last = me.nodes[me.nodes.length - 1];
154+
ctx.lineTo(last.x, height);
155+
}
152156
ctx.stroke();
153157

154158
// 3. Draw "Pulse" Effect
155-
// A glowing segment moving down
159+
// A glowing segment moving down the path
160+
// To do this strictly on the path requires path following logic.
161+
// For now, let's keep the vertical pulse but align it to the node X at that Y.
162+
// Since our nodes are mostly vertical, we can interpolate X based on pulseY.
163+
156164
const pulseSpeed = 0.15; // px per ms
157165
const pulseY = (now * pulseSpeed) % height;
158166
const pulseLength = 100;
159167

160-
// Only draw pulse if it's within the spine range
161-
if (pulseY > startY) {
168+
// Find which segment the pulse is in
169+
// Simple linear interpolation function
170+
const getXAtY = (y) => {
171+
if (me.nodes.length < 2) return me.nodes[0]?.x || 38;
172+
173+
// Before first node
174+
if (y < me.nodes[0].y) return me.nodes[0].x;
175+
176+
for (let i = 0; i < me.nodes.length - 1; i++) {
177+
let curr = me.nodes[i];
178+
let next = me.nodes[i+1];
179+
if (y >= curr.y && y <= next.y) {
180+
let ratio = (y - curr.y) / (next.y - curr.y);
181+
return curr.x + (next.x - curr.x) * ratio;
182+
}
183+
}
184+
185+
// After last node
186+
return me.nodes[me.nodes.length - 1].x;
187+
};
188+
189+
if (me.nodes.length > 0 && pulseY > me.nodes[0].y) {
162190
const pulseGrad = ctx.createLinearGradient(0, pulseY, 0, pulseY + pulseLength);
163191
pulseGrad.addColorStop(0, 'rgba(64, 196, 255, 0)');
164192
pulseGrad.addColorStop(0.5, 'rgba(64, 196, 255, 1)');
@@ -167,18 +195,20 @@ class TicketCanvas extends Base {
167195
ctx.strokeStyle = pulseGrad;
168196
ctx.lineWidth = 4;
169197
ctx.beginPath();
170-
ctx.moveTo(spineX, pulseY);
171-
ctx.lineTo(spineX, Math.min(pulseY + pulseLength, height));
198+
199+
// Draw pulse segment by segment to follow the path
200+
// This is complex for a simple effect.
201+
// Simplified: Draw a vertical line at the interpolated X.
202+
// It might look slightly detached on steep angles but fine for vertical flow.
203+
let pulseX = getXAtY(pulseY);
204+
ctx.moveTo(pulseX, pulseY);
205+
ctx.lineTo(getXAtY(pulseY + pulseLength), Math.min(pulseY + pulseLength, height));
172206
ctx.stroke();
173207
}
174-
175-
// Wrap around pulse (if at bottom)
176-
if (pulseY + pulseLength > height) {
177-
// Draw the remainder at top? Or just let it flow out.
178-
}
179208

180209
// 4. Draw Nodes (Event Markers)
181210
me.nodes.forEach(node => {
211+
const x = node.x;
182212
const y = node.y;
183213

184214
// Interaction: If pulse is near node, glow up
@@ -188,7 +218,7 @@ class TicketCanvas extends Base {
188218
const alpha = isActive ? 1 : 0.5;
189219

190220
ctx.beginPath();
191-
ctx.arc(spineX, y, radius, 0, 2 * Math.PI);
221+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
192222
ctx.fillStyle = `rgba(64, 196, 255, ${alpha})`;
193223
ctx.fill();
194224

apps/portal/view/news/tickets/Component.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ class Component extends ContentComponent {
316316

317317
let bodyItemHtml = `
318318
<div id="${bodyId}" class="neo-timeline-item comment body-item" data-record-id="${bodyId}">
319-
<div class="neo-timeline-avatar">
319+
<div id="${bodyId}-target" class="neo-timeline-avatar">
320320
<img src="${me.repoUserUrl}${author}.png" alt="${author}">
321321
</div>
322322
<div class="neo-timeline-content">
@@ -377,7 +377,7 @@ class Component extends ContentComponent {
377377
let body = marked.parse(commentBuf.join('\n'));
378378
html += `
379379
<div id="${id}" class="neo-timeline-item comment" data-record-id="${id}">
380-
<div class="neo-timeline-avatar">
380+
<div id="${id}-target" class="neo-timeline-avatar">
381381
<img src="${repoUserUrl}${currentUser}.png" alt="${currentUser}">
382382
</div>
383383
<div class="neo-timeline-content">
@@ -451,7 +451,7 @@ class Component extends ContentComponent {
451451

452452
html += `
453453
<div id="${id}" class="neo-timeline-item event ${actionCls}" data-record-id="${id}">
454-
<div class="neo-timeline-badge"><i class="fa-solid ${icon}"></i></div>
454+
<div id="${id}-target" class="neo-timeline-badge"><i class="fa-solid ${icon}"></i></div>
455455
<div class="neo-timeline-body">
456456
<a class="neo-timeline-user" href="${repoUserUrl}${user}" target="_blank">${user}</a> ${cleanAction} <span class="neo-timeline-date">on ${me.formatTimestamp(date)}</span>
457457
</div>

apps/portal/view/news/tickets/TimelineCanvas.mjs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,14 @@ class TimelineCanvas extends Canvas {
121121
let delay = isResize ? 50 : 100;
122122

123123
me.timeout(delay).then(async () => {
124-
let ids = records.map(r => r.id),
124+
// Target the actual Avatar/Badge elements we added IDs to
125+
let ids = records.map(r => `${r.id}-target`),
125126
componentId = me.getStateProvider().data.contentComponentId,
126127
timelineId = `ticket-timeline-${componentId}`,
127128
rects, timelineRect;
128129

129130
try {
130-
// Fetch DOM rects for all timeline items
131+
// Fetch DOM rects for the MARKERS (Avatars/Badges), not the containers
131132
rects = await me.getDomRect(ids);
132133

133134
// Fetch timeline container rect (optional, fallback)
@@ -155,22 +156,21 @@ class TimelineCanvas extends Canvas {
155156
let nodes = [];
156157
let startY = 0;
157158

158-
ids.forEach((id, index) => {
159+
ids.forEach((targetId, index) => {
159160
let rect = rects[index],
160161
record = records[index];
161162

162163
if (rect) {
163-
// Precise offset based on CSS (Avatar/Badge position)
164-
// Comment/Body: Avatar is top: -6px, height: 40px -> Center is -6 + 20 = 14px from rect top.
165-
// Event: Badge is top: -2px, height: 28px -> Center is -2 + 14 = 12px from rect top.
166-
let isComment = record.tag === 'comment' || record.tag === 'body';
167-
let offset = isComment ? 14 : 12;
168-
let nodeY = rect.y - canvasRect.y + offset;
164+
// PRECISE CENTERING
165+
// Now 'rect' is the actual avatar/badge.
166+
let offset = rect.height / 2;
167+
let nodeY = rect.y - canvasRect.y + offset;
168+
let nodeX = rect.x - canvasRect.x + (rect.width / 2);
169169

170170
nodes.push({
171-
id: id,
171+
id: record.id, // Keep original ID for logic
172172
y : nodeY,
173-
x : rect.x - canvasRect.x
173+
x : nodeX
174174
});
175175

176176
// Set the startY of the line to the first node

0 commit comments

Comments
 (0)