Files
2026-10-31 19:23:46 +02:00

5.0 KiB
Raw Permalink Blame History

id, title, status, date, parent
id title status date parent
24-3 24-3 Signature Balance Component Done 2026-10-31 24

Description

Build the redesign's signature: an engraved-gold balance (scales of justice) rendered as an inline SVG, with three behaviours — hero (settles on load, weighed by the pointer), finale (comes level as the reader scrolls to the booking card) and static (always level). Pages place it in later subtasks (24-4 hero and vision card, 24-5 finale, 24-6 success page).

Files to create

  1. resources/views/components/site/balance.blade.php — props: mode (hero | finale | static, default static), label (default null), size (integer px, default null; when set, the SVG gets width and height of that many px). Renders <svg class="site-balance" data-balance="{{ $mode }}" viewBox="0 0 400 400" …> with overflow:visible; when label is set: role="img" aria-label="{{ $label }}", otherwise aria-hidden="true". Pass through $attributes (class, id, style).
  2. resources/js/site/balance.js — exports init() and destroy(); imported and wired in resources/js/public.js exactly like reveal.js (init on livewire:navigated, destroy on livewire:navigating). It contains:
    • build(svg) — fills the SVG with the markup from docs/ux-mockup.html lines 566–595 (the scales() function's template: four gradients gd, gc, gb, gh, two filters sh (drop shadow dy 8, stdDeviation 7, flood #0d1009 opacity .45) and bl (blur 6), the ground shadow ellipse, base, pillar, olive-leaf finial, the .beam group, the pivot, and two .pan groups). Gradient and filter ids must be unique per instance (suffix a running counter) so several balances on one page do not share ids. Returns tilt(deg) (mockup lines 597–602): beam.setAttribute('transform', rotate(deg 200 110)); with r = deg·π/180, c = cos(r)·142, s = sin(r)·142, pan 0 at translate(200−c, 110−s) and pan 1 at translate(200+c, 110+s).
    • spring(tilt, start) (mockup lines 605–610): state a = start, v = 0, target = 0; each animation frame v += (target − a)·0.028; v ·= 0.935; a += v; tilt(a); keep requesting frames while |v| > 0.002 or |target − a| > 0.01. Exposes set(target).
    • Behaviour per data-balance:
      • static: tilt(0).
      • hero: tilt(14), create a spring from 14, after 450ms set(0). If matchMedia('(pointer:fine)').matches, find the closest ancestor .frame (the hero frame, built in 24-4); on pointermove over it: x = (clientX − rect.left) / rect.width, set(clamp((x − 0.5)·2.4, −1, 1)·9); move the frame's .glow element's left/top to the pointer position within the frame; set the frame's img transform to scale(1.08) translate(${(clientX − rect.left − rect.width/2)/−40}px, ${(clientY − rect.top − rect.height/2)/−40}px). On pointerleave: set(0) and clear the image transform.
      • finale: tilt(−12), spring from −12; on a passive window scroll listener (and once on init): p = scrollY / (scrollHeight − innerHeight) (1 if the denominator is ≤ 0), q = clamp((p − 0.7) / 0.25, 0, 1), set(−12·(1 − q)).
    • If matchMedia('(prefers-reduced-motion: reduce)').matches: every balance is built and tilt(0); no spring, no listeners.
    • destroy() removes every listener added and cancels pending animation frames.
  3. CSS in resources/css/public.css: body.site .site-balance{display:block;overflow:visible}.

Files to modify

  • resources/js/public.js (import and wire ./site/balance.js), resources/css/public.css.

Acceptance Criteria

  • <x-site.balance mode="hero" label="ميزان العدالة" /> renders an <svg> with data-balance="hero", viewBox="0 0 400 400", role="img" and the given aria-label; without label it renders aria-hidden="true".
  • With two balances on one page, every id inside the SVGs is unique (document.querySelectorAll('[id]') has no duplicate values).
  • A hero balance is drawn tilted on load and its beam's transform reaches rotate(0…) (|angle| < 0.05) within 4 seconds without pointer input.
  • Moving a fine pointer to the right edge of the hero frame tips the beam toward +9° (in the browser, read the beam's transform); leaving the frame returns it toward 0°.
  • A finale balance's beam is at −12° at the top of the page and within 0.5° of 0° when the page is scrolled to the bottom.
  • With prefers-reduced-motion: reduce emulated, every balance's beam transform is rotate(0 200 110) and no animation frame is requested after load.
  • Navigating between public pages with wire:navigate and back to / re-initialises the hero balance (it renders and settles) and leaves no duplicate scroll listeners (the finale still levels correctly after three navigations).

Sub-tasks

None.