bcabb2ad2d
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
148 lines
9.6 KiB
Markdown
148 lines
9.6 KiB
Markdown
---
|
||
id: 24-1
|
||
title: 24-1 Public Design Foundation (Stylesheet, Script, Fonts, Layout)
|
||
status: QA Review
|
||
date: 2026-10-31
|
||
parent: 24
|
||
---
|
||
|
||
## Description
|
||
|
||
Create the shared foundation every later redesign subtask builds on. It must be loaded only by
|
||
the public layout (and, for the stylesheet only, by the auth layouts in task 24-9), and every rule
|
||
must be scoped under `body.site` so the admin and client dashboards are unaffected.
|
||
|
||
The approved visual reference is the mockup file `docs/ux-mockup.html` (committed in `caec396`).
|
||
Where this task says "port lines X–Y", copy those CSS rules from that file and prefix every
|
||
selector with `body.site ` (for example `.btn-gold` → `body.site .btn-gold`; `:root{…}` →
|
||
`body.site{…}`; `body{…}` → `body.site{…}`; `html[dir=ltr] .x` → `html[dir=ltr] body.site .x`;
|
||
`html[lang=en] .x` → `html[lang=en] body.site .x`). The mockup's lines contain no images, so line
|
||
numbers are exact.
|
||
|
||
### Files to create
|
||
|
||
1. **`resources/css/public.css`** — contains, in this order:
|
||
- Tokens on `body.site` (from mockup lines 12–29):
|
||
`--forest:#2D3624; --forest-2:#232A1C; --gold:#A68966; --gold-ink:#8A7555; --gold-lt:#C4A882;
|
||
--cream:#F4F1EA; --olive:#4A5D23; --ink:#1E2219; --muted:#5B6152; --line:rgba(45,54,36,.14);
|
||
--display:"Markazi Text","Times New Roman",serif; --body:"IBM Plex Sans Arabic",system-ui,sans-serif;
|
||
--r:10px; --ease:cubic-bezier(.2,.7,.2,1);
|
||
--shadow-1:0 1px 2px rgba(30,34,25,.06),0 4px 14px rgba(30,34,25,.06);
|
||
--shadow-2:0 2px 4px rgba(30,34,25,.08),0 18px 40px -12px rgba(30,34,25,.28);`
|
||
- Page ground and type on `body.site` (port lines 32–39): `font-family:var(--body);
|
||
color:var(--ink); font-size:17px; line-height:1.75; background-color:var(--cream)` plus the
|
||
linen weave `background-image: repeating-linear-gradient(0deg, rgba(45,54,36,.025) 0 1px,
|
||
transparent 1px 3px), repeating-linear-gradient(90deg, rgba(45,54,36,.022) 0 1px, transparent
|
||
1px 4px)`. This overrides the inline `style="font-family: var(--font-…)"` on the body, so the
|
||
layout's inline style is removed (see layout changes below).
|
||
- Port lines 40–50 (img, links, `.wrap`, skip link, focus-visible, h1–h3, `.eyebrow`,
|
||
`.art-no`). `.wrap` = `max-width:1200px; margin-inline:auto; padding-inline:28px`.
|
||
Focus = `outline:2px solid var(--gold); outline-offset:3px; border-radius:4px`.
|
||
- Buttons: port lines 52–68 (`.btn`, `.btn-gold` with shimmer `::after`, `.btn-ghost`,
|
||
`.on-dark .btn-ghost`, `.btn .arr` and LTR mirroring). Class names `.btn`, `.btn-gold`,
|
||
`.btn-ghost` do not exist in `resources/css/app.css` (which uses `.btn-primary`,
|
||
`.btn-secondary`), and they are scoped under `body.site`, so there is no collision.
|
||
- Progress rule: port lines 70–73 (`#hem` → rename to `.site-progress`; fixed, top 0, z 60,
|
||
height 2px, gradient `linear-gradient(90deg,#8A7555,#E2CDA4,#A68966)`, `transform:scaleX(0)`,
|
||
origin right; `html[dir=ltr]` origin left).
|
||
- Sections: port lines 139–144 (`section{position:relative}`, `.sec{padding:120px 0}`,
|
||
`.sec-head`, `.sec-head h2`, `.sec-head p`).
|
||
- Reveal: port lines 286–290, changed so content is hidden only when JavaScript runs:
|
||
`body.site.js [data-reveal]{opacity:0;transform:translateY(28px);transition:opacity .9s
|
||
var(--ease),transform .9s var(--ease)}`, `body.site.js [data-reveal].in{opacity:1;transform:none}`,
|
||
and the `data-d` 1–5 delays (.08s, .16s, .24s, .32s, .4s).
|
||
- Responsive base: at `max-width:760px`: `body.site{font-size:16px}`,
|
||
`body.site .wrap{padding-inline:18px}`, `body.site .sec{padding:80px 0}`.
|
||
- Reduced motion (port lines 324–327): under `@media (prefers-reduced-motion:reduce)`,
|
||
`body.site *, body.site *::before, body.site *::after{animation:none!important;
|
||
transition:none!important; scroll-behavior:auto!important}` and
|
||
`body.site [data-reveal]{opacity:1;transform:none}`.
|
||
- `html:has(body.site){scroll-behavior:smooth}`.
|
||
2. **`resources/js/public.js`** — imports `./site/reveal.js` and `./site/progress.js`, and calls
|
||
each module's `init()` on the `livewire:navigated` document event (Livewire 3 dispatches it on
|
||
the first page load and after every `wire:navigate`), and each module's `destroy()` on
|
||
`livewire:navigating`. On init it adds the class `js` to `document.body`.
|
||
3. **`resources/js/site/reveal.js`** — exports `init()`/`destroy()`. If
|
||
`matchMedia('(prefers-reduced-motion: reduce)').matches`, add class `in` to every
|
||
`[data-reveal]` and return. Otherwise create one `IntersectionObserver` with
|
||
`rootMargin: '0px 0px -8% 0px'`; when an observed element intersects, add class `in` and
|
||
unobserve it. `destroy()` disconnects the observer.
|
||
4. **`resources/js/site/progress.js`** — exports `init()`/`destroy()`. Adds a passive `scroll`
|
||
listener on `window` that sets `.site-progress` `style.transform = scaleX(p)` where
|
||
`p = max > 0 ? scrollY / max : 1` and `max = document.documentElement.scrollHeight - innerHeight`,
|
||
and toggles class `scrolled` on `#site-nav` when `scrollY > 8` (if that element exists; it is
|
||
added in task 24-2). Runs once on init. `destroy()` removes the listener.
|
||
5. **`resources/views/partials/site-fonts.blade.php`** — exactly:
|
||
```html
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Markazi+Text:wght@400;500;600;700&family=IBM+Plex+Sans+Arabic:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||
```
|
||
6. **`lang/ar/site.php`** and **`lang/en/site.php`** — each `<?php return [ ... ];` with the
|
||
keys `progress_label` (ar `'تقدّم القراءة'`, en `'Reading progress'`). Later subtasks add keys
|
||
to these files.
|
||
|
||
### Files to modify
|
||
|
||
7. **`vite.config.js`** — `input` becomes
|
||
`['resources/css/app.css', 'resources/js/app.js', 'resources/css/public.css', 'resources/js/public.js']`.
|
||
8. **`resources/views/components/layouts/public.blade.php`**:
|
||
- After `@include('partials.head')` add `@include('partials.site-fonts')` and
|
||
`@vite(['resources/css/public.css', 'resources/js/public.js'])`.
|
||
- `<body>` becomes `<body class="site min-h-screen flex flex-col">` (remove `bg-background`
|
||
and the inline `style="font-family: …"`).
|
||
- Immediately after the skip link add
|
||
`<div class="site-progress" aria-hidden="true"></div>`.
|
||
- Remove the spacer `<div class="h-16"></div>` (the navigation becomes sticky in 24-2; until
|
||
then the existing fixed navigation may overlap the first 64px — acceptable only between
|
||
24-1 and 24-2).
|
||
- `<main>` keeps `id="main-content" role="main" class="flex-1" tabindex="-1"` but its child
|
||
`<div class="max-w-[1200px] mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">` is removed, so
|
||
`{{ $slot }}` is rendered directly inside `<main>`.
|
||
9. Because the wrapper is gone, wrap the root content of each existing public page that is not
|
||
yet redesigned in `<div class="wrap py-8">…</div>` so these pages keep a contained width until
|
||
their own subtask restyles them:
|
||
- `resources/views/livewire/pages/home.blade.php`
|
||
- `resources/views/livewire/pages/about.blade.php` (also remove the negative margins
|
||
`-mx-4 sm:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8` from its first `<section>`)
|
||
- `resources/views/livewire/pages/booking.blade.php`
|
||
- `resources/views/livewire/pages/booking-success.blade.php`
|
||
- `resources/views/livewire/pages/posts/index.blade.php`
|
||
- `resources/views/livewire/pages/posts/show.blade.php`
|
||
- `resources/views/pages/legal.blade.php`
|
||
- `resources/views/pages/terms.blade.php`
|
||
- `resources/views/pages/privacy.blade.php`
|
||
(`resources/views/pages/booking.blade.php` is a dead placeholder — `/booking` is served by the
|
||
Volt page — and is left untouched.)
|
||
For a Volt page whose root is a single element, put the wrapper inside the root element so the
|
||
component keeps one root.
|
||
10. **`resources/views/partials/head.blade.php`** — change
|
||
`<meta name="theme-color" content="#4A4A42">` to `<meta name="theme-color" content="#2D3624">`.
|
||
Change nothing else in this file (the dashboards share it).
|
||
|
||
Do not modify `resources/css/app.css` or any file under `resources/views/livewire/admin/`,
|
||
`resources/views/livewire/client/`, `resources/views/components/layouts/app*`.
|
||
|
||
## Acceptance Criteria
|
||
|
||
- [ ] `npm run build` succeeds and `public/build/manifest.json` contains entries for
|
||
`resources/css/public.css` and `resources/js/public.js`.
|
||
- [ ] Every selector in `resources/css/public.css` is scoped under `body.site` (or
|
||
`html[...] body.site`, or `html:has(body.site)`), verify:
|
||
`grep -nE '^[^@/ }].*\{' resources/css/public.css | grep -v 'body.site'` returns nothing.
|
||
- [ ] The HTML of `/` contains `<body class="site`, the `Markazi+Text` font link, a
|
||
`public.css` and a `public.js` asset, and `<div class="site-progress"`.
|
||
- [ ] The HTML of `/admin/dashboard` (logged in as admin) and `/client/dashboard` (logged in as a
|
||
client) contains neither `public.css`, `public.js`, `Markazi` nor `class="site`.
|
||
- [ ] `resources/css/app.css` is unchanged: `git diff --quiet BASE -- resources/css/app.css`.
|
||
- [ ] On `/` with the browser at 390px wide, the document has no horizontal scroll, and every
|
||
public page listed in item 9 still renders its content inside a 1200px-max container.
|
||
- [ ] Scrolling `/` changes the `.site-progress` element's transform from `scaleX(0)` toward
|
||
`scaleX(1)`; with `prefers-reduced-motion: reduce` emulated, every `[data-reveal]` element
|
||
is visible without scrolling.
|
||
- [ ] `partials/head.blade.php` contains `content="#2D3624"` and no `#4A4A42`.
|
||
|
||
## Sub-tasks
|
||
|
||
None.
|