pocopine-core 0.1.0

Client-side reactive runtime for pocopine — a Rust/WASM port of Alpine.js.
Documentation
/* pocopine animation preset atoms (RFC-038, themed in RFC-039).
 *
 * Injected once at App::new() via `crate::styles::inject_style`. Each
 * preset ships three classes — `*-base` (transition-property +
 * duration + easing), `*-from` (visual state at one extreme) and
 * `*-to` (the other extreme). The pp-transition state machine applies
 * them in this order for enter:
 *
 *   add    enter-start (= *-from)
 *   reflow + next animation frame
 *   swap to enter-end  (= *-to)
 *
 * and the reverse for leave. Duration/easing come from the `*-base`
 * class, which is applied for the whole phase.
 *
 * THEMING (RFC-039 §2). Every duration / easing reads through CSS
 * custom properties so authors can retune all presets globally:
 *
 *   :root {
 *     --pp-tx-duration: 120ms;
 *     --pp-tx-easing: ease-out;
 *   }
 *
 * Per-preset overrides via `--pp-tx-fade-duration` etc. fall back
 * to the global `--pp-tx-duration` when absent.
 *
 * REDUCED MOTION (RFC-039 §1). When the user prefers reduced
 * motion, every duration collapses to 1ms — the transition still
 * fires (so `transitionend` lands and on_done plays out), but the
 * motion is imperceptible. Authors who *want* motion under reduced
 * preference set `--pp-tx-respect-motion-pref: 0` on the element
 * (the `motion="always"` macro arg flips this).
 */

:root {
  --pp-tx-duration: 180ms;
  /* Apple-style sharp deceleration — lands earlier than a plain
   * ease-out so the motion reads as decisive rather than lingering.
   * Feels snappier than `cubic-bezier(0, 0, 0.2, 1)` at the same
   * duration. */
  --pp-tx-easing: cubic-bezier(0.16, 1, 0.3, 1);
  /* `fade` is the overlay/backdrop preset. Keep it aligned with
   * the content duration so dimmed backgrounds do not snap in or
   * disappear before the foreground panel settles. */
  --pp-tx-fade-duration: 180ms;
  --pp-tx-fade-easing: cubic-bezier(0.16, 1, 0.3, 1);
  --pp-tx-collapse-duration: 180ms;
  --pp-tx-collapse-easing: cubic-bezier(0.4, 0, 0.2, 1);
  --pp-tx-flip-duration: 320ms;
  --pp-tx-flip-easing: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ── fade ─────────────────────────────────────────────────────── */
.pp-tx-fade-base {
  transition: opacity var(--pp-tx-fade-duration, var(--pp-tx-duration))
              var(--pp-tx-fade-easing, var(--pp-tx-easing));
  will-change: opacity;
}
.pp-tx-fade-from { opacity: 0; }
.pp-tx-fade-to   { opacity: 1; }

/* ── scale (opacity + 0.95 → 1) ───────────────────────────────── */
.pp-tx-scale-base {
  transition: opacity var(--pp-tx-scale-duration, var(--pp-tx-duration))
              var(--pp-tx-scale-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-scale-duration, var(--pp-tx-duration))
              var(--pp-tx-scale-easing, var(--pp-tx-easing));
}
.pp-tx-scale-from { opacity: 0; transform: scale(0.95); }
.pp-tx-scale-to   { opacity: 1; transform: scale(1); }

/* ── fade-scale (default for Dialog / HoverCard / Command) ────── */
.pp-tx-fade-scale-base {
  transition: opacity var(--pp-tx-fade-scale-duration, var(--pp-tx-duration))
              var(--pp-tx-fade-scale-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-fade-scale-duration, var(--pp-tx-duration))
              var(--pp-tx-fade-scale-easing, var(--pp-tx-easing));
  transform-origin: center;
  will-change: opacity, transform;
}
.pp-tx-fade-scale-from { opacity: 0; transform: scale(0.94); }
.pp-tx-fade-scale-to   { opacity: 1; transform: scale(1); }

/* ── zoom (opacity + 0.8 → 1) ─────────────────────────────────── */
.pp-tx-zoom-base {
  transition: opacity var(--pp-tx-zoom-duration, var(--pp-tx-duration))
              var(--pp-tx-zoom-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-zoom-duration, var(--pp-tx-duration))
              var(--pp-tx-zoom-easing, var(--pp-tx-easing));
}
.pp-tx-zoom-from { opacity: 0; transform: scale(0.80); }
.pp-tx-zoom-to   { opacity: 1; transform: scale(1); }

/* ── slide-up: comes in from BELOW, leaves downward ──────────── */
.pp-tx-slide-up-base {
  transition: opacity var(--pp-tx-slide-up-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-up-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-slide-up-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-up-easing, var(--pp-tx-easing));
}
.pp-tx-slide-up-from { opacity: 0; transform: translateY(8px); }
.pp-tx-slide-up-to   { opacity: 1; transform: translateY(0); }

/* ── slide-down: comes in from ABOVE (typical popover/menu) ──── */
.pp-tx-slide-down-base {
  transition: opacity var(--pp-tx-slide-down-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-down-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-slide-down-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-down-easing, var(--pp-tx-easing));
}
.pp-tx-slide-down-from { opacity: 0; transform: translateY(-8px); }
.pp-tx-slide-down-to   { opacity: 1; transform: translateY(0); }

/* ── slide-left / slide-right (horizontal) ───────────────────── */
.pp-tx-slide-left-base {
  transition: opacity var(--pp-tx-slide-left-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-left-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-slide-left-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-left-easing, var(--pp-tx-easing));
}
.pp-tx-slide-left-from { opacity: 0; transform: translateX(8px); }
.pp-tx-slide-left-to   { opacity: 1; transform: translateX(0); }

.pp-tx-slide-right-base {
  transition: opacity var(--pp-tx-slide-right-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-right-easing, var(--pp-tx-easing)),
              transform var(--pp-tx-slide-right-duration, var(--pp-tx-duration))
              var(--pp-tx-slide-right-easing, var(--pp-tx-easing));
}
.pp-tx-slide-right-from { opacity: 0; transform: translateX(-8px); }
.pp-tx-slide-right-to   { opacity: 1; transform: translateX(0); }

/* ── collapse — auto-height open / close.
 *
 * Uses CSS Grid's `grid-template-rows` interpolation: a parent
 * with `display: grid; grid-template-rows: 0fr | 1fr` smoothly
 * tweens its single row between collapsed and full intrinsic
 * height. The grid item (the inner rendered root of the
 * collapsible content) needs `min-height: 0; overflow: hidden`
 * so its content can actually shrink during the close phase.
 *
 * `display: grid` + the `> *` shrink rule apply on BOTH `from`
 * and `base`, because the two-phase enter swap (RFC-039) applies
 * `from` alone for one frame before adding `base`. Without
 * display:grid on from, `grid-template-rows: 0fr` is inert and
 * the subsequent display-change jumps instead of tweening.
 *
 * Opacity is tweened alongside grid-template-rows so the content
 * fades in sync with the row growth — without it the last 30% of
 * the transition looks like a pop (content suddenly appearing
 * when the row reaches its intrinsic height). The user-visible
 * motion becomes a smooth dissolve + unfold instead of a snap.
 *
 * No JavaScript measurement; works for arbitrary content
 * heights. Pairs with `pp-transition="collapse"` on Pine's
 * Collapsible / Accordion content clone roots.
 */
.pp-tx-collapse-base,
.pp-tx-collapse-from {
  display: grid;
  /* `minmax(0, …)` explicitly sets the row's minimum to 0 so the
   * grid-template-rows value can actually tween to 0 without
   * being clamped by the intrinsic min-content size of the child
   * (which in Firefox otherwise wins even with `min-height: 0`
   * and `overflow: hidden`). Without this, `0fr` collapses to
   * content-height and the transition is invisible. */
  grid-template-rows: minmax(0, 1fr);
}
.pp-tx-collapse-base > *,
.pp-tx-collapse-from > * {
  min-height: 0;
  overflow: hidden;
}
.pp-tx-collapse-base {
  transition: grid-template-rows var(--pp-tx-collapse-duration)
              var(--pp-tx-collapse-easing),
              opacity var(--pp-tx-collapse-duration)
              var(--pp-tx-collapse-easing);
}
.pp-tx-collapse-from { grid-template-rows: minmax(0, 0fr); opacity: 0; }
.pp-tx-collapse-to   { grid-template-rows: minmax(0, 1fr); opacity: 1; }

/* ── flip helper — pp-for reorder animation.
 *    Applied imperatively by the FLIP runtime: the element gets a
 *    transform that matches its OLD position (invert), then a
 *    transition to transform: none is started on the next frame. */
.pp-tx-flip-base {
  transition: transform var(--pp-tx-flip-duration)
              var(--pp-tx-flip-easing);
  will-change: transform;
}

/* ── reduced motion ──────────────────────────────────────────────
 *
 * Collapse every preset's duration to 1ms when the user has set
 * `prefers-reduced-motion: reduce`. The transition still fires
 * (so `transitionend` is delivered and the state machine's
 * on_done callback runs at the right moment) but the motion is
 * imperceptible. Authors who specifically want motion under
 * reduced-motion (e.g. a progress indicator where the motion
 * conveys data) opt out by stamping `data-pp-motion="always"`
 * on the element — the `motion = "always"` macro arg does this.
 */
@media (prefers-reduced-motion: reduce) {
  :root:not([data-pp-motion="always"]) {
    --pp-tx-duration: 1ms;
    --pp-tx-flip-duration: 1ms;
  }
  [data-pp-motion="always"] {
    /* opt-out island — motion plays at the durations defined
       above (or per-element overrides). */
  }
}