From 4569abc351844b0dbc7ffb244bb7d86fcf7a4415 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Thu, 2 Jul 2026 20:00:30 +0100 Subject: [PATCH 1/2] Add command to reprocess existing media with correct aspect-ratio scaling ProcessMedia was using resize(), which distorts images that aren't the target aspect ratio; scale() preserves it. Existing medium/small variants generated before this fix need regenerating, so add an artisan command (with --dry-run) to do that, plus feature tests. Co-Authored-By: Claude Sonnet 5 --- app/Console/Commands/ReprocessMediaImages.php | 68 ++++++++++++ app/Jobs/ProcessMedia.php | 4 +- .../Console/ReprocessMediaImagesTest.php | 105 ++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/ReprocessMediaImages.php create mode 100644 tests/Feature/Console/ReprocessMediaImagesTest.php diff --git a/app/Console/Commands/ReprocessMediaImages.php b/app/Console/Commands/ReprocessMediaImages.php new file mode 100644 index 00000000..299f0103 --- /dev/null +++ b/app/Console/Commands/ReprocessMediaImages.php @@ -0,0 +1,68 @@ +whereNotNull('image_widths') + ->where('image_widths', '>', 1000) + ->get(); + + $dryRun = $this->option('dry-run'); + + $this->info("Found {$media->count()} images to reprocess." . ($dryRun ? ' (dry run)' : '')); + + foreach ($media as $item) { + $path = $item->path; + + if (! Storage::disk('public')->exists($path)) { + $this->warn("{$path}: original not found on public disk, skipping."); + continue; + } + + if ($dryRun) { + $this->line("{$path} ({$item->image_widths}px wide)"); + continue; + } + + $this->info("Processing: {$path}"); + + try { + $file = Storage::disk('public')->get($path); + $image = $manager->read($file); + } catch (DecoderException) { + $this->warn(" Could not decode image, skipping."); + continue; + } + + $filenameParts = explode('.', $path); + $extension = array_pop($filenameParts); + $basename = trim(implode('.', $filenameParts), '.'); + + $medium = $image->scale(width: 1000); + Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); + + $small = $image->scale(width: 500); + Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); + + $this->info(" Done."); + } + + $this->info('Reprocessing complete.' . ($dryRun ? ' (dry run — no files were changed)' : '')); + } +} diff --git a/app/Jobs/ProcessMedia.php b/app/Jobs/ProcessMedia.php index e9e291a6..f9d8af50 100644 --- a/app/Jobs/ProcessMedia.php +++ b/app/Jobs/ProcessMedia.php @@ -56,10 +56,10 @@ class ProcessMedia implements ShouldQueue // foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar $basename = trim(implode('.', $filenameParts), '.'); - $medium = $image->resize(width: 1000); + $medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); - $small = $image->resize(width: 500); + $small = $image->scale(width: 500); Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode()); } diff --git a/tests/Feature/Console/ReprocessMediaImagesTest.php b/tests/Feature/Console/ReprocessMediaImagesTest.php new file mode 100644 index 00000000..eeec5a6f --- /dev/null +++ b/tests/Feature/Console/ReprocessMediaImagesTest.php @@ -0,0 +1,105 @@ +put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); + + Media::factory()->create([ + 'path' => 'media/test-image.jpg', + 'image_widths' => 4592, + ]); + + $this->artisan('app:reprocess-media-images', ['--dry-run' => true]) + ->expectsOutputToContain('Found 1 images to reprocess. (dry run)') + ->expectsOutputToContain('media/test-image.jpg (4592px wide)') + ->assertSuccessful(); + + Storage::disk('public')->assertMissing('media/test-image-medium.jpg'); + Storage::disk('public')->assertMissing('media/test-image-small.jpg'); + + Storage::disk('public')->delete('media/test-image.jpg'); + } + + #[Test] + public function missing_original_file_is_skipped(): void + { + Media::factory()->create([ + 'path' => 'media/does-not-exist.jpg', + 'image_widths' => 2000, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('media/does-not-exist.jpg: original not found on public disk, skipping.') + ->assertSuccessful(); + } + + #[Test] + public function non_decodable_image_is_skipped(): void + { + Storage::disk('public')->put('media/not-an-image.jpg', 'This is not an image'); + + Media::factory()->create([ + 'path' => 'media/not-an-image.jpg', + 'image_widths' => 2000, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('Could not decode image, skipping.') + ->assertSuccessful(); + + Storage::disk('public')->assertMissing('media/not-an-image-medium.jpg'); + Storage::disk('public')->assertMissing('media/not-an-image-small.jpg'); + + Storage::disk('public')->delete('media/not-an-image.jpg'); + } + + #[Test] + public function large_images_are_rescaled_into_medium_and_small_variants(): void + { + Storage::disk('public')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); + + Media::factory()->create([ + 'path' => 'media/test-image.jpg', + 'image_widths' => 4592, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('Processing: media/test-image.jpg') + ->assertSuccessful(); + + Storage::disk('public')->assertExists('media/test-image-medium.jpg'); + Storage::disk('public')->assertExists('media/test-image-small.jpg'); + + Storage::disk('public')->delete('media/test-image.jpg'); + Storage::disk('public')->delete('media/test-image-medium.jpg'); + Storage::disk('public')->delete('media/test-image-small.jpg'); + } + + #[Test] + public function images_with_width_at_or_below_threshold_are_not_processed(): void + { + Media::factory()->create([ + 'path' => 'media/small-image.jpg', + 'image_widths' => 1000, + ]); + + $this->artisan('app:reprocess-media-images') + ->expectsOutputToContain('Found 0 images to reprocess.') + ->assertSuccessful(); + } +} -- 2.55.0 From 11e2c469cb89f5350f176da75e7288ffd3e116f7 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Thu, 2 Jul 2026 20:02:49 +0100 Subject: [PATCH 2/2] Fix images overflowing on mobile by preserving aspect ratio max-width: 100% alone lets img/picture height stay at intrinsic size, so on narrow viewports images could still overflow vertically or look squashed. Adding height: auto keeps the aspect ratio correct as width scales down. Co-Authored-By: Claude Sonnet 5 --- public/assets/css/app.css | 2 +- public/assets/css/app.css.br | Bin 1211 -> 1216 bytes public/assets/css/app.css.map | 2 +- public/assets/css/app.css.zst | Bin 1466 -> 1463 bytes resources/css/reset.css | 1 + 5 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/assets/css/app.css b/public/assets/css/app.css index c298590d..6b2bddd5 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1,2 +1,2 @@ -@layer reset{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin-block-end:0}ul[role=list],ol[role=list]{list-style:none}body{line-height:1.5}h1,h2,h3,h4,button,input,label{line-height:1.1}h1,h2,h3,h4{text-wrap:balance}a:not([class]){text-decoration-skip-ink:auto;color:currentColor}img,picture{max-width:100%;display:block}input,button,textarea,select{font-family:inherit;font-size:inherit}textarea:not([rows]){min-height:10em}:target{scroll-margin-block:5ex}@media (prefers-reduced-motion){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}}@layer base{:root{color-scheme:light dark;--primary-hue:307;--clr-text:light-dark(oklch(5% .1 var(--primary-hue)),oklch(100% .1 var(--primary-hue)));--clr-background:light-dark(oklch(100% .1 var(--primary-hue)),oklch(15% .15 var(--primary-hue)));--clr-border:light-dark(oklch(90% .2 var(--primary-hue)),oklch(25% .1 var(--primary-hue)));--clr-snow-fall:light-dark(oklch(25% .15 var(--primary-hue)),oklch(85% .2 var(--primary-hue)))}body{background-color:var(--clr-background);color:var(--clr-text)}:root{--border-radius:8px}html{font-size:125%}body{grid-template-rows:min-content 1fr min-content;grid-template-columns:1fr min(80ch,80vw) 1fr;padding-inline:4vw;display:grid;&>header{grid-column:1/-1}&>main{grid-column:2/3}&>footer{grid-column:1/-1;margin-block-start:2ex;& search form{flex-direction:row;align-items:center;gap:1ex;display:flex;& input{min-width:0}}}}.h-feed{flex-direction:column;gap:2ex;display:flex}}@layer components{body>header{grid-template-rows:auto auto;grid-template-columns:auto 1fr auto;align-items:baseline;gap:1rem;display:grid;& a{text-decoration:none}& h1{text-wrap:nowrap;margin:0}& nav{flex-direction:row;gap:.5rem;display:flex;@media screen and (width<=1100px){flex-wrap:wrap;grid-area:2/1/auto/span 3}}}.note{border:1px solid var(--clr-border);border-radius:var(--border-radius);flex-direction:column;padding:1ex 2ex;display:flex;& .e-content{&>p:first-child{margin-block-start:0}& .u-photo{width:100%;height:auto}}& .syndication-links{flex-direction:row;gap:1ex;display:flex;& svg{border-radius:4px;width:auto;height:1lh}}}.reply-to{border:1px solid var(--clr-border);border-top-left-radius:var(--border-radius);border-top-right-radius:var(--border-radius);border-bottom:none;margin-inline:2ex;padding-inline:1ex;font-size:85%}.pagination{flex-direction:row;justify-content:center;align-items:center;display:flex;& div{flex-direction:row;gap:1ex;display:flex}}#theme-selector{& button{appearance:none;background:0 0;border:none;&:hover{cursor:pointer}}& #theme-selector-dropdown{position-area:block-end span-inline-start;border:2px solid var(--clr-border);background-color:var(--clr-background);color:var(--clr-text);margin:0;position:absolute;& fieldset{border:0;flex-direction:row;gap:1ch;display:flex;& input{display:none}}}@media (prefers-reduced-motion:no-preference){& #theme-selector-dropdown{&:popover-open{opacity:1;transform:translateY(0)scale(1);@starting-style{opacity:0;transform:translateY(30px)scale(0)}}opacity:0;transition:transform, opacity, display allow-discrete, overlay allow-discrete;transform-origin:100% 0;transition-duration:.5s;transform:translateY(0)scale(0)}}}::view-transition-group(changing-theme){animation-duration:1.25s}::view-transition-new(changing-theme),::view-transition-old(changing-theme){mix-blend-mode:normal}::view-transition-new(changing-theme){animation-name:reveal}::view-transition-old(changing-theme){animation:none}@keyframes reveal{0%{clip-path:polygon(-30% 0,-30% 0,-15% 100%,-10% 115%)}to{clip-path:polygon(-30% 0,130% 0,115% 100%,-10% 115%)}}} +@layer reset{*,:before,:after{box-sizing:border-box}html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin-block-end:0}ul[role=list],ol[role=list]{list-style:none}body{line-height:1.5}h1,h2,h3,h4,button,input,label{line-height:1.1}h1,h2,h3,h4{text-wrap:balance}a:not([class]){text-decoration-skip-ink:auto;color:currentColor}img,picture{max-width:100%;height:auto;display:block}input,button,textarea,select{font-family:inherit;font-size:inherit}textarea:not([rows]){min-height:10em}:target{scroll-margin-block:5ex}@media (prefers-reduced-motion){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}}@layer base{:root{color-scheme:light dark;--primary-hue:307;--clr-text:light-dark(oklch(5% .1 var(--primary-hue)),oklch(100% .1 var(--primary-hue)));--clr-background:light-dark(oklch(100% .1 var(--primary-hue)),oklch(15% .15 var(--primary-hue)));--clr-border:light-dark(oklch(90% .2 var(--primary-hue)),oklch(25% .1 var(--primary-hue)));--clr-snow-fall:light-dark(oklch(25% .15 var(--primary-hue)),oklch(85% .2 var(--primary-hue)))}body{background-color:var(--clr-background);color:var(--clr-text)}:root{--border-radius:8px}html{font-size:125%}body{grid-template-rows:min-content 1fr min-content;grid-template-columns:1fr min(80ch,80vw) 1fr;padding-inline:4vw;display:grid;&>header{grid-column:1/-1}&>main{grid-column:2/3}&>footer{grid-column:1/-1;margin-block-start:2ex;& search form{flex-direction:row;align-items:center;gap:1ex;display:flex;& input{min-width:0}}}}.h-feed{flex-direction:column;gap:2ex;display:flex}}@layer components{body>header{grid-template-rows:auto auto;grid-template-columns:auto 1fr auto;align-items:baseline;gap:1rem;display:grid;& a{text-decoration:none}& h1{text-wrap:nowrap;margin:0}& nav{flex-direction:row;gap:.5rem;display:flex;@media screen and (width<=1100px){flex-wrap:wrap;grid-area:2/1/auto/span 3}}}.note{border:1px solid var(--clr-border);border-radius:var(--border-radius);flex-direction:column;padding:1ex 2ex;display:flex;& .e-content{&>p:first-child{margin-block-start:0}& .u-photo{width:100%;height:auto}}& .syndication-links{flex-direction:row;gap:1ex;display:flex;& svg{border-radius:4px;width:auto;height:1lh}}}.reply-to{border:1px solid var(--clr-border);border-top-left-radius:var(--border-radius);border-top-right-radius:var(--border-radius);border-bottom:none;margin-inline:2ex;padding-inline:1ex;font-size:85%}.pagination{flex-direction:row;justify-content:center;align-items:center;display:flex;& div{flex-direction:row;gap:1ex;display:flex}}#theme-selector{& button{appearance:none;background:0 0;border:none;&:hover{cursor:pointer}}& #theme-selector-dropdown{position-area:block-end span-inline-start;border:2px solid var(--clr-border);background-color:var(--clr-background);color:var(--clr-text);margin:0;position:absolute;& fieldset{border:0;flex-direction:row;gap:1ch;display:flex;& input{display:none}}}@media (prefers-reduced-motion:no-preference){& #theme-selector-dropdown{&:popover-open{opacity:1;transform:translateY(0)scale(1);@starting-style{opacity:0;transform:translateY(30px)scale(0)}}opacity:0;transition:transform, opacity, display allow-discrete, overlay allow-discrete;transform-origin:100% 0;transition-duration:.5s;transform:translateY(0)scale(0)}}}::view-transition-group(changing-theme){animation-duration:1.25s}::view-transition-new(changing-theme),::view-transition-old(changing-theme){mix-blend-mode:normal}::view-transition-new(changing-theme){animation-name:reveal}::view-transition-old(changing-theme){animation:none}@keyframes reveal{0%{clip-path:polygon(-30% 0,-30% 0,-15% 100%,-10% 115%)}to{clip-path:polygon(-30% 0,130% 0,115% 100%,-10% 115%)}}} /*# sourceMappingURL=/assets/css/app.css.map */ diff --git a/public/assets/css/app.css.br b/public/assets/css/app.css.br index d3c7dbc92b69d353c9d7d40681721153dfa91bd3..9c004c03115b5d08c6ac093ab2bd0ff483942c5f 100644 GIT binary patch literal 1216 zcmX>oP|ol`rr>tsq^Y*WObd$-#jjs)7@WGTeC8(^@0~(7mZnzD(@3?tdB}3EHv7BF zIcu(jaPD_FZ6>?tP2!|atG90V(3$Vy8I}_7|6c8~apH|X3Not}Dy&Er`*qto_gs#_ z+VdiuF7~E*lNb6c<*qdFuzJz>Wl^Bf({8rC$-AUp-}?AhJe<$_XV#als#Ld0-}P3R z$SX~&(e9g`A%8z()yJjNvV2wjf=xU@T9m47n0;buy^OUS8r z+?#ItbQYKcTp{u&_j>1jc=EDtYQvax+n7Gos9=LINvG#>~$;r zcWl}lQzr*|{0))zfzI4JOaT?V^3eU1U~w9$Xx{Dey`A?5!^8PZA&S-%zw&diQSkR*v|C z_pZKJvHjW#17G#LOs?jef>SvknC|QJZg}m$6LNmG=o$Gp;h(Fnt&2GIV5efD#Ggy^ zoq5l%6L8qBxM0nx;^R+_1Wu5e_-%&qgt?2ldyYI#zWH}ile6ou;4d#+idExdf;gA1 zd>Lf6nD=(0m`?7Uy`1y2wL)sTXDF_)`uG05qS`JQr&{)(4yO!sxVs}3U%tP`poM{T zuB$K0akVFU3octqoZ1pD9r^!-TSG~bjbG6D@-@e27{BX$sMPU<<@m-GGd(1JhbAVw z`Q_|sJo{3n^tJE-s}B<&>NzSuQ>f-Cuz%uWP49R9GSFzC{oFHqkH^0)-^;S{nS#J; zURlYThihAX{w!ZCeQ%9_CHuTjU2{&I)p(Q7Rh;K)^Ivl75{KD>Nrzk97+)_cwKmxl zQ&-h4_Fe4EmPdI$JNWeDyiP`MYvq@{d{P4i8Bs9qW;~l}?KU-$`?b*?HeYHl`pZ9+EnYKEq-1IMf`X*dn qqwc}B-VEX2ceu4Gc6eIf-}vd*>%gD(b3aa;nlQK3&wh$x04o46N?Tz7 literal 1211 zcmX@Op^PD6$+@hvJ73J2^)*k+F7rX#)P~J6V#}BPj5(<;A{5hMJ=@kN=->X|3#V4p zXsbqket-IO{^vtvYw4wuMPa-(4$sOr-W0 zH~Zej@lkWE_q{%J=3&P^wk+3uD;Yv#cPm`3;W+bg+8jT_JNp>#-dye#zH{Er>z?I7 zCC57|N_Wkx%2H}M@Kun*ySJ_Xa{ZgTajCxp$7k!|Py^go5aT+3)T8%u`duT&R46)7tfrN0i>wZ0dxmVE3z zYc)4VnTG$g*(Y%}Kip@AWaSB=_j8z( zat<5C{5U-4&a_M?F7`Y5j$F$`6pbEpK5%AitLcx?oco8Va8>zNZ;S00MfiS9@OagK zTrxGStu3WQ&}+9qo9+vK$I0xLvR@gPzKOr+W4!+CB6H5NyOB@Vw*8-8ILBY(Wi2SZchSBi@3PghyY$?&Z5a!mN+P39XS*tWzxTA{Tg zqjFtc)-BCKpBnMTH-Sg)hS|+sd3EuOOMC_j20U3`cN|~7WJOK0@uO4n28r`;IfzO< zsA{(6>%6z+tY1x8oBpA&JBjCxKbd^9d_tT&nUwORl7<(sc8S|>Pv?@iAv zj_@BMx0rO&^R;Y!q)U!gF=tQHD!G;Ibm>#QlenIpFx&dPWy}6ES=CFHe|+JzawS89J;jMnMW@Dh^f1dQ$ z_v3A)7~iY4KGykGbNt_xp#IzMV_g_7GPJ+BqB!x-t8mu#juQ`^ebyD(x&8g~Bt*9A zw4R=y&dfxxEH?`&C!G^O+b2h(-&M0u_agzSAH}}Z~twhaQ2jz1;rbpYQ zZnVm-?LYlx%BhDk@11YwOvsP7D7e$yZ0I4SfQk%mpUWq z(4Izv1?@>c%=)hVmrcBP;^E8vmsV^@_li%6V zpFgxL71Od170kFKa7F*Df2yB~?gyKEb9%Gp)rUZa-M9F8 z8*9!#TX!e?(;C$|<(7Tl@E&>Ku_<$$YHf=Jpb0K_#m>Pd{AZv3`}J|M|GY zx{3Mc<3hfkPpP|*_DXoks|$%;m$&wIx0wFB{l&Xtt%B#rb*Dt+YZp9n)``xZy~TRQ j`>w0o6W{zfJte$up3<}B`qo_{XVzXY+CNih^&>t2x64|6 diff --git a/public/assets/css/app.css.map b/public/assets/css/app.css.map index 768c0a72..6f88e2f2 100644 --- a/public/assets/css/app.css.map +++ b/public/assets/css/app.css.map @@ -1 +1 @@ -{"version":3,"mappings":"AACA,aCGE,uCAOA,oFASA,8DAMA,4CAMA,qBAKA,+CAMA,8BAMA,gEAMA,yCAOA,mEAOA,qCAKA,gCAKA,gCACE,wGD/EJ,YEAE,oaAwBA,kECxBA,0BAIA,oBAIA,iIAME,0BAIA,uBAIA,iDAIE,yEAME,uBAON,oDHvCF,kBIAE,wHAOE,yBAIA,+BAKA,gDAKE,kCAAqC,2CCrBzC,+HAOE,aACE,qCAIA,mCAMF,6DAKE,gDAQJ,4ME/BA,sFAME,+CDNF,gBACE,oDAKE,wBAMF,gMAQE,4DAME,uBAUJ,8CACE,2BACE,yDAQE,gBAAiB,8CAST,0KAahB,iEAIA,kGAKA,4DAIA,qDAIA","sources":["resources/css/app.css","resources/css/colours.css","resources/css/layout.css","resources/css/reset.css","resources/css/pagination.css","resources/css/header.css","resources/css/notes.css","resources/css/theme-selector.css"],"sourcesContent":["/* First thing we need to do is declare our cascade layers */\n@layer reset, base, components;\n\n@import url('reset.css');\n@import url('colours.css');\n@import url('layout.css');\n@import url('header.css');\n@import url('notes.css');\n@import url('pagination.css');\n@import url('theme-selector.css');\n","@layer base {\n :root {\n color-scheme: light dark;\n\n --primary-hue: 307;\n --clr-text: light-dark(\n oklch(5% 0.1 var(--primary-hue)),\n oklch(100% 0.1 var(--primary-hue))\n );\n --clr-background: light-dark(\n oklch(100% 0.1 var(--primary-hue)),\n oklch(15% 0.15 var(--primary-hue))\n );\n --clr-border: light-dark(\n oklch(90% 0.2 var(--primary-hue)),\n oklch(25% 0.1 var(--primary-hue))\n );\n\n /* Adding snow fall colour whilst we are using it */\n --clr-snow-fall: light-dark(\n oklch(25% 0.15 var(--primary-hue)),\n oklch(85% 0.2 var(--primary-hue))\n );\n }\n\n body {\n background-color: var(--clr-background);\n color: var(--clr-text);\n }\n}\n","@layer base {\n :root {\n --border-radius: 8px;\n }\n\n html {\n font-size: 125%;\n }\n\n body {\n display: grid;\n grid-template-columns: 1fr min(80ch, 80vw) 1fr;\n grid-template-rows: min-content 1fr min-content;\n padding-inline: 4vw;\n\n > header {\n grid-column: 1/-1;\n }\n\n > main {\n grid-column: 2/3;\n }\n\n > footer {\n grid-column: 1/-1;\n margin-block-start: 2ex;\n\n search form {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 1ex;\n\n input {\n min-width: 0;\n }\n }\n }\n }\n\n .h-feed {\n display: flex;\n flex-direction: column;\n gap: 2ex;\n }\n}\n","@layer reset {\n /* Sourced from https://piccalil.li/blog/a-more-modern-css-reset/ */\n\n /* Box sizing rules */\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n /* Prevent font size inflation */\n html {\n /* stylelint-disable property-no-vendor-prefix */\n -moz-text-size-adjust: none;\n -webkit-text-size-adjust: none;\n /* stylelint-enable property-no-vendor-prefix */\n text-size-adjust: none;\n }\n\n /* Remove default margin in favour of better control in authored CSS */\n body, h1, h2, h3, h4, p,\n figure, blockquote, dl, dd {\n margin-block-end: 0;\n }\n\n /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */\n ul[role='list'],\n ol[role='list'] {\n list-style: none;\n }\n\n /* Set core body defaults */\n body {\n line-height: 1.5;\n }\n\n /* Set shorter line heights on headings and interactive elements */\n h1, h2, h3, h4,\n button, input, label {\n line-height: 1.1;\n }\n\n /* Balance text wrapping on headings */\n h1, h2,\n h3, h4 {\n text-wrap: balance;\n }\n\n /* A elements that don't have a class get default styles */\n a:not([class]) {\n text-decoration-skip-ink: auto;\n color: currentcolor;\n }\n\n /* Make images easier to work with */\n img,\n picture {\n max-width: 100%;\n display: block;\n }\n\n /* Inherit fonts for inputs and buttons */\n input, button,\n textarea, select {\n font-family: inherit;\n font-size: inherit;\n }\n\n /* Make sure textareas without a rows attribute are not tiny */\n textarea:not([rows]) {\n min-height: 10em;\n }\n\n /* Anything that has been anchored to should have extra scroll margin */\n :target {\n scroll-margin-block: 5ex;\n }\n\n /* Disable view transition animations for users who don’t want them */\n @media (prefers-reduced-motion) {\n ::view-transition-group(*),\n ::view-transition-old(*),\n ::view-transition-new(*) {\n animation: none !important;\n }\n }\n}\n","@layer components {\n .pagination {\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: center;\n\n div {\n display: flex;\n flex-direction: row;\n gap: 1ex;\n }\n }\n}\n","@layer components {\n body > header {\n display: grid;\n grid-template-columns: auto 1fr auto;\n grid-template-rows: auto auto;\n align-items: baseline;\n gap: 1rem;\n\n a {\n text-decoration: none;\n }\n\n h1 {\n margin: 0;\n text-wrap: nowrap;\n }\n\n nav {\n display: flex;\n flex-direction: row;\n gap: .5rem;\n\n @media screen and (width <= 1100px) {\n grid-row: 2;\n grid-column: 1 / span 3;\n flex-wrap: wrap;\n }\n }\n }\n}\n","@layer components {\n .note {\n display: flex;\n flex-direction: column;\n border: 1px solid var(--clr-border);\n border-radius: var(--border-radius);\n padding: 1ex 2ex;\n\n .e-content {\n > p:first-child {\n margin-block-start: 0;\n }\n\n .u-photo {\n width: 100%;\n height: auto;\n }\n }\n\n .syndication-links {\n display: flex;\n flex-direction: row;\n gap: 1ex;\n\n svg {\n border-radius: 4px;\n width: auto;\n height: 1lh;\n }\n }\n }\n\n .reply-to {\n font-size: 85%;\n margin-inline: 2ex;\n padding-inline: 1ex;\n border: 1px solid var(--clr-border);\n border-bottom: none;\n border-top-left-radius: var(--border-radius);\n border-top-right-radius: var(--border-radius);\n }\n}\n","@layer components {\n #theme-selector {\n button {\n appearance: none;\n border: none;\n background: none;\n\n &:hover {\n cursor: pointer;\n }\n }\n\n /* This is the element with the [popover] attribute */\n #theme-selector-dropdown {\n position: absolute;\n position-area: block-end span-inline-start;\n margin: 0;\n border: 2px solid var(--clr-border);\n background-color: var(--clr-background);\n color: var(--clr-text);\n\n fieldset {\n border: 0;\n display: flex;\n flex-direction: row;\n gap: 1ch;\n\n input {\n display: none;\n }\n }\n }\n\n /*\n * This is the element with the [popover] attribute\n * Here we are styling the open and closing animations\n */\n @media (prefers-reduced-motion: no-preference) {\n #theme-selector-dropdown {\n &:popover-open {\n transform: translateY(0) scale(1);\n opacity: 1;\n\n /*\n * Start styles for the opening transition.\n * Added to :popover-open, but after the opened styles.\n */\n @starting-style {\n transform: translateY(30px) scale(0);\n opacity: 0;\n }\n }\n\n /*\n * End styles for the closing transition.\n */\n transform: translateY(0) scale(0);\n opacity: 0;\n\n /*\n * Enumerate transitioning properties, including display and overlay.\n */\n transition: transform, opacity, display allow-discrete, overlay allow-discrete;\n transition-duration: 0.5s;\n transform-origin: top right;\n }\n }\n }\n\n ::view-transition-group(changing-theme) {\n animation-duration: 1.25s;\n }\n\n ::view-transition-new(changing-theme),\n ::view-transition-old(changing-theme) {\n mix-blend-mode: normal;\n }\n\n ::view-transition-new(changing-theme) {\n animation-name: reveal;\n }\n\n ::view-transition-old(changing-theme) {\n animation: none;\n }\n\n @keyframes reveal {\n from {\n clip-path: polygon(-30% 0, -30% 0, -15% 100%, -10% 115%);\n }\n\n to {\n clip-path: polygon(-30% 0, 130% 0, 115% 100%, -10% 115%);\n }\n }\n}\n"],"names":[]} \ No newline at end of file +{"version":3,"mappings":"AACA,aCGE,uCAOA,oFASA,8DAMA,4CAMA,qBAKA,+CAMA,8BAMA,gEAMA,qDAQA,mEAOA,qCAKA,gCAKA,gCACE,wGDhFJ,YEAE,oaAwBA,kEKxBA,0BAIA,oBAIA,iIAME,0BAIA,uBAIA,iDAIE,yEAME,uBAON,oDPvCF,kBGAE,wHAOE,yBAIA,+BAKA,gDAKE,kCAAqC,2CErBzC,+HAOE,aACE,qCAIA,mCAMF,6DAKE,gDAQJ,4MD/BA,sFAME,+CENF,gBACE,oDAKE,wBAMF,gMAQE,4DAME,uBAUJ,8CACE,2BACE,yDAQE,gBAAiB,8CAST,0KAahB,iEAIA,kGAKA,4DAIA,qDAIA","sources":["resources/css/app.css","resources/css/reset.css","resources/css/colours.css","resources/css/header.css","resources/css/pagination.css","resources/css/theme-selector.css","resources/css/layout.css","resources/css/notes.css"],"sourcesContent":["/* First thing we need to do is declare our cascade layers */\n@layer reset, base, components;\n\n@import url('reset.css');\n@import url('colours.css');\n@import url('layout.css');\n@import url('header.css');\n@import url('notes.css');\n@import url('pagination.css');\n@import url('theme-selector.css');\n","@layer reset {\n /* Sourced from https://piccalil.li/blog/a-more-modern-css-reset/ */\n\n /* Box sizing rules */\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n /* Prevent font size inflation */\n html {\n /* stylelint-disable property-no-vendor-prefix */\n -moz-text-size-adjust: none;\n -webkit-text-size-adjust: none;\n /* stylelint-enable property-no-vendor-prefix */\n text-size-adjust: none;\n }\n\n /* Remove default margin in favour of better control in authored CSS */\n body, h1, h2, h3, h4, p,\n figure, blockquote, dl, dd {\n margin-block-end: 0;\n }\n\n /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */\n ul[role='list'],\n ol[role='list'] {\n list-style: none;\n }\n\n /* Set core body defaults */\n body {\n line-height: 1.5;\n }\n\n /* Set shorter line heights on headings and interactive elements */\n h1, h2, h3, h4,\n button, input, label {\n line-height: 1.1;\n }\n\n /* Balance text wrapping on headings */\n h1, h2,\n h3, h4 {\n text-wrap: balance;\n }\n\n /* A elements that don't have a class get default styles */\n a:not([class]) {\n text-decoration-skip-ink: auto;\n color: currentcolor;\n }\n\n /* Make images easier to work with */\n img,\n picture {\n max-width: 100%;\n height: auto;\n display: block;\n }\n\n /* Inherit fonts for inputs and buttons */\n input, button,\n textarea, select {\n font-family: inherit;\n font-size: inherit;\n }\n\n /* Make sure textareas without a rows attribute are not tiny */\n textarea:not([rows]) {\n min-height: 10em;\n }\n\n /* Anything that has been anchored to should have extra scroll margin */\n :target {\n scroll-margin-block: 5ex;\n }\n\n /* Disable view transition animations for users who don’t want them */\n @media (prefers-reduced-motion) {\n ::view-transition-group(*),\n ::view-transition-old(*),\n ::view-transition-new(*) {\n animation: none !important;\n }\n }\n}\n","@layer base {\n :root {\n color-scheme: light dark;\n\n --primary-hue: 307;\n --clr-text: light-dark(\n oklch(5% 0.1 var(--primary-hue)),\n oklch(100% 0.1 var(--primary-hue))\n );\n --clr-background: light-dark(\n oklch(100% 0.1 var(--primary-hue)),\n oklch(15% 0.15 var(--primary-hue))\n );\n --clr-border: light-dark(\n oklch(90% 0.2 var(--primary-hue)),\n oklch(25% 0.1 var(--primary-hue))\n );\n\n /* Adding snow fall colour whilst we are using it */\n --clr-snow-fall: light-dark(\n oklch(25% 0.15 var(--primary-hue)),\n oklch(85% 0.2 var(--primary-hue))\n );\n }\n\n body {\n background-color: var(--clr-background);\n color: var(--clr-text);\n }\n}\n","@layer components {\n body > header {\n display: grid;\n grid-template-columns: auto 1fr auto;\n grid-template-rows: auto auto;\n align-items: baseline;\n gap: 1rem;\n\n a {\n text-decoration: none;\n }\n\n h1 {\n margin: 0;\n text-wrap: nowrap;\n }\n\n nav {\n display: flex;\n flex-direction: row;\n gap: .5rem;\n\n @media screen and (width <= 1100px) {\n grid-row: 2;\n grid-column: 1 / span 3;\n flex-wrap: wrap;\n }\n }\n }\n}\n","@layer components {\n .pagination {\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: center;\n\n div {\n display: flex;\n flex-direction: row;\n gap: 1ex;\n }\n }\n}\n","@layer components {\n #theme-selector {\n button {\n appearance: none;\n border: none;\n background: none;\n\n &:hover {\n cursor: pointer;\n }\n }\n\n /* This is the element with the [popover] attribute */\n #theme-selector-dropdown {\n position: absolute;\n position-area: block-end span-inline-start;\n margin: 0;\n border: 2px solid var(--clr-border);\n background-color: var(--clr-background);\n color: var(--clr-text);\n\n fieldset {\n border: 0;\n display: flex;\n flex-direction: row;\n gap: 1ch;\n\n input {\n display: none;\n }\n }\n }\n\n /*\n * This is the element with the [popover] attribute\n * Here we are styling the open and closing animations\n */\n @media (prefers-reduced-motion: no-preference) {\n #theme-selector-dropdown {\n &:popover-open {\n transform: translateY(0) scale(1);\n opacity: 1;\n\n /*\n * Start styles for the opening transition.\n * Added to :popover-open, but after the opened styles.\n */\n @starting-style {\n transform: translateY(30px) scale(0);\n opacity: 0;\n }\n }\n\n /*\n * End styles for the closing transition.\n */\n transform: translateY(0) scale(0);\n opacity: 0;\n\n /*\n * Enumerate transitioning properties, including display and overlay.\n */\n transition: transform, opacity, display allow-discrete, overlay allow-discrete;\n transition-duration: 0.5s;\n transform-origin: top right;\n }\n }\n }\n\n ::view-transition-group(changing-theme) {\n animation-duration: 1.25s;\n }\n\n ::view-transition-new(changing-theme),\n ::view-transition-old(changing-theme) {\n mix-blend-mode: normal;\n }\n\n ::view-transition-new(changing-theme) {\n animation-name: reveal;\n }\n\n ::view-transition-old(changing-theme) {\n animation: none;\n }\n\n @keyframes reveal {\n from {\n clip-path: polygon(-30% 0, -30% 0, -15% 100%, -10% 115%);\n }\n\n to {\n clip-path: polygon(-30% 0, 130% 0, 115% 100%, -10% 115%);\n }\n }\n}\n","@layer base {\n :root {\n --border-radius: 8px;\n }\n\n html {\n font-size: 125%;\n }\n\n body {\n display: grid;\n grid-template-columns: 1fr min(80ch, 80vw) 1fr;\n grid-template-rows: min-content 1fr min-content;\n padding-inline: 4vw;\n\n > header {\n grid-column: 1/-1;\n }\n\n > main {\n grid-column: 2/3;\n }\n\n > footer {\n grid-column: 1/-1;\n margin-block-start: 2ex;\n\n search form {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 1ex;\n\n input {\n min-width: 0;\n }\n }\n }\n }\n\n .h-feed {\n display: flex;\n flex-direction: column;\n gap: 2ex;\n }\n}\n","@layer components {\n .note {\n display: flex;\n flex-direction: column;\n border: 1px solid var(--clr-border);\n border-radius: var(--border-radius);\n padding: 1ex 2ex;\n\n .e-content {\n > p:first-child {\n margin-block-start: 0;\n }\n\n .u-photo {\n width: 100%;\n height: auto;\n }\n }\n\n .syndication-links {\n display: flex;\n flex-direction: row;\n gap: 1ex;\n\n svg {\n border-radius: 4px;\n width: auto;\n height: 1lh;\n }\n }\n }\n\n .reply-to {\n font-size: 85%;\n margin-inline: 2ex;\n padding-inline: 1ex;\n border: 1px solid var(--clr-border);\n border-bottom: none;\n border-top-left-radius: var(--border-radius);\n border-top-right-radius: var(--border-radius);\n }\n}\n"],"names":[]} \ No newline at end of file diff --git a/public/assets/css/app.css.zst b/public/assets/css/app.css.zst index db8c79df55ce45ad12e2b361e339b09079e420a7..eec4c8ba947529614427e21aa987e4f91935c477 100644 GIT binary patch literal 1463 zcmdPcs{c3TF|V&K!zmXD9(4uZIqVyle#{oz_K<6P@2tD$G`AdM5Z-wzrLKh6_(2(0 zwJ-~Zpp)xXhRqB+7`zV2*%lWSI)sbYqe}`c3XQPz*5F~-TaF! z+q&Wpt-oh0B@_7NVEd|~ynDS*JJvPL_PWIT>d*Ogt$XG3)}%Jt`G0$2b91@EnQ4#i zt0xw}%SbsqYhw0muh64`R~9vXyl1C%sZdXhO+wf1hS?O?K!JS|uBcpMU}H*pf05&9 zzi5JlnEQlTKb)QiX+D2=`%*NIWK!_O_upJA^A=zGRd_$zNt|KV4wFM0_r^_OZRxos zn|JJMxM10Ytt&1Cn5G3LhhP4gIK5-bI>V3v)iV*V_LiBw+lR`P(s*`|FvJ^ugWw^#lB`|1IqT`2X$x7{)tMAGEDG*>5f%MXLy+V0 z28W(Zhc%LkPgLq>eKBMZ;r7(?Q4uLNI$ATYSd>G;qC(-RS;Dix^C}6Bj0;|_=h|$b z;gWOw$<~+)>q9Qty0IIW__n=1-y+W?acG}JUJdtN*E(L+!VP}MlrCC4a$4i(rj&K6 za`l!EyO$~by|--3lBHp6ZjFmgIOJEWkXvpjNgc~0u(6Pu>m1hht`gercsxt;v| zgL`l6Ii;O9Cmr)Jzj<#;_Rc-a-FvK@rJ~r?49`6JwP}j?<;JSj;sM%6ww`<(rl$3# zQDw!#l|ddZDK09WT$7GDavVxfXqCCg`00d=e-fAAk`5y#WhGOgB}*0rNnR5ciu5r! zpLShrdP?c^CAAH&z5ZSGvfY~TFUxD8M2XzB*i&|=mvm~?2l@8THJ)Pp<5xt7+lkQi zD!bZ+N(HqI{-uW+b4y-Uoh0{JL_N4=X3C@bO;2;&j6{mR-;j6>+kbK+_i^V;(~yO zpg`qk)rF1xDjzfFy?HN@v%16enUPg+Ehu5HoizV2XHVuup2J(0t#Us2YL7^b;!N8MT0Csc zC(}5tn_czn6FkJ~WRe;hIDg)ym2*F`wmUI=j1^k2&5iBi=Q(GV?=vyTd}SnR;qciZ z=zKWSA(^#btM8?{ou1FI;APa>wqI<8FXlY&<+SpcRerUBN7<>-k}th8dB&55_1r!m zqr7yM8SsA+aMRf(=5LgqR=H}r+?~g#hSD+ literal 1466 zcmdPcs{c3TI&Z2jLzIgEj|M|-hnxZPh4~%#Kd|0d#J~N>pX(NFg))b&OgZurTi@L) zI4$?(|6IG4Eex9(b}$HidwF|uw{z9k1MX$>r9QXao%1jArt&`3`^oEWyuZGCW3bGb zof}L&{)FWfKD6^$=bpj*GjhitmB-OZ|NZ@QHKe{xd|bH2!f4K)m@VS$_VOhOH|{a` zO- za+v0Cx$x_Tz~5sNo^N^+J#)@^OHQ848(!S)Ui0@ozhTU#{q?>JQu-4fd#Es%Iy|e{ z^88Us*sc2$V zIvP6h=ItB}1!>7^8#_gkrXE@M;?IeVORw}koUu?;Q%m__+sRUkbMiYk{tSy*zR`ZW z3Xh^zrL$JnJ{!?P6Aha8zV~O3iZFN-eUf33S@uPP^zAZ|4i>S>R<&8RcVgM5yKApcs`Q^PRoYf^M4%^W$I-8;n$xvdzQ*Y<2s=?M{9lYdRZolglHX>5*CQ`HAwob zc0hk((d+4+IxD7?T=jaHUGryS$V=Ihv<+|1{gv_%3p?psDp(fbtS%St%W@`f9n0j2 zb|I^}1(fn-zGcFZPa;{h2eQDa~fRA?%=Ug~_)d9yX*NAK}Jy`mHK?qvu8treDfi@ojegE{W{Da!u=t`}IHkt~IfBo$k=RwzsQe@f)Q@=k6&e z-j;bM;m15l^nJqDRev7+Ilm%oj?wFv%a}`#&0pL%S(J~5OykiYG zy3zCFk~o`)H1A5qWdaHCYDsP19yP_++yqX9H|?{B!lL=-9%cLGRzA1h!>^jZ+U8z=9stOCy8!?I diff --git a/resources/css/reset.css b/resources/css/reset.css index f1a646a9..10780985 100644 --- a/resources/css/reset.css +++ b/resources/css/reset.css @@ -56,6 +56,7 @@ img, picture { max-width: 100%; + height: auto; display: block; } -- 2.55.0