You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/components/sticky
dev 37e35d7a2d
fix(sticky): sentinelRef is State + eidos types import ProviderProps
3 months ago
..
README.md uix(sticky): F1.1 · position:sticky con detección de estado (soma+eidos) — el primer behavioral 3 months ago
index.ts uix(sticky): F1.1 · position:sticky con detección de estado (soma+eidos) — el primer behavioral 3 months ago
sticky.css uix(sticky): fix · pin con inset FÍSICO (top/bottom), no lógico — hallazgo de review 3 months ago
sticky.svelte uix(sticky): F1.1 · position:sticky con detección de estado (soma+eidos) — el primer behavioral 3 months ago
types.ts fix(sticky): sentinelRef is State + eidos types import ProviderProps 3 months ago

README.md

Sticky

A position: sticky wrapper that KNOWS when it is pinned, so a header / toolbar can change elevation the moment it sticks. Built 2026-07-21 as F1.1 of the blocks initiative (docs/process/PLAN-blocks.md) — the first BEHAVIORAL F1 piece, and the one that unblocks the site-header block (F2.1). Reference floor + reflow doctrine: the research dossier (docs/process/RESEARCH-blocks-references.md §P3 sticky) under the E-2 rule (parity floor = v1). Headless behavior: soma/components/sticky.

Baseline

  • Classification: passive (0 events, 0 keyboard) but membership met by COMPLEX BEHAVIOR — IntersectionObserver stuck-detection + provider/sentinel composition. apg: none.
  • Anatomy: Sticky (box → data-sticky, data-edge, data-stuck) + an internal in-flow data-sticky-sentinel (aria-hidden). The eidos layer is thin: it adds only position: sticky, the inset (from the soma-injected --_sticky-offset), a --sticky-z-index token, and the layout-neutral sentinel. It POSITIONS; it does not decorate.
  • The stuck contract: data-stuck (present while pinned) + data-edge (top | bottom) mirror the platform CSS @container scroll-state(stuck: top|bottom) (Chrome/Edge 133+; not Firefox/Safari ≤2026). So [data-sticky][data-stuck][data-edge='top'] today translates mechanically to the native scroll-state() query when it ships everywhere — this component is a polyfill of a future CSS contract.
  • Detection is IntersectionObserver, never scroll + getBoundingClientRect (the reflow-doctrine violation). The IO motor is the one Chrome itself blesses; uix.perf's reflow detector can prove the demo does zero forced-reflow — a measurable quality claim no reference library makes.

Comparativa

Ref Equivalente Qué adoptamos Qué no
AntD Affix offsetTop/offsetBottom/target/onChange(affixed) offset per edge, an explicit scroll-host (root ≈ AntD target) su implementación scroll-listener + position:fixed swap (su propia FAQ recomienda sticky nativo); onChange → evento reservado, no v1
Mantine Affix — (no comparable) es un slot position:fixed en Portal (FAB), sin noción de «pegado»
Chrome sentinel technique dual-sentinel IntersectionObserver + sticky-change event el motor: centinela IO bendecido por Chrome (cumple la doctrina anti-reflow) un evento síncrono (el nuestro reserva el nombre; v1 solo estampa CSS)
Radix / Base UI / Ark / React Aria — (ninguno lo shippea) seríamos los primeros headless-con-contrato en affix/sticky —
CSS scroll-state(stuck:) @container scroll-state(stuck: top/bottom) espejamos su vocabulario en data-stuck/data-edge esperar a que aterrice (Chromium-only ≤2026); somos el polyfill cross-browser mientras tanto

Decisiones

  • Un solo eje por instancia (edge: 'top' | 'bottom', default top): cubre header (top) y footer (bottom). Detección simultánea en ambos bordes = Gap diferido.
  • offset es un NÚMERO (px), no una string CSS: el provider necesita resolverlo para el rootMargin del observer (acoplado al inset). Offsets en unidades no-px = diferido.
  • El centinela es un hermano de flujo del box, no un hijo (un hijo se fija con el box y nunca detecta). Renderizado como dos nodos raíz hermanos; visibility:hidden, pointer-events:none, altura real 2px con margen negativo → layout-neutral (una superposición cero-área nunca dispara IO).
  • El tratamiento del estado pegado es del consumidor/block, no del recipe: el recipe posiciona (position:sticky + inset + z-index); el consumidor selecciona [data-sticky][data-stuck] para sombra/fondo. Esto mantiene el componente neutral y componible.
  • Scroll-host explícito (root), default viewport: sin helper de scroll-parent en la superficie bendecida de $adom (el único vive en $ethereal, internals de floating-UI que no se importan ad-hoc), la resolución del ancestro con overflow queda del lado del consumidor en v1.
  • Evento change-stuck reservado en README, NO en el morfo: declararlo volvería el componente interactive y arrastraría todo el aparato sema/event-trace para un evento que no dispara en v1. Reservar el nombre aquí mantiene el contrato estable sin el coste.

Passive justification

Sticky declares 0 events because pinning is a fact of layout/scroll, not a perceptual occurrence the user commits — the box slides into its pinned position as a consequence of scrolling, and data-stuck is a styling signal (one frame late, IO being async), never a commit. Membership is met by the complex stuck-detection behavior, not a widget pattern (apg: none). No keyboard, no state machine beyond the observed stuck flag. If a later version needs to notify the app on pin/unpin, the change-stuck event name is reserved (Gaps) and its addition will not reshape the contract.

Sema events

None (see Passive justification). SemaPanel in the demo renders the justified empty state.

Gaps

Gap Disposition
change-stuck / onStuckChange(edge) event (AntD onChange parity) diferir — nombre reservado aquí; su adición pasa por soma+sema (regla de admisión), no por un callback de wrapper
Auto-resolución del scroll-host + dev-warn de ancestro overflow:hidden diferir — necesita un helper getScrollParent/getOverflowAncestors en $adom (hoy solo existe en $ethereal, internals no importables). Candidato a añadir a $adom; hasta entonces el consumidor pasa root
Ambos bordes simultáneos (stuck-top Y stuck-bottom en una instancia) diferir — v1 = un eje por instancia; raro en la práctica
offset en unidades no-px (rem/%) diferir — el provider resuelve px para el rootMargin; una resolución px en runtime (getComputedStyle vía dom.measure) se añade si un consumidor real lo pide
Migración a @container scroll-state(stuck:) nativo cuando el soporte llegue diferir — el contrato de attrs ya lo espeja; el recipe añadirá el bloque @container como progressive enhancement, reteniendo el polyfill IO como fallback

Powered by TurnKey Linux.