Cierra la UI de F4 (las tres piezas elegidas por el usuario: listas · merge/split
· count/TOC · resize). Alcance v1 acordado: solo COLUMNAS (sin filas ni snap).
palabras-table-resize.svelte (NUEVO): overlay de tiradores en el borde superior
de cada tabla top-level (uno por borde interno de columna), materializados solo
al pasar el puntero por la tabla (mirror de palabras-column-inserter: medición
frame-relative, remeasure en api.html + scroll/resize). Arrastrar un tirador
redimensiona la columna a su IZQUIERDA:
- feedback EN VIVO por inline `width` en la celda de la 1ª fila durante el drag
(el navegador dimensiona la columna por la celda más ancha),
- commit a `cell.width` en UN paso de historia al soltar (nada durante el drag),
- doble-clic = auto (limpia el width).
`cell.width` es una LONGITUD CSS en string (el campo `length` del panel commitea
p.ej. "314px") — un número crudo renderiza `width:314` inválido; el commit usa
`${w}px`. Restringido a tablas TOP-LEVEL (path del commit `[table,0,col]`).
Filas + snap a fracciones (1/2·1/3·2/3) diferidos a v1.1. Langs `resize-column`
diferido (entanglement; ts() por fallback).
Verificado Playwright headless: hover tabla → 2 tiradores (3 columnas); arrastrar
+80px → col0 234→314 (las otras se ajustan 183/196), commit width:314px; doble-
clic → auto restaurado (234/221/238); 0 errores consola. Screenshot MIRADO
(tirador de acento en el borde hover). check 0 errores palabras.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alpha-0.1-sec-dom
parent
a97d8db740
commit
86a3386fcc
@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Column resize (F4.3b v1) — spatial handles on a table's column boundaries.
|
||||
*
|
||||
* On pointer over a TOP-LEVEL table, thin vertical handles materialize at each
|
||||
* internal column boundary, spanning the table's height. Dragging one resizes
|
||||
* the column to its LEFT: LIVE via an inline width on the first-row cell during
|
||||
* the drag (the browser sizes the whole column from the widest declared cell),
|
||||
* committed to the model's `cell.width` in ONE history entry on release. A
|
||||
* double-click clears the width (auto). Frame-relative overlay (mirrors
|
||||
* palabras-column-inserter) so it never makes the canvas a containing block.
|
||||
*
|
||||
* Restricted to TOP-LEVEL tables (the commit path is `[table, 0, col]`). Row
|
||||
* height + fraction snap are deferred (v1.1).
|
||||
*/
|
||||
import { untrack } from 'svelte';
|
||||
import type { ActiveDom } from '$adom';
|
||||
import type { ProviderSnippetProps } from '$soma/components/palabras';
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
|
||||
let {
|
||||
api,
|
||||
content,
|
||||
dom
|
||||
}: {
|
||||
api: ProviderSnippetProps;
|
||||
content: HTMLElement;
|
||||
dom: ActiveDom;
|
||||
} = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const ts = (ref: string) => eidos.langs.ts(ref);
|
||||
const MIN_COL = 48; // px — a column never drags narrower than this.
|
||||
|
||||
type Handle = {
|
||||
key: string;
|
||||
left: number; // frame-relative
|
||||
top: number;
|
||||
height: number;
|
||||
tableIdx: number;
|
||||
colIdx: number; // the column to the LEFT of this boundary
|
||||
};
|
||||
let handles = $state<Handle[]>([]);
|
||||
// `${tableIdx}` of the table under the cursor — handles show only for it.
|
||||
let hoveredKey = $state<string | null>(null);
|
||||
let dragKey = $state<string | null>(null);
|
||||
|
||||
function frame(): HTMLElement | null {
|
||||
return (content?.closest('[data-palabras]') as HTMLElement | null) ?? null;
|
||||
}
|
||||
function topBlocks(): HTMLElement[] {
|
||||
return Array.from(content.children).filter(
|
||||
(el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute('data-palabras-id')
|
||||
);
|
||||
}
|
||||
function firstRowCells(table: HTMLElement): HTMLElement[] {
|
||||
const row = table.querySelector('tr');
|
||||
return row
|
||||
? Array.from(row.children).filter((c): c is HTMLElement => c instanceof HTMLElement)
|
||||
: [];
|
||||
}
|
||||
|
||||
function remeasure() {
|
||||
const fr = frame();
|
||||
if (!fr) {
|
||||
handles = [];
|
||||
return;
|
||||
}
|
||||
const frameRect = fr.getBoundingClientRect();
|
||||
const out: Handle[] = [];
|
||||
topBlocks().forEach((el, tableIdx) => {
|
||||
if (el.getAttribute('data-palabras-block') !== 'table') return;
|
||||
const tableRect = el.getBoundingClientRect();
|
||||
const cells = firstRowCells(el);
|
||||
for (let c = 0; c < cells.length - 1; c++) {
|
||||
const cr = cells[c].getBoundingClientRect();
|
||||
out.push({
|
||||
key: `${tableIdx}-${c}`,
|
||||
left: Math.round(cr.right - frameRect.left),
|
||||
top: Math.round(tableRect.top - frameRect.top),
|
||||
height: Math.round(tableRect.height),
|
||||
tableIdx,
|
||||
colIdx: c
|
||||
});
|
||||
}
|
||||
});
|
||||
handles = out;
|
||||
}
|
||||
|
||||
// Re-measure on document change (skip mid-drag — the live width is inline).
|
||||
$effect(() => {
|
||||
void api.html;
|
||||
untrack(() => {
|
||||
if (!dragKey) remeasure();
|
||||
});
|
||||
});
|
||||
// ...and on scroll / resize (positions are viewport-derived).
|
||||
$effect(() => {
|
||||
const offScroll = dom.listen(
|
||||
dom.getDocument(content),
|
||||
'scroll',
|
||||
() => {
|
||||
if (!dragKey) remeasure();
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
const offResize = dom.listen(dom.getWindow(content), 'resize', () => {
|
||||
if (!dragKey) remeasure();
|
||||
});
|
||||
return () => {
|
||||
offScroll?.();
|
||||
offResize?.();
|
||||
};
|
||||
});
|
||||
|
||||
// Show handles only for the table under the cursor (nothing outside a table).
|
||||
$effect(() => {
|
||||
if (!content) return;
|
||||
const canvas = (content.closest('[data-palabras-canvas]') as HTMLElement | null) ?? content;
|
||||
const onMove = (e: Event) => {
|
||||
if (dragKey) return; // frozen during a drag
|
||||
const t = (e as PointerEvent).target as HTMLElement | null;
|
||||
if (t?.closest('[data-palabras-col-resize]')) return; // over a handle → keep shown
|
||||
const table = t?.closest('[data-palabras-block="table"]') as HTMLElement | null;
|
||||
hoveredKey = table ? String(topBlocks().indexOf(table)) : null;
|
||||
};
|
||||
const offMove = dom.listen(canvas, 'pointermove', onMove);
|
||||
const offLeave = dom.listen(canvas, 'pointerleave', () => {
|
||||
if (!dragKey) hoveredKey = null;
|
||||
});
|
||||
return () => {
|
||||
offMove?.();
|
||||
offLeave?.();
|
||||
};
|
||||
});
|
||||
|
||||
// ── Drag runtime ──────────────────────────────────────────────────────────
|
||||
let liveCell: HTMLElement | null = null;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
let liveWidth = 0;
|
||||
let dragHandle: Handle | null = null;
|
||||
let offDragMove: (() => void) | null = null;
|
||||
let offDragUp: (() => void) | null = null;
|
||||
|
||||
function onHandleDown(e: PointerEvent, h: Handle) {
|
||||
e.preventDefault(); // don't move the caret / start a selection
|
||||
const table = topBlocks()[h.tableIdx];
|
||||
liveCell = table ? (firstRowCells(table)[h.colIdx] ?? null) : null;
|
||||
if (!liveCell) return;
|
||||
dragHandle = h;
|
||||
dragKey = h.key;
|
||||
startX = e.clientX;
|
||||
startWidth = liveCell.getBoundingClientRect().width;
|
||||
liveWidth = startWidth;
|
||||
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
||||
const win = dom.getWindow(content);
|
||||
offDragMove = dom.listen(win, 'pointermove', onDragMove);
|
||||
offDragUp = dom.listen(win, 'pointerup', onDragUp);
|
||||
}
|
||||
function onDragMove(e: Event) {
|
||||
if (!liveCell) return;
|
||||
liveWidth = Math.max(MIN_COL, Math.round(startWidth + ((e as PointerEvent).clientX - startX)));
|
||||
liveCell.style.width = `${liveWidth}px`;
|
||||
}
|
||||
function onDragUp() {
|
||||
offDragMove?.();
|
||||
offDragUp?.();
|
||||
offDragMove = null;
|
||||
offDragUp = null;
|
||||
// Only commit a REAL resize (a bare click / double-click never widens).
|
||||
if (dragHandle && liveCell && liveWidth !== Math.round(startWidth)) {
|
||||
liveCell.style.width = ''; // drop the transient inline width; the re-render applies the model
|
||||
// `cell.width` is a CSS LENGTH STRING (the panel's `length` field commits
|
||||
// e.g. "314px"); a bare number renders as an invalid `width:314`.
|
||||
api.applyCommand({
|
||||
type: 'updateBlockAtPath',
|
||||
blockPath: [dragHandle.tableIdx, 0, dragHandle.colIdx],
|
||||
patch: { width: `${liveWidth}px` }
|
||||
});
|
||||
} else if (liveCell) {
|
||||
liveCell.style.width = '';
|
||||
}
|
||||
liveCell = null;
|
||||
dragHandle = null;
|
||||
dragKey = null;
|
||||
remeasure();
|
||||
}
|
||||
function onHandleDblClick(h: Handle) {
|
||||
// Auto — clear the column's declared width.
|
||||
api.applyCommand({
|
||||
type: 'updateBlockAtPath',
|
||||
blockPath: [h.tableIdx, 0, h.colIdx],
|
||||
patch: { width: undefined }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each handles as h (h.key)}
|
||||
<div
|
||||
data-palabras-col-resize
|
||||
data-palabras-external-tool
|
||||
data-dragging={dragKey === h.key ? '' : undefined}
|
||||
data-visible={hoveredKey === String(h.tableIdx) || dragKey === h.key ? '' : undefined}
|
||||
style="left: {h.left}px; top: {h.top}px; height: {h.height}px"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={ts('#?components.palabras.action.resize-column|Redimensionar columna')}
|
||||
onpointerdown={(e) => onHandleDown(e, h)}
|
||||
ondblclick={() => onHandleDblClick(h)}
|
||||
></div>
|
||||
{/each}
|
||||
Loading…
Reference in new issue