feat(words): internal scroll for the content area + capture-phase scroll sync

The editor body was unbounded vertically — long documents pushed the
whole page instead of confining the scroll to the editor surface.
Per spec: "la parte de la edicion deberia tener un scroll".

Recipe:
- Three new tokens `content-max-block-size-sm/md/lg` (50vh / 60vh /
  70vh) mirror the existing `content-min-block-size-*`. Generated CSS
  regenerated.

CSS:
- `[data-words-content]` now carries `max-block-size:
  var(--_words-content-max-block-size)` and `overflow-y: auto`. The
  min-block-size baseline stays as the starting height for empty
  editors. When content grows past the cap, the body scrolls
  internally — the host card no longer balloons.
- `scroll-padding-block: 1rem` hint so caret-into-view nudges leave
  breathing room.

Block-handle / block-inserter / image-float-bar:
- Switch scroll listeners from `window.addEventListener('scroll')` to
  `document.addEventListener('scroll', { capture: true })`. Scroll
  events don't bubble; the previous wiring only caught window scrolls.
  Now the gutter overlays (handle position, "+" snap target, image
  float bar anchor) re-measure when the user scrolls INSIDE the
  content area too.

Verification: dev probe at /uix/components/words confirms
`max-block-size: 600px`, `overflow-y: auto`, content scrollHeight
(615px) > clientHeight (600px), `isInternallyScrollable: true` on a
1000px viewport (60vh of 1000 = 600). 467/467 tests pass across
eidos + morfo + sema + soma/components/words.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent c4be6dbe1b
commit ce95473261

@ -220,12 +220,17 @@
const win = content.ownerDocument.defaultView;
const onscroll = () => measureFromEl(hoverBlockEl);
win?.addEventListener('scroll', onscroll, { passive: true });
// Capture-phase + on document so we also catch scrolls inside
// any scrollable ancestor (the content area itself now has
// overflow-y:auto). Scroll events do not bubble, so a plain
// `document.addEventListener('scroll')` without capture would
// only catch root-document scrolls.
document.addEventListener('scroll', onscroll, { capture: true, passive: true });
win?.addEventListener('resize', onscroll, { passive: true });
return () => {
document.removeEventListener('mousemove', onmove);
win?.removeEventListener('scroll', onscroll);
document.removeEventListener('scroll', onscroll, { capture: true });
win?.removeEventListener('resize', onscroll);
};
});

@ -223,14 +223,18 @@
// on scroll and let the next mousemove re-snap.
seam = null;
};
win?.addEventListener('scroll', onscroll, { passive: true });
// Capture phase so we catch scrolls inside the content area
// (which now scrolls internally) — scroll events do not bubble,
// so plain document.addEventListener would only catch root
// scrolls.
document.addEventListener('scroll', onscroll, { capture: true, passive: true });
win?.addEventListener('resize', onscroll, { passive: true });
return () => {
document.removeEventListener('mousemove', onmove);
document.removeEventListener('dragover', ondragover);
document.removeEventListener('drop', ondrop);
win?.removeEventListener('scroll', onscroll);
document.removeEventListener('scroll', onscroll, { capture: true });
win?.removeEventListener('resize', onscroll);
};
});

@ -63,12 +63,16 @@
const win = targetFigure.ownerDocument.defaultView;
if (!win) return;
const opts: AddEventListenerOptions = { passive: true };
win.addEventListener('scroll', measure, opts);
// Capture-phase on document catches scrolls inside the content
// area (now overflow-y:auto) too — scroll events don't bubble
// so plain `win.addEventListener('scroll')` only catches the
// root document scroll.
document.addEventListener('scroll', measure, { capture: true, passive: true });
win.addEventListener('resize', measure, opts);
const ro = new ResizeObserver(measure);
ro.observe(targetFigure);
return () => {
win.removeEventListener('scroll', measure, opts);
document.removeEventListener('scroll', measure, { capture: true });
win.removeEventListener('resize', measure, opts);
ro.disconnect();
};

@ -4,6 +4,7 @@
--_words-content-px: var(--words-content-px-md);
--_words-content-py: var(--words-content-py-md);
--_words-content-min-block-size: var(--words-content-min-block-size-md);
--_words-content-max-block-size: var(--words-content-max-block-size-md);
/* Gutter reserved for the block-handle grip + inserter "+" overlay.
Rendered as a SEPARATE column outside the editor box (its own
background + dot pattern), with a small gap before the editor.
@ -87,6 +88,7 @@
--_words-content-px: var(--words-content-px-sm);
--_words-content-py: var(--words-content-py-sm);
--_words-content-min-block-size: var(--words-content-min-block-size-sm);
--_words-content-max-block-size: var(--words-content-max-block-size-sm);
--_words-toolbar-px: var(--words-toolbar-px-sm);
--_words-toolbar-py: var(--words-toolbar-py-sm);
--_words-button-size: var(--words-button-size-sm);
@ -99,6 +101,7 @@
--_words-content-px: var(--words-content-px-lg);
--_words-content-py: var(--words-content-py-lg);
--_words-content-min-block-size: var(--words-content-min-block-size-lg);
--_words-content-max-block-size: var(--words-content-max-block-size-lg);
--_words-toolbar-px: var(--words-toolbar-px-lg);
--_words-toolbar-py: var(--words-toolbar-py-lg);
--_words-button-size: var(--words-button-size-lg);
@ -1828,7 +1831,12 @@
}
[data-words-content] {
/* Starting baseline keeps the editor at a sensible height when
* empty; max-block-size caps growth so long documents scroll
* INSIDE the content area instead of pushing the page. */
min-block-size: var(--_words-content-min-block-size);
max-block-size: var(--_words-content-max-block-size);
overflow-y: auto;
padding-block: var(--_words-content-py);
padding-inline: var(--_words-content-px);
color: var(--words-content-color);
@ -1838,6 +1846,10 @@
caret-color: var(--_words-accent-solid);
user-select: text;
-webkit-user-select: text;
/* Hint to the browser that this is a self-contained scroll region
* so the rail / drawer overlays positioned by viewport coords don't
* get clipped weirdly on focus. */
scroll-padding-block: 1rem;
}
[data-words-content] * {

@ -2279,6 +2279,9 @@
--words-content-min-block-size-sm: 10rem;
--words-content-min-block-size-md: 13rem;
--words-content-min-block-size-lg: 16rem;
--words-content-max-block-size-sm: 50vh;
--words-content-max-block-size-md: 60vh;
--words-content-max-block-size-lg: 70vh;
--words-selection-color: var(--color-content-primary);
--words-block-gap: var(--space-2);
--words-heading-color: var(--color-content-primary);

@ -2715,6 +2715,9 @@ export const THEME_BASE_RECIPE_TOKENS = {
'content-min-block-size-sm': '10rem',
'content-min-block-size-md': '13rem',
'content-min-block-size-lg': '16rem',
'content-max-block-size-sm': '50vh',
'content-max-block-size-md': '60vh',
'content-max-block-size-lg': '70vh',
'selection-color': 'var(--color-content-primary)',
'block-gap': 'var(--space-2)',
'heading-color': 'var(--color-content-primary)',

Loading…
Cancel
Save

Powered by TurnKey Linux.