Files
libra/docs/tasks/24-3.md
T
2026-10-31 18:44:10 +02:00

82 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: 24-3
title: 24-3 Signature Balance Component
status: QA Review
date: 2026-10-31
parent: 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.