Merge pull request 'MTM Redesign 2025.1' (#57) from develop into main
Reviewed-on: #57
This commit is contained in:
commit
4409e42029
40 changed files with 2441 additions and 1553 deletions
|
|
@ -4,6 +4,7 @@ use App\Http\Middleware\LinkHeadersMiddleware;
|
|||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Spatie\LaravelFlare\Facades\Flare;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
|
|
@ -24,5 +25,5 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
Flare::handles($exceptions);
|
||||
})->create();
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
"mf2/mf2": "~0.3",
|
||||
"phpdocumentor/reflection-docblock": "^5.3",
|
||||
"spatie/commonmark-highlighter": "^3.0",
|
||||
"spatie/laravel-ignition": "^2.1",
|
||||
"spatie/laravel-flare": "^2.2",
|
||||
"symfony/html-sanitizer": "^7.0",
|
||||
"symfony/property-access": "^7.0",
|
||||
"symfony/serializer": "^7.0",
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
"laravel/sail": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"openai-php/client": "^0.10.1",
|
||||
"openai-php/client": "^0.17.1",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"spatie/laravel-ray": "^1.12",
|
||||
|
|
|
|||
2098
composer.lock
generated
2098
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'enabled' => null,
|
||||
'enabled' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
206
config/flare.php
Normal file
206
config/flare.php
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
use Spatie\FlareClient\Api;
|
||||
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
|
||||
use Spatie\LaravelFlare\FlareConfig;
|
||||
|
||||
return [
|
||||
/*
|
||||
|
|
||||
|--------------------------------------------------------------------------
|
||||
| Flare API key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify Flare's API key below to enable error reporting to the service.
|
||||
|
|
||||
| More info: https://flareapp.io/docs/flare/general/getting-started
|
||||
|
|
||||
*/
|
||||
|
||||
'key' => env('FLARE_KEY'),
|
||||
|
||||
/*
|
||||
|
|
||||
|--------------------------------------------------------------------------
|
||||
| Flare Base URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Which server should be used to send the reports/traces to.
|
||||
|
|
||||
*/
|
||||
'base_url' => env('FLARE_BASE_URL', Api::BASE_URL),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Collects
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Flare will collect a lot of information about your application. You can
|
||||
| disable some of the collectors here, configure them or add your own.
|
||||
|
|
||||
*/
|
||||
|
||||
'collects' => FlareConfig::defaultCollects(
|
||||
ignore: [],
|
||||
extra: []
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Attribute providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When sending an error report or trace to Flare attributes can be added to
|
||||
| the report or trace for common entries. An example of such an entry is
|
||||
| the currently authenticated user. In an attribute provider you can
|
||||
| specify which attributes should be sent.
|
||||
|
|
||||
*/
|
||||
|
||||
'attribute_providers' => [
|
||||
'user' => LaravelUserAttributesProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Censor data
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| It is possible to censor sensitive data from the reports and sent to
|
||||
| Flare. Below you can specify which fields and header should be
|
||||
| censored. It is also possible to hide the client's IP address.
|
||||
|
|
||||
*/
|
||||
|
||||
'censor' => [
|
||||
'body_fields' => [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
],
|
||||
'headers' => [
|
||||
'API-KEY',
|
||||
'Authorization',
|
||||
'Cookie',
|
||||
'Set-Cookie',
|
||||
'X-CSRF-TOKEN',
|
||||
'X-XSRF-TOKEN',
|
||||
],
|
||||
'client_ips' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reporting log statements
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If this setting is `false` log statements won't be sent as events to Flare,
|
||||
| no matter which error level you specified in the Flare log channel.
|
||||
|
|
||||
*/
|
||||
|
||||
'send_logs_as_events' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Report error levels
|
||||
|--------------------------------------------------------------------------
|
||||
| When reporting errors, you can specify which error levels should be
|
||||
| reported. By default, all error levels are reported by setting
|
||||
| this value to `null`.
|
||||
*/
|
||||
|
||||
'report_error_levels' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Share button
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Flare automatically adds a Share button to the laravel error page. This
|
||||
| button allows you to easily share errors with colleagues or friends. It
|
||||
| is enabled by default, but you can disable it here.
|
||||
|
|
||||
*/
|
||||
|
||||
'enable_share_button' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Override grouping
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Flare will try to group errors and exceptions as best as possible, that
|
||||
| being said, sometimes you might want to override the grouping. You can
|
||||
| do this by adding exception classes to this array which should always
|
||||
| be grouped by exception class, exception message or exception class
|
||||
| and message.
|
||||
|
|
||||
*/
|
||||
|
||||
'overridden_groupings' => [
|
||||
// Illuminate\Http\Client\ConnectionException::class => Spatie\FlareClient\Enums\OverriddenGrouping::ExceptionMessageAndClass,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sender
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The sender is responsible for sending the error reports and traces to
|
||||
| Flare it can be configured if needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'sender' => [
|
||||
'class' => \Spatie\LaravelFlare\Senders\LaravelHttpSender::class,
|
||||
'config' => [
|
||||
'timeout' => 10,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Trace
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Tracing allows you to see the flow of your application. It shows you
|
||||
| which parts of your application are slow and which parts are fast.
|
||||
|
|
||||
*/
|
||||
|
||||
'trace' => env('FLARE_TRACE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sampler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The sampler is used to determine which traces should be recorded and
|
||||
| which traces should be dropped. It is possible to set the rate
|
||||
| at which traces should be recorded. The default rate is 0.1
|
||||
| which means that 10% of the traces will be recorded.
|
||||
|
|
||||
*/
|
||||
'sampler' => [
|
||||
'class' => \Spatie\FlareClient\Sampling\RateSampler::class,
|
||||
'config' => [
|
||||
'rate' => 0.1,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Trace limits
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Limits for the tracing data. These limits are used to prevent
|
||||
| the tracing data from growing too large.
|
||||
|
|
||||
*/
|
||||
'trace_limits' => [
|
||||
'max_spans' => 512,
|
||||
'max_attributes_per_span' => 128,
|
||||
'max_span_events_per_span' => 128,
|
||||
'max_attributes_per_span_event' => 128,
|
||||
],
|
||||
];
|
||||
812
package-lock.json
generated
812
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
|
@ -8,7 +8,7 @@
|
|||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@stylistic/eslint-plugin": "^5.1.0",
|
||||
"esbuild": "^0.25.2",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.7.0",
|
||||
"globals": "^16.0.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
|
|
@ -17,13 +17,18 @@
|
|||
"stylelint-config-standard": "^39.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"eslint": "eslint public/assets/js/*.js",
|
||||
"eslint": "eslint resources/js/*.js",
|
||||
"stylelint": "stylelint resources/css/*.css",
|
||||
"lint": "npm run eslint && npm run stylelint",
|
||||
"lightningcss": "lightningcss --output-dir public/assets/css --sourcemap --bundle --minify resources/css/app.css",
|
||||
"fix-sourcemap": "./scripts/fix-sourcemap.sh",
|
||||
"build-css": "npm run lightningcss && npm run fix-sourcemap",
|
||||
"build-js": "esbuild resources/js/app.js --bundle --minify --sourcemap --outfile=public/assets/js/app.js",
|
||||
"compress": "./scripts/compress.sh",
|
||||
"build": "npm run lint && npm run compress"
|
||||
"build": "npm run lint && npm run build-css && npm run build-js && npm run compress"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/is-land": "^5.0.0",
|
||||
"@zachleat/snow-fall": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
:root{--font-family-headings:"Rockwell","Rockwell Nova","Roboto Slab","DejaVu Serif","Sitka Small",serif;--font-family-body:"Charter","Bitstream Charter","Sitka Text","Cambria",serif;--font-family-monospace:ui-monospace,"Cascadia Code","Source Code Pro","Menlo","Consolas","DejaVu Sans Mono",monospace;--font-size-sm:.75rem;--font-size-base:1rem;--font-size-md:1.25rem;--font-size-lg:1.5rem;--font-size-xl:1.75rem;--font-size-xxl:2rem;--font-size-xxxl:2.25rem;--color-primary:oklch(36.8% .1 125.505);--color-secondary:oklch(96.3% .1 125.505);--color-link:oklch(48.09% .146 241.41);--color-link-visited:oklch(70.44% .21 304.41);--color-primary-shadow:oklch(19.56% .054 125.505/.4);--rss-color-link:oklch(67.59% .189 42.04);--color-danger:oklch(64.41% .281 23.29);--color-danger-shadow:oklch(64.41% .281 23.29/.1)}body{font-family:var(--font-family-body);font-size:var(--font-size-md)}code{font-family:var(--font-family-monospace)}h1,h2,h3,h4,h5,h6{font-family:var(--font-family-headings)}.grid{grid-template-rows:min-content 1fr min-content;grid-template-columns:5vw 1fr 5vw;row-gap:1rem;display:grid}#site-header{grid-area:1/2/2/3;& .rss-icon{& svg{width:auto;height:1rem}}}main{grid-area:2/2/3/3}.h-feed{flex-direction:column;gap:2rem;display:flex}.h-entry{& p:first-of-type,& h1:first-of-type{margin-block-start:0}}.pagination{margin-block-start:1rem}footer{grid-area:3/2/4/3;& .iwc-logo{max-width:85vw}& .footer-actions{flex-direction:row;gap:1rem;display:flex}}body{background-color:var(--color-secondary);color:var(--color-primary)}a{color:var(--color-link);&:visited{color:var(--color-link-visited)}&.auth:visited{color:var(--color-link)}}#site-header{& a:visited{color:var(--color-link)}& .rss-icon{& svg{color:var(--rss-color-link)}}}.hljs{border-radius:.5rem}.h-card{& .hovercard{z-index:100;box-shadow:0 .5rem .5rem .5rem var(--color-primary-shadow);background-color:var(--color-secondary);opacity:0;border-radius:1rem;flex-direction:column;gap:.5rem;width:fit-content;padding:1rem;transition:opacity .5s ease-in-out;display:none;position:absolute;& .u-photo{max-width:6rem}& .social-icon{width:1rem;height:1rem}}&:hover{& .hovercard{opacity:1;display:flex}}}.h-entry{border-inline-start:1px solid var(--color-primary);padding-inline-start:.5rem;& .reply-to{font-style:italic}& .post-info{& a{text-decoration:none}}& .note-metadata{flex-direction:row;gap:1rem;display:flex;& .replies,& .likes,& .reposts{flex-direction:row;align-items:center;gap:.5rem;display:inline-flex}& .syndication-links{flex-flow:wrap;& a{text-decoration:none;& svg{width:1rem;height:1rem}}}}}.feather{stroke:currentColor;stroke-width:2px;stroke-linecap:round;stroke-linejoin:round;fill:none;width:24px;height:24px}.sr-only{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}main{&>.u-comment{border-inline-start:1px solid var(--color-primary);margin-block-start:2rem;margin-inline-start:2rem;padding-inline-start:.5rem;& .mini-h-card{flex-direction:row;align-items:baseline;display:inline-flex;& .u-photo{border-radius:50%;width:2rem;height:2rem;margin-block-end:.5rem}}}& .notes-subtitle{font-size:1.2rem;font-weight:600}& .webmentions-author-list{flex-flow:wrap;gap:1rem;display:flex;& img{border-radius:50%;width:4rem;height:4rem}}}.indieauth{& .error{color:var(--color-danger);background-color:var(--color-danger-shadow);border:1px solid var(--color-danger);border-radius:.5rem;width:fit-content;margin-block-end:1rem;padding-block:.5rem;padding-inline:1rem;display:flex}}
|
||||
@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,95vw) 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}}}.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}}}.h-feed{& .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}}& .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 */
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -1,2 +1,2 @@
|
|||
(()=>{var i=class{constructor(){}async register(){let n=await this.getCreateOptions(),e={challenge:this.base64URLStringToBuffer(n.challenge),rp:{id:n.rp.id,name:n.rp.name},user:{id:new TextEncoder().encode(window.atob(n.user.id)),name:n.user.name,displayName:n.user.displayName},pubKeyCredParams:n.pubKeyCredParams,excludeCredentials:[],authenticatorSelection:n.authenticatorSelection,timeout:6e4},t=await navigator.credentials.create({publicKey:e});if(!t)throw new Error("Error generating a passkey");let r={id:t.id?t.id:null,type:t.type?t.type:null,rawId:t.rawId?this.bufferToBase64URLString(t.rawId):null,response:{attestationObject:t.response.attestationObject?this.bufferToBase64URLString(t.response.attestationObject):null,clientDataJSON:t.response.clientDataJSON?this.bufferToBase64URLString(t.response.clientDataJSON):null}};if(!(await window.fetch("/admin/passkeys/register",{method:"POST",body:JSON.stringify(r),cache:"no-cache",headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Error saving the passkey");window.location.reload()}async getCreateOptions(){return await(await fetch("/admin/passkeys/register",{method:"GET"})).json()}async login(){let n=await this.getLoginData(),e=await navigator.credentials.get({publicKey:{challenge:this.base64URLStringToBuffer(n.challenge),userVerification:n.userVerification,timeout:6e4}});if(!e)throw new Error("Authentication failed");let t={id:e.id?e.id:"",type:e.type?e.type:"",rawId:e.rawId?this.bufferToBase64URLString(e.rawId):"",response:{authenticatorData:e.response.authenticatorData?this.bufferToBase64URLString(e.response.authenticatorData):"",clientDataJSON:e.response.clientDataJSON?this.bufferToBase64URLString(e.response.clientDataJSON):"",signature:e.response.signature?this.bufferToBase64URLString(e.response.signature):"",userHandle:e.response.userHandle?this.bufferToBase64URLString(e.response.userHandle):""}};if(!(await window.fetch("/login/passkey",{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Login failed");window.location.assign("/admin")}async getLoginData(){return await(await fetch("/login/passkey",{method:"GET"})).json()}base64URLStringToBuffer(n){let e=n.replace(/-/g,"+").replace(/_/g,"/"),t=(4-e.length%4)%4,r=e.padEnd(e.length+t,"="),a=window.atob(r),c=new ArrayBuffer(a.length),d=new Uint8Array(c);for(let o=0;o<a.length;o++)d[o]=a.charCodeAt(o);return c}bufferToBase64URLString(n){let e=new Uint8Array(n),t="";for(let a of e)t+=String.fromCharCode(a);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}};var l=new i;document.querySelectorAll(".add-passkey").forEach(s=>{s.addEventListener("click",()=>{l.register()})});document.querySelectorAll(".login-passkey").forEach(s=>{s.addEventListener("click",()=>{l.login()})});})();
|
||||
(()=>{var l=class{constructor(){}async register(){let r=await this.getCreateOptions(),e={challenge:this.base64URLStringToBuffer(r.challenge),rp:{id:r.rp.id,name:r.rp.name},user:{id:new TextEncoder().encode(window.atob(r.user.id)),name:r.user.name,displayName:r.user.displayName},pubKeyCredParams:r.pubKeyCredParams,excludeCredentials:[],authenticatorSelection:r.authenticatorSelection,timeout:6e4},t=await navigator.credentials.create({publicKey:e});if(!t)throw new Error("Error generating a passkey");let n={id:t.id?t.id:null,type:t.type?t.type:null,rawId:t.rawId?this.bufferToBase64URLString(t.rawId):null,response:{attestationObject:t.response.attestationObject?this.bufferToBase64URLString(t.response.attestationObject):null,clientDataJSON:t.response.clientDataJSON?this.bufferToBase64URLString(t.response.clientDataJSON):null}};if(!(await window.fetch("/admin/passkeys/register",{method:"POST",body:JSON.stringify(n),cache:"no-cache",headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Error saving the passkey");window.location.reload()}async getCreateOptions(){return await(await fetch("/admin/passkeys/register",{method:"GET"})).json()}async login(){let r=await this.getLoginData(),e=await navigator.credentials.get({publicKey:{challenge:this.base64URLStringToBuffer(r.challenge),userVerification:r.userVerification,timeout:6e4}});if(!e)throw new Error("Authentication failed");let t={id:e.id?e.id:"",type:e.type?e.type:"",rawId:e.rawId?this.bufferToBase64URLString(e.rawId):"",response:{authenticatorData:e.response.authenticatorData?this.bufferToBase64URLString(e.response.authenticatorData):"",clientDataJSON:e.response.clientDataJSON?this.bufferToBase64URLString(e.response.clientDataJSON):"",signature:e.response.signature?this.bufferToBase64URLString(e.response.signature):"",userHandle:e.response.userHandle?this.bufferToBase64URLString(e.response.userHandle):""}};if(!(await window.fetch("/login/passkey",{method:"POST",body:JSON.stringify(t),headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Login failed");window.location.assign("/admin")}async getLoginData(){return await(await fetch("/login/passkey",{method:"GET"})).json()}base64URLStringToBuffer(r){let e=r.replace(/-/g,"+").replace(/_/g,"/"),t=(4-e.length%4)%4,n=e.padEnd(e.length+t,"="),a=window.atob(n),o=new ArrayBuffer(a.length),i=new Uint8Array(o);for(let s=0;s<a.length;s++)i[s]=a.charCodeAt(s);return o}bufferToBase64URLString(r){let e=new Uint8Array(r),t="";for(let a of e)t+=String.fromCharCode(a);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}};var d=class{constructor(){this.widget=document.querySelector("#theme-selector")}setupEventListeners(){this.widget.querySelectorAll("#theme-selector-dropdown input").forEach(r=>r.addEventListener("input",e=>{let t=e.target.value;this.widget.querySelectorAll(".toggle svg").forEach(u=>{u.classList.contains(t)?u.style.display="":u.style.display="none"});let n;switch(t){case"dark":case"light":n=t;break;default:n="light dark"}let a=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light",o=document.querySelector("html"),i=o.style.getPropertyValue("color-scheme");i===""&&(i="light dark");let s=!1;n!==i&&(n==="light dark"?s=i!==a:i==="light dark"?s=n!==a:s=!0),o.style.viewTransitionName="changing-theme",s&&document.startViewTransition?document.startViewTransition(()=>{document.querySelector("#theme-selector-dropdown").togglePopover(),o.style.setProperty("color-scheme",n)}):(document.querySelector("#theme-selector-dropdown").togglePopover(),o.style.setProperty("color-scheme",n))}))}};var h=new l;document.querySelectorAll(".add-passkey").forEach(c=>{c.addEventListener("click",()=>{h.register()})});document.querySelectorAll(".login-passkey").forEach(c=>{c.addEventListener("click",()=>{h.login()})});var p=new d;p.setupEventListeners();})();
|
||||
//# sourceMappingURL=app.js.map
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
2
public/assets/js/is-land.min.js
vendored
Normal file
2
public/assets/js/is-land.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
public/assets/js/is-land.min.js.br
Normal file
BIN
public/assets/js/is-land.min.js.br
Normal file
Binary file not shown.
BIN
public/assets/js/is-land.min.js.zst
Normal file
BIN
public/assets/js/is-land.min.js.zst
Normal file
Binary file not shown.
125
public/assets/js/snow-fall.js
Normal file
125
public/assets/js/snow-fall.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
class Snow extends HTMLElement {
|
||||
static random(min, max) {
|
||||
return min + Math.floor(Math.random() * (max - min) + 1);
|
||||
}
|
||||
|
||||
static attrs = {
|
||||
count: "count", // default: 100
|
||||
mode: "mode",
|
||||
text: "text", // text in snow flake (emoji, too)
|
||||
}
|
||||
|
||||
generateCss(mode, count) {
|
||||
let css = [];
|
||||
css.push(`
|
||||
:host([mode="element"]) {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
:host([mode="page"]) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
:host([mode="page"]),
|
||||
:host([mode="element"]) > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([mode="element"]) ::slotted(*) {
|
||||
pointer-events: all;
|
||||
}
|
||||
* {
|
||||
position: absolute;
|
||||
}
|
||||
:host([text]) * {
|
||||
font-size: var(--snow-fall-size, 1em);
|
||||
}
|
||||
:host(:not([text])) * {
|
||||
width: var(--snow-fall-size, 10px);
|
||||
height: var(--snow-fall-size, 10px);
|
||||
background: var(--snow-fall-color, rgba(255,255,255,.5));
|
||||
border-radius: 50%;
|
||||
}
|
||||
`);
|
||||
|
||||
// using vw units (max 100)
|
||||
let dimensions = { width: 100, height: 100 };
|
||||
let units = { x: "vw", y: "vh"};
|
||||
|
||||
if(mode === "element") {
|
||||
dimensions = {
|
||||
width: this.firstElementChild.clientWidth,
|
||||
height: this.firstElementChild.clientHeight,
|
||||
};
|
||||
units = { x: "px", y: "px"};
|
||||
}
|
||||
|
||||
// Thank you @alphardex: https://codepen.io/alphardex/pen/dyPorwJ
|
||||
for(let j = 1; j<= count; j++ ) {
|
||||
let x = Snow.random(1, 100) * dimensions.width/100; // vw
|
||||
let offset = Snow.random(-10, 10) * dimensions.width/100; // vw
|
||||
|
||||
let yoyo = Math.round(Snow.random(30, 100)); // % time
|
||||
let yStart = yoyo * dimensions.height/100; // vh
|
||||
let yEnd = dimensions.height; // vh
|
||||
|
||||
let scale = Snow.random(1, 10000) * .0001;
|
||||
let duration = Snow.random(10, 30);
|
||||
let delay = Snow.random(0, 30) * -1;
|
||||
|
||||
css.push(`
|
||||
:nth-child(${j}) {
|
||||
opacity: ${Snow.random(0, 1000) * 0.001};
|
||||
transform: translate(${x}${units.x}, -10px) scale(${scale});
|
||||
animation: fall-${j} ${duration}s ${delay}s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes fall-${j} {
|
||||
${yoyo}% {
|
||||
transform: translate(${x + offset}${units.x}, ${yStart}${units.y}) scale(${scale});
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(${x + offset / 2}${units.x}, ${yEnd}${units.y}) scale(${scale});
|
||||
}
|
||||
}`)
|
||||
}
|
||||
return css.join("\n");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// https://caniuse.com/mdn-api_cssstylesheet_replacesync
|
||||
if(this.shadowRoot || !("replaceSync" in CSSStyleSheet.prototype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = parseInt(this.getAttribute(Snow.attrs.count)) || 100;
|
||||
|
||||
let mode;
|
||||
if(this.hasAttribute(Snow.attrs.mode)) {
|
||||
mode = this.getAttribute(Snow.attrs.mode);
|
||||
} else {
|
||||
mode = this.firstElementChild ? "element" : "page";
|
||||
this.setAttribute(Snow.attrs.mode, mode);
|
||||
}
|
||||
|
||||
let sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(this.generateCss(mode, count));
|
||||
|
||||
let shadowroot = this.attachShadow({ mode: "open" });
|
||||
shadowroot.adoptedStyleSheets = [sheet];
|
||||
|
||||
let d = document.createElement("div");
|
||||
let text = this.getAttribute(Snow.attrs.text);
|
||||
d.innerText = text || "";
|
||||
for(let j = 0, k = count; j<k; j++) {
|
||||
shadowroot.appendChild(d.cloneNode(true));
|
||||
}
|
||||
|
||||
shadowroot.appendChild(document.createElement("slot"));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("snow-fall", Snow);
|
||||
BIN
public/assets/js/snow-fall.js.br
Normal file
BIN
public/assets/js/snow-fall.js.br
Normal file
Binary file not shown.
BIN
public/assets/js/snow-fall.js.zst
Normal file
BIN
public/assets/js/snow-fall.js.zst
Normal file
Binary file not shown.
|
|
@ -1,8 +1,10 @@
|
|||
@import url('variables.css');
|
||||
@import url('fonts.css');
|
||||
@import url('layout.css');
|
||||
/* First thing we need to do is declare our cascade layers */
|
||||
@layer reset, base, components;
|
||||
|
||||
@import url('reset.css');
|
||||
@import url('colours.css');
|
||||
@import url('code.css');
|
||||
@import url('content.css');
|
||||
@import url('layout.css');
|
||||
@import url('header.css');
|
||||
@import url('notes.css');
|
||||
@import url('indieauth.css');
|
||||
@import url('pagination.css');
|
||||
@import url('theme-selector.css');
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.hljs {
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
|
@ -1,28 +1,30 @@
|
|||
body {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
@layer base {
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
--primary-hue: 307;
|
||||
--clr-text: light-dark(
|
||||
oklch(5% 0.1 var(--primary-hue)),
|
||||
oklch(100% 0.1 var(--primary-hue))
|
||||
);
|
||||
--clr-background: light-dark(
|
||||
oklch(100% 0.1 var(--primary-hue)),
|
||||
oklch(15% 0.15 var(--primary-hue))
|
||||
);
|
||||
--clr-border: light-dark(
|
||||
oklch(90% 0.2 var(--primary-hue)),
|
||||
oklch(25% 0.1 var(--primary-hue))
|
||||
);
|
||||
|
||||
&:visited {
|
||||
color: var(--color-link-visited);
|
||||
/* Adding snow fall colour whilst we are using it */
|
||||
--clr-snow-fall: light-dark(
|
||||
oklch(25% 0.15 var(--primary-hue)),
|
||||
oklch(85% 0.2 var(--primary-hue))
|
||||
);
|
||||
}
|
||||
|
||||
&.auth:visited {
|
||||
color: var(--color-link);
|
||||
}
|
||||
}
|
||||
|
||||
#site-header {
|
||||
& a:visited {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
& .rss-icon {
|
||||
& svg {
|
||||
color: var(--rss-color-link);
|
||||
}
|
||||
body {
|
||||
background-color: var(--clr-background);
|
||||
color: var(--clr-text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
@import url('h-card.css');
|
||||
|
||||
.h-entry {
|
||||
border-inline-start: 1px solid var(--color-primary);
|
||||
padding-inline-start: .5rem;
|
||||
|
||||
& .reply-to {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
& .post-info {
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .note-metadata {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
& .replies,
|
||||
& .likes,
|
||||
& .reposts {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& .syndication-links {
|
||||
flex-flow: row wrap;
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
|
||||
& svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feather {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke: currentcolor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
body {
|
||||
font-family: var(--font-family-body);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-family-headings);
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
.h-card {
|
||||
& .hovercard {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 .5rem .5rem .5rem var(--color-primary-shadow);
|
||||
background-color: var(--color-secondary);
|
||||
width: fit-content;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
opacity: 0;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
|
||||
& .u-photo {
|
||||
max-width: 6rem;
|
||||
}
|
||||
|
||||
& .social-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .hovercard {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
resources/css/header.css
Normal file
30
resources/css/header.css
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
@layer components {
|
||||
body > header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: .5rem;
|
||||
|
||||
@media screen and (width <= 1100px) {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / span 3;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.indieauth {
|
||||
.error {
|
||||
color: var(--color-danger);
|
||||
background-color: var(--color-danger-shadow);
|
||||
border: 1px solid var(--color-danger);
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
padding-inline: 1rem;
|
||||
padding-block: .5rem;
|
||||
width: fit-content;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +1,42 @@
|
|||
.grid {
|
||||
@layer base {
|
||||
:root {
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 125%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns: 5vw 1fr 5vw;
|
||||
grid-template-columns: 1fr min(80ch, 95vw) 1fr;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
padding-inline: 4vw;
|
||||
|
||||
#site-header {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
|
||||
& .rss-icon {
|
||||
& svg {
|
||||
width: auto;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
.h-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.h-entry {
|
||||
& p:first-of-type,
|
||||
& h1:first-of-type {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-block-start: 1rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 3 / 4;
|
||||
|
||||
& .iwc-logo {
|
||||
max-width: 85vw;
|
||||
> header {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
& .footer-actions {
|
||||
> main {
|
||||
grid-column: 2/3;
|
||||
}
|
||||
|
||||
> footer {
|
||||
grid-column: 1/-1;
|
||||
margin-block-start: 2ex;
|
||||
|
||||
search form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
gap: 1ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.h-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2ex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,25 @@
|
|||
main {
|
||||
& > .u-comment {
|
||||
margin-block-start: 2rem;
|
||||
margin-inline-start: 2rem;
|
||||
border-inline-start: 1px solid var(--color-primary);
|
||||
padding-inline-start: .5rem;
|
||||
|
||||
& .mini-h-card {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
& .u-photo {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .notes-subtitle {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& .webmentions-author-list {
|
||||
@layer components {
|
||||
.h-feed {
|
||||
.note {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1ex 2ex;
|
||||
|
||||
& img {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 50%;
|
||||
.e-content > p:first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
font-size: 85%;
|
||||
margin-inline: 2ex;
|
||||
padding-inline: 1ex;
|
||||
border: 1px solid var(--clr-border);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
resources/css/pagination.css
Normal file
14
resources/css/pagination.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
@layer components {
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
resources/css/reset.css
Normal file
87
resources/css/reset.css
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
@layer reset {
|
||||
/* Sourced from https://piccalil.li/blog/a-more-modern-css-reset/ */
|
||||
|
||||
/* Box sizing rules */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Prevent font size inflation */
|
||||
html {
|
||||
/* stylelint-disable property-no-vendor-prefix */
|
||||
-moz-text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
/* stylelint-enable property-no-vendor-prefix */
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
/* Remove default margin in favour of better control in authored CSS */
|
||||
body, h1, h2, h3, h4, p,
|
||||
figure, blockquote, dl, dd {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||
ul[role='list'],
|
||||
ol[role='list'] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Set core body defaults */
|
||||
body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Set shorter line heights on headings and interactive elements */
|
||||
h1, h2, h3, h4,
|
||||
button, input, label {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Balance text wrapping on headings */
|
||||
h1, h2,
|
||||
h3, h4 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* A elements that don't have a class get default styles */
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
color: currentcolor;
|
||||
}
|
||||
|
||||
/* Make images easier to work with */
|
||||
img,
|
||||
picture {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Inherit fonts for inputs and buttons */
|
||||
input, button,
|
||||
textarea, select {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Make sure textareas without a rows attribute are not tiny */
|
||||
textarea:not([rows]) {
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
/* Anything that has been anchored to should have extra scroll margin */
|
||||
:target {
|
||||
scroll-margin-block: 5ex;
|
||||
}
|
||||
|
||||
/* Disable view transition animations for users who don’t want them */
|
||||
@media (prefers-reduced-motion) {
|
||||
::view-transition-group(*),
|
||||
::view-transition-old(*),
|
||||
::view-transition-new(*) {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
resources/css/theme-selector.css
Normal file
96
resources/css/theme-selector.css
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
@layer components {
|
||||
#theme-selector {
|
||||
button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* This is the element with the [popover] attribute */
|
||||
#theme-selector-dropdown {
|
||||
position: absolute;
|
||||
position-area: block-end span-inline-start;
|
||||
margin: 0;
|
||||
border: 2px solid var(--clr-border);
|
||||
background-color: var(--clr-background);
|
||||
color: var(--clr-text);
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1ch;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the element with the [popover] attribute
|
||||
* Here we are styling the open and closing animations
|
||||
*/
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
#theme-selector-dropdown {
|
||||
&:popover-open {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
|
||||
/*
|
||||
* Start styles for the opening transition.
|
||||
* Added to :popover-open, but after the opened styles.
|
||||
*/
|
||||
@starting-style {
|
||||
transform: translateY(30px) scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* End styles for the closing transition.
|
||||
*/
|
||||
transform: translateY(0) scale(0);
|
||||
opacity: 0;
|
||||
|
||||
/*
|
||||
* Enumerate transitioning properties, including display and overlay.
|
||||
*/
|
||||
transition: transform, opacity, display allow-discrete, overlay allow-discrete;
|
||||
transition-duration: 0.5s;
|
||||
transform-origin: top right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::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 {
|
||||
from {
|
||||
clip-path: polygon(-30% 0, -30% 0, -15% 100%, -10% 115%);
|
||||
}
|
||||
|
||||
to {
|
||||
clip-path: polygon(-30% 0, 130% 0, 115% 100%, -10% 115%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
:root {
|
||||
/* Font Family */
|
||||
--font-family-headings: 'Rockwell', 'Rockwell Nova', 'Roboto Slab', 'DejaVu Serif', 'Sitka Small', serif;
|
||||
--font-family-body: 'Charter', 'Bitstream Charter', 'Sitka Text', 'Cambria', serif;
|
||||
--font-family-monospace: ui-monospace, 'Cascadia Code', 'Source Code Pro', 'Menlo', 'Consolas', 'DejaVu Sans Mono', monospace;
|
||||
|
||||
/* Font Size */
|
||||
--font-size-sm: 0.75rem; /* 12px */
|
||||
--font-size-base: 1rem; /* 16px, base */
|
||||
--font-size-md: 1.25rem; /* 20px */
|
||||
--font-size-lg: 1.5rem; /* 24px */
|
||||
--font-size-xl: 1.75rem; /* 28px */
|
||||
--font-size-xxl: 2rem; /* 32px */
|
||||
--font-size-xxxl: 2.25rem; /* 36px */
|
||||
|
||||
/* Colours */
|
||||
--color-primary: oklch(36.8% 0.1 125.505deg);
|
||||
--color-secondary: oklch(96.3% 0.1 125.505deg);
|
||||
--color-link: oklch(48.09% 0.146 241.41deg);
|
||||
--color-link-visited: oklch(70.44% 0.21 304.41deg);
|
||||
--color-primary-shadow: oklch(19.56% 0.054 125.505deg / 40%);
|
||||
--rss-color-link: oklch(67.59% 0.189 42.04deg);
|
||||
--color-danger: oklch(64.41% 0.281 23.29deg);
|
||||
--color-danger-shadow: oklch(64.41% 0.281 23.29deg / 10%);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Auth } from './auth.js';
|
||||
import { ThemeSelector } from './theme-selector.js';
|
||||
|
||||
let auth = new Auth();
|
||||
|
||||
document.querySelectorAll('.add-passkey').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
auth.register();
|
||||
|
|
@ -13,3 +13,6 @@ document.querySelectorAll('.login-passkey').forEach((el) => {
|
|||
auth.login();
|
||||
});
|
||||
});
|
||||
|
||||
let themeSelector = new ThemeSelector();
|
||||
themeSelector.setupEventListeners();
|
||||
|
|
|
|||
70
resources/js/theme-selector.js
Normal file
70
resources/js/theme-selector.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
class ThemeSelector {
|
||||
constructor() {
|
||||
this.widget = document.querySelector('#theme-selector');
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.widget.querySelectorAll('#theme-selector-dropdown input').forEach((radioInput) => radioInput.addEventListener('input', (e) => {
|
||||
let theme = e.target.value;
|
||||
|
||||
// Update current icon
|
||||
this.widget.querySelectorAll('.toggle svg').forEach((svg) => {
|
||||
if (svg.classList.contains(theme)) {
|
||||
svg.style.display = '';
|
||||
} else {
|
||||
svg.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Set the theme
|
||||
let selectedTheme;
|
||||
switch (theme) {
|
||||
case 'dark':
|
||||
case 'light':
|
||||
selectedTheme = theme;
|
||||
break;
|
||||
default:
|
||||
selectedTheme = 'light dark';
|
||||
}
|
||||
|
||||
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||||
const html = document.querySelector('html');
|
||||
let currentTheme = html.style.getPropertyValue('color-scheme');
|
||||
if (currentTheme === '') {
|
||||
currentTheme = 'light dark';
|
||||
}
|
||||
|
||||
/* do we need to transition */
|
||||
let doTransition = false;
|
||||
|
||||
if (selectedTheme !== currentTheme) {
|
||||
if (selectedTheme === 'light dark') {
|
||||
doTransition = currentTheme !== systemTheme;
|
||||
} else if (currentTheme === 'light dark') {
|
||||
doTransition = selectedTheme !== systemTheme;
|
||||
} else {
|
||||
doTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
html.style.viewTransitionName = 'changing-theme';
|
||||
if (doTransition && document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
// Close the popover
|
||||
document.querySelector('#theme-selector-dropdown').togglePopover();
|
||||
|
||||
// Set the colour theme
|
||||
html.style.setProperty('color-scheme', selectedTheme);
|
||||
});
|
||||
} else {
|
||||
// Close the popover
|
||||
document.querySelector('#theme-selector-dropdown').togglePopover();
|
||||
|
||||
// Set the colour theme
|
||||
html.style.setProperty('color-scheme', selectedTheme);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export { ThemeSelector };
|
||||
|
|
@ -2,23 +2,24 @@
|
|||
<html lang="en-GB">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title'){{ config('app.name') }}</title>
|
||||
<link rel="stylesheet" href="/assets/highlight/zenburn.css">
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
<link rel="alternate" type="application/rss+xml" title="Blog RSS Feed" href="/blog/feed.rss">
|
||||
<link rel="alternate" type="application/atom+xml" title="Blog Atom Feed" href="/blog/feed.atom">
|
||||
<link rel="alternate" type="application/json" title="Blog JSON Feed" href="/blog/feed.json">
|
||||
<link rel="alternate" type="application/jf2feed+json" title="Blog JF2 Feed" href="/blog/feed.jf2">
|
||||
<link rel="alternate" type="application/rss+xml" title="Notes RSS Feed" href="/notes/feed.rss">
|
||||
<link rel="alternate" type="application/atom+xml" title="Notes Atom Feed" href="/notes/feed.atom">
|
||||
<link rel="alternate" type="application/json" title="Notes JSON Feed" href="/notes/feed.json">
|
||||
<link rel="alternate" type="application/jf2feed+json" title="Notes JF2 Feed" href="/blog/feed.jf2">
|
||||
<link rel="alternate" type="application/rss+xml" title="Blog RSS Feed" href="{{ route('feed.blog.rss') }}">
|
||||
<link rel="alternate" type="application/atom+xml" title="Blog Atom Feed" href="{{ route('feed.blog.atom') }}">
|
||||
<link rel="alternate" type="application/json" title="Blog JSON Feed" href="{{ route('feed.blog.json') }}">
|
||||
<link rel="alternate" type="application/jf2feed+json" title="Blog JF2 Feed" href="{{ route('feed.blog.jf2') }}">
|
||||
<link rel="alternate" type="application/rss+xml" title="Notes RSS Feed" href="{{ route('feed.notes.rss') }}">
|
||||
<link rel="alternate" type="application/atom+xml" title="Notes Atom Feed" href="{{ route('feed.notes.atom') }}">
|
||||
<link rel="alternate" type="application/json" title="Notes JSON Feed" href="{{ route('feed.notes.json') }}">
|
||||
<link rel="alternate" type="application/jf2feed+json" title="Notes JF2 Feed" href="{{ route('feed.notes.jf2') }}">
|
||||
<link rel="indieauth-metadata" href="{{ route('indieauth.metadata') }}">
|
||||
<link rel="authorization_endpoint" href="{{ route('indieauth.start') }}">
|
||||
<link rel="token_endpoint" href="{{ route('indieauth.token') }}">
|
||||
<link rel="micropub" href="{{ route('micropub-endpoint') }}">
|
||||
<link rel="webmention" href="{{ config('app.url') }}/webmention">
|
||||
<link rel="webmention" href="{{ route('webmention-endpoint') }}">
|
||||
@if (File::exists(public_path('assets/img/favicon.png')))
|
||||
<link rel="icon" href="{{ config('app.url') }}/assets/img/favicon.png">
|
||||
@endif
|
||||
|
|
@ -26,8 +27,8 @@
|
|||
<link rel="pgpkey" href="{{ config('app.url')}}/gpg.key">
|
||||
@endif
|
||||
</head>
|
||||
<body class="grid">
|
||||
<header id="site-header">
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
<a rel="author" href="/">{{ config('user.display_name') }}</a>
|
||||
</h1>
|
||||
|
|
@ -39,15 +40,47 @@
|
|||
<a href="/likes">Likes</a>
|
||||
<a href="/contacts">Contacts</a>
|
||||
<a href="/projects">Projects</a>
|
||||
<a href="/notes/feed.json" class="rss-icon">@include('icons.rss', ['title' => 'RSS Feed'])</a>
|
||||
<a href="{{ route('feed.notes.json') }}" class="rss-icon">@include('icons.rss', ['title' => 'RSS Feed'])</a>
|
||||
</nav>
|
||||
<div id="theme-selector">
|
||||
<button class="toggle" popovertarget="theme-selector-dropdown">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="system" style=""><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-1.5v-17a8.5 8.5 0 0 1 0 17"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="dark" style="display: none"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A10 10 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.233-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.96 9.96 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" class="light" style="display: none"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2m5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0m4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19m-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06m1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06m-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06z"/></svg>
|
||||
</button>
|
||||
<div id="theme-selector-dropdown" popover>
|
||||
<fieldset>
|
||||
<legend>Select theme:</legend>
|
||||
<div>
|
||||
<input type="radio" id="theme-system" name="theme" value="system" checked>
|
||||
<label for="theme-system">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-1.5v-17a8.5 8.5 0 0 1 0 17"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="theme-dark" name="theme" value="dark">
|
||||
<label for="theme-dark">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A10 10 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.233-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.96 9.96 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="theme-light" name="theme" value="light">
|
||||
<label for="theme-light">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Fluent UI System Icons by Microsoft Corporation - https://github.com/microsoft/fluentui-system-icons/blob/main/LICENSE --><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2m5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0m4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19m-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06m1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06m-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06z"/></svg>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@yield('content')
|
||||
|
||||
@isset($bio)
|
||||
<div class="h-card">
|
||||
{!! $bio !!}
|
||||
</div>
|
||||
@endisset
|
||||
</main>
|
||||
|
||||
|
|
@ -72,6 +105,15 @@
|
|||
<!--scripts go here when needed-->
|
||||
@section('scripts')
|
||||
<script type="module" src="/assets/js/app.js"></script>
|
||||
|
||||
<script type="module" src="/assets/js/is-land.min.js"></script>
|
||||
<script type="module" src="/assets/js/snow-fall.js"></script>
|
||||
<is-land on:media="(prefers-reduced-motion: no-preference)">
|
||||
<snow-fall
|
||||
count="150"
|
||||
style="--snow-fall-color: var(--clr-snow-fall)"
|
||||
></snow-fall>
|
||||
</is-land>
|
||||
@show
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -163,10 +163,10 @@ Route::middleware(MyAuthMiddleware::class)->prefix('admin')->group(function () {
|
|||
|
||||
// Blog pages using ArticlesController
|
||||
Route::prefix('blog')->group(function () {
|
||||
Route::get('/feed.rss', [FeedsController::class, 'blogRss']);
|
||||
Route::get('/feed.atom', [FeedsController::class, 'blogAtom']);
|
||||
Route::get('/feed.json', [FeedsController::class, 'blogJson']);
|
||||
Route::get('/feed.jf2', [FeedsController::class, 'blogJf2']);
|
||||
Route::get('/feed.rss', [FeedsController::class, 'blogRss'])->name('feed.blog.rss');
|
||||
Route::get('/feed.atom', [FeedsController::class, 'blogAtom'])->name('feed.blog.atom');
|
||||
Route::get('/feed.json', [FeedsController::class, 'blogJson'])->name('feed.blog.json');
|
||||
Route::get('/feed.jf2', [FeedsController::class, 'blogJf2'])->name('feed.blog.jf2');
|
||||
Route::get('/s/{id}', [ArticlesController::class, 'onlyIdInURL']);
|
||||
Route::get('/{year?}/{month?}', [ArticlesController::class, 'index'])->where(['year' => '[0-9]{4}', 'month' => '[0-9]{2}']);
|
||||
Route::get('/{year}/{month}/{slug}', [ArticlesController::class, 'show'])->where(['year' => '[0-9]{4}', 'month' => '[0-9]{2}']);
|
||||
|
|
@ -175,10 +175,10 @@ Route::prefix('blog')->group(function () {
|
|||
// Notes pages using NotesController
|
||||
Route::prefix('notes')->group(function () {
|
||||
Route::get('/', [NotesController::class, 'index']);
|
||||
Route::get('/feed.rss', [FeedsController::class, 'notesRss']);
|
||||
Route::get('/feed.atom', [FeedsController::class, 'notesAtom']);
|
||||
Route::get('/feed.json', [FeedsController::class, 'notesJson']);
|
||||
Route::get('/feed.jf2', [FeedsController::class, 'notesJf2']);
|
||||
Route::get('/feed.rss', [FeedsController::class, 'notesRss'])->name('feed.notes.rss');
|
||||
Route::get('/feed.atom', [FeedsController::class, 'notesAtom'])->name('feed.notes.atom');
|
||||
Route::get('/feed.json', [FeedsController::class, 'notesJson'])->name('feed.notes.json');
|
||||
Route::get('/feed.jf2', [FeedsController::class, 'notesJf2'])->name('feed.notes.jf2');
|
||||
Route::get('/new', [NotesController::class, 'create']);
|
||||
Route::get('/{id}', [NotesController::class, 'show']);
|
||||
Route::get('/tagged/{tag}', [NotesController::class, 'tagged']);
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env zsh
|
||||
|
||||
echo "Build script"
|
||||
Loading…
Reference in a new issue