From 8532d1c02bbafb6c18b2a645b87c9c8542152586 Mon Sep 17 00:00:00 2001 From: Donato Silva Date: Fri, 6 Nov 2026 16:02:15 -0500 Subject: [PATCH 1/3] =?UTF-8?q?chore(widget):=20tests=20de=20la=20retenci?= =?UTF-8?q?=C3=B3n=20tras=20reordenar=20(#241)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- odd/tasks/241-reorder-hold-tests.md | 53 ++++++++++++++++++++++++++ src/Widget.tsx | 20 +++++----- src/lib/anchor-panel.test.ts | 59 ----------------------------- src/lib/anchor-panel.ts | 27 ------------- src/lib/pointer-viewport.test.ts | 26 +++++++++++++ src/lib/pointer-viewport.ts | 12 ++++++ 6 files changed, 100 insertions(+), 97 deletions(-) create mode 100644 odd/tasks/241-reorder-hold-tests.md create mode 100644 src/lib/pointer-viewport.test.ts create mode 100644 src/lib/pointer-viewport.ts diff --git a/odd/tasks/241-reorder-hold-tests.md b/odd/tasks/241-reorder-hold-tests.md new file mode 100644 index 0000000..c24c6c2 --- /dev/null +++ b/odd/tasks/241-reorder-hold-tests.md @@ -0,0 +1,53 @@ +# 241-reorder-hold-tests + +> Issue: https://github.com/egdev6/quorb/issues/241 — `chore(widget)`, asignada a `DonatoSilva`. +> Rama: `chore/241-reorder-hold-tests` (desde `upstream/main` 29c9188). Fork: `DonatoSilva/quorb`. +> Alcance aprobado por el mantenedor en la propia issue (`status:approved`, `type:chore`); no cambia la regla de #222. + +## Specs + +- S1: "**Funciones sin lógica propia.** `holdAfterReorderDrop`, `releaseOnStripLeave` y `canPointerOpen` (`src/lib/anchor-panel.ts`) envuelven un booleano (`x => x`, `_ => false`, `h => !h`), así que sus tests no pueden fallar. Los que comprueban la aridad (`canPointerOpen.length`, `hoverPanel.length`, `clickPanel.length`) y «never changes the panel state» (`src/lib/anchor-panel.test.ts`) no fijan ningún comportamiento." +- S2: "**La lógica con bordes reales no tiene test.** La comprobación de «dentro de la ventana» al soltar un reordenado está en línea en `src/Widget.tsx:1219`:" + + ```ts + e.clientX >= 0 && e.clientY >= 0 && e.clientX < window.innerWidth && e.clientY < window.innerHeight + ``` + +- S3: "Un error de `<` frente a `<=` dejaría la retención activa sin que nada la libere, y el hover no volvería a abrir el panel hasta salir de la tira." +- S4: "Dejar la retención como un `useRef` en `Widget.tsx` y quitar las tres funciones envoltorio y sus tests." +- S5: "Extraer `pointerInsideViewport(x, y, width, height)` como función pura y cubrir los bordes en ambos ejes: `0`, `-1`, `width - 1` y `width` (y lo mismo con `height`)." +- S6: "Mantener el test de `ProviderGauge` sobre el enrutado de `mouseenter` a `onPointerEnter`, que sí aporta." +- S7: "Sin cambio de comportamiento: mismas condiciones de retención y liberación que #240." +- S8: "`npm test`, `npm run typecheck` y `npm run build` en verde." +- S9: "Fuera de alcance: cambiar la regla de #222, el temporizador de 400 ms (`REORDER_GUARD_MS`) o el cableado de foco y clic." + +## Mecanismo (verificado en el código) + +- La retención ya es un `useRef` (`Widget.tsx:255`); `ReorderHold` es un alias de `boolean` (`anchor-panel.ts:151`) y `NO_REORDER_HOLD` es `false` (`:154`). +- Las tres envoltorio son identidades: `holdAfterReorderDrop` devuelve su argumento, `releaseOnStripLeave` devuelve `false` ignorando el argumento y `canPointerOpen` devuelve `!hold`. +- Único consumidor en producción: `Widget.tsx:1220` (escribe al soltar), `:1256` (libera al salir de la tira) y `:1320` (consulta en `onPointerEnter`). El reinicio al ocultar el widget está en `:789`. + +## Tasks + +- [x] T1 — S5 — Test en rojo de los bordes de `pointerInsideViewport` y la función pura en `src/lib/pointer-viewport.ts` (módulo nuevo: el viewport no comparte dominio con el estado del panel de `anchor-panel.ts`). Route: inline. Commit: `16c69cf`. +- [x] T2 — S2, S3, S7 — Cablear la función pura en el `onPointerUp` de `Widget.tsx`: misma comparación, con `window.innerWidth`/`innerHeight` leídos al soltar. Route: inline. Commit: `16c69cf`. +- [x] T3 — S1, S4 — Borrar `holdAfterReorderDrop`, `releaseOnStripLeave`, `canPointerOpen`, `ReorderHold` y `NO_REORDER_HOLD` de `src/lib/anchor-panel.ts`, y el bloque «the reorder hold» de su test. Route: inline. Commit: `16c69cf`. +- [x] T4 — S4, S7 — Aplanar la retención a `useRef` en `Widget.tsx`: `= inside` al soltar, `= false` al salir de la tira, `= false` al ocultar el widget y `if (!reorderHold.current)` en `onPointerEnter`. Route: inline. Commit: `16c69cf`. +- [x] T5 — S8 — `npm test`, `npm run typecheck`, `npm run build` y regenerar `docs/i18n-inventory.md` (el inventario fija el hash del árbol `src/` de HEAD). Route: verify. Commit: `16c69cf` (código) y `6a31862` (inventario i18n). +- [x] T6 — S6, S9 — Commits en work units, push al fork y PR a `egdev6/quorb` con `Closes #241`, más el comentario de aviso en la issue. Route: inline. Commit: PR https://github.com/egdev6/quorb/pull/242 (rama `chore/241-reorder-hold-tests`). + +## Riesgo + +Risk: item 3 (se eliminan exports de `src/lib/anchor-panel.ts` que `Widget.tsx` consumía; los únicos consumidores son `Widget.tsx` y su test, ambos actualizados en el mismo cambio, y `tsc` detecta imports sobrantes). + +## Estrategia de entrega + +Dos commits: (1) el refactor con sus tests y el ODD, (2) el inventario i18n regenerado (solo cambia la línea de hashes del árbol de `src/`). Push: `git push -u origin chore/241-reorder-hold-tests`, porque la rama hoy rastrea `upstream/main`, donde no hay permiso de escritura. + +## Log + +- L1: "perfecto ya sabes realiza la nueva rama nacida del main de upstream/main y push al fork que tengo y pr al proyecto con mencion de comentario en el issuo par aque lo revise... recuerdas como lo hicimos con el issue 222 ??" +- L2: Decisión de diseño (parent): `pointerInsideViewport` va en un módulo nuevo `src/lib/pointer-viewport.ts` y no en `src/lib/anchor-panel.ts`. Motivo: el header de `anchor-panel.ts` declara «El estado del panel de detalle (#170)» y el consumidor conceptual del predicado (el hold puro) desaparece en este mismo cambio; el repo agrupa por dominio y los otros predicados geométricos (`dock.ts:80 contains`, `detail-presence.ts:84 pointInBounds`) viven en el módulo de su dominio. Alternativa descartada: `src/lib/anchor-focus.ts` («El seam de foco (#170)»). +- L3: Convención de estilo verificada: `export function` para el predicado (como `dock.ts:80` y `detail-presence.ts:84`; `anchor-panel.ts` usa arrow-const, que es la excepción local), TSDoc en español y breve (familia anchor/reorder), títulos de test en inglés sin acentos (96/96 describes de nivel superior en inglés), módulo DOM-free (vitest corre en entorno `node`, sin jsdom), `tsconfig` con `strict`, `noUnusedLocals` y `noUnusedParameters`. +- L4: El inventario i18n fija `src/` = `252ab0dd…`, que es el hash del árbol `src/` de HEAD; cualquier cambio en `src/` lo invalida y `node scripts/i18n-inventory.mjs --check` (CI, `ci.yml:49`) falla. El script lee de un commit, no del working tree: hay que commitear `src/` primero y regenerar después. +- L5: Entrega: rama `chore/241-reorder-hold-tests` desde `upstream/main` 29c9188, push al fork `DonatoSilva/quorb` y PR https://github.com/egdev6/quorb/pull/242 (`Closes #241`). Comprobaciones: `npm test` (vitest 69 archivos / 910 tests; `node --test` 298; 0 fallos), `npm run typecheck`, `npm run build` y `node scripts/i18n-inventory.mjs --check`, todo en verde. `cargo test` no aplica: no hay cambios en `src-tauri/`. Sin prueba manual en GUI porque el comportamiento no cambia: las funciones borradas eran identidades y el predicado extraído reproduce la misma comparación. La etiqueta `type:chore` y la asignación de la PR las aplica el mantenedor: un contribuidor no tiene permiso de etiquetado ni de autoasignación en el repo upstream (la PR #240 quedó también sin etiquetas). diff --git a/src/Widget.tsx b/src/Widget.tsx index a269fce..8ebb9a0 100644 --- a/src/Widget.tsx +++ b/src/Widget.tsx @@ -18,19 +18,15 @@ import { needsRefresh } from './lib/widget-refresh'; import { createWidgetRecovery, shouldAuditNativePosition } from './lib/widget-recovery'; import { placementOutcome, readPlacementWithRetries, reasonKey, samePlacement, type WidgetPlacementReason, type WidgetPlacementReport } from './lib/widget-placement'; import { - canPointerOpen, clickPanel, closePanel, - holdAfterReorderDrop, hoverPanel, isReorderGesture, NO_PANEL, - NO_REORDER_HOLD, panelHoldsOpen, type PanelState, - releaseOnStripLeave, - type ReorderHold, } from './lib/anchor-panel'; +import { pointerInsideViewport } from './lib/pointer-viewport'; import { createFocusTracker, FOCUS_EVENT, type OwnWindow } from './lib/anchor-focus'; import { createLatestQueue, physicalPanelBounds, samePhysical, DETAIL_EVENTS, DETAIL_WINDOW, isCurrentDetailInteraction, panelWidthFor, type DetailInteraction, type DetailState, type PhysicalBounds } from './lib/detail-panel'; import { createDetailPresence, presenceFromPoint, shouldSampleNativePresence } from './lib/detail-presence'; @@ -252,7 +248,9 @@ export function Widget() { const orbDrag = useRef<{ id: string; x: number; y: number; moved: boolean } | null>(null); const lastReorderAt = useRef(0); // Tras reordenar, el hover no abre hasta que el puntero salga de la tira (#222). - const reorderHold = useRef(NO_REORDER_HOLD); + // Solo la libera el `mouseleave` de la tira: el `mouseenter` puede llegar + // segundos después de soltar. Solo frena la vía del puntero; foco y clic no la consultan. + const reorderHold = useRef(false); const openPeakRef = useRef<() => void>(() => {}); const dragWork = useRef>(Promise.resolve()); const dragMounted = useRef(true); @@ -786,7 +784,7 @@ export function Widget() { cancelTimers(); detailInsideRef.current = false; insideRef.current = false; - reorderHold.current = NO_REORDER_HOLD; + reorderHold.current = false; setInside(false); setPanel(closePanel()); })); @@ -1216,8 +1214,8 @@ export function Widget() { lastReorderAt.current = Date.now(); // `.dock` es `inset: 0`: dentro de la ventana es dentro de la tira. Con // la captura, `insideRef` queda desactualizado: se usan las coordenadas. - const inside = e.clientX >= 0 && e.clientY >= 0 && e.clientX < window.innerWidth && e.clientY < window.innerHeight; - reorderHold.current = holdAfterReorderDrop(inside); + const inside = pointerInsideViewport(e.clientX, e.clientY, window.innerWidth, window.innerHeight); + reorderHold.current = inside; } if (od?.moved && reorder) { const next = reorder.order; @@ -1253,7 +1251,7 @@ export function Widget() { }} onMouseLeave={() => { insideRef.current = false; - reorderHold.current = releaseOnStripLeave(reorderHold.current); + reorderHold.current = false; setInside(false); scheduleClose(); }} @@ -1317,7 +1315,7 @@ export function Widget() { dragging={reorder?.id === r.id} onHover={(el) => openFor(r.id, el)} onPointerEnter={(el) => { - if (canPointerOpen(reorderHold.current)) openFor(r.id, el); + if (!reorderHold.current) openFor(r.id, el); }} onClick={(el) => anchorOn(r.id, el)} pointer={orbHandlers(r.id)} diff --git a/src/lib/anchor-panel.test.ts b/src/lib/anchor-panel.test.ts index d1963b0..fa70814 100644 --- a/src/lib/anchor-panel.test.ts +++ b/src/lib/anchor-panel.test.ts @@ -7,13 +7,9 @@ import { clickPanel, closePanel, createPanelCoordinator, - canPointerOpen, hoverPanel, - holdAfterReorderDrop, isReorderGesture, NO_PANEL, - NO_REORDER_HOLD, - releaseOnStripLeave, panelHoldsOpen, type PanelState, REORDER_GUARD_MS, @@ -96,61 +92,6 @@ describe('the reorder guard', () => { }); }); -describe('the reorder hold (hover after a reorder, #222)', () => { - it('holds the hover when a reorder is dropped with the pointer inside the strip', () => { - expect(NO_REORDER_HOLD).toBe(false); - expect(holdAfterReorderDrop(true)).toBe(true); - }); - - it('keeps the pointer from opening while held, however much time passes', () => { - // El `mouseenter` diferido llega entre 0,2 s y 2,3 s después de soltar: - // por eso `canPointerOpen` no recibe ninguna marca de tiempo. - const held = holdAfterReorderDrop(true); - expect(canPointerOpen.length).toBe(1); - expect(canPointerOpen(held)).toBe(false); - expect(canPointerOpen(held)).toBe(false); - }); - - it('releases the hold only with a real strip leave, and then the pointer opens', () => { - const held = holdAfterReorderDrop(true); - const released = releaseOnStripLeave(held); - expect(released).toBe(false); - expect(canPointerOpen(released)).toBe(true); - }); - - it('does not hold when the reorder is dropped outside the strip', () => { - // Con la captura del puntero se puede soltar fuera de la ventana: su - // `mouseleave` ya ocurrió y nada liberaría la retención después. - const hold = holdAfterReorderDrop(false); - expect(hold).toBe(false); - expect(canPointerOpen(hold)).toBe(true); - }); - - it('keeps a strip leave idempotent while nothing is held', () => { - expect(releaseOnStripLeave(NO_REORDER_HOLD)).toBe(false); - expect(canPointerOpen(releaseOnStripLeave(NO_REORDER_HOLD))).toBe(true); - }); - - it('never changes the panel state: hover and click do not take the hold', () => { - const states: PanelState[] = [NO_PANEL, { providerId: 'claude', anchored: false }, { providerId: 'claude', anchored: true }]; - const results = (state: PanelState) => ({ - hover: hoverPanel(state, 'codex'), - click: clickPanel(state, 'codex'), - close: closePanel(), - }); - for (const state of states) { - const held = holdAfterReorderDrop(true); - const whileHeld = results(state); - const released = releaseOnStripLeave(held); - const afterRelease = results(state); - expect(held).not.toBe(released); - expect(afterRelease).toEqual(whileHeld); - expect(hoverPanel.length).toBe(2); - expect(clickPanel.length).toBe(2); - } - }); -}); - describe('the panel coordinator (delayed open and escape cancellation)', () => { it('cancels pending open when Escape is pressed before the delay expires', () => { let timerRun: (() => void) | null = null; diff --git a/src/lib/anchor-panel.ts b/src/lib/anchor-panel.ts index b697bb0..b4fd8d2 100644 --- a/src/lib/anchor-panel.ts +++ b/src/lib/anchor-panel.ts @@ -137,30 +137,3 @@ export const REORDER_GUARD_MS = 400; export const isReorderGesture = (lastReorderAt: number, now: number): boolean => now - lastReorderAt < REORDER_GUARD_MS; -/** - * La retención del hover tras reordenar (#222): `true` mientras el puntero no - * haya salido de la tira desde que se soltó el orbe. - * - * Una ventana de tiempo no puede acotar el `mouseenter` sintetizado tarde por - * el navegador (medido entre 0,2 s y 2,3 s después de soltar): el orbe se - * mueve bajo un puntero quieto y la entrada llega con el siguiente movimiento. - * Por eso solo la libera el `mouseleave` real de `.dock`, nunca un - * `mouseenter`; pasar a otro orbe tampoco la libera (para abrir otro orbe - * queda el clic). Solo frena la vía del puntero: foco y clic no la consultan. - */ -export type ReorderHold = boolean; - -/** Sin retención: el hover abre con normalidad. */ -export const NO_REORDER_HOLD: ReorderHold = false; - -/** - * Al soltar un reordenado: retiene solo si el puntero sigue dentro de la - * ventana. Fuera, el `mouseleave` ya ocurrió y nada la liberaría después. - */ -export const holdAfterReorderDrop = (pointerInside: boolean): ReorderHold => pointerInside; - -/** El `mouseleave` real de la tira libera la retención (y es idempotente). */ -export const releaseOnStripLeave = (_hold: ReorderHold): ReorderHold => false; - -/** True si el hover del puntero puede abrir el panel; no depende del tiempo. */ -export const canPointerOpen = (hold: ReorderHold): boolean => !hold; diff --git a/src/lib/pointer-viewport.test.ts b/src/lib/pointer-viewport.test.ts new file mode 100644 index 0000000..5833317 --- /dev/null +++ b/src/lib/pointer-viewport.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { pointerInsideViewport } from './pointer-viewport'; + +describe('pointerInsideViewport', () => { + it('accepts a point inside the viewport', () => { + expect(pointerInsideViewport(10, 20, 100, 50)).toBe(true); + }); + + it('accepts the top-left border and rejects one pixel before it', () => { + expect(pointerInsideViewport(0, 0, 100, 50)).toBe(true); + expect(pointerInsideViewport(-1, 0, 100, 50)).toBe(false); + expect(pointerInsideViewport(0, -1, 100, 50)).toBe(false); + }); + + it('accepts the last pixel inside the viewport and rejects the first one past it', () => { + expect(pointerInsideViewport(99, 49, 100, 50)).toBe(true); + expect(pointerInsideViewport(100, 49, 100, 50)).toBe(false); + expect(pointerInsideViewport(99, 50, 100, 50)).toBe(false); + }); + + it('rejects a point outside the viewport', () => { + expect(pointerInsideViewport(150, 20, 100, 50)).toBe(false); + expect(pointerInsideViewport(10, 80, 100, 50)).toBe(false); + }); +}); diff --git a/src/lib/pointer-viewport.ts b/src/lib/pointer-viewport.ts new file mode 100644 index 0000000..01d8b6c --- /dev/null +++ b/src/lib/pointer-viewport.ts @@ -0,0 +1,12 @@ +// El puntero dentro del viewport de la ventana. + +/** + * True cuando el puntero cae dentro del viewport. + * + * El borde derecho e inferior quedan fuera: `width` y `height` ya están fuera. + * Al soltar un reordenado decide si se retiene el hover: fuera de la ventana el + * `mouseleave` ya ocurrió y nada liberaría la retención después. + */ +export function pointerInsideViewport(x: number, y: number, width: number, height: number): boolean { + return x >= 0 && y >= 0 && x < width && y < height; +} From 5be45219cbddb8ec60e729d29a1a48f217d2d57a Mon Sep 17 00:00:00 2001 From: Donato Silva Date: Fri, 6 Nov 2026 16:02:25 -0500 Subject: [PATCH 2/3] chore(i18n): regenerar el inventario tras #241 --- docs/i18n-inventory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/i18n-inventory.md b/docs/i18n-inventory.md index c5cbf3f..b239a2c 100644 --- a/docs/i18n-inventory.md +++ b/docs/i18n-inventory.md @@ -1,6 +1,6 @@ # i18n inventory (egdev6/quorb#66) -Generated by `node scripts/i18n-inventory.mjs` from the source trees `src/` = `129601dcd1c39752bf3a0d5e2a484ed622db7138` and `src-tauri/src/` = `fa2fd75f1124453a881a835169cde8f4ffb08839`. +Generated by `node scripts/i18n-inventory.mjs` from the source trees `src/` = `0cda9fef36e559dcffcd3bf53611c07f89c49a2b` and `src-tauri/src/` = `fa2fd75f1124453a881a835169cde8f4ffb08839`. Any commit whose `git rev-parse :src` and `:src-tauri/src` match has these exact texts. Do not edit by hand: regenerate, or verify with `node scripts/i18n-inventory.mjs --check`. From a804fd7325ceeda81ec25055dd011fdf47c9f24f Mon Sep 17 00:00:00 2001 From: Donato Silva Date: Fri, 6 Nov 2026 16:03:24 -0500 Subject: [PATCH 3/3] docs(odd): registrar la entrega de #241 y el review del mantenedor --- odd/tasks/241-reorder-hold-tests.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/odd/tasks/241-reorder-hold-tests.md b/odd/tasks/241-reorder-hold-tests.md index c24c6c2..c97e42b 100644 --- a/odd/tasks/241-reorder-hold-tests.md +++ b/odd/tasks/241-reorder-hold-tests.md @@ -1,7 +1,7 @@ # 241-reorder-hold-tests > Issue: https://github.com/egdev6/quorb/issues/241 — `chore(widget)`, asignada a `DonatoSilva`. -> Rama: `chore/241-reorder-hold-tests` (desde `upstream/main` 29c9188). Fork: `DonatoSilva/quorb`. +> Rama: `chore/241-reorder-hold-tests` (reconstruida sobre `upstream/main` f08203b tras el review del mantenedor). Fork: `DonatoSilva/quorb`. > Alcance aprobado por el mantenedor en la propia issue (`status:approved`, `type:chore`); no cambia la regla de #222. ## Specs @@ -29,11 +29,11 @@ ## Tasks -- [x] T1 — S5 — Test en rojo de los bordes de `pointerInsideViewport` y la función pura en `src/lib/pointer-viewport.ts` (módulo nuevo: el viewport no comparte dominio con el estado del panel de `anchor-panel.ts`). Route: inline. Commit: `16c69cf`. -- [x] T2 — S2, S3, S7 — Cablear la función pura en el `onPointerUp` de `Widget.tsx`: misma comparación, con `window.innerWidth`/`innerHeight` leídos al soltar. Route: inline. Commit: `16c69cf`. -- [x] T3 — S1, S4 — Borrar `holdAfterReorderDrop`, `releaseOnStripLeave`, `canPointerOpen`, `ReorderHold` y `NO_REORDER_HOLD` de `src/lib/anchor-panel.ts`, y el bloque «the reorder hold» de su test. Route: inline. Commit: `16c69cf`. -- [x] T4 — S4, S7 — Aplanar la retención a `useRef` en `Widget.tsx`: `= inside` al soltar, `= false` al salir de la tira, `= false` al ocultar el widget y `if (!reorderHold.current)` en `onPointerEnter`. Route: inline. Commit: `16c69cf`. -- [x] T5 — S8 — `npm test`, `npm run typecheck`, `npm run build` y regenerar `docs/i18n-inventory.md` (el inventario fija el hash del árbol `src/` de HEAD). Route: verify. Commit: `16c69cf` (código) y `6a31862` (inventario i18n). +- [x] T1 — S5 — Test en rojo de los bordes de `pointerInsideViewport` y la función pura en `src/lib/pointer-viewport.ts` (módulo nuevo: el viewport no comparte dominio con el estado del panel de `anchor-panel.ts`). Route: inline. Commit: `8532d1c`. +- [x] T2 — S2, S3, S7 — Cablear la función pura en el `onPointerUp` de `Widget.tsx`: misma comparación, con `window.innerWidth`/`innerHeight` leídos al soltar. Route: inline. Commit: `8532d1c`. +- [x] T3 — S1, S4 — Borrar `holdAfterReorderDrop`, `releaseOnStripLeave`, `canPointerOpen`, `ReorderHold` y `NO_REORDER_HOLD` de `src/lib/anchor-panel.ts`, y el bloque «the reorder hold» de su test. Route: inline. Commit: `8532d1c`. +- [x] T4 — S4, S7 — Aplanar la retención a `useRef` en `Widget.tsx`: `= inside` al soltar, `= false` al salir de la tira, `= false` al ocultar el widget y `if (!reorderHold.current)` en `onPointerEnter`. Route: inline. Commit: `8532d1c`. +- [x] T5 — S8 — `npm test`, `npm run typecheck`, `npm run build` y regenerar `docs/i18n-inventory.md` (el inventario fija el hash del árbol `src/` de HEAD). Route: verify. Commit: `8532d1c` (código) y `5be4521` (inventario i18n). - [x] T6 — S6, S9 — Commits en work units, push al fork y PR a `egdev6/quorb` con `Closes #241`, más el comentario de aviso en la issue. Route: inline. Commit: PR https://github.com/egdev6/quorb/pull/242 (rama `chore/241-reorder-hold-tests`). ## Riesgo @@ -42,12 +42,15 @@ Risk: item 3 (se eliminan exports de `src/lib/anchor-panel.ts` que `Widget.tsx` ## Estrategia de entrega -Dos commits: (1) el refactor con sus tests y el ODD, (2) el inventario i18n regenerado (solo cambia la línea de hashes del árbol de `src/`). Push: `git push -u origin chore/241-reorder-hold-tests`, porque la rama hoy rastrea `upstream/main`, donde no hay permiso de escritura. +Tres commits: (1) el refactor con sus tests, el motivo del borde en el JSDoc y el ODD (`8532d1c`), (2) el inventario i18n regenerado (`5be4521`; solo cambia la línea de hashes del árbol de `src/`), (3) este registro de la entrega y del review. Push: `git push -u origin chore/241-reorder-hold-tests`, porque la rama rastrea `upstream/main`, donde no hay permiso de escritura. ## Log - L1: "perfecto ya sabes realiza la nueva rama nacida del main de upstream/main y push al fork que tengo y pr al proyecto con mencion de comentario en el issuo par aque lo revise... recuerdas como lo hicimos con el issue 222 ??" - L2: Decisión de diseño (parent): `pointerInsideViewport` va en un módulo nuevo `src/lib/pointer-viewport.ts` y no en `src/lib/anchor-panel.ts`. Motivo: el header de `anchor-panel.ts` declara «El estado del panel de detalle (#170)» y el consumidor conceptual del predicado (el hold puro) desaparece en este mismo cambio; el repo agrupa por dominio y los otros predicados geométricos (`dock.ts:80 contains`, `detail-presence.ts:84 pointInBounds`) viven en el módulo de su dominio. Alternativa descartada: `src/lib/anchor-focus.ts` («El seam de foco (#170)»). - L3: Convención de estilo verificada: `export function` para el predicado (como `dock.ts:80` y `detail-presence.ts:84`; `anchor-panel.ts` usa arrow-const, que es la excepción local), TSDoc en español y breve (familia anchor/reorder), títulos de test en inglés sin acentos (96/96 describes de nivel superior en inglés), módulo DOM-free (vitest corre en entorno `node`, sin jsdom), `tsconfig` con `strict`, `noUnusedLocals` y `noUnusedParameters`. -- L4: El inventario i18n fija `src/` = `252ab0dd…`, que es el hash del árbol `src/` de HEAD; cualquier cambio en `src/` lo invalida y `node scripts/i18n-inventory.mjs --check` (CI, `ci.yml:49`) falla. El script lee de un commit, no del working tree: hay que commitear `src/` primero y regenerar después. -- L5: Entrega: rama `chore/241-reorder-hold-tests` desde `upstream/main` 29c9188, push al fork `DonatoSilva/quorb` y PR https://github.com/egdev6/quorb/pull/242 (`Closes #241`). Comprobaciones: `npm test` (vitest 69 archivos / 910 tests; `node --test` 298; 0 fallos), `npm run typecheck`, `npm run build` y `node scripts/i18n-inventory.mjs --check`, todo en verde. `cargo test` no aplica: no hay cambios en `src-tauri/`. Sin prueba manual en GUI porque el comportamiento no cambia: las funciones borradas eran identidades y el predicado extraído reproduce la misma comparación. La etiqueta `type:chore` y la asignación de la PR las aplica el mantenedor: un contribuidor no tiene permiso de etiquetado ni de autoasignación en el repo upstream (la PR #240 quedó también sin etiquetas). +- L4: El inventario i18n fija el hash del árbol `src/` de HEAD; cualquier cambio en `src/` lo invalida y `node scripts/i18n-inventory.mjs --check` (CI, `ci.yml:49`) falla. El script lee de un commit (`--ref`, por defecto HEAD), no del working tree: hay que commitear `src/` primero y regenerar después. +- L5: Entrega: rama `chore/241-reorder-hold-tests` desde `upstream/main` 29c9188, push al fork `DonatoSilva/quorb` y PR https://github.com/egdev6/quorb/pull/242 (`Closes #241`). Comprobaciones: `npm test` (vitest 69 archivos / 910 tests; `node --test` 298; 0 fallos), `npm run typecheck`, `npm run build` y `node scripts/i18n-inventory.mjs --check`, todo en verde. `cargo test` no aplica: no hay cambios en `src-tauri/`. Sin prueba manual en GUI porque el comportamiento no cambia: las funciones borradas eran identidades y el predicado extraído reproduce la misma comparación. La etiqueta `type:chore` y la asignación de la PR las aplica el mantenedor. +- L6: Review del mantenedor en la PR #242 (`CHANGES_REQUESTED`, 2026-11-06T06:52:01Z). Punto 1, textual: «El JSDoc que se borra de `holdAfterReorderDrop` explicaba por qué no se retiene al soltar fuera de la ventana: el `mouseleave` ya ocurrió y nada liberaría la retención después. Es justo la razón por la que `<` frente a `<=` importa, y hoy ya no aparece en ningún sitio.» Punto 2, textual: «En `odd/tasks/241-reorder-hold-tests.md`, la estrategia de entrega dice «Dos commits», pero el PR tiene tres (el tercero registra la entrega).» Cierra con «Con eso lo apruebo. La etiqueta y la asignación las pongo yo.» +- L7: Aplicados los dos ajustes. El motivo conductual del `<` pasa al JSDoc de `pointerInsideViewport` (el spec S3 ya lo razonaba, pero no vivía en el código) y este documento declara los tres commits con sus hashes reales. +- L8: `upstream/main` avanzó de 29c9188 a f08203b (cuatro commits, entre ellos el selector de idioma #244, que agregó `src/LocalePicker.tsx` y tocó `src/main.tsx`), así que el hash del árbol `src/` cambió y `docs/i18n-inventory.md` quedó en conflicto: era el único archivo compartido entre upstream y esta rama. La rama se reconstruyó sobre f08203b con los mismos tres commits, el inventario regenerado contra el árbol nuevo y las mismas comprobaciones en verde.