3d1fe40be6
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
5.0 KiB
5.0 KiB
id, title, status, date, parent
| id | title | status | date | parent |
|---|---|---|---|---|
| 24-3 | 24-3 Signature Balance Component | Ready for Dev | 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
resources/views/components/site/balance.blade.php— props:mode(hero|finale|static, defaultstatic),label(default null),size(integer px, default null; when set, the SVG getswidthandheightof that many px). Renders<svg class="site-balance" data-balance="{{ $mode }}" viewBox="0 0 400 400" …>withoverflow:visible; whenlabelis set:role="img" aria-label="{{ $label }}", otherwisearia-hidden="true". Pass through$attributes(class, id, style).resources/js/site/balance.js— exportsinit()anddestroy(); imported and wired inresources/js/public.jsexactly likereveal.js(init onlivewire:navigated, destroy onlivewire:navigating). It contains:build(svg)— fills the SVG with the markup fromdocs/ux-mockup.htmllines 566–595 (thescales()function's template: four gradientsgd,gc,gb,gh, two filterssh(drop shadow dy 8, stdDeviation 7, flood#0d1009opacity .45) andbl(blur 6), the ground shadow ellipse, base, pillar, olive-leaf finial, the.beamgroup, the pivot, and two.pangroups). Gradient and filter ids must be unique per instance (suffix a running counter) so several balances on one page do not share ids. Returnstilt(deg)(mockup lines 597–602):beam.setAttribute('transform', rotate(deg 200 110)); withr = deg·π/180, c = cos(r)·142, s = sin(r)·142, pan 0 attranslate(200−c, 110−s)and pan 1 attranslate(200+c, 110+s).spring(tilt, start)(mockup lines 605–610): statea = start, v = 0, target = 0; each animation framev += (target − a)·0.028; v ·= 0.935; a += v; tilt(a); keep requesting frames while|v| > 0.002or|target − a| > 0.01. Exposesset(target).- Behaviour per
data-balance:static:tilt(0).hero:tilt(14), create a spring from 14, after 450msset(0). IfmatchMedia('(pointer:fine)').matches, find the closest ancestor.frame(the hero frame, built in 24-4); onpointermoveover it:x = (clientX − rect.left) / rect.width,set(clamp((x − 0.5)·2.4, −1, 1)·9); move the frame's.glowelement'sleft/topto the pointer position within the frame; set the frame'simgtransform toscale(1.08) translate(${(clientX − rect.left − rect.width/2)/−40}px, ${(clientY − rect.top − rect.height/2)/−40}px). Onpointerleave:set(0)and clear the image transform.finale:tilt(−12), spring from −12; on a passive windowscrolllistener (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 andtilt(0); no spring, no listeners. destroy()removes every listener added and cancels pending animation frames.
- 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>withdata-balance="hero",viewBox="0 0 400 400",role="img"and the givenaria-label; withoutlabelit rendersaria-hidden="true".- With two balances on one page, every
idinside the SVGs is unique (document.querySelectorAll('[id]')has no duplicate values). - A
herobalance is drawn tilted on load and its beam'stransformreachesrotate(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
finalebalance'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: reduceemulated, every balance's beam transform isrotate(0 200 110)and no animation frame is requested after load. - Navigating between public pages with
wire:navigateand 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.