Skip to content

Commit b520179

Browse files
committed
feat: enhance SW Timeline with search functionality and constellation visualization
- Integrated SearchBar component for user input and mode selection. - Added ConstellationTimeline component to visualize data based on search queries. - Updated package.json with new scripts for development and build processes. - Improved project structure by organizing components and enhancing TypeScript definitions.
1 parent 8496c11 commit b520179

File tree

11 files changed

+638
-18
lines changed

11 files changed

+638
-18
lines changed

package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@
99
"services/*"
1010
],
1111
"scripts": {
12+
"kill": "bash -c 'turbo kill || true'",
13+
"clean": "bash -c 'turbo kill || true; rm -rf .pages web/dist apps/*/dist services/*/.turbo services/*/.pages; rm -rf .turbo 2>/dev/null || true; rm -rf node_modules 2>/dev/null || true'",
14+
"install:all": "yarn install",
1215
"build": "turbo run build",
13-
"dev": "turbo run dev --parallel",
16+
"compose:pages": "bash -c 'mkdir -p .pages && cp -r web/dist/* .pages/ && mkdir -p .pages/apps && if ls apps 1> /dev/null 2>&1; then cp -r apps/* .pages/apps/ || true; fi && cp .pages/index.html .pages/404.html'",
17+
"serve:pages": "npx serve .pages -l 3000",
18+
"start:dev": "turbo run dev --parallel",
19+
"start:fast": "yarn build && yarn compose:pages && yarn serve:pages",
20+
"start:fresh": "yarn kill && yarn clean && yarn install:all && yarn start:fast",
21+
"rebuild": "yarn clean && yarn build",
1422
"lint": "turbo run lint",
1523
"typecheck": "turbo run typecheck"
1624
},

web/docs/vision-sw-timeline.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SW Timeline – Visão, Escopo e Princípios de UX
2+
3+
## Visão
4+
Representar a evolução da Teoria da Computação e da Engenharia de Software como **constelações temporais**: marcos (nós) interligados por influências, citações e derivações (arestas), navegáveis num céu escuro com eixo vertical do tempo e ligações horizontais entre áreas (ex.: OOP → IA).
5+
6+
## Escopo (MVP)
7+
- Dataset seed (20–30 marcos) com fontes primárias.
8+
- Renderização Canvas 2D de nós/arestas com eixo temporal.
9+
- Interações básicas: pan/zoom, hover/tooltip, foco e trilhas de influência.
10+
- Buscar/filtrar por tema/época/autor em etapa seguinte.
11+
12+
## Princípios de UX
13+
- Minimalista, legível, dark; camadas ativáveis (constelações, overlay Hubble, trilhas).
14+
- Conteúdo comprovável: cada aresta tem evidência/ligação primária.
15+
- Navegação com teleports (deep links) e foco progressivo.
16+
17+
## Entidades e Relações (resumo)
18+
- `node`: { id, type, label, date, authors[], sources[], tags[] }
19+
- `edge`: { from, to, relation, weight?, evidence[] }
20+
- Relações: influences, cites, derives-from, precedes, contradicts, synthesizes.
21+
22+
## Layout
23+
- Y = tempo (escala contínua por ano/data ISO).
24+
- X = clusterização por tema (paradigmas, IA, linguagens) + separação por constelações.
25+
- LOD: reduzir detalhe fora do foco.
26+
27+
## Curadoria
28+
- Somente fontes primárias para **evidenciar** ligações (papers, livros, relatórios originais, anais, DOIs/ISBNs).
29+
- Cada ligação (edge) deve possuir pelo menos um `evidence` com URL, DOI, página/trecho ou citação.
30+
31+
## Entregáveis do MVP
32+
- `schema.json` e `seed.json` validados no CI.
33+
- Renderer Canvas básico com pan/zoom e hover.
34+
- Página `sw-timeline` integrada ao portfólio (fullscreen opcional).
35+
36+
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2+
import { useEffect, useRef, useState } from 'react';
3+
import dataset from '../../data/sw-timeline/seed.json';
4+
function yearOf(date) {
5+
const y = parseInt(date.slice(0, 4), 10);
6+
return Number.isFinite(y) ? y : 0;
7+
}
8+
export const ConstellationTimeline = ({ height = 600, query = '', mode = 'highlight' }) => {
9+
const ref = useRef(null);
10+
const containerRef = useRef(null);
11+
// pan/zoom state
12+
const transformRef = useRef({ offsetX: 0, offsetY: 0, scale: 1 });
13+
const isPanningRef = useRef(false);
14+
const lastPosRef = useRef({ x: 0, y: 0 });
15+
const [hover, setHover] = useState({ x: 0, y: 0, nodeIndex: null });
16+
const [tick, setTick] = useState(0); // force redraws
17+
useEffect(() => {
18+
const canvas = ref.current;
19+
if (!canvas)
20+
return;
21+
const ctx = canvas.getContext('2d');
22+
if (!ctx)
23+
return;
24+
const dpr = window.devicePixelRatio || 1;
25+
const width = canvas.clientWidth * dpr;
26+
const heightPx = height * dpr;
27+
canvas.width = width;
28+
canvas.height = heightPx;
29+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
30+
// time range
31+
const years = dataset.nodes.map(n => yearOf(n.date)).filter(Boolean);
32+
const minY = Math.min(...years);
33+
const maxY = Math.max(...years);
34+
const yScale = (y) => {
35+
const t = (y - minY) / (maxY - minY || 1);
36+
return 40 + t * (height - 80);
37+
};
38+
const xBuckets = { person: 0.2, work: 0.5, paradigm: 0.8, event: 0.65, technology: 0.35 };
39+
const xScale = (type) => Math.floor((xBuckets[type] ?? 0.5) * (canvas.clientWidth - 80)) + 40;
40+
const q = query.trim().toLowerCase();
41+
const matches = (label) => q.length === 0 || label.toLowerCase().includes(q);
42+
const { offsetX, offsetY, scale } = transformRef.current;
43+
ctx.save();
44+
ctx.translate(offsetX, offsetY);
45+
ctx.scale(scale, scale);
46+
// Draw edges
47+
ctx.strokeStyle = 'rgba(0,240,255,0.15)';
48+
ctx.lineWidth = 1.2;
49+
dataset.edges.forEach((e) => {
50+
const a = dataset.nodes.find(n => n.id === e.from);
51+
const b = dataset.nodes.find(n => n.id === e.to);
52+
if (!a || !b)
53+
return;
54+
if (mode === 'filter') {
55+
const aText = `${a.label} ${a.tags?.join(' ') ?? ''}`;
56+
const bText = `${b.label} ${b.tags?.join(' ') ?? ''}`;
57+
if (!(matches(aText) || matches(bText)))
58+
return;
59+
}
60+
const ax = xScale(a.type);
61+
const ay = yScale(yearOf(a.date));
62+
const bx = xScale(b.type);
63+
const by = yScale(yearOf(b.date));
64+
ctx.beginPath();
65+
ctx.moveTo(ax, ay);
66+
ctx.lineTo(bx, by);
67+
if (mode === 'highlight' && q) {
68+
const h = 1 + 2; // thickness boost
69+
ctx.lineWidth = 2;
70+
ctx.globalAlpha = 0.35;
71+
ctx.stroke();
72+
ctx.lineWidth = 1.2;
73+
ctx.globalAlpha = 1;
74+
}
75+
else {
76+
ctx.stroke();
77+
}
78+
});
79+
// Draw nodes
80+
dataset.nodes.forEach((n) => {
81+
const x = xScale(n.type);
82+
const y = yScale(yearOf(n.date));
83+
const text = `${n.label} ${(n.tags ?? []).join(' ')}`;
84+
const isMatch = matches(text);
85+
if (mode === 'filter' && q && !isMatch)
86+
return;
87+
// base point
88+
ctx.fillStyle = isMatch && q ? '#00f0ff' : '#fff';
89+
ctx.beginPath();
90+
ctx.arc(x, y, isMatch && q ? 5 : 4, 0, Math.PI * 2);
91+
ctx.fill();
92+
ctx.fillStyle = isMatch && q ? 'rgba(0,240,255,0.95)' : 'rgba(226,232,240,0.9)';
93+
ctx.font = '12px Inter, system-ui, sans-serif';
94+
ctx.fillText(`${n.label} (${yearOf(n.date)})`, x + 8, y - 8);
95+
});
96+
ctx.restore();
97+
}, [height, query, mode, tick]);
98+
// interactions: pan, zoom, hover tooltip
99+
useEffect(() => {
100+
const canvas = ref.current;
101+
if (!canvas)
102+
return;
103+
const redraw = () => setTick(t => t + 1);
104+
const onWheel = (e) => {
105+
e.preventDefault();
106+
const rect = canvas.getBoundingClientRect();
107+
const mx = e.clientX - rect.left;
108+
const my = e.clientY - rect.top;
109+
const t = transformRef.current;
110+
const factor = e.deltaY < 0 ? 1.1 : 0.9;
111+
const newScale = Math.min(4, Math.max(0.25, t.scale * factor));
112+
const worldX = (mx - t.offsetX) / t.scale;
113+
const worldY = (my - t.offsetY) / t.scale;
114+
const newOffsetX = mx - worldX * newScale;
115+
const newOffsetY = my - worldY * newScale;
116+
transformRef.current = { offsetX: newOffsetX, offsetY: newOffsetY, scale: newScale };
117+
redraw();
118+
};
119+
const onMouseDown = (e) => {
120+
isPanningRef.current = true;
121+
lastPosRef.current = { x: e.clientX, y: e.clientY };
122+
if (containerRef.current)
123+
containerRef.current.style.cursor = 'grabbing';
124+
};
125+
const onMouseMove = (e) => {
126+
const rect = canvas.getBoundingClientRect();
127+
const mx = e.clientX - rect.left;
128+
const my = e.clientY - rect.top;
129+
if (isPanningRef.current) {
130+
const dx = e.clientX - lastPosRef.current.x;
131+
const dy = e.clientY - lastPosRef.current.y;
132+
lastPosRef.current = { x: e.clientX, y: e.clientY };
133+
const t = transformRef.current;
134+
transformRef.current = { offsetX: t.offsetX + dx, offsetY: t.offsetY + dy, scale: t.scale };
135+
setHover(h => ({ ...h, nodeIndex: null }));
136+
redraw();
137+
return;
138+
}
139+
// hover hit test in world space
140+
const t = transformRef.current;
141+
const wx = (mx - t.offsetX) / t.scale;
142+
const wy = (my - t.offsetY) / t.scale;
143+
// reconstruct layout
144+
const years = dataset.nodes.map(n => yearOf(n.date)).filter(Boolean);
145+
const minY = Math.min(...years);
146+
const maxY = Math.max(...years);
147+
const yScale = (y) => {
148+
const tt = (y - minY) / (maxY - minY || 1);
149+
return 40 + tt * (height - 80);
150+
};
151+
const xBuckets = { person: 0.2, work: 0.5, paradigm: 0.8, event: 0.65, technology: 0.35 };
152+
const xScale = (type) => Math.floor((xBuckets[type] ?? 0.5) * (canvas.clientWidth - 80)) + 40;
153+
const q = query.trim().toLowerCase();
154+
const inFilter = (n) => {
155+
if (!q)
156+
return true;
157+
const text = `${n.label} ${(n.tags ?? []).join(' ')}`.toLowerCase();
158+
return text.includes(q);
159+
};
160+
let found = null;
161+
for (let i = 0; i < dataset.nodes.length; i++) {
162+
const n = dataset.nodes[i];
163+
if (mode === 'filter' && q && !inFilter(n))
164+
continue;
165+
const x = xScale(n.type);
166+
const y = yScale(yearOf(n.date));
167+
const dx = wx - x;
168+
const dy = wy - y;
169+
if (dx * dx + dy * dy <= 9 * 9) { // radius 9 world units
170+
found = i;
171+
break;
172+
}
173+
}
174+
if (found !== null)
175+
setHover({ x: mx + 12, y: my + 12, nodeIndex: found });
176+
else if (hover.nodeIndex !== null)
177+
setHover(h => ({ ...h, nodeIndex: null }));
178+
};
179+
const onMouseUp = () => {
180+
isPanningRef.current = false;
181+
if (containerRef.current)
182+
containerRef.current.style.cursor = 'default';
183+
};
184+
const onMouseLeave = () => {
185+
isPanningRef.current = false;
186+
if (containerRef.current)
187+
containerRef.current.style.cursor = 'default';
188+
if (hover.nodeIndex !== null)
189+
setHover(h => ({ ...h, nodeIndex: null }));
190+
};
191+
canvas.addEventListener('wheel', onWheel, { passive: false });
192+
canvas.addEventListener('mousedown', onMouseDown);
193+
window.addEventListener('mousemove', onMouseMove);
194+
window.addEventListener('mouseup', onMouseUp);
195+
canvas.addEventListener('mouseleave', onMouseLeave);
196+
return () => {
197+
canvas.removeEventListener('wheel', onWheel);
198+
canvas.removeEventListener('mousedown', onMouseDown);
199+
window.removeEventListener('mousemove', onMouseMove);
200+
window.removeEventListener('mouseup', onMouseUp);
201+
canvas.removeEventListener('mouseleave', onMouseLeave);
202+
};
203+
}, [height, query, mode, hover.nodeIndex]);
204+
return (_jsxs("div", { ref: containerRef, className: "w-full relative", style: { cursor: 'default' }, children: [_jsx("canvas", { ref: ref, style: { width: '100%', height } }), hover.nodeIndex !== null && (_jsxs("div", { className: "pointer-events-none absolute z-10 p-3 rounded-md text-xs bg-slate-900/90 border border-slate-700 text-slate-200 shadow-lg max-w-xs", style: { left: hover.x, top: hover.y }, children: [_jsx("div", { className: "font-semibold text-neon mb-1", children: dataset.nodes[hover.nodeIndex].label }), dataset.nodes[hover.nodeIndex].summary && (_jsx("div", { className: "text-slate-300", children: dataset.nodes[hover.nodeIndex].summary })), dataset.nodes[hover.nodeIndex].tags && dataset.nodes[hover.nodeIndex].tags.length > 0 && (_jsxs("div", { className: "mt-1 text-slate-400", children: ["Tags: ", dataset.nodes[hover.nodeIndex].tags.slice(0, 6).join(', ')] }))] }))] }));
205+
};

0 commit comments

Comments
 (0)