diff --git a/docs/tasks/24-1.md b/docs/tasks/24-1.md index 3fdee7a..7a22bfb 100644 --- a/docs/tasks/24-1.md +++ b/docs/tasks/24-1.md @@ -1,7 +1,7 @@ --- id: 24-1 title: 24-1 Public Design Foundation (Stylesheet, Script, Fonts, Layout) -status: Ready for Dev +status: QA Review date: 2026-10-31 parent: 24 --- diff --git a/docs/tasks/statuses.md b/docs/tasks/statuses.md index f08753b..7d0fa82 100644 --- a/docs/tasks/statuses.md +++ b/docs/tasks/statuses.md @@ -137,7 +137,7 @@ | 22 | 22 Fix Footer Address - Add Missing Street Name | Done | | 23 | 23 Fix AdminNotificationService Carbon TypeError Masking Exceptions | Done | | 24 | 24 Public-Site Redesign (PRD 1.4) | Ready for Dev | -| 24-1 | 24-1 Public Design Foundation (Stylesheet, Script, Fonts, Layout) | Ready for Dev | +| 24-1 | 24-1 Public Design Foundation (Stylesheet, Script, Fonts, Layout) | QA Review | | 24-2 | 24-2 Navigation and Footer | Ready for Dev | | 24-3 | 24-3 Signature Balance Component | Ready for Dev | | 24-4 | 24-4 Home Page Sections (Hero to Latest Articles) | Ready for Dev | diff --git a/lang/ar/site.php b/lang/ar/site.php new file mode 100644 index 0000000..ec7f463 --- /dev/null +++ b/lang/ar/site.php @@ -0,0 +1,5 @@ + 'تقدّم القراءة', +]; diff --git a/lang/en/site.php b/lang/en/site.php new file mode 100644 index 0000000..ee7a2e8 --- /dev/null +++ b/lang/en/site.php @@ -0,0 +1,5 @@ + 'Reading progress', +]; diff --git a/resources/css/public.css b/resources/css/public.css new file mode 100644 index 0000000..d86e5e3 --- /dev/null +++ b/resources/css/public.css @@ -0,0 +1,89 @@ +/* Public-site redesign foundation (PRD 1.4, task 24-1). + Every selector is scoped under body.site so the dashboards are unaffected. + Reference: docs/ux-mockup.html */ + +/* ---------- Tokens ---------- */ +body.site{ + --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); +} +html:has(body.site){scroll-behavior:smooth} + +/* ---------- Ground and type ---------- */ +body.site{ + font-family:var(--body);color:var(--ink);font-size:17px;line-height:1.75; + background-color:var(--cream); + 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); +} +body.site img{max-width:100%;display:block} +body.site a{color:inherit} +body.site .wrap{max-width:1200px;margin-inline:auto;padding-inline:28px} +body.site .skip{position:absolute;inset-inline-start:12px;top:-60px;background:var(--forest);color:var(--cream);padding:10px 16px;border-radius:8px;z-index:100} +body.site .skip:focus{top:12px} +body.site :focus-visible{outline:2px solid var(--gold);outline-offset:3px;border-radius:4px} +body.site h1,body.site h2,body.site h3{font-family:var(--display);font-weight:700;line-height:1.25;margin:0;color:var(--ink)} +body.site .eyebrow{display:inline-flex;align-items:center;gap:10px;font-size:.82rem;font-weight:600;letter-spacing:0;color:var(--gold-ink)} +html[lang=en] body.site .eyebrow{letter-spacing:.16em;text-transform:uppercase;font-size:.74rem} +body.site .eyebrow::before{content:"";width:22px;height:2px;background:currentColor;border-radius:2px} +body.site .art-no{font-family:var(--display);font-size:1.15rem;font-weight:600;color:var(--gold-ink)} + + +/* ---------- Buttons ---------- */ +body.site .btn{position:relative;display:inline-flex;align-items:center;gap:10px;min-height:48px;padding:12px 26px;border-radius:999px; + font:600 1rem/1 var(--body);text-decoration:none;cursor:pointer;border:0;overflow:hidden;isolation:isolate; + transition:transform .25s var(--ease), box-shadow .25s var(--ease), background-color .25s} +body.site .btn-gold{background:var(--gold);color:var(--ink);box-shadow:0 1px 0 rgba(255,255,255,.35) inset, 0 8px 22px -8px rgba(166,137,102,.8)} +body.site .btn-gold::after{content:"";position:absolute;inset:0;z-index:-1; + background:linear-gradient(105deg,transparent 30%,rgba(255,255,255,.55) 50%,transparent 70%);transform:translateX(-120%);transition:transform .7s var(--ease)} +body.site .btn-gold:hover{transform:translateY(-2px);box-shadow:0 1px 0 rgba(255,255,255,.35) inset, 0 16px 34px -10px rgba(166,137,102,.95), 0 0 0 6px rgba(166,137,102,.14)} +body.site .btn-gold:hover::after{transform:translateX(120%)} +body.site .btn-ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 1.5px var(--forest)} +body.site .btn-ghost:hover{background:var(--forest);color:var(--cream);transform:translateY(-2px);box-shadow:inset 0 0 0 1.5px var(--forest), 0 12px 26px -12px rgba(45,54,36,.7)} +body.site .on-dark .btn-ghost{color:var(--cream);box-shadow:inset 0 0 0 1.5px rgba(244,241,234,.6)} +body.site .on-dark .btn-ghost:hover{background:var(--cream);color:var(--forest)} +body.site .btn .arr{transition:transform .25s var(--ease)} +body.site .btn:hover .arr{transform:translateX(-4px)} +html[dir=ltr] body.site .btn .arr{transform:scaleX(-1)} +html[dir=ltr] body.site .btn:hover .arr{transform:scaleX(-1) translateX(-4px)} + + +/* ---------- Scroll progress ---------- */ +body.site .site-progress{position:fixed;z-index:60;top:0;inset-inline:0;height:2px;pointer-events:none;transform:scaleX(0);transform-origin:right; + background:linear-gradient(90deg,#8A7555,#E2CDA4,#A68966)} +html[dir=ltr] body.site .site-progress{transform-origin:left} + +/* ---------- Sections ---------- */ +body.site section{position:relative} +body.site .sec{padding:120px 0} +body.site .sec-head{display:flex;align-items:end;justify-content:space-between;gap:32px;margin-bottom:54px;flex-wrap:wrap} +body.site .sec-head h2{font-size:clamp(2rem,3.4vw,3rem);margin-top:12px} +body.site .sec-head p{max-width:30em;color:var(--muted);margin:0} + + +/* ---------- Reveal (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} +body.site.js [data-reveal][data-d="1"]{transition-delay:.08s} +body.site.js [data-reveal][data-d="2"]{transition-delay:.16s} +body.site.js [data-reveal][data-d="3"]{transition-delay:.24s} +body.site.js [data-reveal][data-d="4"]{transition-delay:.32s} +body.site.js [data-reveal][data-d="5"]{transition-delay:.4s} + +/* ---------- Responsive base ---------- */ +@media (max-width:760px){ + body.site{font-size:16px} + body.site .wrap{padding-inline:18px} + body.site .sec{padding:80px 0} +} + +/* ---------- Reduced motion ---------- */ +@media (prefers-reduced-motion:reduce){ + body.site *, body.site *::before, body.site *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important} + body.site [data-reveal]{opacity:1;transform:none} +} diff --git a/resources/js/public.js b/resources/js/public.js new file mode 100644 index 0000000..39bc672 --- /dev/null +++ b/resources/js/public.js @@ -0,0 +1,13 @@ +import * as reveal from './site/reveal.js'; +import * as progress from './site/progress.js'; + +const modules = [reveal, progress]; + +document.addEventListener('livewire:navigated', () => { + document.body.classList.add('js'); + modules.forEach((m) => m.init()); +}); + +document.addEventListener('livewire:navigating', () => { + modules.forEach((m) => m.destroy()); +}); diff --git a/resources/js/site/progress.js b/resources/js/site/progress.js new file mode 100644 index 0000000..0c79105 --- /dev/null +++ b/resources/js/site/progress.js @@ -0,0 +1,22 @@ +let onScroll = null; + +export function init() { + const bar = document.querySelector('.site-progress'); + const nav = document.getElementById('site-nav'); + + onScroll = () => { + const max = document.documentElement.scrollHeight - window.innerHeight; + const p = max > 0 ? window.scrollY / max : 1; + + if (bar) bar.style.transform = `scaleX(${p})`; + nav?.classList.toggle('scrolled', window.scrollY > 8); + }; + + window.addEventListener('scroll', onScroll, { passive: true }); + onScroll(); +} + +export function destroy() { + if (onScroll) window.removeEventListener('scroll', onScroll); + onScroll = null; +} diff --git a/resources/js/site/reveal.js b/resources/js/site/reveal.js new file mode 100644 index 0000000..69e5257 --- /dev/null +++ b/resources/js/site/reveal.js @@ -0,0 +1,26 @@ +let observer = null; + +export function init() { + const items = document.querySelectorAll('[data-reveal]'); + + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + items.forEach((el) => el.classList.add('in')); + return; + } + + observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('in'); + observer.unobserve(entry.target); + } + }); + }, { rootMargin: '0px 0px -8% 0px' }); + + items.forEach((el) => observer.observe(el)); +} + +export function destroy() { + observer?.disconnect(); + observer = null; +} diff --git a/resources/views/components/layouts/public.blade.php b/resources/views/components/layouts/public.blade.php index 49f19d6..1d89dbd 100644 --- a/resources/views/components/layouts/public.blade.php +++ b/resources/views/components/layouts/public.blade.php @@ -2,22 +2,20 @@
@include('partials.head') + @include('partials.site-fonts') + @vite(['resources/css/public.css', 'resources/js/public.js']) - + {{ __('accessibility.skip_to_content') }} +{{ __('Privacy policy content will be added here.') }}
diff --git a/resources/views/pages/terms.blade.php b/resources/views/pages/terms.blade.php index 9bd6e49..0634821 100644 --- a/resources/views/pages/terms.blade.php +++ b/resources/views/pages/terms.blade.php @@ -1,5 +1,5 @@{{ __('Terms of service content will be added here.') }}
diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index 9da9338..69b0c67 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -10,7 +10,7 @@ - + diff --git a/resources/views/partials/site-fonts.blade.php b/resources/views/partials/site-fonts.blade.php new file mode 100644 index 0000000..20bc16d --- /dev/null +++ b/resources/views/partials/site-fonts.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/tests/Feature/Design/PublicFoundationTest.php b/tests/Feature/Design/PublicFoundationTest.php new file mode 100644 index 0000000..d369c66 --- /dev/null +++ b/tests/Feature/Design/PublicFoundationTest.php @@ -0,0 +1,57 @@ +toBeTrue(); + + $lines = preg_grep('/^[^@\/ }].*\{/', file($path, FILE_IGNORE_NEW_LINES)); + $unscoped = array_filter($lines, fn ($l) => ! str_contains($l, 'body.site') && ! str_contains($l, 'html:has(body.site)')); + + expect($unscoped)->toBe([]); +}); + +test('public script modules exist', function () { + foreach (['public.js', 'site/reveal.js', 'site/progress.js'] as $f) { + expect(file_exists(resource_path("js/$f")))->toBeTrue(); + } +}); + +test('vite config lists the public entries', function () { + $cfg = file_get_contents(base_path('vite.config.js')); + expect($cfg)->toContain('resources/css/public.css')->toContain('resources/js/public.js'); +}); + +test('home page uses the site foundation', function () { + $html = $this->get('/')->assertOk()->getContent(); + + expect($html) + ->toContain('admin()->create(), '/admin/dashboard'], [User::factory()->individual()->create(), '/client/dashboard']] as [$user, $url]) { + $html = $this->actingAs($user)->get($url)->assertOk()->getContent(); + + expect($html) + ->not->toContain('public.css') + ->not->toContain('public.js') + ->not->toContain('Markazi') + ->not->toContain('class="site'); + } +}); + +test('head theme colour is the forest green', function () { + $head = file_get_contents(resource_path('views/partials/head.blade.php')); + expect($head)->toContain('content="#2D3624"')->not->toContain('#4A4A42'); +}); + +test('site lang files carry the progress label', function () { + expect(trans('site.progress_label', [], 'en'))->toBe('Reading progress'); + expect(trans('site.progress_label', [], 'ar'))->toBe('تقدّم القراءة'); +}); diff --git a/vite.config.js b/vite.config.js index f65249e..0b7a614 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,7 +7,7 @@ import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], + input: ['resources/css/app.css', 'resources/js/app.js', 'resources/css/public.css', 'resources/js/public.js'], refresh: true, }), tailwindcss(),