hbauzan Cursor commited on
Commit
a70c667
·
1 Parent(s): dea3910

feat(amiga): scaffold MagicWB /amiga/ MPA at 2.4.0

Browse files

Add parallel Workbench skin with self-hosted Topaz, MagicWB tokens,
and Vite MPA entry; keep legacy and v25 skins untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>

.agents/skills/dev-protocol/lessons-learned.md CHANGED
@@ -288,11 +288,20 @@ Portable findings from VHectorLab 3D `v2.1.0` — apply if the older app shares
288
 
289
  ## 6. Dev tooling / ngrok / Vite ↔ backend
290
 
 
 
 
 
 
 
 
 
291
  ### 6.0d. `src/v25/**` — no tocar salvo pedido explícito del humano
292
  - **Regla**: el árbol `src/v25/` (y `v25/index.html` / assets MPA asociados) es un **proyecto aparte**. En roadmaps/epics del UI legado (`src/main.js`, `src/ui/*`, `src/visualizer/*`) está **prohibido** editar `src/v25/**`.
293
  - **Invariante**: no “sync de cortesía” (versión, copy, presets compartidos, imports) en v25 **a menos que el humano lo indique explícitamente** en el prompt (“tocá v25”, “sync version en v25”, “también en /v25/”, etc.). Silencio ≠ permiso.
294
  - **Verificación**: antes de approval gate / merge, `git diff -- src/v25` debe estar vacío si el epic no autorizó v25.
295
  - **Excepción**: solo cuando el humano lo pida; no inferir desde SemVer, CHANGELOG, o que v25 importe módulos de `src/ui/`.
 
296
 
297
  ### 6.0c. `v25:` MODE switch = visibility toggle, never wipe left host
298
  - **Problema**: montar Compare con `zones.left.innerHTML = …` destruye el panel Arithmetic (form state + Top-10). Además `display: flex` en `.lab-left-host > [data-panel]` **pisa** el UA `[hidden]{display:none}` → el slot Arithmetic oculto sigue ocupando flex y deja un **hueco negro** arriba del Compare (lista cosine aplastada abajo).
@@ -305,15 +314,15 @@ Portable findings from VHectorLab 3D `v2.1.0` — apply if the older app shares
305
  - **Invariante**: no fork de `SceneSetup` solo por MPA; no contaminar `src/style.css` legado. Startup canvas = `ARITHMETIC|ANALYSIS|POINTS` (`canvasStartupContext` / `appViewDefaults`).
306
  - **No hacer en Fase 8+ inmediata**: wire live de sliders (Fase 9) ni MODE Compare (Fase 10) desde header tabs.
307
 
308
- ### 6.0. `v25:` Vite MPA + FastAPI directory index
309
- - **Problema**: un solo `index.html` en la raíz no alcanza para la bandera blanca `/v25/`. En prod, el catch-all que solo chequea `is_file()` trata `dist/v25/` como miss y cae al SPA legado. En **dev**, Vite SPA fallback sirve el legado en `/v25` **sin** trailing slash → canvas oscuro “vacío”.
310
  - **Solución Obligatoria**:
311
- - Vite `build.rollupOptions.input` vía `getViteInputs(root)` (`vite.mpa.js`) → `main` + `v25` → emite `dist/index.html` y `dist/v25/index.html`.
312
- - `appType: 'mpa'` + plugin `mpaTrailingSlashRedirect` → `/v25` 302 → `/v25/`.
313
  - FastAPI: `resolve_dist_file(dist, path)` → archivo exacto, si no `path/index.html`, si no root `index.html`.
314
- - **Invariante**: nuevas páginas MPA = entry en `getViteInputs` **y** redirect bare-dir → slash; no hardcodear solo `/v25/index.html` en el cliente si `/v25/` debe funcionar.
315
- - **No hacer**: contaminar `src/style.css` / `src/main.js` legado desde v25.
316
- - **URL de prueba**: preferí `http://127.0.0.1:5173/v25/` (con slash); sin slash debe redirigir, no cargar el legado.
317
 
318
  ### 6.1. Proxy `/api` en Vite: prefijo general, no ruta-a-ruta
319
  - **Problema**: Exponer frontend y backend con **el mismo** hostname ngrok (dos agentes → `:5173` y `:8000`) se pisa; el celu no puede llamar a `127.0.0.1:8000`.
 
288
 
289
  ## 6. Dev tooling / ngrok / Vite ↔ backend
290
 
291
+ ### 6.0e. `/amiga` (MagicWB) — proyecto aparte; v25 y legado off-limits
292
+ - **Regla**: el skin Amiga Workbench / MagicWB vive **solo** bajo `amiga/` + `src/amiga/**` (MPA `/amiga/`). Es un epic **independiente** de `gui-art` / `gui-art-v25` / `/v25/`.
293
+ - **PROHIBIDO** en el epic Amiga: editar `src/v25/**`, `v25/**`, o “alinear” v25 (versión, tokens, copy). También evitar contaminar `src/style.css` / `src/main.js` / `src/ui/*` legado salvo cambios MPA compartidos mínimos (`vite.mpa.js`, redirect `/amiga`, tests MPA).
294
+ - **Invariante**: SemVer de producto en este epic = línea **`2.4.x`** (MINOR `2.4.0` al primer scaffold demoable). HF Space público = **último** slice del roadmap, no en slices intermedios.
295
+ - **Verificación**: antes de approval / merge Amiga → `git diff -- src/v25 v25` vacío; revisar que el diff legado no meta skin Amiga en `/`.
296
+ - **Norte visual**: Topaz self-hosted + sin antialiasing (múltiplos de 8px) + paleta MagicWB 8 colores; CSS estático (no inyección `<style>` por JS). Cosplay full (iconos/drawers) = roadmap futuro, no este epic.
297
+ - **Mobile**: desktop y mobile desde el inicio; si Topaz/density falla → preguntar al humano, no bajar de múltiplos de 8 “a ciegas”.
298
+
299
  ### 6.0d. `src/v25/**` — no tocar salvo pedido explícito del humano
300
  - **Regla**: el árbol `src/v25/` (y `v25/index.html` / assets MPA asociados) es un **proyecto aparte**. En roadmaps/epics del UI legado (`src/main.js`, `src/ui/*`, `src/visualizer/*`) está **prohibido** editar `src/v25/**`.
301
  - **Invariante**: no “sync de cortesía” (versión, copy, presets compartidos, imports) en v25 **a menos que el humano lo indique explícitamente** en el prompt (“tocá v25”, “sync version en v25”, “también en /v25/”, etc.). Silencio ≠ permiso.
302
  - **Verificación**: antes de approval gate / merge, `git diff -- src/v25` debe estar vacío si el epic no autorizó v25.
303
  - **Excepción**: solo cuando el humano lo pida; no inferir desde SemVer, CHANGELOG, o que v25 importe módulos de `src/ui/`.
304
+ - **Nota**: el epic `/amiga` (§6.0e) también deja v25 intacto; no usar Amiga como excusa para tocar v25.
305
 
306
  ### 6.0c. `v25:` MODE switch = visibility toggle, never wipe left host
307
  - **Problema**: montar Compare con `zones.left.innerHTML = …` destruye el panel Arithmetic (form state + Top-10). Además `display: flex` en `.lab-left-host > [data-panel]` **pisa** el UA `[hidden]{display:none}` → el slot Arithmetic oculto sigue ocupando flex y deja un **hueco negro** arriba del Compare (lista cosine aplastada abajo).
 
314
  - **Invariante**: no fork de `SceneSetup` solo por MPA; no contaminar `src/style.css` legado. Startup canvas = `ARITHMETIC|ANALYSIS|POINTS` (`canvasStartupContext` / `appViewDefaults`).
315
  - **No hacer en Fase 8+ inmediata**: wire live de sliders (Fase 9) ni MODE Compare (Fase 10) desde header tabs.
316
 
317
+ ### 6.0. `v25:` / `amiga:` Vite MPA + FastAPI directory index
318
+ - **Problema**: un solo `index.html` en la raíz no alcanza para banderas blancas (`/v25/`, `/amiga/`). En prod, el catch-all que solo chequea `is_file()` trata `dist/<dir>/` como miss y cae al SPA legado. En **dev**, Vite SPA fallback sirve el legado en `/v25` o `/amiga` **sin** trailing slash.
319
  - **Solución Obligatoria**:
320
+ - Vite `build.rollupOptions.input` vía `getViteInputs(root)` (`vite.mpa.js`) → `main` + `v25` + `amiga` → emite `dist/index.html`, `dist/v25/index.html`, `dist/amiga/index.html`.
321
+ - `appType: 'mpa'` + plugin `mpaTrailingSlashRedirect` → bare dir 302 → trailing slash (`/v25/`, `/amiga/`).
322
  - FastAPI: `resolve_dist_file(dist, path)` → archivo exacto, si no `path/index.html`, si no root `index.html`.
323
+ - **Invariante**: nuevas páginas MPA = entry en `getViteInputs` **y** redirect bare-dir → slash; no hardcodear solo `/index.html` en el cliente si `/dir/` debe funcionar.
324
+ - **No hacer**: contaminar `src/style.css` / `src/main.js` legado desde v25 o amiga; no editar `src/v25/**` desde el epic amiga (ver §6.0e).
325
+ - **URL de prueba**: preferí `http://127.0.0.1:5173/v25/` y `http://127.0.0.1:5173/amiga/` (con slash); sin slash debe redirigir, no cargar el legado.
326
 
327
  ### 6.1. Proxy `/api` en Vite: prefijo general, no ruta-a-ruta
328
  - **Problema**: Exponer frontend y backend con **el mismo** hostname ngrok (dos agentes → `:5173` y `:8000`) se pisa; el celu no puede llamar a `127.0.0.1:8000`.
CHANGELOG.md CHANGED
@@ -23,6 +23,11 @@ All notable changes to VHectorLab 3D will be documented in this file.
23
  - **v25 layout shell** (Fase 3): 5-zone grid (header / left / canvas / right / footer); mobile stack via legacy `MOBILE_MQ`.
24
  - **v25 design tokens** (Fase 2): dark/fluo lab `:root`, Oxanium + IBM Plex Mono, `.lab-panel` / `.lab-btn` pressable utilities, WCAG contrast checks via `contrastRatio`.
25
 
 
 
 
 
 
26
  ## [2.3.0] - 2026-08-08
27
 
28
  ### Added
 
23
  - **v25 layout shell** (Fase 3): 5-zone grid (header / left / canvas / right / footer); mobile stack via legacy `MOBILE_MQ`.
24
  - **v25 design tokens** (Fase 2): dark/fluo lab `:root`, Oxanium + IBM Plex Mono, `.lab-panel` / `.lab-btn` pressable utilities, WCAG contrast checks via `contrastRatio`.
25
 
26
+ ## [2.4.0] - 2026-08-08
27
+
28
+ ### Added
29
+ - **`/amiga/` MPA scaffold** (Slice 1): Magic Workbench skin — self-hosted Topaz (`Topaz_a1200`), serrucho (no AA), 8-pen MagicWB tokens; Vite MPA entry + `/amiga` → `/amiga/` redirect. Parallel to legacy `/` and `/v25/` (neither skin touched). Plan: `roadmap/amiga-workbench.md`.
30
+
31
  ## [2.3.0] - 2026-08-08
32
 
33
  ### Added
CONTEXT.md CHANGED
@@ -61,6 +61,11 @@ Percent of the |t| range held at the zero color (default black) before blending
61
  ### Group Contrast
62
  Visualization paint for Compare with ≥2 groups: **Shared noise** blackens same-sign dims by similarity; **Sign conflict** highlights opposite-sign dims (custom color × |Δ|) and can blacken them by difference. Uses group means on raw embeddings; geometry Y unchanged.
63
 
 
 
 
 
 
64
  ### Shared Noise Similarity
65
  `1 − |mean_G1 − mean_G2| / (|mean_G1| + |mean_G2|)` per dimension — high values mean shared-sign “noise” between groups.
66
 
 
61
  ### Group Contrast
62
  Visualization paint for Compare with ≥2 groups: **Shared noise** blackens same-sign dims by similarity; **Sign conflict** highlights opposite-sign dims (custom color × |Δ|) and can blacken them by difference. Uses group means on raw embeddings; geometry Y unchanged.
63
 
64
+ ### Amiga Skin
65
+ Parallel MPA UI at `/amiga/` with Magic Workbench palette and Topaz pixel typography. Independent of legacy `/` and of `/v25/`; does not change product math or API contracts.
66
+
67
+ _Avoid_: treating Amiga as a reskin of v25 or legacy; editing `src/v25/**` for Amiga work.
68
+
69
  ### Shared Noise Similarity
70
  `1 − |mean_G1 − mean_G2| / (|mean_G1| + |mean_G2|)` per dimension — high values mean shared-sign “noise” between groups.
71
 
amiga/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>VHectorLab-3D Amiga</title>
7
+ <meta name="description" content="VHectorLab-3D Amiga — Magic Workbench skin (parallel MPA)." />
8
+ <link rel="stylesheet" href="/src/amiga/style.css" />
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+ <script type="module" src="/src/amiga/main.js"></script>
13
+ </body>
14
+ </html>
backend/server.py CHANGED
@@ -51,7 +51,7 @@ async def lifespan(app: FastAPI):
51
  app = FastAPI(
52
  title="VHectorLab 3D API",
53
  description="Backend API for 3D Vector Arithmetic & Semantic Embedding Visualizer",
54
- version="2.3.0",
55
  lifespan=lifespan,
56
  )
57
 
 
51
  app = FastAPI(
52
  title="VHectorLab 3D API",
53
  description="Backend API for 3D Vector Arithmetic & Semantic Embedding Visualizer",
54
+ version="2.4.0",
55
  lifespan=lifespan,
56
  )
57
 
manifest.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "project": "vhectorlab-3d",
3
- "version": "2.3.0",
4
  "bootstrap_run": false,
5
  "state_schema": {
6
  "model_name": "all-mpnet-base-v2",
 
1
  {
2
  "project": "vhectorlab-3d",
3
+ "version": "2.4.0",
4
  "bootstrap_run": false,
5
  "state_schema": {
6
  "model_name": "all-mpnet-base-v2",
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "vhectorlab-3d",
3
- "version": "2.3.0",
4
  "type": "module",
5
  "scripts": {
6
  "dev": "vite",
 
1
  {
2
  "name": "vhectorlab-3d",
3
+ "version": "2.4.0",
4
  "type": "module",
5
  "scripts": {
6
  "dev": "vite",
roadmap/PROMPT-amiga-workbench.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Prompt — Amiga Workbench `/amiga` (MagicWB)
2
+
3
+ **Modo:** **serial** — una sesión / un slice. No agentes en paralelo.
4
+ Empezá por el slice que indique el humano (default: **Slice 1** tras Slice 0 docs). No re-litigar Q1–Q10.
5
+
6
+ ---
7
+
8
+ ```text
9
+ Usando dev-protocol, implementá el roadmap Amiga Workbench `/amiga`.
10
+
11
+ ## Contexto
12
+ Repo: VHectorLab 3D / vhectorlab (Python/uv + Vite/Three.js + FastAPI).
13
+ Base: `main` actualizado (`git pull`).
14
+ SemVer: línea **2.4.x** (Slice 1 cierra con MINOR **2.4.0**).
15
+
16
+ Roadmap canónico (leelo COMPLETO antes de código):
17
+ `roadmap/amiga-workbench.md`
18
+ Lecciones: `.agents/skills/dev-protocol/lessons-learned.md` — **§6.0e obligatorio**
19
+ (+ §6.0d v25; §6.0 MPA).
20
+
21
+ ## AISLAMIENTO (NO negociable)
22
+ 1. **PROHIBIDO** tocar `src/v25/**`, `v25/**`, o “sync” a v25.
23
+ 2. Trabajo nuevo SOLO en `amiga/` + `src/amiga/**`.
24
+ 3. Legado `/` intacto salvo cambios MPA compartidos mínimos (`vite.mpa.js`, `vite.config.js`, tests MPA).
25
+ 4. Antes de approval: `git diff -- src/v25 v25` vacío.
26
+ 5. Este epic **NO** es gui-art / gui-art-v25. Cero dependencia de ese norte visual.
27
+
28
+ ## Decisiones CERRADAS (no re-preguntar)
29
+ - Bandera blanca `/amiga/`; Magic Workbench palette; Topaz self-host; CSS estático.
30
+ - Q2-A only (tipo+paleta+chrome mínimo). Cosplay Q2-C = otro roadmap.
31
+ - Canvas 3D look no se artea (Q3-A).
32
+ - Desktop + mobile; si mobile se complica → preguntar con diseño, no improvisar.
33
+ - HF Space = Slice 7 / final solamente.
34
+
35
+ ## Estilo
36
+ No-fluff; TDD donde haya helpers puros (tokens, MPA inputs); módulos profundos.
37
+ Seguí `.agents/skills/dev-protocol/SKILL.md`
38
+ (clarify → branch `feat/amiga-<slice>` → implement → verify → docs → approval gate → git delivery con OK).
39
+
40
+ ## Slice 1 — DoD (sesión típica de arranque)
41
+ 1. `amiga/index.html` + `src/amiga/main.js` + `tokens.css` / `style.css`.
42
+ 2. Topaz self-hosted bajo `src/amiga/assets/fonts/` (+ nota LICENSE en carpeta o README amiga).
43
+ 3. Clase raíz / `:root` con serrucho (no AA) + font-size 16px (múltiplos de 8) + paleta MagicWB (§3.2 roadmap).
44
+ 4. Vite MPA: entry `amiga` + redirect `/amiga` → `/amiga/`.
45
+ 5. Tests MPA actualizados (amiga en `getViteInputs` / redirect).
46
+ 6. Bump producto a **2.4.0** (manifest / package / CHANGELOG; Navbar legado solo si el DoD de SemVer del repo lo exige — **no** tocar version.js de v25).
47
+ 7. Smoke: `/` y `/v25/` sin cambios visuales; `/amiga/` se ve MagicWB+Topaz.
48
+ 8. APPROVAL GATE; ESPERÁ OK antes de push/merge.
49
+
50
+ ## Slices siguientes
51
+ Ver tabla §6 de `roadmap/amiga-workbench.md`. Un slice por sesión.
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Prompt — Slice 2+
57
+
58
+ ```text
59
+ Usando dev-protocol, continuá Amiga `/amiga` — SOLO Slice N.
60
+
61
+ Roadmap: `roadmap/amiga-workbench.md` §6 Slice N.
62
+ PROHIBIDO: `src/v25/**`, `v25/**`.
63
+ Legado `/` solo MPA shared si hace falta.
64
+ SemVer: PATCH 2.4.x salvo que el humano diga otro bump.
65
+
66
+ DoD: el de la fila Slice N del roadmap.
67
+ APPROVAL GATE; no push/merge sin OK.
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Prompt — Slice 7 (HF Space)
73
+
74
+ ```text
75
+ Usando dev-protocol, cerrá Amiga `/amiga` — Slice 7 polish + HF Space.
76
+
77
+ Roadmap: `roadmap/amiga-workbench.md` §7–§9.
78
+ Confirmá con el humano si Space default queda en `/`, `/amiga/`, o link.
79
+ PROHIBIDO: tocar v25.
80
+ APPROVAL GATE → OK → git delivery + publish Space según proceso del repo.
81
+ ```
roadmap/README.md CHANGED
@@ -4,6 +4,8 @@ Documentos de planificación **activos** viven en esta carpeta. Históricos en *
4
 
5
  | Doc | Tema | Estado |
6
  |---|---|---|
 
 
7
  | [`galaxy-view.md`](./galaxy-view.md) | VIEW **Galaxy**: UMAP backend, 1 punto/token, COMPARE+POINTS locked, IT core×100 | **Activo (plan)** |
8
  | [`PROMPT-galaxy-view.md`](./PROMPT-galaxy-view.md) | Prompt serial por slice (1→7) para agentes Galaxy | **Activo** |
9
  | [`gui-art-v25.md`](./gui-art-v25.md) | GUI/Art **v25**: layout+skin paralelo en `/v25/` (fases 0–13, hand-offs) | **Activo (plan)** |
 
4
 
5
  | Doc | Tema | Estado |
6
  |---|---|---|
7
+ | [`amiga-workbench.md`](./amiga-workbench.md) | Skin **Amiga**/MagicWB en bandera blanca `/amiga/` (Topaz + paleta; SemVer 2.4.x) | **Activo (plan)** |
8
+ | [`PROMPT-amiga-workbench.md`](./PROMPT-amiga-workbench.md) | Prompt serial por slice para agentes `/amiga` | **Activo** |
9
  | [`galaxy-view.md`](./galaxy-view.md) | VIEW **Galaxy**: UMAP backend, 1 punto/token, COMPARE+POINTS locked, IT core×100 | **Activo (plan)** |
10
  | [`PROMPT-galaxy-view.md`](./PROMPT-galaxy-view.md) | Prompt serial por slice (1→7) para agentes Galaxy | **Activo** |
11
  | [`gui-art-v25.md`](./gui-art-v25.md) | GUI/Art **v25**: layout+skin paralelo en `/v25/` (fases 0–13, hand-offs) | **Activo (plan)** |
roadmap/amiga-workbench.md ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Amiga Workbench skin — `/amiga` (MagicWB)
2
+
3
+ **Estado:** activo — Slice 1 listo (approval gate)
4
+ **Protocolo:** `.agents/skills/dev-protocol/` (ciclo completo + approval gate)
5
+ **Prompt agente:** [`PROMPT-amiga-workbench.md`](./PROMPT-amiga-workbench.md)
6
+ **SemVer:** línea **`2.4.x`** (MINOR `2.4.0` en Slice 1)
7
+ **HF Space público:** **solo al final** (§7 Slice final) — no en slices intermedios
8
+
9
+ ---
10
+
11
+ ## 0. Norte
12
+
13
+ Bandera blanca **`/amiga/`**: UI paralela con estética **Commodore Amiga Workbench 3.0 + Magic Workbench** (tipografía pixel / efecto serrucho + paleta MagicWB).
14
+
15
+ - **No** es un reskin del legado `/`.
16
+ - **No** es v25 ni continúa `gui-art` / `gui-art-v25`.
17
+ - Canvas 3D **no se toca** en este epic (clear color / shaders / POINTS / RIBBONS quedan como el engine actual cuando se cablee).
18
+ - Profundidad de este epic = **Q2-A**: tipografía + paleta + chrome mínimo legible.
19
+ **Q2-C** (cosplay full: iconos MagicWB, drawers, pointer, metaphors) → roadmap **aparte**, después de que `/amiga` esté usable.
20
+
21
+ ---
22
+
23
+ ## 1. Decisiones cerradas (Q1–Q10)
24
+
25
+ | ID | Decisión |
26
+ | :--- | :--- |
27
+ | **Q1** | Bandera blanca **`/amiga/`**. Legado `/` intacto. **`src/v25/**` y `v25/**` — PROHIBIDO tocar (explícito; ver lessons §6.0e). |
28
+ | **Q2** | **A** — tipografía pixel + paleta MagicWB (+ chrome 2D mínimo para que se lea como WB). **C** (cosplay) → epic futuro aparte. |
29
+ | **Q3** | **A** — no artear el look 3D; canvas héroe reusa engine sin “pixelizar” puntos/ribbons. |
30
+ | **Q4** | **A** — self-host Topaz (`.ttf` / `.woff2` en repo bajo `amiga/` o `src/amiga/assets/`). Sin CDN GitHub raw. |
31
+ | **Q5** | **A** — CSS estático (tokens + clase raíz). **No** inyección dinámica de `<style>` por JS. |
32
+ | **Q6** | Paleta **Magic Workbench** (8 colores canónicos + tokens semánticos). |
33
+ | **Q7** | Desktop **y** mobile desde el inicio. Si mobile se complica (legibilidad Topaz / density) → **parar y preguntar** con propuesta, no inventar. |
34
+ | **Q8** | Cero relación con `gui-art` / v25. Separación total. Lesson obligatoria. SemVer **2.4.x**. |
35
+ | **Q9** | HF Space público = **último slice** del epic (§7). |
36
+ | **Q10** | Primer slice demoable = shell `/amiga/` con Topaz + serrucho + fondo/paleta MagicWB. |
37
+
38
+ ### Fuera de alcance (este epic)
39
+
40
+ - Editar `src/v25/**`, `v25/index.html`, tokens/skin v25.
41
+ - Contaminar `src/style.css` / `src/main.js` legado “para que amiga funcione”.
42
+ - Cosplay Q2-C (icon packs, drawer metaphors, Amiga pointer custom) — doc futuro.
43
+ - Cambiar contratos `/api/*` / SAE math / Galaxy.
44
+ - Publicar HF Space antes del slice final.
45
+
46
+ ---
47
+
48
+ ## 2. Arquitectura bandera blanca `/amiga/`
49
+
50
+ ### 2.1 Layout de archivos (objetivo)
51
+
52
+ ```text
53
+ amiga/index.html # entry MPA → /amiga/
54
+ src/amiga/
55
+ main.js # bootstrap solo amiga
56
+ style.css # importa tokens + skin
57
+ tokens.css # :root MagicWB + Topaz
58
+ assets/fonts/ # TopazNew self-hosted (+ LICENSE)
59
+ ui/ # shell / paneles (crecer por slice)
60
+ … # copy-adapt desde legado si hace falta; NO editar legado para “arreglar” amiga
61
+ ```
62
+
63
+ Legado intacto: `index.html`, `src/main.js`, `src/ui/*`, `src/style.css`.
64
+ v25 intacto: `v25/**`, `src/v25/**`.
65
+
66
+ ### 2.2 Vite / serve
67
+
68
+ Extender el MPA existente (mismo patrón que v25):
69
+
70
+ - `getViteInputs` → agregar `amiga: …/amiga/index.html`.
71
+ - `mpaTrailingSlashRedirect` / `mpaTrailingSlashLocation` → incluir `'/amiga'`.
72
+ - Dev: `http://127.0.0.1:5173/amiga/` (con slash).
73
+ - Build: `dist/amiga/index.html`; FastAPI `resolve_dist_file` ya cubre `path/index.html`.
74
+ - Proxy `/api` sin cambios.
75
+
76
+ ### 2.3 Reglas de convivencia
77
+
78
+ | Árbol | Política en este epic |
79
+ | :--- | :--- |
80
+ | `/` legado | **No modificar** salvo smoke check de no-regresión (idealmente `git diff` vacío en paths legado). |
81
+ | `/v25/` | **PROHIBIDO**. Verificación: `git diff -- src/v25 v25` vacío antes de approval. |
82
+ | `/amiga/` | Único lugar de trabajo nuevo. |
83
+ | `src/engine/*`, `src/visualizer/*`, `src/core/*` | Reusar por import en slices de wire (como v25); **no** fork de math/shaders. |
84
+ | Helpers puros compartidos | Preferir copy-adapt en `src/amiga/` al inicio; extraer a módulo neutro solo con test + OK humano. |
85
+
86
+ ---
87
+
88
+ ## 3. Norte visual (MagicWB + Topaz)
89
+
90
+ ### 3.1 Tipografía
91
+
92
+ - Familia: **TopazNew** (o Topaz equivalente self-hosted).
93
+ - Tamaños en **múltiplos de 8** (base **16px**; line-height tip. **20px**).
94
+ - Sin antialiasing / efecto serrucho:
95
+
96
+ ```css
97
+ -webkit-font-smoothing: none;
98
+ -moz-osx-font-smoothing: grayscale; /* o unset según plataforma; documentar en lesson si diverge */
99
+ text-rendering: optimizeSpeed;
100
+ font-smooth: never; /* non-standard; harmless where ignored */
101
+ ```
102
+
103
+ - Imágenes UI (si hay): `image-rendering: pixelated` / `crisp-edges`.
104
+
105
+ ### 3.2 Paleta Magic Workbench (8 pens)
106
+
107
+ Fuente canónica (Wikipedia / MagicWB docs), RGB → hex:
108
+
109
+ | Idx | Hex | Rol sugerido (token) |
110
+ | :---: | :--- | :--- |
111
+ | 0 | `#959595` | `--mwb-gray` fondo escritorio / paneles |
112
+ | 1 | `#000000` | `--mwb-black` texto |
113
+ | 2 | `#FFFFFF` | `--mwb-white` highlight / shine |
114
+ | 3 | `#3B67A2` | `--mwb-blue` acento / titlebar-ish |
115
+ | 4 | `#7B7B7B` | `--mwb-halfshadow` |
116
+ | 5 | `#AFAFAF` | `--mwb-halfshine` |
117
+ | 6 | `#AA907C` | `--mwb-tan` |
118
+ | 7 | `#FFA997` | `--mwb-peach` |
119
+
120
+ Tokens semánticos mínimos (Slice 1+):
121
+
122
+ ```text
123
+ --amiga-bg: var(--mwb-gray);
124
+ --amiga-fg: var(--mwb-black);
125
+ --amiga-accent: var(--mwb-blue);
126
+ --amiga-font: 'TopazNew', monospace;
127
+ --amiga-font-size: 16px;
128
+ --amiga-line-height: 20px;
129
+ ```
130
+
131
+ > Nota: el gris clásico WB `#aaaaaa` / azul titlebar `#0055aa` del snippet original **ceden** a MagicWB (§Q6). Si hace falta un azul más “WB 3.0 puro” para titlebars en un slice de chrome, preguntar antes de desviarse.
132
+
133
+ ### 3.3 Mobile
134
+
135
+ - Mismos tokens; tipografía 16px puede ser densa → medir portrait + landscape corto (`MOBILE_MQ` del legado como referencia).
136
+ - Si Topaz ilegible o docks imposibles: **diseñar 1–2 opciones y preguntar** (no bajar a 14px “porque sí”; múltiplos de 8 o pregunta).
137
+
138
+ ---
139
+
140
+ ## 4. SemVer / docs
141
+
142
+ | Entrega | SemVer | Docs |
143
+ | :--- | :--- | :--- |
144
+ | Slice 1: scaffold `/amiga/` + Topaz + tokens MagicWB visibles | **MINOR `2.4.0`** | CHANGELOG + CONTEXT término `amiga` + lessons §6.0e |
145
+ | Slices siguientes (shell, wire producto) | **PATCH `2.4.x`** | CHANGELOG Unreleased / sección 2.4.x |
146
+ | Cosplay Q2-C (otro roadmap) | a decidir | — |
147
+ | HF Space cutover / demo pública | Slice final (§7) | README/HF docs según `hf-space-cpu-demo.md` |
148
+
149
+ Versión producto en Navbar/manifest: **solo** cuando el slice de amiga lo pida en su DoD; **no** sync cortesía a v25 (§6.0d + §6.0e).
150
+
151
+ ---
152
+
153
+ ## 5. Invariantes que NO romper
154
+
155
+ - English-only product copy.
156
+ - Contratos API `/api/*`.
157
+ - Lecciones WebGL existentes si se cablea canvas (frustum, fog, etc.).
158
+ - `git diff -- src/v25 v25` vacío siempre en este epic.
159
+ - Preferir `git diff` vacío en legado `/` (`index.html`, `src/main.js`, `src/style.css`, `src/ui/*`) salvo cambios MPA compartidos inevitables (`vite.mpa.js`, `vite.config.js`, tests MPA).
160
+
161
+ Cambios MPA compartidos permitidos (mínimos):
162
+
163
+ - `vite.mpa.js` / `vite.config.js` — agregar entry + redirect `/amiga`.
164
+ - `tests/viteMpaInputs.test.js` (o equivalente) — cubrir `amiga`.
165
+ - FastAPI solo si hace falta test de `resolve_dist_file` con `/amiga/` (probablemente ya genérico).
166
+
167
+ ---
168
+
169
+ ## 6. Slices secuenciales
170
+
171
+ > **No en paralelo.** Un slice / sesión. Approval gate → OK → merge antes del siguiente.
172
+
173
+ | Slice | Nombre | DoD (testeable) | Dep | Status |
174
+ | :---: | :--- | :--- | :---: | :--- |
175
+ | **0** | Plan + prompt | Este doc + `PROMPT-amiga-workbench.md` + README roadmap + lesson §6.0e | — | ✅ 2026-08-08 |
176
+ | **1** | Scaffold MPA + Topaz + tokens | `/amiga/` carga; Topaz self-host; serrucho; fondo MagicWB; `/` y `/v25/` intactos; bump **2.4.0**; tests MPA verdes | 0 | ✅ 2026-08-08 (branch `feat/amiga-scaffold`) |
177
+ | **2** | Shell layout desktop+mobile | Zonas mínimas (header / left / canvas-host / right o equivalente) con tokens; responsive OK o pregunta si falla | 1 |
178
+ | **3** | Chrome 2D mínimo | Paneles legibles “WB-ish” **sin** cosplay Q2-C (bordes/contraste MagicWB, no icon packs) | 2 |
179
+ | **4** | Wire Arithmetic (o panel primario legado) | CTA real vía `/api`; lista resultados; canvas aún stub OK | 3 |
180
+ | **5** | Canvas host + engine reuse | Montar scene existente en host amiga; POINTS startup; **sin** retocar shaders/look 3D | 4 |
181
+ | **6** | Compare / SAE / viz (según prioridad humana) | Port vertical de superficies críticas; mobile no roto | 5 |
182
+ | **7** | Polish + HF Space | Smoke desktop+mobile; CONTEXT/CHANGELOG; **publicar Space** con OK explícito | 6 |
183
+
184
+ ### Hand-off (plantilla)
185
+
186
+ ```markdown
187
+ ### Hand-off — Slice N
188
+ - **Branch / commit:** …
189
+ - **Cómo probar:** `/amiga/` … ; smoke `/` y `/v25/` sin cambios visuales
190
+ - **Tests:** …
191
+ - **Hecho:** …
192
+ - **NO hecho:** …
193
+ - **Verificación aislamiento:** `git diff -- src/v25 v25` vacío; legado solo MPA shared si aplica
194
+ - **Riesgos / preguntas mobile:** …
195
+ ```
196
+
197
+ ---
198
+
199
+ ## 7. HF Space (último)
200
+
201
+ Solo cuando Slices 1–6 cumplan DoD y el humano diga OK:
202
+
203
+ 1. Confirmar qué URL es default en Space (`/` vs `/amiga/` vs link). **Preguntar** si no está cerrado.
204
+ 2. Seguir proceso existente de publish (`hf-space-cpu-demo.md` / force-push Space).
205
+ 3. Documentar en CHANGELOG + README cómo abrir el skin Amiga.
206
+
207
+ ---
208
+
209
+ ## 8. Epic futuro (no este doc)
210
+
211
+ **`amiga-workbench-cosplay.md`** (crear cuando se pida): Q2-C — iconos MagicWB/NewIcons, drawers, pointer, patterns de escritorio, gadgets pressable full. Depende de `/amiga` usable.
212
+
213
+ ---
214
+
215
+ ## 9. Smoke checklist (acumulativa)
216
+
217
+ 1. `http://127.0.0.1:5173/` — legado igual.
218
+ 2. `http://127.0.0.1:5173/v25/` — v25 igual (no tocado).
219
+ 3. `http://127.0.0.1:5173/amiga/` — Topaz + MagicWB visibles.
220
+ 4. `/amiga` sin slash → redirect a `/amiga/`.
221
+ 5. Phone portrait + landscape corto: texto legible o issue abierto al humano.
222
+ 6. Tras wire: Arithmetic/Compare no rompen contratos Top-10 / cosine.
223
+
224
+ ---
225
+
226
+ ## 10. Slice 0 — hecho al crear este plan
227
+
228
+ - [x] Decisiones Q1–Q10 cerradas
229
+ - [x] Este roadmap
230
+ - [x] Prompt agente
231
+ - [x] Entrada README roadmap
232
+ - [x] Lesson §6.0e aislamiento `/amiga` + v25 off-limits en este epic
233
+
234
+ ### Hand-off — Slice 1
235
+ - **Branch / commit:** `feat/amiga-scaffold` (local; approval gate)
236
+ - **Cómo probar:** `npm run dev` → `http://127.0.0.1:5173/amiga/` (Topaz + gris MagicWB + titlebar azul + 8 swatches); smoke `/` y `/v25/` sin cambios de skin; `/amiga` sin slash → `/amiga/`
237
+ - **Tests:** `npm test` (incl. `amigaTokens`, `viteMpaInputs`)
238
+ - **Hecho:** MPA entry, tokens MagicWB, Topaz self-host GPL-FE, SemVer **2.4.0**, CONTEXT término Amiga Skin, lessons §6.0/§6.0e
239
+ - **NO hecho:** shell 5 zonas (Slice 2), wire API/canvas, HF Space
240
+ - **Verificación aislamiento:** `git diff -- src/v25 v25` vacío; legado solo version-tag Navbar + MPA shared
241
+ - **Riesgos / preguntas mobile:** Topaz 16px en phone portrait — revisar visualmente; si ilegible, preguntar (no bajar de múltiplos de 8 a ciegas)
src/amiga/assets/fonts/LICENSE.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ Topaz (TrueType remake) — Multi Platform Amiga Fonts
2
+ Source: https://github.com/rewtnull/amigafonts (ttf/Topaz_a1200_v1.0.ttf)
3
+
4
+ License: GPL with Font Exception (GPL-FE)
5
+ https://www.gnu.org/licenses/gpl-faq.html#FontException
6
+
7
+ Topaz is © AmigaInc.
8
+ TrueType version © dMG/t!s^dS! (Trueschool / Divine Stylers), 2009.
9
+
10
+ Vendored here for the VHectorLab `/amiga` MagicWB skin (self-host; no CDN).
src/amiga/assets/fonts/Topaz_a1200_v1.0.ttf ADDED
Binary file (24.3 kB). View file
 
src/amiga/main.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './style.css';
2
+ import { PRODUCT_NAME, PRODUCT_VERSION } from './version.js';
3
+
4
+ document.body.classList.add('amiga-workbench');
5
+
6
+ const app = document.getElementById('app');
7
+ if (app) {
8
+ app.innerHTML = `
9
+ <div class="amiga-shell" data-amiga-shell>
10
+ <header class="amiga-titlebar">
11
+ <h1 class="amiga-titlebar__brand">${PRODUCT_NAME}</h1>
12
+ <span class="amiga-titlebar__meta">Amiga / MagicWB · v${PRODUCT_VERSION}</span>
13
+ </header>
14
+ <section class="amiga-panel" aria-label="Workbench scaffold">
15
+ <p>Topaz + Magic Workbench palette scaffold.</p>
16
+ <p>Parallel skin at /amiga/ — legacy / and /v25/ untouched.</p>
17
+ <ul class="amiga-swatches" aria-label="MagicWB pens">
18
+ <li data-pen="0" title="0 gray"></li>
19
+ <li data-pen="1" title="1 black"></li>
20
+ <li data-pen="2" title="2 white"></li>
21
+ <li data-pen="3" title="3 blue"></li>
22
+ <li data-pen="4" title="4 halfshadow"></li>
23
+ <li data-pen="5" title="5 halfshine"></li>
24
+ <li data-pen="6" title="6 tan"></li>
25
+ <li data-pen="7" title="7 peach"></li>
26
+ </ul>
27
+ </section>
28
+ </div>
29
+ `;
30
+ }
src/amiga/style.css ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import './tokens.css';
2
+
3
+ *,
4
+ *::before,
5
+ *::after {
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ html,
10
+ body {
11
+ margin: 0;
12
+ min-height: 100%;
13
+ }
14
+
15
+ body.amiga-workbench {
16
+ font-family: var(--amiga-font);
17
+ font-size: var(--amiga-font-size);
18
+ line-height: var(--amiga-line-height);
19
+ letter-spacing: 0.5px;
20
+ color: var(--amiga-fg);
21
+ background-color: var(--amiga-bg);
22
+
23
+ /* Pixel / sawtooth — disable font smoothing */
24
+ -webkit-font-smoothing: none;
25
+ -moz-osx-font-smoothing: grayscale;
26
+ text-rendering: optimizeSpeed;
27
+ font-smooth: never;
28
+ }
29
+
30
+ body.amiga-workbench img {
31
+ image-rendering: pixelated;
32
+ image-rendering: crisp-edges;
33
+ }
34
+
35
+ #app {
36
+ min-height: 100vh;
37
+ min-height: 100dvh;
38
+ padding: 16px;
39
+ }
40
+
41
+ .amiga-shell {
42
+ max-width: 960px;
43
+ margin: 0 auto;
44
+ }
45
+
46
+ .amiga-titlebar {
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ gap: 16px;
51
+ background: var(--amiga-accent);
52
+ color: var(--mwb-white);
53
+ padding: 4px 8px;
54
+ margin-bottom: 0;
55
+ }
56
+
57
+ .amiga-titlebar__brand {
58
+ margin: 0;
59
+ font-size: 16px;
60
+ line-height: 20px;
61
+ font-weight: normal;
62
+ }
63
+
64
+ .amiga-titlebar__meta {
65
+ font-size: 16px;
66
+ line-height: 20px;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .amiga-panel {
71
+ background: var(--amiga-bg);
72
+ border: 2px solid var(--mwb-black);
73
+ border-top: none;
74
+ padding: 16px;
75
+ }
76
+
77
+ .amiga-panel p {
78
+ margin: 0 0 16px;
79
+ }
80
+
81
+ .amiga-panel p:last-child {
82
+ margin-bottom: 0;
83
+ }
84
+
85
+ .amiga-swatches {
86
+ display: flex;
87
+ flex-wrap: wrap;
88
+ gap: 8px;
89
+ margin: 16px 0 0;
90
+ padding: 0;
91
+ list-style: none;
92
+ }
93
+
94
+ .amiga-swatches li {
95
+ width: 32px;
96
+ height: 32px;
97
+ border: 2px solid var(--mwb-black);
98
+ }
99
+
100
+ .amiga-swatches li[data-pen='0'] { background: var(--mwb-gray); }
101
+ .amiga-swatches li[data-pen='1'] { background: var(--mwb-black); }
102
+ .amiga-swatches li[data-pen='2'] { background: var(--mwb-white); }
103
+ .amiga-swatches li[data-pen='3'] { background: var(--mwb-blue); }
104
+ .amiga-swatches li[data-pen='4'] { background: var(--mwb-halfshadow); }
105
+ .amiga-swatches li[data-pen='5'] { background: var(--mwb-halfshine); }
106
+ .amiga-swatches li[data-pen='6'] { background: var(--mwb-tan); }
107
+ .amiga-swatches li[data-pen='7'] { background: var(--mwb-peach); }
108
+
109
+ @media (max-width: 640px), ((max-height: 500px) and (orientation: landscape)) {
110
+ #app {
111
+ padding: 8px;
112
+ }
113
+ }
src/amiga/tokenValues.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Magic Workbench 8-pen palette + semantic aliases.
3
+ * Keep hex in sync with `tokens.css`.
4
+ * Source: MagicWB canonical RGB (Wikipedia / MagicWB docs).
5
+ */
6
+ export const MWB_PENS = Object.freeze({
7
+ 0: '#959595',
8
+ 1: '#000000',
9
+ 2: '#FFFFFF',
10
+ 3: '#3B67A2',
11
+ 4: '#7B7B7B',
12
+ 5: '#AFAFAF',
13
+ 6: '#AA907C',
14
+ 7: '#FFA997',
15
+ });
16
+
17
+ export const AMIGA_TOKEN_HEX = Object.freeze({
18
+ bg: MWB_PENS[0],
19
+ fg: MWB_PENS[1],
20
+ white: MWB_PENS[2],
21
+ accent: MWB_PENS[3],
22
+ halfshadow: MWB_PENS[4],
23
+ halfshine: MWB_PENS[5],
24
+ tan: MWB_PENS[6],
25
+ peach: MWB_PENS[7],
26
+ fontSizePx: 16,
27
+ lineHeightPx: 20,
28
+ });
src/amiga/tokens.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Magic Workbench tokens — source of truth for `/amiga` chrome.
3
+ * Hex mirrored in `tokenValues.js` for tests.
4
+ */
5
+
6
+ @font-face {
7
+ font-family: 'TopazNew';
8
+ src: url('./assets/fonts/Topaz_a1200_v1.0.ttf') format('truetype');
9
+ font-weight: normal;
10
+ font-style: normal;
11
+ font-display: block;
12
+ }
13
+
14
+ :root {
15
+ --mwb-gray: #959595;
16
+ --mwb-black: #000000;
17
+ --mwb-white: #ffffff;
18
+ --mwb-blue: #3b67a2;
19
+ --mwb-halfshadow: #7b7b7b;
20
+ --mwb-halfshine: #afafaf;
21
+ --mwb-tan: #aa907c;
22
+ --mwb-peach: #ffa997;
23
+
24
+ --amiga-bg: var(--mwb-gray);
25
+ --amiga-fg: var(--mwb-black);
26
+ --amiga-accent: var(--mwb-blue);
27
+ --amiga-font: 'TopazNew', monospace;
28
+ --amiga-font-size: 16px;
29
+ --amiga-line-height: 20px;
30
+ }
src/amiga/version.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ /** Product version for the `/amiga` MPA shell (sync with manifest on bump). */
2
+ export const PRODUCT_NAME = 'VHectorLab-3D';
3
+ export const PRODUCT_VERSION = '2.4.0';
src/ui/Navbar.js CHANGED
@@ -53,7 +53,7 @@ export class Navbar {
53
  <div class="logo-icon">🌐</div>
54
  <div class="title-group">
55
  <h1>VHectorLab <span class="accent-3d">3D</span></h1>
56
- <span class="version-tag">v2.3.0</span>
57
  </div>
58
  </div>
59
 
 
53
  <div class="logo-icon">🌐</div>
54
  <div class="title-group">
55
  <h1>VHectorLab <span class="accent-3d">3D</span></h1>
56
+ <span class="version-tag">v2.4.0</span>
57
  </div>
58
  </div>
59
 
tests/amigaTokens.test.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AMIGA_TOKEN_HEX, MWB_PENS } from '../src/amiga/tokenValues.js';
3
+ import { PRODUCT_VERSION } from '../src/amiga/version.js';
4
+
5
+ describe('MagicWB pens', () => {
6
+ it('exposes the canonical 8-pen palette', () => {
7
+ expect(MWB_PENS).toEqual({
8
+ 0: '#959595',
9
+ 1: '#000000',
10
+ 2: '#FFFFFF',
11
+ 3: '#3B67A2',
12
+ 4: '#7B7B7B',
13
+ 5: '#AFAFAF',
14
+ 6: '#AA907C',
15
+ 7: '#FFA997',
16
+ });
17
+ });
18
+ });
19
+
20
+ describe('amiga semantic tokens', () => {
21
+ it('maps bg/fg/accent to MagicWB pens', () => {
22
+ expect(AMIGA_TOKEN_HEX.bg).toBe(MWB_PENS[0]);
23
+ expect(AMIGA_TOKEN_HEX.fg).toBe(MWB_PENS[1]);
24
+ expect(AMIGA_TOKEN_HEX.accent).toBe(MWB_PENS[3]);
25
+ });
26
+
27
+ it('uses pixel-perfect 16/20 typography sizes', () => {
28
+ expect(AMIGA_TOKEN_HEX.fontSizePx).toBe(16);
29
+ expect(AMIGA_TOKEN_HEX.lineHeightPx).toBe(20);
30
+ expect(AMIGA_TOKEN_HEX.fontSizePx % 8).toBe(0);
31
+ });
32
+ });
33
+
34
+ describe('amiga product version', () => {
35
+ it('starts the 2.4.x line', () => {
36
+ expect(PRODUCT_VERSION).toMatch(/^2\.4\.\d+$/);
37
+ });
38
+ });
tests/viteMpaInputs.test.js CHANGED
@@ -3,12 +3,13 @@ import { describe, expect, it } from 'vitest';
3
  import { getViteInputs, mpaTrailingSlashLocation } from '../vite.mpa.js';
4
 
5
  describe('getViteInputs', () => {
6
- it('includes legacy root and v25 MPA HTML entries', () => {
7
  const root = path.resolve('/tmp/vhectorlab-fixture');
8
  const inputs = getViteInputs(root);
9
  expect(inputs).toEqual({
10
  main: path.resolve(root, 'index.html'),
11
  v25: path.resolve(root, 'v25/index.html'),
 
12
  });
13
  });
14
  });
@@ -18,13 +19,20 @@ describe('mpaTrailingSlashLocation', () => {
18
  expect(mpaTrailingSlashLocation('/v25')).toBe('/v25/');
19
  });
20
 
 
 
 
 
21
  it('preserves query string', () => {
22
  expect(mpaTrailingSlashLocation('/v25?x=1')).toBe('/v25/?x=1');
 
23
  });
24
 
25
- it('leaves /v25/ and legacy / alone', () => {
26
  expect(mpaTrailingSlashLocation('/v25/')).toBeNull();
 
27
  expect(mpaTrailingSlashLocation('/')).toBeNull();
28
  expect(mpaTrailingSlashLocation('/v25/index.html')).toBeNull();
 
29
  });
30
  });
 
3
  import { getViteInputs, mpaTrailingSlashLocation } from '../vite.mpa.js';
4
 
5
  describe('getViteInputs', () => {
6
+ it('includes legacy root, v25, and amiga MPA HTML entries', () => {
7
  const root = path.resolve('/tmp/vhectorlab-fixture');
8
  const inputs = getViteInputs(root);
9
  expect(inputs).toEqual({
10
  main: path.resolve(root, 'index.html'),
11
  v25: path.resolve(root, 'v25/index.html'),
12
+ amiga: path.resolve(root, 'amiga/index.html'),
13
  });
14
  });
15
  });
 
19
  expect(mpaTrailingSlashLocation('/v25')).toBe('/v25/');
20
  });
21
 
22
+ it('redirects bare /amiga to /amiga/', () => {
23
+ expect(mpaTrailingSlashLocation('/amiga')).toBe('/amiga/');
24
+ });
25
+
26
  it('preserves query string', () => {
27
  expect(mpaTrailingSlashLocation('/v25?x=1')).toBe('/v25/?x=1');
28
+ expect(mpaTrailingSlashLocation('/amiga?x=1')).toBe('/amiga/?x=1');
29
  });
30
 
31
+ it('leaves trailing-slash MPA paths and legacy / alone', () => {
32
  expect(mpaTrailingSlashLocation('/v25/')).toBeNull();
33
+ expect(mpaTrailingSlashLocation('/amiga/')).toBeNull();
34
  expect(mpaTrailingSlashLocation('/')).toBeNull();
35
  expect(mpaTrailingSlashLocation('/v25/index.html')).toBeNull();
36
+ expect(mpaTrailingSlashLocation('/amiga/index.html')).toBeNull();
37
  });
38
  });
vite.mpa.js CHANGED
@@ -1,28 +1,29 @@
1
  import path from 'node:path';
2
 
3
  /**
4
- * Vite multi-page inputs: legacy `/` + parallel skin `/v25/`.
5
- * Keep keys stable — build emits `dist/index.html` and `dist/v25/index.html`.
6
  *
7
  * @param {string} rootDir Absolute project root (repo cwd).
8
- * @returns {{ main: string, v25: string }}
9
  */
10
  export function getViteInputs(rootDir) {
11
  return {
12
  main: path.resolve(rootDir, 'index.html'),
13
  v25: path.resolve(rootDir, 'v25/index.html'),
 
14
  };
15
  }
16
 
17
  /**
18
- * Vite SPA fallback treats `/v25` (no slash) as unknown → serves legacy `index.html`.
19
  * Redirect bare MPA dirs to their trailing-slash URL.
20
  *
21
  * @param {string} urlPath `req.url` (path + optional query)
22
  * @param {string[]} mpaDirs Absolute path prefixes without trailing slash
23
  * @returns {string | null} Location header value, or null if no redirect
24
  */
25
- export function mpaTrailingSlashLocation(urlPath, mpaDirs = ['/v25']) {
26
  const qIndex = urlPath.indexOf('?');
27
  const pathname = qIndex === -1 ? urlPath : urlPath.slice(0, qIndex);
28
  const query = qIndex === -1 ? '' : urlPath.slice(qIndex);
@@ -32,8 +33,8 @@ export function mpaTrailingSlashLocation(urlPath, mpaDirs = ['/v25']) {
32
  return null;
33
  }
34
 
35
- /** Vite plugin: 302 `/v25``/v25/` (dev + preview). */
36
- export function mpaTrailingSlashRedirect(mpaDirs = ['/v25']) {
37
  const attach = (middlewares) => {
38
  middlewares.use((req, res, next) => {
39
  const location = mpaTrailingSlashLocation(req.url || '', mpaDirs);
 
1
  import path from 'node:path';
2
 
3
  /**
4
+ * Vite multi-page inputs: legacy `/` + parallel skins `/v25/` + `/amiga/`.
5
+ * Keep keys stable — build emits matching `dist/.../index.html` paths.
6
  *
7
  * @param {string} rootDir Absolute project root (repo cwd).
8
+ * @returns {{ main: string, v25: string, amiga: string }}
9
  */
10
  export function getViteInputs(rootDir) {
11
  return {
12
  main: path.resolve(rootDir, 'index.html'),
13
  v25: path.resolve(rootDir, 'v25/index.html'),
14
+ amiga: path.resolve(rootDir, 'amiga/index.html'),
15
  };
16
  }
17
 
18
  /**
19
+ * Vite SPA fallback treats bare MPA dirs (no slash) as unknown → serves legacy `index.html`.
20
  * Redirect bare MPA dirs to their trailing-slash URL.
21
  *
22
  * @param {string} urlPath `req.url` (path + optional query)
23
  * @param {string[]} mpaDirs Absolute path prefixes without trailing slash
24
  * @returns {string | null} Location header value, or null if no redirect
25
  */
26
+ export function mpaTrailingSlashLocation(urlPath, mpaDirs = ['/v25', '/amiga']) {
27
  const qIndex = urlPath.indexOf('?');
28
  const pathname = qIndex === -1 ? urlPath : urlPath.slice(0, qIndex);
29
  const query = qIndex === -1 ? '' : urlPath.slice(qIndex);
 
33
  return null;
34
  }
35
 
36
+ /** Vite plugin: 302 bare MPA dirs trailing slash (dev + preview). */
37
+ export function mpaTrailingSlashRedirect(mpaDirs = ['/v25', '/amiga']) {
38
  const attach = (middlewares) => {
39
  middlewares.use((req, res, next) => {
40
  const location = mpaTrailingSlashLocation(req.url || '', mpaDirs);