diff --git a/experiments/init/datastar.js b/experiments/init/datastar.js new file mode 100644 index 0000000..be57e9d --- /dev/null +++ b/experiments/init/datastar.js @@ -0,0 +1,9 @@ +// Datastar v1.0.0-RC.6 +var nt=/🖕JS_DS🚀/.source,De=nt.slice(0,5),Ve=nt.slice(4),q="datastar-fetch",z="datastar-signal-patch";var de=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/([a-z])([0-9]+)/gi,"$1-$2").replace(/([0-9]+)([a-z])/gi,"$1-$2").toLowerCase();var rt=e=>de(e).replace(/-/g,"_");var re=e=>{try{return JSON.parse(e)}catch{return Function(`return (${e})`)()}},st={camel:e=>e.replace(/-[a-z]/g,t=>t[1].toUpperCase()),snake:e=>e.replace(/-/g,"_"),pascal:e=>e[0].toUpperCase()+st.camel(e.slice(1))},R=(e,t,n="camel")=>{for(let r of t.get("case")||[n])e=st[r]?.(e)||e;return e},Q=e=>`data-${e}`;var _=Object.hasOwn??Object.prototype.hasOwnProperty.call;var se=e=>e!==null&&typeof e=="object"&&(Object.getPrototypeOf(e)===Object.prototype||Object.getPrototypeOf(e)===null),it=e=>{for(let t in e)if(_(e,t))return!1;return!0},Z=(e,t)=>{for(let n in e){let r=e[n];se(r)||Array.isArray(r)?Z(r,t):e[n]=t(r)}},Te=e=>{let t={};for(let[n,r]of e){let s=n.split("."),i=s.pop(),a=s.reduce((o,c)=>o[c]??={},t);a[i]=r}return t};var Ae=[],Ie=[],Fe=0,Re=0,$e=0,qe,G,we=0,w=()=>{Fe++},M=()=>{--Fe||(ct(),Y())},V=e=>{qe=G,G=e},I=()=>{G=qe,qe=void 0},me=e=>It.bind(0,{previousValue:e,t:e,e:1}),Ge=Symbol("computed"),Ne=e=>{let t=$t.bind(0,{e:17,getter:e});return t[Ge]=1,t},E=e=>{let t={d:e,e:2};G&&Be(t,G),V(t),w();try{t.d()}finally{M(),I()}return dt.bind(0,t)},ct=()=>{for(;Re<$e;){let e=Ie[Re];Ie[Re++]=void 0,ut(e,e.e&=-65)}Re=0,$e=0},ot=e=>"getter"in e?lt(e):ft(e,e.t),lt=e=>{V(e),mt(e);try{let t=e.t;return t!==(e.t=e.getter(t))}finally{I(),pt(e)}},ft=(e,t)=>(e.e=1,e.previousValue!==(e.previousValue=t)),je=e=>{let t=e.e;if(!(t&64)){e.e=t|64;let n=e.r;n?je(n.o):Ie[$e++]=e}},ut=(e,t)=>{if(t&16||t&32&>(e.s,e)){V(e),mt(e),w();try{e.d()}finally{M(),I(),pt(e)}return}t&32&&(e.e=t&-33);let n=e.s;for(;n;){let r=n.c,s=r.e;s&64&&ut(r,r.e=s&-65),n=n.i}},It=(e,...t)=>{if(t.length){if(e.t!==(e.t=t[0])){e.e=17;let r=e.r;return r&&(qt(r),Fe||ct()),!0}return!1}let n=e.t;if(e.e&16&&ft(e,n)){let r=e.r;r&&Le(r)}return G&&Be(e,G),n},$t=e=>{let t=e.e;if(t&16||t&32&>(e.s,e)){if(lt(e)){let n=e.r;n&&Le(n)}}else t&32&&(e.e=t&-33);return G&&Be(e,G),e.t},dt=e=>{let t=e.s;for(;t;)t=xe(t,e);let n=e.r;n&&xe(n),e.e=0},Be=(e,t)=>{let n=t.a;if(n&&n.c===e)return;let r=n?n.i:t.s;if(r&&r.c===e){r.m=we,t.a=r;return}let s=e.p;if(s&&s.m===we&&s.o===t)return;let i=t.a=e.p={m:we,c:e,o:t,l:n,i:r,f:s};r&&(r.l=i),n?n.i=i:t.s=i,s?s.n=i:e.r=i},xe=(e,t=e.o)=>{let n=e.c,r=e.l,s=e.i,i=e.n,a=e.f;if(s?s.l=r:t.a=r,r?r.i=s:t.s=s,i?i.f=a:n.p=a,a)a.n=i;else if(!(n.r=i))if("getter"in n){let o=n.s;if(o){n.e=17;do o=xe(o,n);while(o)}}else"previousValue"in n||dt(n);return s},qt=e=>{let t=e.n,n;e:for(;;){let r=e.o,s=r.e;if(s&60?s&12?s&4?!(s&48)&&Gt(e,r)?(r.e=s|40,s&=1):s=0:r.e=s&-9|32:s=0:r.e=s|32,s&2&&je(r),s&1){let i=r.r;if(i){let a=(e=i).n;a&&(n={t,u:n},t=a);continue}}if(e=t){t=e.n;continue}for(;n;)if(e=n.t,n=n.u,e){t=e.n;continue e}break}},mt=e=>{we++,e.a=void 0,e.e=e.e&-57|4},pt=e=>{let t=e.a,n=t?t.i:e.s;for(;n;)n=xe(n,e);e.e&=-5},gt=(e,t)=>{let n,r=0,s=!1;e:for(;;){let i=e.c,a=i.e;if(t.e&16)s=!0;else if((a&17)===17){if(ot(i)){let o=i.r;o.n&&Le(o),s=!0}}else if((a&33)===33){(e.n||e.f)&&(n={t:e,u:n}),e=i.s,t=i,++r;continue}if(!s){let o=e.i;if(o){e=o;continue}}for(;r--;){let o=t.r,c=o.n;if(c?(e=n.t,n=n.u):e=o,s){if(ot(t)){c&&Le(o),t=e.o;continue}s=!1}else t.e&=-33;if(t=e.o,e.i){e=e.i;continue e}}return s}},Le=e=>{do{let t=e.o,n=t.e;(n&48)===32&&(t.e=n|16,n&2&&je(t))}while(e=e.n)},Gt=(e,t)=>{let n=t.a;for(;n;){if(n===e)return!0;n=n.l}return!1},ie=e=>{let t=X,n=e.split(".");for(let r of n){if(t==null||!_(t,r))return;t=t[r]}return t},Me=(e,t="")=>{let n=Array.isArray(e);if(n||se(e)){let r=n?[]:{};for(let i in e)r[i]=me(Me(e[i],`${t+i}.`));let s=me(0);return new Proxy(r,{get(i,a){if(!(a==="toJSON"&&!_(r,a)))return n&&a in Array.prototype?(s(),r[a]):typeof a=="symbol"?r[a]:((!_(r,a)||r[a]()==null)&&(r[a]=me(""),Y(t+a,""),s(s()+1)),r[a]())},set(i,a,o){let c=t+a;if(n&&a==="length"){let l=r[a]-o;if(r[a]=o,l>0){let f={};for(let d=o;d{if(e!==void 0&&t!==void 0&&Ae.push([e,t]),!Fe&&Ae.length){let n=Te(Ae);Ae.length=0,document.dispatchEvent(new CustomEvent(z,{detail:n}))}},C=(e,{ifMissing:t}={})=>{w();for(let n in e)e[n]==null?t||delete X[n]:ht(e[n],n,X,"",t);M()},S=(e,t)=>C(Te(e),t),ht=(e,t,n,r,s)=>{if(se(e)){_(n,t)&&(se(n[t])||Array.isArray(n[t]))||(n[t]={});for(let i in e)e[i]==null?s||delete n[t][i]:ht(e[i],i,n[t],`${r+t}.`,s)}else s&&_(n,t)||(n[t]=e)},at=e=>typeof e=="string"?RegExp(e.replace(/^\/|\/$/g,"")):e,D=({include:e=/.*/,exclude:t=/(?!)/}={},n=X)=>{let r=at(e),s=at(t),i=[],a=[[n,""]];for(;a.length;){let[o,c]=a.pop();for(let l in o){let f=c+l;se(o[l])?a.push([o[l],`${f}.`]):r.test(f)&&!s.test(f)&&i.push([f,ie(f)])}}return Te(i)},X=Me({});var W=e=>e instanceof HTMLElement||e instanceof SVGElement||e instanceof MathMLElement;var jt="https://data-star.dev/errors",pe=(e,t,n={})=>{Object.assign(n,e);let r=new Error,s=rt(t),i=new URLSearchParams({metadata:JSON.stringify(n)}).toString(),a=JSON.stringify(n,null,2);return r.message=`${t} +More info: ${jt}/${s}?${i} +Context: ${a}`,r},Oe=new Map,vt=new Map,bt=new Map,Et=new Proxy({},{get:(e,t)=>Oe.get(t)?.apply,has:(e,t)=>Oe.has(t),ownKeys:()=>Reflect.ownKeys(Oe),set:()=>!1,deleteProperty:()=>!1}),ge=new Map,Ce=[],We=new Set,p=e=>{Ce.push(e),Ce.length===1&&setTimeout(()=>{for(let t of Ce)We.add(t.name),vt.set(t.name,t);Ce.length=0,Jt(),We.clear()})},O=e=>{Oe.set(e.name,e)};document.addEventListener(q,e=>{let t=bt.get(e.detail.type);t&&t.apply({error:pe.bind(0,{plugin:{type:"watcher",name:t.name},element:{id:e.target.id,tag:e.target.tagName}})},e.detail.argsRaw)});var he=e=>{bt.set(e.name,e)},yt=e=>{for(let t of e){let n=ge.get(t);if(ge.delete(t)){for(let r of n.values())r();n.clear()}}},St=Q("ignore"),Bt=`[${St}]`,Tt=e=>e.hasAttribute(`${St}__self`)||!!e.closest(Bt),Pe=(e,t)=>{for(let n of e)if(!Tt(n))for(let r in n.dataset)At(n,r.replace(/[A-Z]/g,"-$&").toLowerCase(),n.dataset[r],t)},Wt=e=>{for(let{target:t,type:n,attributeName:r,addedNodes:s,removedNodes:i}of e)if(n==="childList"){for(let a of i)W(a)&&(yt([a]),yt(a.querySelectorAll("*")));for(let a of s)W(a)&&(Pe([a]),Pe(a.querySelectorAll("*")))}else if(n==="attributes"&&r.startsWith("data-")&&W(t)&&!Tt(t)){let a=r.slice(5),o=t.getAttribute(r);if(o===null){let c=ge.get(t);c&&(c.get(a)?.(),c.delete(a))}else At(t,a,o)}},Ut=new MutationObserver(Wt),Jt=(e=document.documentElement)=>{W(e)&&Pe([e],!0),Pe(e.querySelectorAll("*"),!0),Ut.observe(e,{subtree:!0,childList:!0,attributes:!0})},At=(e,t,n,r)=>{{let s=t,[i,...a]=s.split("__"),[o,c]=i.split(/:(.+)/),l=vt.get(o);if((!r||We.has(o))&&l){let f={el:e,rawKey:s,mods:new Map,error:pe.bind(0,{plugin:{type:"attribute",name:l.name},element:{id:e.id,tag:e.tagName},expression:{rawKey:s,key:c,value:n}}),key:c,value:n,rx:void 0},d=l.requirement&&(typeof l.requirement=="string"?l.requirement:l.requirement.key)||"allowed",x=l.requirement&&(typeof l.requirement=="string"?l.requirement:l.requirement.value)||"allowed";if(c){if(d==="denied")throw f.error("KeyNotAllowed")}else if(d==="must")throw f.error("KeyRequired");if(n){if(x==="denied")throw f.error("ValueNotAllowed")}else if(x==="must")throw f.error("ValueRequired");if(d==="exclusive"||x==="exclusive"){if(c&&n)throw f.error("KeyAndValueProvided");if(!c&&!n)throw f.error("KeyOrValueRequired")}if(n){let m;f.rx=(...h)=>(m||(m=Kt(n,{returnsValue:l.returnsValue,argNames:l.argNames})),m(e,...h))}for(let m of a){let[h,...v]=m.split(".");f.mods.set(h,new Set(v))}let u=l.apply(f);if(u){let m=ge.get(e);m?m.get(s)?.():(m=new Map,ge.set(e,m)),m.set(s,u)}}}},Kt=(e,{returnsValue:t=!1,argNames:n=[]}={})=>{let r="";if(t){let o=/(\/(\\\/|[^/])*\/|"(\\"|[^"])*"|'(\\'|[^'])*'|`(\\`|[^`])*`|\(\s*((function)\s*\(\s*\)|(\(\s*\))\s*=>)\s*(?:\{[\s\S]*?\}|[^;){]*)\s*\)\s*\(\s*\)|[^;])+/gm,c=e.trim().match(o);if(c){let l=c.length-1,f=c[l].trim();f.startsWith("return")||(c[l]=`return (${f});`),r=c.join(`; +`)}}else r=e.trim();let s=new Map,i=RegExp(`(?:${De})(.*?)(?:${Ve})`,"gm"),a=0;for(let o of r.matchAll(i)){let c=o[1],l=`__escaped${a++}`;s.set(l,c),r=r.replace(De+c+Ve,l)}r=r.replace(/\$\['([a-zA-Z_$\d][\w$]*)'\]/g,"$$$1").replace(/\$([a-zA-Z_\d]\w*(?:[.-]\w+)*)/g,(o,c)=>c.split(".").reduce((l,f)=>`${l}['${f}']`,"$")).replace(/\[(\$[a-zA-Z_\d]\w*)\]/g,(o,c)=>`[$['${c.slice(1)}']]`),r=r.replaceAll(/@(\w+)\(/g,'__action("$1",evt,');for(let[o,c]of s)r=r.replace(o,c);try{let o=Function("el","$","__action","evt",...n,r);return(c,...l)=>{let f=(d,x,...u)=>{let m=pe.bind(0,{plugin:{type:"action",name:d},element:{id:c.id,tag:c.tagName},expression:{fnContent:r,value:e}}),h=Et[d];if(h)return h({el:c,evt:x,error:m},...u);throw m("UndefinedAction")};try{return o(c,X,f,void 0,...l)}catch(d){throw console.error(d),pe({element:{id:c.id,tag:c.tagName},expression:{fnContent:r,value:e},error:d.message},"ExecuteExpression")}}}catch(o){throw console.error(o),pe({expression:{fnContent:r,value:e},error:o.message},"GenerateExpression")}};var P=new Map,ae=new Set,oe=new Map,ye=new Set,ce=document.createElement("div");ce.hidden=!0;var ve=Q("ignore-morph"),zt=`[${ve}]`,Ke=(e,t,n="outer")=>{if(W(e)&&W(t)&&e.hasAttribute(ve)&&t.hasAttribute(ve)||e.parentElement?.closest(zt))return;let r=document.createElement("div");r.append(t),document.body.insertAdjacentElement("afterend",ce);let s=e.querySelectorAll("[id]");for(let{id:o,tagName:c}of s)oe.has(o)?ye.add(o):oe.set(o,c);e instanceof Element&&e.id&&(oe.has(e.id)?ye.add(e.id):oe.set(e.id,e.tagName)),ae.clear();let i=r.querySelectorAll("[id]");for(let{id:o,tagName:c}of i)ae.has(o)?ye.add(o):oe.get(o)===c&&ae.add(o);for(let o of ye)ae.delete(o);oe.clear(),ye.clear(),P.clear();let a=n==="outer"?e.parentElement:e;wt(a,s),wt(r,i),Mt(a,r,n==="outer"?e:null,e.nextSibling),ce.remove()},Mt=(e,t,n=null,r=null)=>{e instanceof HTMLTemplateElement&&t instanceof HTMLTemplateElement&&(e=e.content,t=t.content),n??=e.firstChild;for(let s of t.childNodes){if(n&&n!==r){let i=Qt(s,n,r);if(i){if(i!==n){let a=n;for(;a&&a!==i;){let o=a;a=a.nextSibling,Je(o)}}Ue(i,s),n=i.nextSibling;continue}}if(s instanceof Element&&ae.has(s.id)){let i=document.getElementById(s.id),a=i;for(;a=a.parentNode;){let o=P.get(a);o&&(o.delete(s.id),o.size||P.delete(a))}xt(e,i,n),Ue(i,s),n=i.nextSibling;continue}if(P.has(s)){let i=document.createElement(s.tagName);e.insertBefore(i,n),Ue(i,s),n=i.nextSibling}else{let i=document.importNode(s,!0);e.insertBefore(i,n),n=i.nextSibling}}for(;n&&n!==r;){let s=n;n=n.nextSibling,Je(s)}},Qt=(e,t,n)=>{let r=null,s=e.nextSibling,i=0,a=0,o=P.get(e)?.size||0,c=t;for(;c&&c!==n;){if(Rt(c,e)){let l=!1,f=P.get(c),d=P.get(e);if(d&&f){for(let x of f)if(d.has(x)){l=!0;break}}if(l)return c;if(!r&&!P.has(c)){if(!o)return c;r=c}}if(a+=P.get(c)?.size||0,a>o)break;r===null&&s&&Rt(c,s)&&(i++,s=s.nextSibling,i>=2&&(r=void 0)),c=c.nextSibling}return r||null},Rt=(e,t)=>e.nodeType===t.nodeType&&e.tagName===t.tagName&&(!e.id||e.id===t.id),Je=e=>{P.has(e)?xt(ce,e,null):e.parentNode?.removeChild(e)},xt=Je.call.bind(ce.moveBefore??ce.insertBefore),Zt=Q("preserve-attr"),Ue=(e,t)=>{let n=t.nodeType;if(n===1){let r=e,s=t;if(r.hasAttribute(ve)&&s.hasAttribute(ve))return e;r instanceof HTMLInputElement&&s instanceof HTMLInputElement&&s.type!=="file"?s.getAttribute("value")!==r.getAttribute("value")&&(r.value=s.getAttribute("value")??""):r instanceof HTMLTextAreaElement&&s instanceof HTMLTextAreaElement&&(s.value!==r.value&&(r.value=s.value),r.firstChild&&r.firstChild.nodeValue!==s.value&&(r.firstChild.nodeValue=s.value));let i=(t.getAttribute(Zt)??"").split(" ");for(let{name:a,value:o}of s.attributes)r.getAttribute(a)!==o&&!i.includes(a)&&r.setAttribute(a,o);for(let a=r.attributes.length-1;a>=0;a--){let{name:o}=r.attributes[a];!s.hasAttribute(o)&&!i.includes(o)&&r.removeAttribute(o)}r.isEqualNode(s)||Mt(r,s)}return(n===8||n===3)&&e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e},wt=(e,t)=>{for(let n of t)if(ae.has(n.id)){let r=n;for(;r&&r!==e;){let s=P.get(r);s||(s=new Set,P.set(r,s)),s.add(n.id),r=r.parentElement}}};O({name:"peek",apply(e,t){V();try{return t()}finally{I()}}});O({name:"setAll",apply(e,t,n){V();let r=D(n);Z(r,()=>t),C(r),I()}});O({name:"toggleAll",apply(e,t){V();let n=D(t);Z(n,r=>!r),C(n),I()}});var He=new WeakMap,be=(e,t)=>O({name:e,apply:async({el:n,evt:r,error:s},i,{selector:a,headers:o,contentType:c="json",filterSignals:{include:l=/.*/,exclude:f=/(^|\.)_/}={},openWhenHidden:d=!1,retryInterval:x=1e3,retryScaler:u=2,retryMaxWaitMs:m=3e4,retryMaxCount:h=10,requestCancellation:v="auto"}={})=>{let L=v instanceof AbortController?v:new AbortController,A=v==="disabled";if(!A){let T=He.get(n);T&&(T.abort(),await Promise.resolve())}!A&&!(v instanceof AbortController)&&He.set(n,L);try{let T=new MutationObserver(j=>{for(let H of j)for(let B of H.removedNodes)B===n&&(L.abort(),K())});n.parentNode&&T.observe(n.parentNode,{childList:!0});let K=()=>{T.disconnect()};try{if(!i?.length)throw s("FetchNoUrlProvided",{action:O});let j={Accept:"text/event-stream, text/html, application/json","Datastar-Request":!0};c==="json"&&(j["Content-Type"]="application/json");let H=Object.assign({},j,o),B={method:t,headers:H,openWhenHidden:d,retryInterval:x,retryScaler:u,retryMaxWaitMs:m,retryMaxCount:h,signal:L.signal,onopen:async g=>{g.status>=400&&ee(Yt,n,{status:g.status.toString()})},onmessage:g=>{if(!g.event.startsWith("datastar"))return;let $=g.event,b={};for(let N of g.data.split(` +`)){let y=N.indexOf(" "),k=N.slice(0,y),ue=N.slice(y+1);(b[k]||=[]).push(ue)}let F=Object.fromEntries(Object.entries(b).map(([N,y])=>[N,y.join(` +`)]));ee($,n,F)},onerror:g=>{if(Lt(g))throw g("FetchExpectedTextEventStream",{url:i});g&&(console.error(g.message),ee(Xt,n,{message:g.message}))}},fe=new URL(i,document.baseURI),ne=new URLSearchParams(fe.search);if(c==="json"){let g=JSON.stringify(D({include:l,exclude:f}));t==="GET"?ne.set("datastar",g):B.body=g}else if(c==="form"){let g=a?document.querySelector(a):n.closest("form");if(!g)throw s("FetchFormNotFound",{action:O,selector:a});if(!g.checkValidity()){g.reportValidity(),K();return}let $=new FormData(g),b=n;if(n===g&&r instanceof SubmitEvent)b=r.submitter;else{let y=k=>k.preventDefault();g.addEventListener("submit",y),K=()=>{g.removeEventListener("submit",y),T.disconnect()}}if(b instanceof HTMLButtonElement){let y=b.getAttribute("name");y&&$.append(y,b.value)}let F=g.getAttribute("enctype")==="multipart/form-data";F||(H["Content-Type"]="application/x-www-form-urlencoded");let N=new URLSearchParams($);if(t==="GET")for(let[y,k]of N)ne.append(y,k);else F?B.body=$:B.body=N}else throw s("FetchInvalidContentType",{action:O,contentType:c});ee(ze,n,{}),fe.search=ne.toString();try{await on(fe.toString(),n,B)}catch(g){if(!Lt(g))throw s("FetchFailed",{method:t,url:i,error:g.message})}}finally{ee(Qe,n,{}),K()}}finally{He.get(n)===L&&He.delete(n)}}});be("delete","DELETE");be("get","GET");be("patch","PATCH");be("post","POST");be("put","PUT");var ze="started",Qe="finished",Yt="error",Xt="retrying",en="retries-failed",ee=(e,t,n)=>document.dispatchEvent(new CustomEvent(q,{detail:{type:e,el:t,argsRaw:n}})),Lt=e=>`${e}`.includes("text/event-stream"),tn=async(e,t)=>{let n=e.getReader(),r=await n.read();for(;!r.done;)t(r.value),r=await n.read()},nn=e=>{let t,n,r,s=!1;return i=>{t?t=sn(t,i):(t=i,n=0,r=-1);let a=t.length,o=0;for(;n{let r=Ft(),s=new TextDecoder;return(i,a)=>{if(!i.length)n?.(r),r=Ft();else if(a>0){let o=s.decode(i.subarray(0,a)),c=a+(i[a+1]===32?2:1),l=s.decode(i.subarray(c));switch(o){case"data":r.data=r.data?`${r.data} +${l}`:l;break;case"event":r.event=l;break;case"id":e(r.id=l);break;case"retry":{let f=+l;Number.isNaN(f)||t(r.retry=f);break}}}}},sn=(e,t)=>{let n=new Uint8Array(e.length+t.length);return n.set(e),n.set(t,e.length),n},Ft=()=>({data:"",event:"",id:"",retry:void 0}),on=(e,t,{signal:n,headers:r,onopen:s,onmessage:i,onclose:a,onerror:o,openWhenHidden:c,fetch:l,retryInterval:f=1e3,retryScaler:d=2,retryMaxWaitMs:x=3e4,retryMaxCount:u=10,overrides:m,...h})=>new Promise((v,L)=>{let A={...r},T,K=()=>{T.abort(),document.hidden||$()};c||document.addEventListener("visibilitychange",K);let j=0,H=()=>{document.removeEventListener("visibilitychange",K),clearTimeout(j),T.abort()};n?.addEventListener("abort",()=>{H(),v()});let B=l||window.fetch,fe=s||(()=>{}),ne=0,g=f,$=async()=>{T=new AbortController;try{let b=await B(e,{...h,headers:A,signal:T.signal});ne=0,f=g,await fe(b);let F=async(y,k,ue,Ee,...Vt)=>{let tt={[ue]:await k.text()};for(let ke of Vt){let _e=k.headers.get(`datastar-${de(ke)}`);if(Ee){let Se=Ee[ke];Se&&(_e=typeof Se=="string"?Se:JSON.stringify(Se))}_e&&(tt[ke]=_e)}ee(y,t,tt),H(),v()},N=b.headers.get("Content-Type");if(N?.includes("text/html"))return await F("datastar-patch-elements",b,"elements",m,"selector","mode","useViewTransition");if(N?.includes("application/json"))return await F("datastar-patch-signals",b,"signals",m,"onlyIfMissing");if(N?.includes("text/javascript")){let y=document.createElement("script"),k=b.headers.get("datastar-script-attributes");if(k)for(let[ue,Ee]of Object.entries(JSON.parse(k)))y.setAttribute(ue,Ee);y.textContent=await b.text(),document.head.appendChild(y),H();return}await tn(b.body,nn(rn(y=>{y?A["last-event-id"]=y:delete A["last-event-id"]},y=>{g=f=y},i))),a?.(),H(),v()}catch(b){if(!T.signal.aborted)try{let F=o?.(b)||f;clearTimeout(j),j=setTimeout($,F),f=Math.min(f*d,x),++ne>=u?(ee(en,t,{}),H(),L("Max retries reached.")):console.error(`Datastar failed to reach ${e.toString()} retrying in ${F}ms.`)}catch(F){H(),L(F)}}};$()});p({name:"attr",requirement:{value:"must"},returnsValue:!0,apply({el:e,key:t,rx:n}){let r=(o,c)=>{c===""||c===!0?e.setAttribute(o,""):c===!1||c==null?e.removeAttribute(o):typeof c=="string"?e.setAttribute(o,c):e.setAttribute(o,JSON.stringify(c))},s=t?()=>{i.disconnect();let o=n();r(t,o),i.observe(e,{attributeFilter:[t]})}:()=>{i.disconnect();let o=n(),c=Object.keys(o);for(let l of c)r(l,o[l]);i.observe(e,{attributeFilter:c})},i=new MutationObserver(s),a=E(s);return()=>{i.disconnect(),a()}}});var an=/^data:(?[^;]+);base64,(?.*)$/,Nt=Symbol("empty"),Ct=Q("bind");p({name:"bind",requirement:"exclusive",apply({el:e,key:t,mods:n,value:r,error:s}){let i=t!=null?R(t,n):r,a=(u,m)=>m==="number"?+u.value:u.value,o=u=>{e.value=`${u}`};if(e instanceof HTMLInputElement)switch(e.type){case"range":case"number":a=(u,m)=>m==="string"?u.value:+u.value;break;case"checkbox":a=(u,m)=>u.value!=="on"?m==="boolean"?u.checked:u.checked?u.value:"":m==="string"?u.checked?u.value:"":u.checked,o=u=>{e.checked=typeof u=="string"?u===e.value:u};break;case"radio":e.getAttribute("name")?.length||e.setAttribute("name",i),a=(u,m)=>u.checked?m==="number"?+u.value:u.value:Nt,o=u=>{e.checked=u===(typeof u=="number"?+e.value:e.value)};break;case"file":{let u=()=>{let m=[...e.files||[]],h=[];Promise.all(m.map(v=>new Promise(L=>{let A=new FileReader;A.onload=()=>{if(typeof A.result!="string")throw s("InvalidFileResultType",{resultType:typeof A.result});let T=A.result.match(an);if(!T?.groups)throw s("InvalidDataUri",{result:A.result});h.push({name:v.name,contents:T.groups.contents,mime:T.groups.mime})},A.onloadend=()=>L(),A.readAsDataURL(v)}))).then(()=>{S([[i,h]])})};return e.addEventListener("change",u),e.addEventListener("input",u),()=>{e.removeEventListener("change",u),e.removeEventListener("input",u)}}}else if(e instanceof HTMLSelectElement){if(e.multiple){let u=new Map;a=m=>[...m.selectedOptions].map(h=>{let v=u.get(h.value);return v==="string"||v==null?h.value:+h.value}),o=m=>{for(let h of e.options)m.includes(h.value)?(u.set(h.value,"string"),h.selected=!0):m.includes(+h.value)?(u.set(h.value,"number"),h.selected=!0):h.selected=!1}}}else e instanceof HTMLTextAreaElement||(a=u=>"value"in u?u.value:u.getAttribute("value"),o=u=>{"value"in e?e.value=u:e.setAttribute("value",u)});let c=ie(i),l=typeof c,f=i;if(Array.isArray(c)&&!(e instanceof HTMLSelectElement&&e.multiple)){let u=t||r,m=document.querySelectorAll(`[${Ct}\\:${CSS.escape(u)}],[${Ct}="${CSS.escape(u)}"]`),h=[],v=0;for(let L of m){if(h.push([`${f}.${v}`,a(L,"none")]),e===L)break;v++}S(h,{ifMissing:!0}),f=`${f}.${v}`}else S([[f,a(e,l)]],{ifMissing:!0});let d=()=>{let u=ie(f);if(u!=null){let m=a(e,typeof u);m!==Nt&&S([[f,m]])}};e.addEventListener("input",d),e.addEventListener("change",d);let x=E(()=>{o(ie(f))});return()=>{x(),e.removeEventListener("input",d),e.removeEventListener("change",d)}}});p({name:"class",requirement:{value:"must"},returnsValue:!0,apply({key:e,el:t,mods:n,rx:r}){e&&(e=R(e,n,"kebab"));let s=()=>{i.disconnect();let o=e?{[e]:r()}:r();for(let c in o){let l=c.split(/\s+/).filter(f=>f.length>0);if(o[c])for(let f of l)t.classList.contains(f)||t.classList.add(f);else for(let f of l)t.classList.contains(f)&&t.classList.remove(f)}i.observe(t,{attributeFilter:["class"]})},i=new MutationObserver(s),a=E(s);return()=>{i.disconnect(),a();let o=e?{[e]:r()}:r();for(let c in o){let l=c.split(/\s+/).filter(f=>f.length>0);for(let f of l)t.classList.remove(f)}}}});p({name:"computed",requirement:{value:"must"},returnsValue:!0,apply({key:e,mods:t,rx:n,error:r}){if(e)S([[R(e,t),Ne(n)]]);else{let s=Object.assign({},n());Z(s,i=>{if(typeof i=="function")return Ne(i);throw r("ComputedExpectedFunction")}),C(s)}}});p({name:"effect",requirement:{key:"denied",value:"must"},apply:({rx:e})=>E(e)});p({name:"indicator",requirement:"exclusive",apply({el:e,key:t,mods:n,value:r}){let s=t!=null?R(t,n):r;S([[s,!1]]);let i=a=>{let{type:o,el:c}=a.detail;if(c===e)switch(o){case ze:S([[s,!0]]);break;case Qe:S([[s,!1]]);break}};return document.addEventListener(q,i),()=>{S([[s,!1]]),document.removeEventListener(q,i)}}});p({name:"json-signals",requirement:{key:"denied"},apply({el:e,value:t,mods:n}){let r=n.has("terse")?0:2,s={};t&&(s=re(t));let i=()=>{a.disconnect(),e.textContent=JSON.stringify(D(s),null,r),a.observe(e,{childList:!0,characterData:!0,subtree:!0})},a=new MutationObserver(i),o=E(i);return()=>{a.disconnect(),o()}}});var U=e=>{if(!e||e.size<=0)return 0;for(let t of e){if(t.endsWith("ms"))return+t.replace("ms","");if(t.endsWith("s"))return+t.replace("s","")*1e3;try{return Number.parseFloat(t)}catch{}}return 0},te=(e,t,n=!1)=>e?e.has(t.toLowerCase()):n;var Ze=(e,t)=>(...n)=>{setTimeout(()=>{e(...n)},t)},cn=(e,t,n=!1,r=!0)=>{let s=0;return(...i)=>{s&&clearTimeout(s),n&&!s&&e(...i),s=setTimeout(()=>{r&&e(...i),s&&clearTimeout(s),s=0},t)}},ln=(e,t,n=!0,r=!1)=>{let s=!1;return(...i)=>{s||(n&&e(...i),s=!0,setTimeout(()=>{r&&e(...i),s=!1},t))}},le=(e,t)=>{let n=t.get("delay");if(n){let i=U(n);e=Ze(e,i)}let r=t.get("debounce");if(r){let i=U(r),a=te(r,"leading",!1),o=!te(r,"notrailing",!1);e=cn(e,i,a,o)}let s=t.get("throttle");if(s){let i=U(s),a=!te(s,"noleading",!1),o=te(s,"trailing",!1);e=ln(e,i,a,o)}return e};var Ye=!!document.startViewTransition,J=(e,t)=>{if(t.has("viewtransition")&&Ye){let n=e;e=(...r)=>document.startViewTransition(()=>n(...r))}return e};p({name:"on",requirement:"must",argNames:["evt"],apply({el:e,key:t,mods:n,rx:r}){let s=e;n.has("window")&&(s=window);let i=c=>{c&&(n.has("prevent")&&c.preventDefault(),n.has("stop")&&c.stopPropagation()),w(),r(c),M()};i=J(i,n),i=le(i,n);let a={capture:n.has("capture"),passive:n.has("passive"),once:n.has("once")};if(n.has("outside")){s=document;let c=i;i=l=>{e.contains(l?.target)||c(l)}}let o=R(t,n,"kebab");if((o===q||o===z)&&(s=document),e instanceof HTMLFormElement&&o==="submit"){let c=i;i=l=>{l?.preventDefault(),c(l)}}return s.addEventListener(o,i,a),()=>{s.removeEventListener(o,i)}}});var Xe=new WeakSet;p({name:"on-intersect",requirement:{key:"denied",value:"must"},apply({el:e,mods:t,rx:n}){let r=()=>{w(),n(),M()};r=J(r,t),r=le(r,t);let s={threshold:0};t.has("full")?s.threshold=1:t.has("half")&&(s.threshold=.5);let i=new IntersectionObserver(a=>{for(let o of a)o.isIntersecting&&(r(),i&&Xe.has(e)&&i.disconnect())},s);return i.observe(e),t.has("once")&&Xe.add(e),()=>{t.has("once")||Xe.delete(e),i&&(i.disconnect(),i=null)}}});p({name:"on-interval",requirement:{key:"denied",value:"must"},apply({mods:e,rx:t}){let n=()=>{w(),t(),M()};n=J(n,e);let r=1e3,s=e.get("duration");s&&(r=U(s),te(s,"leading",!1)&&n());let i=setInterval(n,r);return()=>{clearInterval(i)}}});p({name:"init",requirement:{key:"denied",value:"must"},apply({rx:e,mods:t}){let n=()=>{w(),e(),M()};n=J(n,t);let r=0,s=t.get("delay");s&&(r=U(s),r>0&&(n=Ze(n,r))),n()}});p({name:"on-signal-patch",requirement:{value:"must"},argNames:["patch"],returnsValue:!0,apply({el:e,key:t,mods:n,rx:r,error:s}){if(t&&t!=="filter")throw s("KeyNotAllowed");let i=e.getAttribute("data-on-signal-patch-filter"),a={};i&&(a=re(i));let o=le(c=>{let l=D(a,c.detail);it(l)||(w(),r(l),M())},n);return document.addEventListener(z,o),()=>{document.removeEventListener(z,o)}}});p({name:"ref",requirement:"exclusive",apply({el:e,key:t,mods:n,value:r}){let s=t!=null?R(t,n):r;S([[s,e]])}});var Ot="none",Pt="display";p({name:"show",requirement:{key:"denied",value:"must"},returnsValue:!0,apply({el:e,rx:t}){let n=()=>{r.disconnect(),t()?e.style.display===Ot&&e.style.removeProperty(Pt):e.style.setProperty(Pt,Ot),r.observe(e,{attributeFilter:["style"]})},r=new MutationObserver(n),s=E(n);return()=>{r.disconnect(),s()}}});p({name:"signals",returnsValue:!0,apply({key:e,mods:t,rx:n}){let r=t.has("ifmissing");if(e)e=R(e,t),S([[e,n?.()]],{ifMissing:r});else{let s=Object.assign({},n?.());C(s,{ifMissing:r})}}});p({name:"style",requirement:{value:"must"},returnsValue:!0,apply({key:e,el:t,rx:n}){let{style:r}=t,s=new Map,i=(l,f)=>{let d=s.get(l);!f&&f!==0?d!==void 0&&(d?r.setProperty(l,d):r.removeProperty(l)):(d===void 0&&s.set(l,r.getPropertyValue(l)),r.setProperty(l,String(f)))},a=()=>{if(o.disconnect(),e)i(e,n());else{let l=n();for(let[f,d]of s)f in l||(d?r.setProperty(f,d):r.removeProperty(f));for(let f in l)i(de(f),l[f])}o.observe(t,{attributeFilter:["style"]})},o=new MutationObserver(a),c=E(a);return()=>{o.disconnect(),c();for(let[l,f]of s)f?r.setProperty(l,f):r.removeProperty(l)}}});p({name:"text",requirement:{key:"denied",value:"must"},returnsValue:!0,apply({el:e,rx:t}){let n=()=>{r.disconnect(),e.textContent=`${t()}`,r.observe(e,{childList:!0,characterData:!0,subtree:!0})},r=new MutationObserver(n),s=E(n);return()=>{r.disconnect(),s()}}});he({name:"datastar-patch-elements",apply(e,{elements:t="",selector:n="",mode:r="outer",useViewTransition:s}){switch(r){case"remove":case"outer":case"inner":case"replace":case"prepend":case"append":case"before":case"after":break;default:throw e.error("PatchElementsInvalidMode",{mode:r})}if(!n&&r!=="outer"&&r!=="replace")throw e.error("PatchElementsExpectedSelector");let i={mode:r,selector:n,elements:t,useViewTransition:s?.trim()==="true"};Ye&&s?document.startViewTransition(()=>Ht(e,i)):Ht(e,i)}});var Ht=({error:e},{elements:t,selector:n,mode:r})=>{let s=t.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,""),i=/<\/html>/.test(s),a=/<\/head>/.test(s),o=/<\/body>/.test(s),c=new DOMParser().parseFromString(i||a||o?t:``,"text/html"),l=document.createDocumentFragment();if(i?l.appendChild(c.documentElement):a&&o?(l.appendChild(c.head),l.appendChild(c.body)):a?l.appendChild(c.head):o?l.appendChild(c.body):l=c.querySelector("template").content,!n&&(r==="outer"||r==="replace"))for(let f of l.children){let d;if(f instanceof HTMLHtmlElement)d=document.documentElement;else if(f instanceof HTMLBodyElement)d=document.body;else if(f instanceof HTMLHeadElement)d=document.head;else if(d=document.getElementById(f.id),!d){console.warn(e("PatchElementsNoTargetsFound"),{element:{id:f.id}});continue}_t(r,f,[d])}else{let f=document.querySelectorAll(n);if(!f.length){console.warn(e("PatchElementsNoTargetsFound"),{selector:n});return}_t(r,l,f)}},et=new WeakSet;for(let e of document.querySelectorAll("script"))et.add(e);var Dt=e=>{let t=e instanceof HTMLScriptElement?[e]:e.querySelectorAll("script");for(let n of t)if(!et.has(n)){let r=document.createElement("script");for(let{name:s,value:i}of n.attributes)r.setAttribute(s,i);r.text=n.text,n.replaceWith(r),et.add(r)}},kt=(e,t,n)=>{for(let r of e){let s=t.cloneNode(!0);Dt(s),r[n](s)}},_t=(e,t,n)=>{switch(e){case"remove":for(let r of n)r.remove();break;case"outer":case"inner":for(let r of n)Ke(r,t.cloneNode(!0),e),Dt(r);break;case"replace":kt(n,t,"replaceWith");break;case"prepend":case"append":case"before":case"after":kt(n,t,e)}};he({name:"datastar-patch-signals",apply({error:e},{signals:t,onlyIfMissing:n}){if(t){let r=n?.trim()==="true";C(re(t),{ifMissing:r})}else throw e("PatchSignalsExpectedSignals")}});export{O as action,Et as actions,p as attribute,w as beginBatch,Ne as computed,E as effect,M as endBatch,D as filtered,ie as getPath,C as mergePatch,S as mergePaths,Ke as morph,X as root,me as signal,V as startPeeking,I as stopPeeking,he as watcher}; +//# sourceMappingURL=datastar.js.map diff --git a/experiments/init/index.html b/experiments/init/index.html new file mode 100644 index 0000000..c10f532 --- /dev/null +++ b/experiments/init/index.html @@ -0,0 +1,23 @@ + + + + + + + Hypermedia + + + + + + + +

Hypermedia as the Engine of Application State

+ +
+
+
+ + + + diff --git a/experiments/init/init.html b/experiments/init/init.html new file mode 100644 index 0000000..c82e716 --- /dev/null +++ b/experiments/init/init.html @@ -0,0 +1,9 @@ +
+
+

I am the init

+
+

Init record content

+
+

Init record footer

+
+
diff --git a/experiments/init/init2.html b/experiments/init/init2.html new file mode 100644 index 0000000..2d8013d --- /dev/null +++ b/experiments/init/init2.html @@ -0,0 +1,9 @@ +
+
+

I am the init 2

+
+

Init record content

+
+

Init record footer

+
+
diff --git a/experiments/init/pico.blue.css b/experiments/init/pico.blue.css new file mode 100644 index 0000000..e76f23b --- /dev/null +++ b/experiments/init/pico.blue.css @@ -0,0 +1,2808 @@ +@charset "UTF-8"; +/*! + * Pico CSS ✨ v2.0.6 (https://picocss.com) + * Copyright 2019-2024 - Licensed under MIT + */ +/** + * Styles + */ + +@view-transition { + navigation: auto; +} + + +:root { + --pico-font-family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --pico-font-family-sans-serif: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji); + --pico-font-family-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--pico-font-family-emoji); + --pico-font-family: var(--pico-font-family-sans-serif); + --pico-line-height: 1.5; + --pico-font-weight: 400; + --pico-font-size: 100%; + --pico-text-underline-offset: 0.1rem; + --pico-border-radius: 0.25rem; + --pico-border-width: 0.0625rem; + --pico-outline-width: 0.125rem; + --pico-transition: 0.2s ease-in-out; + --pico-spacing: 1rem; + --pico-typography-spacing-vertical: 1rem; + --pico-block-spacing-vertical: var(--pico-spacing); + --pico-block-spacing-horizontal: var(--pico-spacing); + --pico-grid-column-gap: var(--pico-spacing); + --pico-grid-row-gap: var(--pico-spacing); + --pico-form-element-spacing-vertical: 0.75rem; + --pico-form-element-spacing-horizontal: 1rem; + --pico-group-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); + --pico-group-box-shadow-focus-with-input: 0 0 0 0.0625rem var(--pico-form-element-border-color); + --pico-modal-overlay-backdrop-filter: blur(0.375rem); + --pico-nav-element-spacing-vertical: 1rem; + --pico-nav-element-spacing-horizontal: 0.5rem; + --pico-nav-link-spacing-vertical: 0.5rem; + --pico-nav-link-spacing-horizontal: 0.5rem; + --pico-nav-breadcrumb-divider: ">"; + --pico-icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-loading: url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E"); +} +@media (min-width: 576px) { + :root { + --pico-font-size: 106.25%; + } +} +@media (min-width: 768px) { + :root { + --pico-font-size: 112.5%; + } +} +@media (min-width: 1024px) { + :root { + --pico-font-size: 118.75%; + } +} +@media (min-width: 1280px) { + :root { + --pico-font-size: 125%; + } +} +@media (min-width: 1536px) { + :root { + --pico-font-size: 131.25%; + } +} + +a { + --pico-text-decoration: underline; +} +a.secondary, a.contrast { + --pico-text-decoration: underline; +} + +small { + --pico-font-size: 0.875em; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + --pico-font-weight: 700; +} + +h1 { + --pico-font-size: 2rem; + --pico-line-height: 1.125; + --pico-typography-spacing-top: 3rem; +} + +h2 { + --pico-font-size: 1.75rem; + --pico-line-height: 1.15; + --pico-typography-spacing-top: 2.625rem; +} + +h3 { + --pico-font-size: 1.5rem; + --pico-line-height: 1.175; + --pico-typography-spacing-top: 2.25rem; +} + +h4 { + --pico-font-size: 1.25rem; + --pico-line-height: 1.2; + --pico-typography-spacing-top: 1.874rem; +} + +h5 { + --pico-font-size: 1.125rem; + --pico-line-height: 1.225; + --pico-typography-spacing-top: 1.6875rem; +} + +h6 { + --pico-font-size: 1rem; + --pico-line-height: 1.25; + --pico-typography-spacing-top: 1.5rem; +} + +thead th, +thead td, +tfoot th, +tfoot td { + --pico-font-weight: 600; + --pico-border-width: 0.1875rem; +} + +pre, +code, +kbd, +samp { + --pico-font-family: var(--pico-font-family-monospace); +} + +kbd { + --pico-font-weight: bolder; +} + +input:not([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]), +:where(select, textarea) { + --pico-outline-width: 0.0625rem; +} + +[type=search] { + --pico-border-radius: 5rem; +} + +[type=checkbox], +[type=radio] { + --pico-border-width: 0.125rem; +} + +[type=checkbox][role=switch] { + --pico-border-width: 0.1875rem; +} + +details.dropdown summary:not([role=button]) { + --pico-outline-width: 0.0625rem; +} + +nav details.dropdown summary:focus-visible { + --pico-outline-width: 0.125rem; +} + +[role=search] { + --pico-border-radius: 5rem; +} + +[role=search]:has(button.secondary:focus, +[type=submit].secondary:focus, +[type=button].secondary:focus, +[role=button].secondary:focus), +[role=group]:has(button.secondary:focus, +[type=submit].secondary:focus, +[type=button].secondary:focus, +[role=button].secondary:focus) { + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} +[role=search]:has(button.contrast:focus, +[type=submit].contrast:focus, +[type=button].contrast:focus, +[role=button].contrast:focus), +[role=group]:has(button.contrast:focus, +[type=submit].contrast:focus, +[type=button].contrast:focus, +[role=button].contrast:focus) { + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); +} +[role=search] button, +[role=search] [type=submit], +[role=search] [type=button], +[role=search] [role=button], +[role=group] button, +[role=group] [type=submit], +[role=group] [type=button], +[role=group] [role=button] { + --pico-form-element-spacing-horizontal: 2rem; +} + +details summary[role=button]:not(.outline)::after { + filter: brightness(0) invert(1); +} + +[aria-busy=true]:not(input, select, textarea):is(button, [type=submit], [type=button], [type=reset], [role=button]):not(.outline)::before { + filter: brightness(0) invert(1); +} + +/** + * Color schemes + */ +[data-theme=light], +:root:not([data-theme=dark]) { + --pico-background-color: #fff; + --pico-color: #373c44; + --pico-text-selection-color: rgba(116, 139, 248, 0.25); + --pico-muted-color: #646b79; + --pico-muted-border-color: #e7eaf0; + --pico-primary: #2060df; + --pico-primary-background: #2060df; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(32, 96, 223, 0.5); + --pico-primary-hover: #184eb8; + --pico-primary-hover-background: #1d59d0; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(116, 139, 248, 0.5); + --pico-primary-inverse: #fff; + --pico-secondary: #5d6b89; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(93, 107, 137, 0.5); + --pico-secondary-hover: #48536b; + --pico-secondary-hover-background: #48536b; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(93, 107, 137, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #181c25; + --pico-contrast-background: #181c25; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(24, 28, 37, 0.5); + --pico-contrast-hover: #000; + --pico-contrast-hover-background: #000; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-secondary-hover); + --pico-contrast-focus: rgba(93, 107, 137, 0.25); + --pico-contrast-inverse: #fff; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024), 0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03), 0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036), 0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302), 0.5rem 1rem 6rem rgba(129, 145, 181, 0.06), 0 0 0 0.0625rem rgba(129, 145, 181, 0.015); + --pico-h1-color: #2d3138; + --pico-h2-color: #373c44; + --pico-h3-color: #424751; + --pico-h4-color: #4d535e; + --pico-h5-color: #5c6370; + --pico-h6-color: #646b79; + --pico-mark-background-color: #fde7c0; + --pico-mark-color: #0f1114; + --pico-ins-color: #1d6a54; + --pico-del-color: #883935; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #f3f5f7; + --pico-code-color: #646b79; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #fbfcfc; + --pico-form-element-selected-background-color: #dfe3eb; + --pico-form-element-border-color: #cfd5e2; + --pico-form-element-color: #23262c; + --pico-form-element-placeholder-color: var(--pico-muted-color); + --pico-form-element-active-background-color: #fff; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #b86a6b; + --pico-form-element-invalid-active-border-color: #c84f48; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #4c9b8a; + --pico-form-element-valid-active-border-color: #279977; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #bfc7d9; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #dfe3eb; + --pico-range-active-border-color: #bfc7d9; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: var(--pico-background-color); + --pico-card-border-color: var(--pico-muted-border-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #fbfcfc; + --pico-dropdown-background-color: #fff; + --pico-dropdown-border-color: #eff1f4; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #eff1f4; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(232, 234, 237, 0.75); + --pico-progress-background-color: #dfe3eb; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 155, 138)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200, 79, 72)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: light; +} +[data-theme=light] input:is([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]), +:root:not([data-theme=dark]) input:is([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]) { + --pico-form-element-focus-color: var(--pico-primary-focus); +} + +@media only screen and (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --pico-background-color: #13171f; + --pico-color: #c2c7d0; + --pico-text-selection-color: rgba(137, 153, 249, 0.1875); + --pico-muted-color: #7b8495; + --pico-muted-border-color: #202632; + --pico-primary: #8999f9; + --pico-primary-background: #2060df; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(137, 153, 249, 0.5); + --pico-primary-hover: #aeb5fb; + --pico-primary-hover-background: #3c71f7; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(137, 153, 249, 0.375); + --pico-primary-inverse: #fff; + --pico-secondary: #969eaf; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(150, 158, 175, 0.5); + --pico-secondary-hover: #b3b9c5; + --pico-secondary-hover-background: #5d6b89; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(144, 158, 190, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #dfe3eb; + --pico-contrast-background: #eff1f4; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(223, 227, 235, 0.5); + --pico-contrast-hover: #fff; + --pico-contrast-hover-background: #fff; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-contrast-hover); + --pico-contrast-focus: rgba(207, 213, 226, 0.25); + --pico-contrast-inverse: #000; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); + --pico-h1-color: #f0f1f3; + --pico-h2-color: #e0e3e7; + --pico-h3-color: #c2c7d0; + --pico-h4-color: #b3b9c5; + --pico-h5-color: #a4acba; + --pico-h6-color: #8891a4; + --pico-mark-background-color: #014063; + --pico-mark-color: #fff; + --pico-ins-color: #62af9a; + --pico-del-color: #ce7e7b; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #1a1f28; + --pico-code-color: #8891a4; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #1c212c; + --pico-form-element-selected-background-color: #2a3140; + --pico-form-element-border-color: #2a3140; + --pico-form-element-color: #e0e3e7; + --pico-form-element-placeholder-color: #8891a4; + --pico-form-element-active-background-color: #1a1f28; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #964a50; + --pico-form-element-invalid-active-border-color: #b7403b; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #2a7b6f; + --pico-form-element-valid-active-border-color: #16896a; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #333c4e; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #202632; + --pico-range-active-border-color: #2a3140; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: #181c25; + --pico-card-border-color: var(--pico-card-background-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #1a1f28; + --pico-dropdown-background-color: #181c25; + --pico-dropdown-border-color: #202632; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #202632; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); + --pico-progress-background-color: #202632; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: dark; + } + :root:not([data-theme]) input:is([type=submit], + [type=button], + [type=reset], + [type=checkbox], + [type=radio], + [type=file]) { + --pico-form-element-focus-color: var(--pico-primary-focus); + } + :root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after { + filter: brightness(0); + } + :root:not([data-theme]) [aria-busy=true]:not(input, select, textarea).contrast:is(button, + [type=submit], + [type=button], + [type=reset], + [role=button]):not(.outline)::before { + filter: brightness(0); + } +} +[data-theme=dark] { + --pico-background-color: #13171f; + --pico-color: #c2c7d0; + --pico-text-selection-color: rgba(137, 153, 249, 0.1875); + --pico-muted-color: #7b8495; + --pico-muted-border-color: #202632; + --pico-primary: #8999f9; + --pico-primary-background: #2060df; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(137, 153, 249, 0.5); + --pico-primary-hover: #aeb5fb; + --pico-primary-hover-background: #3c71f7; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(137, 153, 249, 0.375); + --pico-primary-inverse: #fff; + --pico-secondary: #969eaf; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(150, 158, 175, 0.5); + --pico-secondary-hover: #b3b9c5; + --pico-secondary-hover-background: #5d6b89; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(144, 158, 190, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #dfe3eb; + --pico-contrast-background: #eff1f4; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(223, 227, 235, 0.5); + --pico-contrast-hover: #fff; + --pico-contrast-hover-background: #fff; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-contrast-hover); + --pico-contrast-focus: rgba(207, 213, 226, 0.25); + --pico-contrast-inverse: #000; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); + --pico-h1-color: #f0f1f3; + --pico-h2-color: #e0e3e7; + --pico-h3-color: #c2c7d0; + --pico-h4-color: #b3b9c5; + --pico-h5-color: #a4acba; + --pico-h6-color: #8891a4; + --pico-mark-background-color: #014063; + --pico-mark-color: #fff; + --pico-ins-color: #62af9a; + --pico-del-color: #ce7e7b; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #1a1f28; + --pico-code-color: #8891a4; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #1c212c; + --pico-form-element-selected-background-color: #2a3140; + --pico-form-element-border-color: #2a3140; + --pico-form-element-color: #e0e3e7; + --pico-form-element-placeholder-color: #8891a4; + --pico-form-element-active-background-color: #1a1f28; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #964a50; + --pico-form-element-invalid-active-border-color: #b7403b; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #2a7b6f; + --pico-form-element-valid-active-border-color: #16896a; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #333c4e; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #202632; + --pico-range-active-border-color: #2a3140; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: #181c25; + --pico-card-border-color: var(--pico-card-background-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #1a1f28; + --pico-dropdown-background-color: #181c25; + --pico-dropdown-border-color: #202632; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #202632; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); + --pico-progress-background-color: #202632; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: dark; +} +[data-theme=dark] input:is([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[type=file]) { + --pico-form-element-focus-color: var(--pico-primary-focus); +} +[data-theme=dark] details summary[role=button].contrast:not(.outline)::after { + filter: brightness(0); +} +[data-theme=dark] [aria-busy=true]:not(input, select, textarea).contrast:is(button, +[type=submit], +[type=button], +[type=reset], +[role=button]):not(.outline)::before { + filter: brightness(0); +} + +progress, +[type=checkbox], +[type=radio], +[type=range] { + accent-color: var(--pico-primary); +} + +/** + * Document + * Content-box & Responsive typography + */ +*, +*::before, +*::after { + box-sizing: border-box; + background-repeat: no-repeat; +} + +::before, +::after { + text-decoration: inherit; + vertical-align: inherit; +} + +:where(:root) { + -webkit-tap-highlight-color: transparent; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; + background-color: var(--pico-background-color); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: var(--pico-font-size); + line-height: var(--pico-line-height); + font-family: var(--pico-font-family); + text-underline-offset: var(--pico-text-underline-offset); + text-rendering: optimizeLegibility; + overflow-wrap: break-word; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; +} + +/** + * Landmarks + */ +body { + width: 100%; + margin: 0; +} + +main { + display: block; +} + +body > header, +body > main, +body > footer { + padding-block: var(--pico-block-spacing-vertical); +} + +/** + * Section + */ +section { + margin-bottom: var(--pico-block-spacing-vertical); +} + +/** + * Container + */ +.container, +.container-fluid { + width: 100%; + margin-right: auto; + margin-left: auto; + padding-right: var(--pico-spacing); + padding-left: var(--pico-spacing); +} + +@media (min-width: 576px) { + .container { + max-width: 510px; + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 768px) { + .container { + max-width: 700px; + } +} +@media (min-width: 1024px) { + .container { + max-width: 950px; + } +} +@media (min-width: 1280px) { + .container { + max-width: 1200px; + } +} +@media (min-width: 1536px) { + .container { + max-width: 1450px; + } +} + +/** + * Grid + * Minimal grid system with auto-layout columns + */ +.grid { + grid-column-gap: var(--pico-grid-column-gap); + grid-row-gap: var(--pico-grid-row-gap); + display: grid; + grid-template-columns: 1fr; +} +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(auto-fit, minmax(0%, 1fr)); + } +} +.grid > * { + min-width: 0; +} + +/** + * Overflow auto + */ +.overflow-auto { + overflow: auto; +} + +/** + * Typography + */ +b, +strong { + font-weight: bolder; +} + +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +address, +blockquote, +dl, +ol, +p, +pre, +table, +ul { + margin-top: 0; + margin-bottom: var(--pico-typography-spacing-vertical); + color: var(--pico-color); + font-style: normal; + font-weight: var(--pico-font-weight); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: var(--pico-typography-spacing-vertical); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: var(--pico-font-size); + line-height: var(--pico-line-height); + font-family: var(--pico-font-family); +} + +h1 { + --pico-color: var(--pico-h1-color); +} + +h2 { + --pico-color: var(--pico-h2-color); +} + +h3 { + --pico-color: var(--pico-h3-color); +} + +h4 { + --pico-color: var(--pico-h4-color); +} + +h5 { + --pico-color: var(--pico-h5-color); +} + +h6 { + --pico-color: var(--pico-h6-color); +} + +:where(article, address, blockquote, dl, figure, form, ol, p, pre, table, ul) ~ :is(h1, h2, h3, h4, h5, h6) { + margin-top: var(--pico-typography-spacing-top); +} + +p { + margin-bottom: var(--pico-typography-spacing-vertical); +} + +hgroup { + margin-bottom: var(--pico-typography-spacing-vertical); +} +hgroup > * { + margin-top: 0; + margin-bottom: 0; +} +hgroup > *:not(:first-child):last-child { + --pico-color: var(--pico-muted-color); + --pico-font-weight: unset; + font-size: 1rem; +} + +:where(ol, ul) li { + margin-bottom: calc(var(--pico-typography-spacing-vertical) * 0.25); +} + +:where(dl, ol, ul) :where(dl, ol, ul) { + margin: 0; + margin-top: calc(var(--pico-typography-spacing-vertical) * 0.25); +} + +ul li { + list-style: square; +} + +mark { + padding: 0.125rem 0.25rem; + background-color: var(--pico-mark-background-color); + color: var(--pico-mark-color); + vertical-align: baseline; +} + +blockquote { + display: block; + margin: var(--pico-typography-spacing-vertical) 0; + padding: var(--pico-spacing); + border-right: none; + border-left: 0.25rem solid var(--pico-blockquote-border-color); + border-inline-start: 0.25rem solid var(--pico-blockquote-border-color); + border-inline-end: none; +} +blockquote footer { + margin-top: calc(var(--pico-typography-spacing-vertical) * 0.5); + color: var(--pico-blockquote-footer-color); +} + +abbr[title] { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; +} + +ins { + color: var(--pico-ins-color); + text-decoration: none; +} + +del { + color: var(--pico-del-color); +} + +::-moz-selection { + background-color: var(--pico-text-selection-color); +} + +::selection { + background-color: var(--pico-text-selection-color); +} + +/** + * Link + */ +:where(a:not([role=button])), +[role=link] { + --pico-color: var(--pico-primary); + --pico-background-color: transparent; + --pico-underline: var(--pico-primary-underline); + outline: none; + background-color: var(--pico-background-color); + color: var(--pico-color); + -webkit-text-decoration: var(--pico-text-decoration); + text-decoration: var(--pico-text-decoration); + text-decoration-color: var(--pico-underline); + text-underline-offset: 0.125em; + transition: background-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition), -webkit-text-decoration var(--pico-transition); + transition: background-color var(--pico-transition), color var(--pico-transition), text-decoration var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), color var(--pico-transition), text-decoration var(--pico-transition), box-shadow var(--pico-transition), -webkit-text-decoration var(--pico-transition); +} +:where(a:not([role=button])):is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[role=link]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-primary-hover); + --pico-underline: var(--pico-primary-hover-underline); + --pico-text-decoration: underline; +} +:where(a:not([role=button])):focus-visible, +[role=link]:focus-visible { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} +:where(a:not([role=button])).secondary, +[role=link].secondary { + --pico-color: var(--pico-secondary); + --pico-underline: var(--pico-secondary-underline); +} +:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[role=link].secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-secondary-hover); + --pico-underline: var(--pico-secondary-hover-underline); +} +:where(a:not([role=button])).contrast, +[role=link].contrast { + --pico-color: var(--pico-contrast); + --pico-underline: var(--pico-contrast-underline); +} +:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[role=link].contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-contrast-hover); + --pico-underline: var(--pico-contrast-hover-underline); +} + +a[role=button] { + display: inline-block; +} + +/** + * Button + */ +button { + margin: 0; + overflow: visible; + font-family: inherit; + text-transform: none; +} + +button, +[type=submit], +[type=reset], +[type=button] { + -webkit-appearance: button; +} + +button, +[type=submit], +[type=reset], +[type=button], +[type=file]::file-selector-button, +[role=button] { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + --pico-color: var(--pico-primary-inverse); + --pico-box-shadow: var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + outline: none; + background-color: var(--pico-background-color); + box-shadow: var(--pico-box-shadow); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: 1rem; + line-height: var(--pico-line-height); + text-align: center; + text-decoration: none; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); +} +button:is([aria-current]:not([aria-current=false])), button:is(:hover, :active, :focus), +[type=submit]:is([aria-current]:not([aria-current=false])), +[type=submit]:is(:hover, :active, :focus), +[type=reset]:is([aria-current]:not([aria-current=false])), +[type=reset]:is(:hover, :active, :focus), +[type=button]:is([aria-current]:not([aria-current=false])), +[type=button]:is(:hover, :active, :focus), +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])), +[type=file]::file-selector-button:is(:hover, :active, :focus), +[role=button]:is([aria-current]:not([aria-current=false])), +[role=button]:is(:hover, :active, :focus) { + --pico-background-color: var(--pico-primary-hover-background); + --pico-border-color: var(--pico-primary-hover-border); + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + --pico-color: var(--pico-primary-inverse); +} +button:focus, button:is([aria-current]:not([aria-current=false])):focus, +[type=submit]:focus, +[type=submit]:is([aria-current]:not([aria-current=false])):focus, +[type=reset]:focus, +[type=reset]:is([aria-current]:not([aria-current=false])):focus, +[type=button]:focus, +[type=button]:is([aria-current]:not([aria-current=false])):focus, +[type=file]::file-selector-button:focus, +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus, +[role=button]:focus, +[role=button]:is([aria-current]:not([aria-current=false])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} + +[type=submit], +[type=reset], +[type=button] { + margin-bottom: var(--pico-spacing); +} + +:is(button, [type=submit], [type=button], [role=button]).secondary, +[type=reset], +[type=file]::file-selector-button { + --pico-background-color: var(--pico-secondary-background); + --pico-border-color: var(--pico-secondary-border); + --pico-color: var(--pico-secondary-inverse); + cursor: pointer; +} +:is(button, [type=submit], [type=button], [role=button]).secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=reset]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-background-color: var(--pico-secondary-hover-background); + --pico-border-color: var(--pico-secondary-hover-border); + --pico-color: var(--pico-secondary-inverse); +} +:is(button, [type=submit], [type=button], [role=button]).secondary:focus, :is(button, [type=submit], [type=button], [role=button]).secondary:is([aria-current]:not([aria-current=false])):focus, +[type=reset]:focus, +[type=reset]:is([aria-current]:not([aria-current=false])):focus, +[type=file]::file-selector-button:focus, +[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} + +:is(button, [type=submit], [type=button], [role=button]).contrast { + --pico-background-color: var(--pico-contrast-background); + --pico-border-color: var(--pico-contrast-border); + --pico-color: var(--pico-contrast-inverse); +} +:is(button, [type=submit], [type=button], [role=button]).contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-background-color: var(--pico-contrast-hover-background); + --pico-border-color: var(--pico-contrast-hover-border); + --pico-color: var(--pico-contrast-inverse); +} +:is(button, [type=submit], [type=button], [role=button]).contrast:focus, :is(button, [type=submit], [type=button], [role=button]).contrast:is([aria-current]:not([aria-current=false])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); +} + +:is(button, [type=submit], [type=button], [role=button]).outline, +[type=reset].outline { + --pico-background-color: transparent; + --pico-color: var(--pico-primary); + --pico-border-color: var(--pico-primary); +} +:is(button, [type=submit], [type=button], [role=button]).outline:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=reset].outline:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-background-color: transparent; + --pico-color: var(--pico-primary-hover); + --pico-border-color: var(--pico-primary-hover); +} + +:is(button, [type=submit], [type=button], [role=button]).outline.secondary, +[type=reset].outline { + --pico-color: var(--pico-secondary); + --pico-border-color: var(--pico-secondary); +} +:is(button, [type=submit], [type=button], [role=button]).outline.secondary:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), +[type=reset].outline:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-secondary-hover); + --pico-border-color: var(--pico-secondary-hover); +} + +:is(button, [type=submit], [type=button], [role=button]).outline.contrast { + --pico-color: var(--pico-contrast); + --pico-border-color: var(--pico-contrast); +} +:is(button, [type=submit], [type=button], [role=button]).outline.contrast:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + --pico-color: var(--pico-contrast-hover); + --pico-border-color: var(--pico-contrast-hover); +} + +:where(button, [type=submit], [type=reset], [type=button], [role=button])[disabled], +:where(fieldset[disabled]) :is(button, [type=submit], [type=button], [type=reset], [role=button]) { + opacity: 0.5; + pointer-events: none; +} + +/** + * Table + */ +:where(table) { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + text-indent: 0; +} + +th, +td { + padding: calc(var(--pico-spacing) / 2) var(--pico-spacing); + border-bottom: var(--pico-border-width) solid var(--pico-table-border-color); + background-color: var(--pico-background-color); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + text-align: left; + text-align: start; +} + +tfoot th, +tfoot td { + border-top: var(--pico-border-width) solid var(--pico-table-border-color); + border-bottom: 0; +} + +table.striped tbody tr:nth-child(odd) th, +table.striped tbody tr:nth-child(odd) td { + background-color: var(--pico-table-row-stripped-background-color); +} + +/** + * Embedded content + */ +:where(audio, canvas, iframe, img, svg, video) { + vertical-align: middle; +} + +audio, +video { + display: inline-block; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +:where(iframe) { + border-style: none; +} + +img { + max-width: 100%; + height: auto; + border-style: none; +} + +:where(svg:not([fill])) { + fill: currentColor; +} + +svg:not(:root) { + overflow: hidden; +} + +/** + * Code + */ +pre, +code, +kbd, +samp { + font-size: 0.875em; + font-family: var(--pico-font-family); +} + +pre code { + font-size: inherit; + font-family: inherit; +} + +pre { + -ms-overflow-style: scrollbar; + overflow: auto; +} + +pre, +code, +kbd { + border-radius: var(--pico-border-radius); + background: var(--pico-code-background-color); + color: var(--pico-code-color); + font-weight: var(--pico-font-weight); + line-height: initial; +} + +code, +kbd { + display: inline-block; + padding: 0.375rem; +} + +pre { + display: block; + margin-bottom: var(--pico-spacing); + overflow-x: auto; +} +pre > code { + display: block; + padding: var(--pico-spacing); + background: none; + line-height: var(--pico-line-height); +} + +kbd { + background-color: var(--pico-code-kbd-background-color); + color: var(--pico-code-kbd-color); + vertical-align: baseline; +} + +/** + * Figure + */ +figure { + display: block; + margin: 0; + padding: 0; +} +figure figcaption { + padding: calc(var(--pico-spacing) * 0.5) 0; + color: var(--pico-muted-color); +} + +/** + * Miscs + */ +hr { + height: 0; + margin: var(--pico-typography-spacing-vertical) 0; + border: 0; + border-top: 1px solid var(--pico-muted-border-color); + color: inherit; +} + +[hidden], +template { + display: none !important; +} + +canvas { + display: inline-block; +} + +/** + * Basics form elements + */ +input, +optgroup, +select, +textarea { + margin: 0; + font-size: 1rem; + line-height: var(--pico-line-height); + font-family: inherit; + letter-spacing: inherit; +} + +input { + overflow: visible; +} + +select { + text-transform: none; +} + +legend { + max-width: 100%; + padding: 0; + color: inherit; + white-space: normal; +} + +textarea { + overflow: auto; +} + +[type=checkbox], +[type=radio] { + padding: 0; +} + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px; +} + +[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} + +::-moz-focus-inner { + padding: 0; + border-style: none; +} + +:-moz-focusring { + outline: none; +} + +:-moz-ui-invalid { + box-shadow: none; +} + +::-ms-expand { + display: none; +} + +[type=file], +[type=range] { + padding: 0; + border-width: 0; +} + +input:not([type=checkbox], [type=radio], [type=range]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); +} + +fieldset { + width: 100%; + margin: 0; + margin-bottom: var(--pico-spacing); + padding: 0; + border: 0; +} + +label, +fieldset legend { + display: block; + margin-bottom: calc(var(--pico-spacing) * 0.375); + color: var(--pico-color); + font-weight: var(--pico-form-label-font-weight, var(--pico-font-weight)); +} + +fieldset legend { + margin-bottom: calc(var(--pico-spacing) * 0.5); +} + +input:not([type=checkbox], [type=radio]), +button[type=submit], +select, +textarea { + width: 100%; +} + +input:not([type=checkbox], [type=radio], [type=range], [type=file]), +select, +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); +} + +input, +select, +textarea { + --pico-background-color: var(--pico-form-element-background-color); + --pico-border-color: var(--pico-form-element-border-color); + --pico-color: var(--pico-form-element-color); + --pico-box-shadow: none; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + outline: none; + background-color: var(--pico-background-color); + box-shadow: var(--pico-box-shadow); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); +} + +input:not([type=submit], +[type=button], +[type=reset], +[type=checkbox], +[type=radio], +[readonly]):is(:active, :focus), +:where(select, textarea):not([readonly]):is(:active, :focus) { + --pico-background-color: var(--pico-form-element-active-background-color); +} + +input:not([type=submit], [type=button], [type=reset], [role=switch], [readonly]):is(:active, :focus), +:where(select, textarea):not([readonly]):is(:active, :focus) { + --pico-border-color: var(--pico-form-element-active-border-color); +} + +input:not([type=submit], +[type=button], +[type=reset], +[type=range], +[type=file], +[readonly]):focus, +:where(select, textarea):not([readonly]):focus { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); +} + +input:not([type=submit], [type=button], [type=reset])[disabled], +select[disabled], +textarea[disabled], +label[aria-disabled=true], +:where(fieldset[disabled]) :is(input:not([type=submit], [type=button], [type=reset]), select, textarea) { + opacity: var(--pico-form-element-disabled-opacity); + pointer-events: none; +} + +label[aria-disabled=true] input[disabled] { + opacity: 1; +} + +:where(input, select, textarea):not([type=checkbox], +[type=radio], +[type=date], +[type=datetime-local], +[type=month], +[type=time], +[type=week], +[type=range])[aria-invalid] { + padding-right: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem) !important; + padding-left: var(--pico-form-element-spacing-horizontal); + padding-inline-start: var(--pico-form-element-spacing-horizontal) !important; + padding-inline-end: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem) !important; + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; +} +:where(input, select, textarea):not([type=checkbox], +[type=radio], +[type=date], +[type=datetime-local], +[type=month], +[type=time], +[type=week], +[type=range])[aria-invalid=false]:not(select) { + background-image: var(--pico-icon-valid); +} +:where(input, select, textarea):not([type=checkbox], +[type=radio], +[type=date], +[type=datetime-local], +[type=month], +[type=time], +[type=week], +[type=range])[aria-invalid=true]:not(select) { + background-image: var(--pico-icon-invalid); +} +:where(input, select, textarea)[aria-invalid=false] { + --pico-border-color: var(--pico-form-element-valid-border-color); +} +:where(input, select, textarea)[aria-invalid=false]:is(:active, :focus) { + --pico-border-color: var(--pico-form-element-valid-active-border-color) !important; +} +:where(input, select, textarea)[aria-invalid=false]:is(:active, :focus):not([type=checkbox], [type=radio]) { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color) !important; +} +:where(input, select, textarea)[aria-invalid=true] { + --pico-border-color: var(--pico-form-element-invalid-border-color); +} +:where(input, select, textarea)[aria-invalid=true]:is(:active, :focus) { + --pico-border-color: var(--pico-form-element-invalid-active-border-color) !important; +} +:where(input, select, textarea)[aria-invalid=true]:is(:active, :focus):not([type=checkbox], [type=radio]) { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color) !important; +} + +[dir=rtl] :where(input, select, textarea):not([type=checkbox], [type=radio]):is([aria-invalid], [aria-invalid=true], [aria-invalid=false]) { + background-position: center left 0.75rem; +} + +input::placeholder, +input::-webkit-input-placeholder, +textarea::placeholder, +textarea::-webkit-input-placeholder, +select:invalid { + color: var(--pico-form-element-placeholder-color); + opacity: 1; +} + +input:not([type=checkbox], [type=radio]), +select, +textarea { + margin-bottom: var(--pico-spacing); +} + +select::-ms-expand { + border: 0; + background-color: transparent; +} +select:not([multiple], [size]) { + padding-right: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem); + padding-left: var(--pico-form-element-spacing-horizontal); + padding-inline-start: var(--pico-form-element-spacing-horizontal); + padding-inline-end: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem); + background-image: var(--pico-icon-chevron); + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; +} +select[multiple] option:checked { + background: var(--pico-form-element-selected-background-color); + color: var(--pico-form-element-color); +} + +[dir=rtl] select:not([multiple], [size]) { + background-position: center left 0.75rem; +} + +textarea { + display: block; + resize: vertical; +} +textarea[aria-invalid] { + --pico-icon-height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); + background-position: top right 0.75rem !important; + background-size: 1rem var(--pico-icon-height) !important; +} + +:where(input, select, textarea, fieldset, .grid) + small { + display: block; + width: 100%; + margin-top: calc(var(--pico-spacing) * -0.75); + margin-bottom: var(--pico-spacing); + color: var(--pico-muted-color); +} +:where(input, select, textarea, fieldset, .grid)[aria-invalid=false] + small { + color: var(--pico-ins-color); +} +:where(input, select, textarea, fieldset, .grid)[aria-invalid=true] + small { + color: var(--pico-del-color); +} + +label > :where(input, select, textarea) { + margin-top: calc(var(--pico-spacing) * 0.25); +} + +/** + * Checkboxes, Radios and Switches + */ +label:has([type=checkbox], [type=radio]) { + width: -moz-fit-content; + width: fit-content; + cursor: pointer; +} + +[type=checkbox], +[type=radio] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 1.25em; + height: 1.25em; + margin-top: -0.125em; + margin-inline-end: 0.5em; + border-width: var(--pico-border-width); + vertical-align: middle; + cursor: pointer; +} +[type=checkbox]::-ms-check, +[type=radio]::-ms-check { + display: none; +} +[type=checkbox]:checked, [type=checkbox]:checked:active, [type=checkbox]:checked:focus, +[type=radio]:checked, +[type=radio]:checked:active, +[type=radio]:checked:focus { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + background-image: var(--pico-icon-checkbox); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; +} +[type=checkbox] ~ label, +[type=radio] ~ label { + display: inline-block; + margin-bottom: 0; + cursor: pointer; +} +[type=checkbox] ~ label:not(:last-of-type), +[type=radio] ~ label:not(:last-of-type) { + margin-inline-end: 1em; +} + +[type=checkbox]:indeterminate { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + background-image: var(--pico-icon-minus); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; +} + +[type=radio] { + border-radius: 50%; +} +[type=radio]:checked, [type=radio]:checked:active, [type=radio]:checked:focus { + --pico-background-color: var(--pico-primary-inverse); + border-width: 0.35em; + background-image: none; +} + +[type=checkbox][role=switch] { + --pico-background-color: var(--pico-switch-background-color); + --pico-color: var(--pico-switch-color); + width: 2.25em; + height: 1.25em; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: 1.25em; + background-color: var(--pico-background-color); + line-height: 1.25em; +} +[type=checkbox][role=switch]:not([aria-invalid]) { + --pico-border-color: var(--pico-switch-background-color); +} +[type=checkbox][role=switch]:before { + display: block; + aspect-ratio: 1; + height: 100%; + border-radius: 50%; + background-color: var(--pico-color); + box-shadow: var(--pico-switch-thumb-box-shadow); + content: ""; + transition: margin 0.1s ease-in-out; +} +[type=checkbox][role=switch]:focus { + --pico-background-color: var(--pico-switch-background-color); + --pico-border-color: var(--pico-switch-background-color); +} +[type=checkbox][role=switch]:checked { + --pico-background-color: var(--pico-switch-checked-background-color); + --pico-border-color: var(--pico-switch-checked-background-color); + background-image: none; +} +[type=checkbox][role=switch]:checked::before { + margin-inline-start: calc(2.25em - 1.25em); +} +[type=checkbox][role=switch][disabled] { + --pico-background-color: var(--pico-border-color); +} + +[type=checkbox][aria-invalid=false]:checked, [type=checkbox][aria-invalid=false]:checked:active, [type=checkbox][aria-invalid=false]:checked:focus, +[type=checkbox][role=switch][aria-invalid=false]:checked, +[type=checkbox][role=switch][aria-invalid=false]:checked:active, +[type=checkbox][role=switch][aria-invalid=false]:checked:focus { + --pico-background-color: var(--pico-form-element-valid-border-color); +} +[type=checkbox]:checked[aria-invalid=true], [type=checkbox]:checked:active[aria-invalid=true], [type=checkbox]:checked:focus[aria-invalid=true], +[type=checkbox][role=switch]:checked[aria-invalid=true], +[type=checkbox][role=switch]:checked:active[aria-invalid=true], +[type=checkbox][role=switch]:checked:focus[aria-invalid=true] { + --pico-background-color: var(--pico-form-element-invalid-border-color); +} + +[type=checkbox][aria-invalid=false]:checked, [type=checkbox][aria-invalid=false]:checked:active, [type=checkbox][aria-invalid=false]:checked:focus, +[type=radio][aria-invalid=false]:checked, +[type=radio][aria-invalid=false]:checked:active, +[type=radio][aria-invalid=false]:checked:focus, +[type=checkbox][role=switch][aria-invalid=false]:checked, +[type=checkbox][role=switch][aria-invalid=false]:checked:active, +[type=checkbox][role=switch][aria-invalid=false]:checked:focus { + --pico-border-color: var(--pico-form-element-valid-border-color); +} +[type=checkbox]:checked[aria-invalid=true], [type=checkbox]:checked:active[aria-invalid=true], [type=checkbox]:checked:focus[aria-invalid=true], +[type=radio]:checked[aria-invalid=true], +[type=radio]:checked:active[aria-invalid=true], +[type=radio]:checked:focus[aria-invalid=true], +[type=checkbox][role=switch]:checked[aria-invalid=true], +[type=checkbox][role=switch]:checked:active[aria-invalid=true], +[type=checkbox][role=switch]:checked:focus[aria-invalid=true] { + --pico-border-color: var(--pico-form-element-invalid-border-color); +} + +/** + * Input type color + */ +[type=color]::-webkit-color-swatch-wrapper { + padding: 0; +} +[type=color]::-moz-focus-inner { + padding: 0; +} +[type=color]::-webkit-color-swatch { + border: 0; + border-radius: calc(var(--pico-border-radius) * 0.5); +} +[type=color]::-moz-color-swatch { + border: 0; + border-radius: calc(var(--pico-border-radius) * 0.5); +} + +/** + * Input type datetime + */ +input:not([type=checkbox], [type=radio], [type=range], [type=file]):is([type=date], [type=datetime-local], [type=month], [type=time], [type=week]) { + --pico-icon-position: 0.75rem; + --pico-icon-width: 1rem; + padding-right: calc(var(--pico-icon-width) + var(--pico-icon-position)); + background-image: var(--pico-icon-date); + background-position: center right var(--pico-icon-position); + background-size: var(--pico-icon-width) auto; + background-repeat: no-repeat; +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=time] { + background-image: var(--pico-icon-time); +} + +[type=date]::-webkit-calendar-picker-indicator, +[type=datetime-local]::-webkit-calendar-picker-indicator, +[type=month]::-webkit-calendar-picker-indicator, +[type=time]::-webkit-calendar-picker-indicator, +[type=week]::-webkit-calendar-picker-indicator { + width: var(--pico-icon-width); + margin-right: calc(var(--pico-icon-width) * -1); + margin-left: var(--pico-icon-position); + opacity: 0; +} + +@-moz-document url-prefix() { + [type=date], + [type=datetime-local], + [type=month], + [type=time], + [type=week] { + padding-right: var(--pico-form-element-spacing-horizontal) !important; + background-image: none !important; + } +} +[dir=rtl] :is([type=date], [type=datetime-local], [type=month], [type=time], [type=week]) { + text-align: right; +} + +/** + * Input type file + */ +[type=file] { + --pico-color: var(--pico-muted-color); + margin-left: calc(var(--pico-outline-width) * -1); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) 0; + padding-left: var(--pico-outline-width); + border: 0; + border-radius: 0; + background: none; +} +[type=file]::file-selector-button { + margin-right: calc(var(--pico-spacing) / 2); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); +} +[type=file]:is(:hover, :active, :focus)::file-selector-button { + --pico-background-color: var(--pico-secondary-hover-background); + --pico-border-color: var(--pico-secondary-hover-border); +} +[type=file]:focus::file-selector-button { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} + +/** + * Input type range + */ +[type=range] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + height: 1.25rem; + background: none; +} +[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + -webkit-transition: background-color var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type=range]::-moz-range-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + -moz-transition: background-color var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type=range]::-ms-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + -ms-transition: background-color var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + -webkit-transition: background-color var(--pico-transition), transform var(--pico-transition); + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type=range]::-moz-range-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + -moz-transition: background-color var(--pico-transition), transform var(--pico-transition); + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type=range]::-ms-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + -ms-transition: background-color var(--pico-transition), transform var(--pico-transition); + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type=range]:active, [type=range]:focus-within { + --pico-range-border-color: var(--pico-range-active-border-color); + --pico-range-thumb-color: var(--pico-range-thumb-active-color); +} +[type=range]:active::-webkit-slider-thumb { + transform: scale(1.25); +} +[type=range]:active::-moz-range-thumb { + transform: scale(1.25); +} +[type=range]:active::-ms-thumb { + transform: scale(1.25); +} + +/** + * Input type search + */ +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search] { + padding-inline-start: calc(var(--pico-form-element-spacing-horizontal) + 1.75rem); + background-image: var(--pico-icon-search); + background-position: center left calc(var(--pico-form-element-spacing-horizontal) + 0.125rem); + background-size: 1rem auto; + background-repeat: no-repeat; +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid] { + padding-inline-start: calc(var(--pico-form-element-spacing-horizontal) + 1.75rem) !important; + background-position: center left 1.125rem, center right 0.75rem; +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid=false] { + background-image: var(--pico-icon-search), var(--pico-icon-valid); +} +input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid=true] { + background-image: var(--pico-icon-search), var(--pico-icon-invalid); +} + +[dir=rtl] :where(input):not([type=checkbox], [type=radio], [type=range], [type=file])[type=search] { + background-position: center right 1.125rem; +} +[dir=rtl] :where(input):not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid] { + background-position: center right 1.125rem, center left 0.75rem; +} + +/** + * Accordion (
) + */ +details { + display: block; + margin-bottom: var(--pico-spacing); +} +details summary { + line-height: 1rem; + list-style-type: none; + cursor: pointer; + transition: color var(--pico-transition); +} +details summary:not([role]) { + color: var(--pico-accordion-close-summary-color); +} +details summary::-webkit-details-marker { + display: none; +} +details summary::marker { + display: none; +} +details summary::-moz-list-bullet { + list-style-type: none; +} +details summary::after { + display: block; + width: 1rem; + height: 1rem; + margin-inline-start: calc(var(--pico-spacing, 1rem) * 0.5); + float: right; + transform: rotate(-90deg); + background-image: var(--pico-icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; + transition: transform var(--pico-transition); +} +details summary:focus { + outline: none; +} +details summary:focus:not([role]) { + color: var(--pico-accordion-active-summary-color); +} +details summary:focus-visible:not([role]) { + outline: var(--pico-outline-width) solid var(--pico-primary-focus); + outline-offset: calc(var(--pico-spacing, 1rem) * 0.5); + color: var(--pico-primary); +} +details summary[role=button] { + width: 100%; + text-align: left; +} +details summary[role=button]::after { + height: calc(1rem * var(--pico-line-height, 1.5)); +} +details[open] > summary { + margin-bottom: var(--pico-spacing); +} +details[open] > summary:not([role]):not(:focus) { + color: var(--pico-accordion-open-summary-color); +} +details[open] > summary::after { + transform: rotate(0); +} + +[dir=rtl] details summary { + text-align: right; +} +[dir=rtl] details summary::after { + float: left; + background-position: left center; +} + +/** + * Card (
) + */ +article { + margin-bottom: var(--pico-block-spacing-vertical); + padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal); + border-radius: var(--pico-border-radius); + background: var(--pico-card-background-color); + box-shadow: var(--pico-card-box-shadow); +} +article > header, +article > footer { + margin-right: calc(var(--pico-block-spacing-horizontal) * -1); + margin-left: calc(var(--pico-block-spacing-horizontal) * -1); + padding: calc(var(--pico-block-spacing-vertical) * 0.66) var(--pico-block-spacing-horizontal); + background-color: var(--pico-card-sectioning-background-color); +} +article > header { + margin-top: calc(var(--pico-block-spacing-vertical) * -1); + margin-bottom: var(--pico-block-spacing-vertical); + border-bottom: var(--pico-border-width) solid var(--pico-card-border-color); + border-top-right-radius: var(--pico-border-radius); + border-top-left-radius: var(--pico-border-radius); +} +article > footer { + margin-top: var(--pico-block-spacing-vertical); + margin-bottom: calc(var(--pico-block-spacing-vertical) * -1); + border-top: var(--pico-border-width) solid var(--pico-card-border-color); + border-bottom-right-radius: var(--pico-border-radius); + border-bottom-left-radius: var(--pico-border-radius); +} + +/** + * Dropdown (details.dropdown) + */ +details.dropdown { + position: relative; + border-bottom: none; +} +details.dropdown summary::after, +details.dropdown > button::after, +details.dropdown > a::after { + display: block; + width: 1rem; + height: calc(1rem * var(--pico-line-height, 1.5)); + margin-inline-start: 0.25rem; + float: right; + transform: rotate(0deg) translateX(0.2rem); + background-image: var(--pico-icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; +} + +nav details.dropdown { + margin-bottom: 0; +} + +details.dropdown summary:not([role]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + border: var(--pico-border-width) solid var(--pico-form-element-border-color); + border-radius: var(--pico-border-radius); + background-color: var(--pico-form-element-background-color); + color: var(--pico-form-element-placeholder-color); + line-height: inherit; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition); +} +details.dropdown summary:not([role]):active, details.dropdown summary:not([role]):focus { + border-color: var(--pico-form-element-active-border-color); + background-color: var(--pico-form-element-active-background-color); +} +details.dropdown summary:not([role]):focus { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); +} +details.dropdown summary:not([role]):focus-visible { + outline: none; +} +details.dropdown summary:not([role])[aria-invalid=false] { + --pico-form-element-border-color: var(--pico-form-element-valid-border-color); + --pico-form-element-active-border-color: var(--pico-form-element-valid-focus-color); + --pico-form-element-focus-color: var(--pico-form-element-valid-focus-color); +} +details.dropdown summary:not([role])[aria-invalid=true] { + --pico-form-element-border-color: var(--pico-form-element-invalid-border-color); + --pico-form-element-active-border-color: var(--pico-form-element-invalid-focus-color); + --pico-form-element-focus-color: var(--pico-form-element-invalid-focus-color); +} + +nav details.dropdown { + display: inline; + margin: calc(var(--pico-nav-element-spacing-vertical) * -1) 0; +} +nav details.dropdown summary::after { + transform: rotate(0deg) translateX(0rem); +} +nav details.dropdown summary:not([role]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2); + padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal); +} +nav details.dropdown summary:not([role]):focus-visible { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} + +details.dropdown summary + ul { + display: flex; + z-index: 99; + position: absolute; + left: 0; + flex-direction: column; + width: 100%; + min-width: -moz-fit-content; + min-width: fit-content; + margin: 0; + margin-top: var(--pico-outline-width); + padding: 0; + border: var(--pico-border-width) solid var(--pico-dropdown-border-color); + border-radius: var(--pico-border-radius); + background-color: var(--pico-dropdown-background-color); + box-shadow: var(--pico-dropdown-box-shadow); + color: var(--pico-dropdown-color); + white-space: nowrap; + opacity: 0; + transition: opacity var(--pico-transition), transform 0s ease-in-out 1s; +} +details.dropdown summary + ul[dir=rtl] { + right: 0; + left: auto; +} +details.dropdown summary + ul li { + width: 100%; + margin-bottom: 0; + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); + list-style: none; +} +details.dropdown summary + ul li:first-of-type { + margin-top: calc(var(--pico-form-element-spacing-vertical) * 0.5); +} +details.dropdown summary + ul li:last-of-type { + margin-bottom: calc(var(--pico-form-element-spacing-vertical) * 0.5); +} +details.dropdown summary + ul li a { + display: block; + margin: calc(var(--pico-form-element-spacing-vertical) * -0.5) calc(var(--pico-form-element-spacing-horizontal) * -1); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); + overflow: hidden; + border-radius: 0; + color: var(--pico-dropdown-color); + text-decoration: none; + text-overflow: ellipsis; +} +details.dropdown summary + ul li a:hover, details.dropdown summary + ul li a:focus, details.dropdown summary + ul li a:active, details.dropdown summary + ul li a:focus-visible, details.dropdown summary + ul li a[aria-current]:not([aria-current=false]) { + background-color: var(--pico-dropdown-hover-background-color); +} +details.dropdown summary + ul li label { + width: 100%; +} +details.dropdown summary + ul li:has(label):hover { + background-color: var(--pico-dropdown-hover-background-color); +} + +details.dropdown[open] summary { + margin-bottom: 0; +} + +details.dropdown[open] summary + ul { + transform: scaleY(1); + opacity: 1; + transition: opacity var(--pico-transition), transform 0s ease-in-out 0s; +} + +details.dropdown[open] summary::before { + display: block; + z-index: 1; + position: fixed; + width: 100vw; + height: 100vh; + inset: 0; + background: none; + content: ""; + cursor: default; +} + +label > details.dropdown { + margin-top: calc(var(--pico-spacing) * 0.25); +} + +/** + * Group ([role="group"], [role="search"]) + */ +[role=search], +[role=group] { + display: inline-flex; + position: relative; + width: 100%; + margin-bottom: var(--pico-spacing); + border-radius: var(--pico-border-radius); + box-shadow: var(--pico-group-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + vertical-align: middle; + transition: box-shadow var(--pico-transition); +} +[role=search] > *, +[role=search] input:not([type=checkbox], [type=radio]), +[role=search] select, +[role=group] > *, +[role=group] input:not([type=checkbox], [type=radio]), +[role=group] select { + position: relative; + flex: 1 1 auto; + margin-bottom: 0; +} +[role=search] > *:not(:first-child), +[role=search] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=search] select:not(:first-child), +[role=group] > *:not(:first-child), +[role=group] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=group] select:not(:first-child) { + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +[role=search] > *:not(:last-child), +[role=search] input:not([type=checkbox], [type=radio]):not(:last-child), +[role=search] select:not(:last-child), +[role=group] > *:not(:last-child), +[role=group] input:not([type=checkbox], [type=radio]):not(:last-child), +[role=group] select:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +[role=search] > *:focus, +[role=search] input:not([type=checkbox], [type=radio]):focus, +[role=search] select:focus, +[role=group] > *:focus, +[role=group] input:not([type=checkbox], [type=radio]):focus, +[role=group] select:focus { + z-index: 2; +} +[role=search] button:not(:first-child), +[role=search] [type=submit]:not(:first-child), +[role=search] [type=reset]:not(:first-child), +[role=search] [type=button]:not(:first-child), +[role=search] [role=button]:not(:first-child), +[role=search] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=search] select:not(:first-child), +[role=group] button:not(:first-child), +[role=group] [type=submit]:not(:first-child), +[role=group] [type=reset]:not(:first-child), +[role=group] [type=button]:not(:first-child), +[role=group] [role=button]:not(:first-child), +[role=group] input:not([type=checkbox], [type=radio]):not(:first-child), +[role=group] select:not(:first-child) { + margin-left: calc(var(--pico-border-width) * -1); +} +[role=search] button, +[role=search] [type=submit], +[role=search] [type=reset], +[role=search] [type=button], +[role=search] [role=button], +[role=group] button, +[role=group] [type=submit], +[role=group] [type=reset], +[role=group] [type=button], +[role=group] [role=button] { + width: auto; +} +@supports selector(:has(*)) { + [role=search]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus), + [role=group]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) { + --pico-group-box-shadow: var(--pico-group-box-shadow-focus-with-button); + } + [role=search]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) input:not([type=checkbox], [type=radio]), + [role=search]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) select, + [role=group]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) input:not([type=checkbox], [type=radio]), + [role=group]:has(button:focus, [type=submit]:focus, [type=button]:focus, [role=button]:focus) select { + border-color: transparent; + } + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus), + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) { + --pico-group-box-shadow: var(--pico-group-box-shadow-focus-with-input); + } + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) button, + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=submit], + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=button], + [role=search]:has(input:not([type=submit], [type=button]):focus, select:focus) [role=button], + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) button, + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=submit], + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) [type=button], + [role=group]:has(input:not([type=submit], [type=button]):focus, select:focus) [role=button] { + --pico-button-box-shadow: 0 0 0 var(--pico-border-width) var(--pico-primary-border); + --pico-button-hover-box-shadow: 0 0 0 var(--pico-border-width) var(--pico-primary-hover-border); + } + [role=search] button:focus, + [role=search] [type=submit]:focus, + [role=search] [type=reset]:focus, + [role=search] [type=button]:focus, + [role=search] [role=button]:focus, + [role=group] button:focus, + [role=group] [type=submit]:focus, + [role=group] [type=reset]:focus, + [role=group] [type=button]:focus, + [role=group] [role=button]:focus { + box-shadow: none; + } +} + +[role=search] > *:first-child { + border-top-left-radius: 5rem; + border-bottom-left-radius: 5rem; +} +[role=search] > *:last-child { + border-top-right-radius: 5rem; + border-bottom-right-radius: 5rem; +} + +/** + * Loading ([aria-busy=true]) + */ +[aria-busy=true]:not(input, select, textarea, html) { + white-space: nowrap; +} +[aria-busy=true]:not(input, select, textarea, html)::before { + display: inline-block; + width: 1em; + height: 1em; + background-image: var(--pico-icon-loading); + background-size: 1em auto; + background-repeat: no-repeat; + content: ""; + vertical-align: -0.125em; +} +[aria-busy=true]:not(input, select, textarea, html):not(:empty)::before { + margin-inline-end: calc(var(--pico-spacing) * 0.5); +} +[aria-busy=true]:not(input, select, textarea, html):empty { + text-align: center; +} + +button[aria-busy=true], +[type=submit][aria-busy=true], +[type=button][aria-busy=true], +[type=reset][aria-busy=true], +[role=button][aria-busy=true], +a[aria-busy=true] { + pointer-events: none; +} + +/** + * Modal () + */ +:root { + --pico-scrollbar-width: 0px; +} + +dialog { + display: flex; + z-index: 999; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + align-items: center; + justify-content: center; + width: inherit; + min-width: 100%; + height: inherit; + min-height: 100%; + padding: 0; + border: 0; + -webkit-backdrop-filter: var(--pico-modal-overlay-backdrop-filter); + backdrop-filter: var(--pico-modal-overlay-backdrop-filter); + background-color: var(--pico-modal-overlay-background-color); + color: var(--pico-color); +} +dialog article { + width: 100%; + max-height: calc(100vh - var(--pico-spacing) * 2); + margin: var(--pico-spacing); + overflow: auto; +} +@media (min-width: 576px) { + dialog article { + max-width: 510px; + } +} +@media (min-width: 768px) { + dialog article { + max-width: 700px; + } +} +dialog article > header > * { + margin-bottom: 0; +} +dialog article > header .close, dialog article > header :is(a, button)[rel=prev] { + margin: 0; + margin-left: var(--pico-spacing); + padding: 0; + float: right; +} +dialog article > footer { + text-align: right; +} +dialog article > footer button, +dialog article > footer [role=button] { + margin-bottom: 0; +} +dialog article > footer button:not(:first-of-type), +dialog article > footer [role=button]:not(:first-of-type) { + margin-left: calc(var(--pico-spacing) * 0.5); +} +dialog article .close, dialog article :is(a, button)[rel=prev] { + display: block; + width: 1rem; + height: 1rem; + margin-top: calc(var(--pico-spacing) * -1); + margin-bottom: var(--pico-spacing); + margin-left: auto; + border: none; + background-image: var(--pico-icon-close); + background-position: center; + background-size: auto 1rem; + background-repeat: no-repeat; + background-color: transparent; + opacity: 0.5; + transition: opacity var(--pico-transition); +} +dialog article .close:is([aria-current]:not([aria-current=false]), :hover, :active, :focus), dialog article :is(a, button)[rel=prev]:is([aria-current]:not([aria-current=false]), :hover, :active, :focus) { + opacity: 1; +} +dialog:not([open]), dialog[open=false] { + display: none; +} + +.modal-is-open { + padding-right: var(--pico-scrollbar-width, 0px); + overflow: hidden; + pointer-events: none; + touch-action: none; +} +.modal-is-open dialog { + pointer-events: auto; + touch-action: auto; +} + +:where(.modal-is-opening, .modal-is-closing) dialog, +:where(.modal-is-opening, .modal-is-closing) dialog > article { + animation-duration: 0.2s; + animation-timing-function: ease-in-out; + animation-fill-mode: both; +} +:where(.modal-is-opening, .modal-is-closing) dialog { + animation-duration: 0.8s; + animation-name: modal-overlay; +} +:where(.modal-is-opening, .modal-is-closing) dialog > article { + animation-delay: 0.2s; + animation-name: modal; +} + +.modal-is-closing dialog, +.modal-is-closing dialog > article { + animation-delay: 0s; + animation-direction: reverse; +} + +@keyframes modal-overlay { + from { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background-color: transparent; + } +} +@keyframes modal { + from { + transform: translateY(-100%); + opacity: 0; + } +} +/** + * Nav + */ +:where(nav li)::before { + float: left; + content: "​"; +} + +nav, +nav ul { + display: flex; +} + +nav { + justify-content: space-between; + overflow: visible; +} +nav ol, +nav ul { + align-items: center; + margin-bottom: 0; + padding: 0; + list-style: none; +} +nav ol:first-of-type, +nav ul:first-of-type { + margin-left: calc(var(--pico-nav-element-spacing-horizontal) * -1); +} +nav ol:last-of-type, +nav ul:last-of-type { + margin-right: calc(var(--pico-nav-element-spacing-horizontal) * -1); +} +nav li { + display: inline-block; + margin: 0; + padding: var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal); +} +nav li :where(a, [role=link]) { + display: inline-block; + margin: calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1); + padding: var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal); + border-radius: var(--pico-border-radius); +} +nav li :where(a, [role=link]):not(:hover) { + text-decoration: none; +} +nav li button, +nav li [role=button], +nav li [type=button], +nav li input:not([type=checkbox], [type=radio], [type=range], [type=file]), +nav li select { + height: auto; + margin-right: inherit; + margin-bottom: 0; + margin-left: inherit; + padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal); +} +nav[aria-label=breadcrumb] { + align-items: center; + justify-content: start; +} +nav[aria-label=breadcrumb] ul li:not(:first-child) { + margin-inline-start: var(--pico-nav-link-spacing-horizontal); +} +nav[aria-label=breadcrumb] ul li a { + margin: calc(var(--pico-nav-link-spacing-vertical) * -1) 0; + margin-inline-start: calc(var(--pico-nav-link-spacing-horizontal) * -1); +} +nav[aria-label=breadcrumb] ul li:not(:last-child)::after { + display: inline-block; + position: absolute; + width: calc(var(--pico-nav-link-spacing-horizontal) * 4); + margin: 0 calc(var(--pico-nav-link-spacing-horizontal) * -1); + content: var(--pico-nav-breadcrumb-divider); + color: var(--pico-muted-color); + text-align: center; + text-decoration: none; + white-space: nowrap; +} +nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]) { + background-color: transparent; + color: inherit; + text-decoration: none; + pointer-events: none; +} + +aside nav, +aside ol, +aside ul, +aside li { + display: block; +} +aside li { + padding: calc(var(--pico-nav-element-spacing-vertical) * 0.5) var(--pico-nav-element-spacing-horizontal); +} +aside li a { + display: block; +} +aside li [role=button] { + margin: inherit; +} + +[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after { + content: "\\"; +} + +/** + * Progress + */ +progress { + display: inline-block; + vertical-align: baseline; +} + +progress { + -webkit-appearance: none; + -moz-appearance: none; + display: inline-block; + appearance: none; + width: 100%; + height: 0.5rem; + margin-bottom: calc(var(--pico-spacing) * 0.5); + overflow: hidden; + border: 0; + border-radius: var(--pico-border-radius); + background-color: var(--pico-progress-background-color); + color: var(--pico-progress-color); +} +progress::-webkit-progress-bar { + border-radius: var(--pico-border-radius); + background: none; +} +progress[value]::-webkit-progress-value { + background-color: var(--pico-progress-color); + -webkit-transition: inline-size var(--pico-transition); + transition: inline-size var(--pico-transition); +} +progress::-moz-progress-bar { + background-color: var(--pico-progress-color); +} +@media (prefers-reduced-motion: no-preference) { + progress:indeterminate { + background: var(--pico-progress-background-color) linear-gradient(to right, var(--pico-progress-color) 30%, var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat; + animation: progress-indeterminate 1s linear infinite; + } + progress:indeterminate[value]::-webkit-progress-value { + background-color: transparent; + } + progress:indeterminate::-moz-progress-bar { + background-color: transparent; + } +} + +@media (prefers-reduced-motion: no-preference) { + [dir=rtl] progress:indeterminate { + animation-direction: reverse; + } +} + +@keyframes progress-indeterminate { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} +/** + * Tooltip ([data-tooltip]) + */ +[data-tooltip] { + position: relative; +} +[data-tooltip]:not(a, button, input) { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; +} +[data-tooltip][data-placement=top]::before, [data-tooltip][data-placement=top]::after, [data-tooltip]::before, [data-tooltip]::after { + display: block; + z-index: 99; + position: absolute; + bottom: 100%; + left: 50%; + padding: 0.25rem 0.5rem; + overflow: hidden; + transform: translate(-50%, -0.25rem); + border-radius: var(--pico-border-radius); + background: var(--pico-tooltip-background-color); + content: attr(data-tooltip); + color: var(--pico-tooltip-color); + font-style: normal; + font-weight: var(--pico-font-weight); + font-size: 0.875rem; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0; + pointer-events: none; +} +[data-tooltip][data-placement=top]::after, [data-tooltip]::after { + padding: 0; + transform: translate(-50%, 0rem); + border-top: 0.3rem solid; + border-right: 0.3rem solid transparent; + border-left: 0.3rem solid transparent; + border-radius: 0; + background-color: transparent; + content: ""; + color: var(--pico-tooltip-background-color); +} +[data-tooltip][data-placement=bottom]::before, [data-tooltip][data-placement=bottom]::after { + top: 100%; + bottom: auto; + transform: translate(-50%, 0.25rem); +} +[data-tooltip][data-placement=bottom]:after { + transform: translate(-50%, -0.3rem); + border: 0.3rem solid transparent; + border-bottom: 0.3rem solid; +} +[data-tooltip][data-placement=left]::before, [data-tooltip][data-placement=left]::after { + top: 50%; + right: 100%; + bottom: auto; + left: auto; + transform: translate(-0.25rem, -50%); +} +[data-tooltip][data-placement=left]:after { + transform: translate(0.3rem, -50%); + border: 0.3rem solid transparent; + border-left: 0.3rem solid; +} +[data-tooltip][data-placement=right]::before, [data-tooltip][data-placement=right]::after { + top: 50%; + right: auto; + bottom: auto; + left: 100%; + transform: translate(0.25rem, -50%); +} +[data-tooltip][data-placement=right]:after { + transform: translate(-0.3rem, -50%); + border: 0.3rem solid transparent; + border-right: 0.3rem solid; +} +[data-tooltip]:focus::before, [data-tooltip]:focus::after, [data-tooltip]:hover::before, [data-tooltip]:hover::after { + opacity: 1; +} +@media (hover: hover) and (pointer: fine) { + [data-tooltip]:focus::before, [data-tooltip]:focus::after, [data-tooltip]:hover::before, [data-tooltip]:hover::after { + --pico-tooltip-slide-to: translate(-50%, -0.25rem); + transform: translate(-50%, 0.75rem); + animation-duration: 0.2s; + animation-fill-mode: forwards; + animation-name: tooltip-slide; + opacity: 0; + } + [data-tooltip]:focus::after, [data-tooltip]:hover::after { + --pico-tooltip-caret-slide-to: translate(-50%, 0rem); + transform: translate(-50%, -0.25rem); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement=bottom]:focus::before, [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover::before, [data-tooltip][data-placement=bottom]:hover::after { + --pico-tooltip-slide-to: translate(-50%, 0.25rem); + transform: translate(-50%, -0.75rem); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover::after { + --pico-tooltip-caret-slide-to: translate(-50%, -0.3rem); + transform: translate(-50%, -0.5rem); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement=left]:focus::before, [data-tooltip][data-placement=left]:focus::after, [data-tooltip][data-placement=left]:hover::before, [data-tooltip][data-placement=left]:hover::after { + --pico-tooltip-slide-to: translate(-0.25rem, -50%); + transform: translate(0.75rem, -50%); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement=left]:focus::after, [data-tooltip][data-placement=left]:hover::after { + --pico-tooltip-caret-slide-to: translate(0.3rem, -50%); + transform: translate(0.05rem, -50%); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement=right]:focus::before, [data-tooltip][data-placement=right]:focus::after, [data-tooltip][data-placement=right]:hover::before, [data-tooltip][data-placement=right]:hover::after { + --pico-tooltip-slide-to: translate(0.25rem, -50%); + transform: translate(-0.75rem, -50%); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement=right]:focus::after, [data-tooltip][data-placement=right]:hover::after { + --pico-tooltip-caret-slide-to: translate(-0.3rem, -50%); + transform: translate(-0.05rem, -50%); + animation-name: tooltip-caret-slide; + } +} +@keyframes tooltip-slide { + to { + transform: var(--pico-tooltip-slide-to); + opacity: 1; + } +} +@keyframes tooltip-caret-slide { + 50% { + opacity: 0; + } + to { + transform: var(--pico-tooltip-caret-slide-to); + opacity: 1; + } +} + +/** + * Accessibility & User interaction + */ +[aria-controls] { + cursor: pointer; +} + +[aria-disabled=true], +[disabled] { + cursor: not-allowed; +} + +[aria-hidden=false][hidden] { + display: initial; +} + +[aria-hidden=false][hidden]:not(:focus) { + clip: rect(0, 0, 0, 0); + position: absolute; +} + +a, +area, +button, +input, +label, +select, +summary, +textarea, +[tabindex] { + -ms-touch-action: manipulation; +} + +[dir=rtl] { + direction: rtl; +} + +/** + * Reduce Motion Features + */ +@media (prefers-reduced-motion: reduce) { + *:not([aria-busy=true]), + :not([aria-busy=true])::before, + :not([aria-busy=true])::after { + background-attachment: initial !important; + animation-duration: 1ms !important; + animation-delay: -1ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-delay: 0s !important; + transition-duration: 0s !important; + } +} \ No newline at end of file diff --git a/lcars/lcars_v5/eventbus/eventbus.go b/lcars/lcars_v5/eventbus/eventbus.go index 7d13bea..b33ab57 100644 --- a/lcars/lcars_v5/eventbus/eventbus.go +++ b/lcars/lcars_v5/eventbus/eventbus.go @@ -1,6 +1,3 @@ -// modified version, see original: -// https://github.com/dtomasi/go-event-bus/tree/main - package eventbus import ( @@ -8,6 +5,11 @@ import ( "sync/atomic" ) +// DefaultEventChannelBuffer is the buffer size used for new event channels. +// Use a small buffer to avoid accidental blocking in common cases while still +// keeping memory usage modest. +const DefaultEventChannelBuffer = 8 + type Data map[string]interface{} // SafeCounter is a concurrency safe counter. @@ -55,7 +57,9 @@ type TopicStats struct { type topicStatsMap map[string]*TopicStats +// Stats is concurrency-safe. type Stats struct { + mu sync.RWMutex data topicStatsMap } @@ -66,22 +70,31 @@ func newStats() *Stats { } func (s *Stats) getOrCreateTopicStats(topicName string) *TopicStats { - _, ok := s.data[topicName] - if !ok { - s.data[topicName] = &TopicStats{ - Name: topicName, - PublishedCount: NewSafeCounter(), - SubscriberCount: NewSafeCounter(), - } + s.mu.Lock() + defer s.mu.Unlock() + + if ts, ok := s.data[topicName]; ok { + return ts } - return s.data[topicName] + ts := &TopicStats{ + Name: topicName, + PublishedCount: NewSafeCounter(), + SubscriberCount: NewSafeCounter(), + } + s.data[topicName] = ts + return ts } func (s *Stats) incSubscriberCountByTopic(topicName string) { s.getOrCreateTopicStats(topicName).SubscriberCount.Inc() } +func (s *Stats) decSubscriberCountByTopic(topicName string) { + // ensure topic exists, then decrement + s.getOrCreateTopicStats(topicName).SubscriberCount.Dec() +} + func (s *Stats) GetSubscriberCountByTopic(topicName string) int { return s.getOrCreateTopicStats(topicName).SubscriberCount.Value() } @@ -95,7 +108,10 @@ func (s *Stats) GetPublishedCountByTopic(topicName string) int { } func (s *Stats) GetTopicStats() []*TopicStats { - var tStatsSlice []*TopicStats + s.mu.RLock() + defer s.mu.RUnlock() + + tStatsSlice := make([]*TopicStats, 0, len(s.data)) for _, tStats := range s.data { tStatsSlice = append(tStatsSlice, tStats) } @@ -127,12 +143,17 @@ type CallbackFunc func(topic string, data Data) // EventChannel is a channel which can accept an Event. type EventChannel chan Event -// NewEventChannel Creates a new EventChannel. +// NewEventChannel Creates a new EventChannel with default buffer. func NewEventChannel() EventChannel { + return make(EventChannel, DefaultEventChannelBuffer) +} + +// NewUnbufferedEventChannel creates an unbuffered channel (exposed if caller wants it). +func NewUnbufferedEventChannel() EventChannel { return make(EventChannel) } -// dataChannelSlice is a slice of DataChannels. +// eventChannelSlice is a slice of EventChannels. type eventChannelSlice []EventChannel // EventBus stores the information about subscribers interested for a particular topic. @@ -152,27 +173,41 @@ func NewEventBus() *EventBus { // getSubscribingChannels returns all subscribing channels including wildcard matches. func (eb *EventBus) getSubscribingChannels(topic string) eventChannelSlice { + eb.mu.RLock() + defer eb.mu.RUnlock() + subChannels := eventChannelSlice{} - for topicName := range eb.subscribers { + for topicName, chans := range eb.subscribers { if topicName == topic || matchWildcard(topicName, topic) { - subChannels = append(subChannels, eb.subscribers[topicName]...) + // append clone to avoid races on the underlying slice + subChannels = append(subChannels, chans...) } } return subChannels } -// doPublish is publishing events to channels internally. +// doPublish sends the event to each channel synchronously (blocking sends). +// This is used by Publish (synchronous) so the WaitGroup semantics are preserved. func (eb *EventBus) doPublish(channels eventChannelSlice, evt Event) { - eb.mu.RLock() - defer eb.mu.RUnlock() + for _, ch := range channels { + ch <- evt // blocking send; Publish relies on this behavior + } +} - go func(channels eventChannelSlice, evt Event) { - for _, ch := range channels { - ch <- evt +// doPublishAsync tries to send to each channel without blocking — dropped sends are ignored. +// This is used by PublishAsync to avoid goroutine leaks when subscribers are slow/missing. +func (eb *EventBus) doPublishAsync(channels eventChannelSlice, evt Event) { + for _, ch := range channels { + select { + case ch <- evt: + // delivered + default: + // subscriber not ready; drop the event for this subscriber + // optionally: count drops or log } - }(channels, evt) + } } // Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go @@ -209,16 +244,19 @@ func deepMatchRune(str, pattern []rune, simple bool) bool { //nolint:unparam return len(str) == 0 && len(pattern) == 0 } -// PublishAsync data to a topic asynchronously -// This function returns a bool channel which indicates that all subscribers where called. +// PublishAsync data to a topic asynchronously. +// This will try to deliver events without blocking; slow/missing subscribers may miss events. func (eb *EventBus) PublishAsync(topic string, data Data) { - eb.doPublish( - eb.getSubscribingChannels(topic), - Event{ - Data: data, - Topic: topic, - wg: nil, - }) + channels := eb.getSubscribingChannels(topic) + + evt := Event{ + Data: data, + Topic: topic, + wg: nil, + } + + // run async non-blocking publisher in a goroutine so caller isn't blocked + go eb.doPublishAsync(channels, evt) eb.stats.incPublishedCountByTopic(topic) } @@ -235,16 +273,19 @@ func (eb *EventBus) PublishAsyncOnce(topic string, data Data) { // Publish data to a topic and wait for all subscribers to finish // This function creates a waitGroup internally. All subscribers must call Done() function on Event. func (eb *EventBus) Publish(topic string, data Data) interface{} { - wg := sync.WaitGroup{} channels := eb.getSubscribingChannels(topic) + + wg := sync.WaitGroup{} wg.Add(len(channels)) - eb.doPublish( - channels, - Event{ - Data: data, - Topic: topic, - wg: &wg, - }) + + evt := Event{ + Data: data, + Topic: topic, + wg: &wg, + } + + // synchronous blocking publish: callers wait until subscribers call Done() + eb.doPublish(channels, evt) wg.Wait() eb.stats.incPublishedCountByTopic(topic) @@ -261,13 +302,10 @@ func (eb *EventBus) PublishOnce(topic string, data Data) interface{} { return eb.Publish(topic, data) } -// Subscribe to a topic passing a EventChannel. +// Subscribe to a topic returning a buffered EventChannel (to reduce accidental blocking). func (eb *EventBus) Subscribe(topic string) EventChannel { - ch := make(EventChannel) + ch := NewEventChannel() eb.SubscribeChannel(topic, ch) - - eb.stats.incSubscriberCountByTopic(topic) - return ch } @@ -285,18 +323,60 @@ func (eb *EventBus) SubscribeChannel(topic string, ch EventChannel) { eb.stats.incSubscriberCountByTopic(topic) } +// Unsubscribe removes a previously-subscribed channel for a topic. +func (eb *EventBus) Unsubscribe(topic string, ch EventChannel) { + eb.UnsubscribeChannel(topic, ch) +} + +// UnsubscribeChannel removes a channel from subscribers and decrements the counter. +func (eb *EventBus) UnsubscribeChannel(topic string, ch EventChannel) { + eb.mu.Lock() + defer eb.mu.Unlock() + + if chans, ok := eb.subscribers[topic]; ok { + newChans := make(eventChannelSlice, 0, len(chans)) + removed := false + for _, c := range chans { + if c == ch && !removed { + removed = true + continue + } + newChans = append(newChans, c) + } + if removed { + if len(newChans) == 0 { + delete(eb.subscribers, topic) + } else { + eb.subscribers[topic] = newChans + } + // decrement subscriber counter once + eb.stats.decSubscriberCountByTopic(topic) + } + } +} + // SubscribeCallback provides a simple wrapper that allows to directly register CallbackFunc instead of channels. -func (eb *EventBus) SubscribeCallback(topic string, callable CallbackFunc) { +// The callback keeps receiving events until the channel is unsubscribed or closed. +// recover() is used so panics in user callback won't kill the goroutine. +func (eb *EventBus) SubscribeCallback(topic string, callable CallbackFunc) EventChannel { ch := NewEventChannel() eb.SubscribeChannel(topic, ch) - go func(callable CallbackFunc) { - evt := <-ch - callable(evt.Topic, evt.Data) - evt.Done() - }(callable) + go func(callable CallbackFunc, ch EventChannel) { + for evt := range ch { + func() { + defer func() { + if r := recover(); r != nil { + // recovered from panic in callback; swallow or log as needed + } + }() + callable(evt.Topic, evt.Data) + evt.Done() + }() + } + }(callable, ch) - eb.stats.incSubscriberCountByTopic(topic) + return ch } // HasSubscribers Check if a topic has subscribers. diff --git a/lcars/lcars_v5/eventbus/eventbus.go.txt b/lcars/lcars_v5/eventbus/eventbus.go.txt new file mode 100644 index 0000000..7d13bea --- /dev/null +++ b/lcars/lcars_v5/eventbus/eventbus.go.txt @@ -0,0 +1,310 @@ +// modified version, see original: +// https://github.com/dtomasi/go-event-bus/tree/main + +package eventbus + +import ( + "sync" + "sync/atomic" +) + +type Data map[string]interface{} + +// SafeCounter is a concurrency safe counter. +type SafeCounter struct { + v *uint64 +} + +// NewSafeCounter creates a new counter. +func NewSafeCounter() *SafeCounter { + return &SafeCounter{ + v: new(uint64), + } +} + +// Value returns the current value. +func (c *SafeCounter) Value() int { + return int(atomic.LoadUint64(c.v)) +} + +// IncBy increments the counter by given delta. +func (c *SafeCounter) IncBy(add uint) { + atomic.AddUint64(c.v, uint64(add)) +} + +// Inc increments the counter by 1. +func (c *SafeCounter) Inc() { + c.IncBy(1) +} + +// DecBy decrements the counter by given delta. +func (c *SafeCounter) DecBy(dec uint) { + atomic.AddUint64(c.v, ^uint64(dec-1)) +} + +// Dec decrements the counter by 1. +func (c *SafeCounter) Dec() { + c.DecBy(1) +} + +type TopicStats struct { + Name string + PublishedCount *SafeCounter + SubscriberCount *SafeCounter +} + +type topicStatsMap map[string]*TopicStats + +type Stats struct { + data topicStatsMap +} + +func newStats() *Stats { + return &Stats{ + data: map[string]*TopicStats{}, + } +} + +func (s *Stats) getOrCreateTopicStats(topicName string) *TopicStats { + _, ok := s.data[topicName] + if !ok { + s.data[topicName] = &TopicStats{ + Name: topicName, + PublishedCount: NewSafeCounter(), + SubscriberCount: NewSafeCounter(), + } + } + + return s.data[topicName] +} + +func (s *Stats) incSubscriberCountByTopic(topicName string) { + s.getOrCreateTopicStats(topicName).SubscriberCount.Inc() +} + +func (s *Stats) GetSubscriberCountByTopic(topicName string) int { + return s.getOrCreateTopicStats(topicName).SubscriberCount.Value() +} + +func (s *Stats) incPublishedCountByTopic(topicName string) { + s.getOrCreateTopicStats(topicName).PublishedCount.Inc() +} + +func (s *Stats) GetPublishedCountByTopic(topicName string) int { + return s.getOrCreateTopicStats(topicName).PublishedCount.Value() +} + +func (s *Stats) GetTopicStats() []*TopicStats { + var tStatsSlice []*TopicStats + for _, tStats := range s.data { + tStatsSlice = append(tStatsSlice, tStats) + } + + return tStatsSlice +} + +func (s *Stats) GetTopicStatsByName(topicName string) *TopicStats { + return s.getOrCreateTopicStats(topicName) +} + +// Event holds topic name and data. +type Event struct { + Data Data + Topic string + wg *sync.WaitGroup +} + +// Done calls Done on sync.WaitGroup if set. +func (e *Event) Done() { + if e.wg != nil { + e.wg.Done() + } +} + +// CallbackFunc Defines a CallbackFunc. +type CallbackFunc func(topic string, data Data) + +// EventChannel is a channel which can accept an Event. +type EventChannel chan Event + +// NewEventChannel Creates a new EventChannel. +func NewEventChannel() EventChannel { + return make(EventChannel) +} + +// dataChannelSlice is a slice of DataChannels. +type eventChannelSlice []EventChannel + +// EventBus stores the information about subscribers interested for a particular topic. +type EventBus struct { + mu sync.RWMutex + subscribers map[string]eventChannelSlice + stats *Stats +} + +// NewEventBus returns a new EventBus instance. +func NewEventBus() *EventBus { + return &EventBus{ //nolint:exhaustivestruct + subscribers: map[string]eventChannelSlice{}, + stats: newStats(), + } +} + +// getSubscribingChannels returns all subscribing channels including wildcard matches. +func (eb *EventBus) getSubscribingChannels(topic string) eventChannelSlice { + subChannels := eventChannelSlice{} + + for topicName := range eb.subscribers { + if topicName == topic || matchWildcard(topicName, topic) { + subChannels = append(subChannels, eb.subscribers[topicName]...) + } + } + + return subChannels +} + +// doPublish is publishing events to channels internally. +func (eb *EventBus) doPublish(channels eventChannelSlice, evt Event) { + eb.mu.RLock() + defer eb.mu.RUnlock() + + go func(channels eventChannelSlice, evt Event) { + for _, ch := range channels { + ch <- evt + } + }(channels, evt) +} + +// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go +func matchWildcard(pattern, name string) bool { + if pattern == "" { + return name == pattern + } + + if pattern == "*" { + return true + } + // Does only wildcard '*' match. + return deepMatchRune([]rune(name), []rune(pattern), true) +} + +// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go +func deepMatchRune(str, pattern []rune, simple bool) bool { //nolint:unparam + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '*': + return deepMatchRune(str, pattern[1:], simple) || + (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + } + + str = str[1:] + + pattern = pattern[1:] + } + + return len(str) == 0 && len(pattern) == 0 +} + +// PublishAsync data to a topic asynchronously +// This function returns a bool channel which indicates that all subscribers where called. +func (eb *EventBus) PublishAsync(topic string, data Data) { + eb.doPublish( + eb.getSubscribingChannels(topic), + Event{ + Data: data, + Topic: topic, + wg: nil, + }) + + eb.stats.incPublishedCountByTopic(topic) +} + +// PublishAsyncOnce same as PublishAsync but makes sure that topic is only published once. +func (eb *EventBus) PublishAsyncOnce(topic string, data Data) { + if eb.stats.GetPublishedCountByTopic(topic) > 0 { + return + } + + eb.PublishAsync(topic, data) +} + +// Publish data to a topic and wait for all subscribers to finish +// This function creates a waitGroup internally. All subscribers must call Done() function on Event. +func (eb *EventBus) Publish(topic string, data Data) interface{} { + wg := sync.WaitGroup{} + channels := eb.getSubscribingChannels(topic) + wg.Add(len(channels)) + eb.doPublish( + channels, + Event{ + Data: data, + Topic: topic, + wg: &wg, + }) + wg.Wait() + + eb.stats.incPublishedCountByTopic(topic) + + return data +} + +// PublishOnce same as Publish but makes sure only published once on topic. +func (eb *EventBus) PublishOnce(topic string, data Data) interface{} { + if eb.stats.GetPublishedCountByTopic(topic) > 0 { + return nil + } + + return eb.Publish(topic, data) +} + +// Subscribe to a topic passing a EventChannel. +func (eb *EventBus) Subscribe(topic string) EventChannel { + ch := make(EventChannel) + eb.SubscribeChannel(topic, ch) + + eb.stats.incSubscriberCountByTopic(topic) + + return ch +} + +// SubscribeChannel subscribes to a given Channel. +func (eb *EventBus) SubscribeChannel(topic string, ch EventChannel) { + eb.mu.Lock() + defer eb.mu.Unlock() + + if prev, found := eb.subscribers[topic]; found { + eb.subscribers[topic] = append(prev, ch) + } else { + eb.subscribers[topic] = append([]EventChannel{}, ch) + } + + eb.stats.incSubscriberCountByTopic(topic) +} + +// SubscribeCallback provides a simple wrapper that allows to directly register CallbackFunc instead of channels. +func (eb *EventBus) SubscribeCallback(topic string, callable CallbackFunc) { + ch := NewEventChannel() + eb.SubscribeChannel(topic, ch) + + go func(callable CallbackFunc) { + evt := <-ch + callable(evt.Topic, evt.Data) + evt.Done() + }(callable) + + eb.stats.incSubscriberCountByTopic(topic) +} + +// HasSubscribers Check if a topic has subscribers. +func (eb *EventBus) HasSubscribers(topic string) bool { + return len(eb.getSubscribingChannels(topic)) > 0 +} + +// Stats returns the stats map. +func (eb *EventBus) Stats() *Stats { + return eb.stats +} diff --git a/lcars/lcars_v5/experiments/ebus/eventbus.go b/lcars/lcars_v5/experiments/ebus/eventbus.go new file mode 100644 index 0000000..9ac30ca --- /dev/null +++ b/lcars/lcars_v5/experiments/ebus/eventbus.go @@ -0,0 +1,390 @@ +package main + +import ( + "sync" + "sync/atomic" +) + +// DefaultEventChannelBuffer is the buffer size used for new event channels. +// Use a small buffer to avoid accidental blocking in common cases while still +// keeping memory usage modest. +const DefaultEventChannelBuffer = 8 + +type Data map[string]interface{} + +// SafeCounter is a concurrency safe counter. +type SafeCounter struct { + v *uint64 +} + +// NewSafeCounter creates a new counter. +func NewSafeCounter() *SafeCounter { + return &SafeCounter{ + v: new(uint64), + } +} + +// Value returns the current value. +func (c *SafeCounter) Value() int { + return int(atomic.LoadUint64(c.v)) +} + +// IncBy increments the counter by given delta. +func (c *SafeCounter) IncBy(add uint) { + atomic.AddUint64(c.v, uint64(add)) +} + +// Inc increments the counter by 1. +func (c *SafeCounter) Inc() { + c.IncBy(1) +} + +// DecBy decrements the counter by given delta. +func (c *SafeCounter) DecBy(dec uint) { + atomic.AddUint64(c.v, ^uint64(dec-1)) +} + +// Dec decrements the counter by 1. +func (c *SafeCounter) Dec() { + c.DecBy(1) +} + +type TopicStats struct { + Name string + PublishedCount *SafeCounter + SubscriberCount *SafeCounter +} + +type topicStatsMap map[string]*TopicStats + +// Stats is concurrency-safe. +type Stats struct { + mu sync.RWMutex + data topicStatsMap +} + +func newStats() *Stats { + return &Stats{ + data: map[string]*TopicStats{}, + } +} + +func (s *Stats) getOrCreateTopicStats(topicName string) *TopicStats { + s.mu.Lock() + defer s.mu.Unlock() + + if ts, ok := s.data[topicName]; ok { + return ts + } + + ts := &TopicStats{ + Name: topicName, + PublishedCount: NewSafeCounter(), + SubscriberCount: NewSafeCounter(), + } + s.data[topicName] = ts + return ts +} + +func (s *Stats) incSubscriberCountByTopic(topicName string) { + s.getOrCreateTopicStats(topicName).SubscriberCount.Inc() +} + +func (s *Stats) decSubscriberCountByTopic(topicName string) { + // ensure topic exists, then decrement + s.getOrCreateTopicStats(topicName).SubscriberCount.Dec() +} + +func (s *Stats) GetSubscriberCountByTopic(topicName string) int { + return s.getOrCreateTopicStats(topicName).SubscriberCount.Value() +} + +func (s *Stats) incPublishedCountByTopic(topicName string) { + s.getOrCreateTopicStats(topicName).PublishedCount.Inc() +} + +func (s *Stats) GetPublishedCountByTopic(topicName string) int { + return s.getOrCreateTopicStats(topicName).PublishedCount.Value() +} + +func (s *Stats) GetTopicStats() []*TopicStats { + s.mu.RLock() + defer s.mu.RUnlock() + + tStatsSlice := make([]*TopicStats, 0, len(s.data)) + for _, tStats := range s.data { + tStatsSlice = append(tStatsSlice, tStats) + } + + return tStatsSlice +} + +func (s *Stats) GetTopicStatsByName(topicName string) *TopicStats { + return s.getOrCreateTopicStats(topicName) +} + +// Event holds topic name and data. +type Event struct { + Data Data + Topic string + wg *sync.WaitGroup +} + +// Done calls Done on sync.WaitGroup if set. +func (e *Event) Done() { + if e.wg != nil { + e.wg.Done() + } +} + +// CallbackFunc Defines a CallbackFunc. +type CallbackFunc func(topic string, data Data) + +// EventChannel is a channel which can accept an Event. +type EventChannel chan Event + +// NewEventChannel Creates a new EventChannel with default buffer. +func NewEventChannel() EventChannel { + return make(EventChannel, DefaultEventChannelBuffer) +} + +// NewUnbufferedEventChannel creates an unbuffered channel (exposed if caller wants it). +func NewUnbufferedEventChannel() EventChannel { + return make(EventChannel) +} + +// eventChannelSlice is a slice of EventChannels. +type eventChannelSlice []EventChannel + +// EventBus stores the information about subscribers interested for a particular topic. +type EventBus struct { + mu sync.RWMutex + subscribers map[string]eventChannelSlice + stats *Stats +} + +// NewEventBus returns a new EventBus instance. +func NewEventBus() *EventBus { + return &EventBus{ //nolint:exhaustivestruct + subscribers: map[string]eventChannelSlice{}, + stats: newStats(), + } +} + +// getSubscribingChannels returns all subscribing channels including wildcard matches. +func (eb *EventBus) getSubscribingChannels(topic string) eventChannelSlice { + eb.mu.RLock() + defer eb.mu.RUnlock() + + subChannels := eventChannelSlice{} + + for topicName, chans := range eb.subscribers { + if topicName == topic || matchWildcard(topicName, topic) { + // append clone to avoid races on the underlying slice + subChannels = append(subChannels, chans...) + } + } + + return subChannels +} + +// doPublish sends the event to each channel synchronously (blocking sends). +// This is used by Publish (synchronous) so the WaitGroup semantics are preserved. +func (eb *EventBus) doPublish(channels eventChannelSlice, evt Event) { + for _, ch := range channels { + ch <- evt // blocking send; Publish relies on this behavior + } +} + +// doPublishAsync tries to send to each channel without blocking — dropped sends are ignored. +// This is used by PublishAsync to avoid goroutine leaks when subscribers are slow/missing. +func (eb *EventBus) doPublishAsync(channels eventChannelSlice, evt Event) { + for _, ch := range channels { + select { + case ch <- evt: + // delivered + default: + // subscriber not ready; drop the event for this subscriber + // optionally: count drops or log + } + } +} + +// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go +func matchWildcard(pattern, name string) bool { + if pattern == "" { + return name == pattern + } + + if pattern == "*" { + return true + } + // Does only wildcard '*' match. + return deepMatchRune([]rune(name), []rune(pattern), true) +} + +// Code from https://github.com/minio/minio/blob/master/pkg/wildcard/match.go +func deepMatchRune(str, pattern []rune, simple bool) bool { //nolint:unparam + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '*': + return deepMatchRune(str, pattern[1:], simple) || + (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + } + + str = str[1:] + + pattern = pattern[1:] + } + + return len(str) == 0 && len(pattern) == 0 +} + +// PublishAsync data to a topic asynchronously. +// This will try to deliver events without blocking; slow/missing subscribers may miss events. +func (eb *EventBus) PublishAsync(topic string, data Data) { + channels := eb.getSubscribingChannels(topic) + + evt := Event{ + Data: data, + Topic: topic, + wg: nil, + } + + // run async non-blocking publisher in a goroutine so caller isn't blocked + go eb.doPublishAsync(channels, evt) + + eb.stats.incPublishedCountByTopic(topic) +} + +// PublishAsyncOnce same as PublishAsync but makes sure that topic is only published once. +func (eb *EventBus) PublishAsyncOnce(topic string, data Data) { + if eb.stats.GetPublishedCountByTopic(topic) > 0 { + return + } + + eb.PublishAsync(topic, data) +} + +// Publish data to a topic and wait for all subscribers to finish +// This function creates a waitGroup internally. All subscribers must call Done() function on Event. +func (eb *EventBus) Publish(topic string, data Data) interface{} { + channels := eb.getSubscribingChannels(topic) + + wg := sync.WaitGroup{} + wg.Add(len(channels)) + + evt := Event{ + Data: data, + Topic: topic, + wg: &wg, + } + + // synchronous blocking publish: callers wait until subscribers call Done() + eb.doPublish(channels, evt) + wg.Wait() + + eb.stats.incPublishedCountByTopic(topic) + + return data +} + +// PublishOnce same as Publish but makes sure only published once on topic. +func (eb *EventBus) PublishOnce(topic string, data Data) interface{} { + if eb.stats.GetPublishedCountByTopic(topic) > 0 { + return nil + } + + return eb.Publish(topic, data) +} + +// Subscribe to a topic returning a buffered EventChannel (to reduce accidental blocking). +func (eb *EventBus) Subscribe(topic string) EventChannel { + ch := NewEventChannel() + eb.SubscribeChannel(topic, ch) + return ch +} + +// SubscribeChannel subscribes to a given Channel. +func (eb *EventBus) SubscribeChannel(topic string, ch EventChannel) { + eb.mu.Lock() + defer eb.mu.Unlock() + + if prev, found := eb.subscribers[topic]; found { + eb.subscribers[topic] = append(prev, ch) + } else { + eb.subscribers[topic] = append([]EventChannel{}, ch) + } + + eb.stats.incSubscriberCountByTopic(topic) +} + +// Unsubscribe removes a previously-subscribed channel for a topic. +func (eb *EventBus) Unsubscribe(topic string, ch EventChannel) { + eb.UnsubscribeChannel(topic, ch) +} + +// UnsubscribeChannel removes a channel from subscribers and decrements the counter. +func (eb *EventBus) UnsubscribeChannel(topic string, ch EventChannel) { + eb.mu.Lock() + defer eb.mu.Unlock() + + if chans, ok := eb.subscribers[topic]; ok { + newChans := make(eventChannelSlice, 0, len(chans)) + removed := false + for _, c := range chans { + if c == ch && !removed { + removed = true + continue + } + newChans = append(newChans, c) + } + if removed { + if len(newChans) == 0 { + delete(eb.subscribers, topic) + } else { + eb.subscribers[topic] = newChans + } + // decrement subscriber counter once + eb.stats.decSubscriberCountByTopic(topic) + } + } +} + +// SubscribeCallback provides a simple wrapper that allows to directly register CallbackFunc instead of channels. +// The callback keeps receiving events until the channel is unsubscribed or closed. +// recover() is used so panics in user callback won't kill the goroutine. +func (eb *EventBus) SubscribeCallback(topic string, callable CallbackFunc) EventChannel { + ch := NewEventChannel() + eb.SubscribeChannel(topic, ch) + + go func(callable CallbackFunc, ch EventChannel) { + for evt := range ch { + func() { + defer func() { + if r := recover(); r != nil { + // recovered from panic in callback; swallow or log as needed + } + }() + callable(evt.Topic, evt.Data) + evt.Done() + }() + } + }(callable, ch) + + return ch +} + +// HasSubscribers Check if a topic has subscribers. +func (eb *EventBus) HasSubscribers(topic string) bool { + return len(eb.getSubscribingChannels(topic)) > 0 +} + +// Stats returns the stats map. +func (eb *EventBus) Stats() *Stats { + return eb.stats +} diff --git a/lcars/lcars_v5/experiments/ebus/main.go b/lcars/lcars_v5/experiments/ebus/main.go new file mode 100644 index 0000000..798bedb --- /dev/null +++ b/lcars/lcars_v5/experiments/ebus/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "time" +) + +func main() { + eb := NewEventBus() + + fmt.Println("=== EventBus Demo ===") + + // Subscribe to specific topic + ch1 := eb.Subscribe("orders.created") + + // Subscribe to wildcard + ch2 := eb.Subscribe("orders.*") + + // Subscribe using a callback + eb.SubscribeCallback("payments.*", func(topic string, data Data) { + fmt.Printf("[Callback] Payment event received: %s -> %+v\n", topic, data) + }) + + // Run listener goroutines + go func() { + for evt := range ch1 { + fmt.Printf("[Listener 1] Topic=%s Data=%v\n", evt.Topic, evt.Data) + time.Sleep(50 * time.Millisecond) // simulate work + evt.Done() + } + }() + + go func() { + for evt := range ch2 { + fmt.Printf("[Listener 2] Wildcard match for %s Data=%v\n", evt.Topic, evt.Data) + evt.Done() + } + }() + + // ---- PUBLISH SYNCHRONOUS EVENTS ---- + fmt.Println("\n--- Publish Sync ---") + eb.Publish("orders.created", Data{"id": 42, "customer": "Alice"}) + eb.Publish("orders.updated", Data{"id": 42, "status": "shipped"}) + + // ---- PUBLISH ASYNC EVENTS ---- + fmt.Println("\n--- Publish Async ---") + eb.PublishAsync("payments.received", Data{"id": 1001, "amount": 99.50}) + eb.PublishAsync("payments.refunded", Data{"id": 1002, "amount": 20.00}) + + // Give async events time to deliver + time.Sleep(200 * time.Millisecond) + + // ---- PUBLISH ONCE semantics ---- + fmt.Println("\n--- Publish Once ---") + eb.PublishOnce("orders.once", Data{"msg": "first time"}) + eb.PublishOnce("orders.once", Data{"msg": "second time (ignored)"}) + eb.PublishAsyncOnce("payments.once", Data{"msg": "async first"}) + eb.PublishAsyncOnce("payments.once", Data{"msg": "async second (ignored)"}) + + time.Sleep(100 * time.Millisecond) + + // ---- UNSUBSCRIBE ---- + fmt.Println("\n--- Unsubscribe Listener 2 (orders.*) ---") + eb.Unsubscribe("orders.*", ch2) + close(ch2) + + eb.Publish("orders.updated", Data{"id": 42, "status": "delivered"}) + + time.Sleep(100 * time.Millisecond) + + // ---- PRINT STATS ---- + fmt.Println("\n--- Stats ---") + for _, ts := range eb.Stats().GetTopicStats() { + fmt.Printf("Topic: %-20s | Published: %-3d | Subscribers: %-3d\n", + ts.Name, ts.PublishedCount.Value(), ts.SubscriberCount.Value()) + } + + fmt.Println("\n=== Done ===") +} diff --git a/lcars/lcars_v5/experiments/main.go b/lcars/lcars_v5/experiments/startdate/main.go similarity index 100% rename from lcars/lcars_v5/experiments/main.go rename to lcars/lcars_v5/experiments/startdate/main.go diff --git a/lcars/lcars_v5/go.mod b/lcars/lcars_v5/go.mod index 5a773da..ba2f7f4 100644 --- a/lcars/lcars_v5/go.mod +++ b/lcars/lcars_v5/go.mod @@ -2,9 +2,15 @@ module ld go 1.25.0 -require modernc.org/sqlite v1.39.0 +require ( + github.com/CAFxX/httpcompression v0.0.9 + github.com/klauspost/compress v1.18.1 + github.com/valyala/bytebufferpool v1.0.0 + modernc.org/sqlite v1.39.0 +) require ( + github.com/andybalholm/brotli v1.0.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/lcars/lcars_v5/go.sum b/lcars/lcars_v5/go.sum index c46fb2d..5370e06 100644 --- a/lcars/lcars_v5/go.sum +++ b/lcars/lcars_v5/go.sum @@ -1,15 +1,44 @@ +github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg= +github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= +github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= +github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= @@ -21,6 +50,10 @@ golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= diff --git a/lcars/lcars_v5/main.go b/lcars/lcars_v5/main.go index 16e3759..602bf48 100644 --- a/lcars/lcars_v5/main.go +++ b/lcars/lcars_v5/main.go @@ -66,7 +66,7 @@ func run() error { ctx := context.Background() - stateDB, err := createStateDB(true) + stateDB, err := createStateDB(ctx) if err != nil { log.Fatalf("Failed to create internal StateDB: %v", err) } diff --git a/lcars/lcars_v5/server/server.go b/lcars/lcars_v5/server/server.go index 2a89209..d75bd31 100644 --- a/lcars/lcars_v5/server/server.go +++ b/lcars/lcars_v5/server/server.go @@ -26,6 +26,7 @@ type Server struct { } func New( + ctx context.Context, stateDB *sqlite.Database, embedded embed.FS, ebus *eventbus.EventBus, @@ -34,6 +35,7 @@ func New( // creating the server return &Server{ + Ctx: ctx, StateDB: stateDB, Embedded: embedded, Ebus: ebus, diff --git a/slides/datastar/slides.md b/slides/datastar/slides.md index 5f85fca..4c32ce4 100644 --- a/slides/datastar/slides.md +++ b/slides/datastar/slides.md @@ -148,7 +148,7 @@ To embrace the Creativity Paradox and stay within our C -

+

The benefits are clear: - Less code → faster performance and fewer bugs @@ -156,7 +156,7 @@ The benefits are clear: - Lower cognitive load → simpler to understand and extend - Fewer dependencies → more robust and secure - Leverage browser specifications and built-in capabilities -

+
@@ -630,3 +630,281 @@ transition: slide-up level: 2 --- # to be continued ... + +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +flowchart LR + %% Publishers + Scheduler["⏰ Scheduler
(periodic tasks)"] + API["🌐 REST API
(user actions)"] + Worker["⚙️ Background Job
(processing tasks)"] + + %% EventBus + EventBus["🧭 EventBus"] + + %% Subscribers + SSE["📡 SSE Broadcaster
(push to clients)"] + Logger["📝 Audit Logger
(write to SQLite)"] + Cache["🗃 Cache Updater"] + Notifier["🔔 Notification Service"] + + %% Client UI + Client["💻 Clients
via SSE"] + + %% Flows + Scheduler -->|publishes: task.tick| EventBus + API -->|publishes: ticket.created| EventBus + Worker -->|publishes: task.completed| EventBus + + EventBus -->|subscribes: task.*| SSE + EventBus -->|subscribes: ticket.*| SSE + + EventBus -->|subscribes: *| Logger + EventBus -->|subscribes: cache.invalidate| Cache + EventBus -->|subscribes: notify.*| Notifier + + SSE -->|SSE stream| Client + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +flowchart LR + subgraph Publishers + Scheduler["⏰ Scheduler
(periodic tasks)"] + API["🌐 API
(user triggers)"] + Worker["⚙️ Background Job"] + end + + EventBus["🧭 EventBus
(pub/sub hub)"] + + subgraph Subscribers + SSE["📡 SSE Broadcaster"] + Logger["📝 Audit Logger"] + Notifier["🔔 Notification Service"] + Cache["🗃 Cache Updater"] + end + + Client["💻 Clients via SSE"] + + Scheduler --> EventBus + API --> EventBus + Worker --> EventBus + + EventBus --> SSE + EventBus --> Logger + EventBus --> Notifier + EventBus --> Cache + + SSE --> Client + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +sequenceDiagram + participant P as Publisher (Scheduler) + participant EB as EventBus + participant S1 as SSE Broadcaster + participant S2 as Audit Logger + participant UI as Client UI + + P->>EB: Publish("task.tick", data) + EB->>S1: Dispatch event + EB->>S2: Dispatch event + + S1->>UI: Push via SSE + S2->>S2: Write to SQLite + + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +classDiagram + class EventBus { + - mu sync.RWMutex + - subscribers map[string][]EventChannel + - stats *Stats + + Publish(topic, data) + + PublishAsync(topic, data) + + Subscribe(topic) EventChannel + + SubscribeCallback(topic, cb) + } + + class EventChannel { + <> chan Event + } + + class Event { + Data map[string]interface + Topic string + wg *sync.WaitGroup + + Done() + } + + class Stats { + - data map[string]TopicStats + + GetPublishedCountByTopic(t) int + + GetSubscriberCountByTopic(t) int + } + + EventBus --> EventChannel : delivers Events + EventBus --> Stats + Event --> Stats : increments counters + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +flowchart TD + Publish["Publish()"] + Mutex["RWMutex Lock"] + Channels["subscribers[topic]"] + GoRoutine["Goroutine for dispatch"] + Sub1["Subscriber #1"] + Sub2["Subscriber #2"] + + Publish --> Mutex + Mutex --> Channels + Channels --> GoRoutine + GoRoutine --> Sub1 + GoRoutine --> Sub2 + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +flowchart TB + subgraph Backend + EB["EventBus"] + DB["SQLite Database"] + Scheduler + API + Worker + SSE["SSE Broadcaster"] + Logger + end + + subgraph Clients + Browser["Browser UI"] + end + + Scheduler --> EB + API --> EB + Worker --> EB + + EB --> SSE + EB --> Logger + + Logger --> DB + SSE --> Browser + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +flowchart LR + Client["🌐 Browser\n(HTML + JS)"] + + subgraph Server["🖥 Go Application Server"] + EB["EventBus"] + SSE["SSE Handler"] + API["HTTP API"] + Worker["Background Worker"] + Scheduler["Cron Scheduler"] + DB["SQLite File"] + end + + Client <--> SSE + Client <--> API + + Scheduler --> Worker + Worker --> EB + API --> EB + EB --> SSE + EB --> DB + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} + +flowchart TD + A["1️⃣ Publisher creates event"] + B["2️⃣ EventBus receives event"] + C["3️⃣ Match subscribers (wildcard + exact)"] + D["4️⃣ Dispatch to channels"] + E["5️⃣ Subscribers handle event"] + F["6️⃣ Optional: push to client via SSE"] + G["7️⃣ Stats updated"] + + A --> B --> C --> D --> E --> F --> G + +``` +--- +class: default +--- + +# Eventbus Architecture +
+ +```mermaid { scale: 0.68} +flowchart LR + Topic["Incoming Topic: ticket.updated"] + Exact["Exact Match: ticket.updated"] + Wild1["Wildcard: ticket.*"] + Wild2["Wildcard: *"] + Result["Matched Subscribers"] + + Topic --> Exact + Topic --> Wild1 + Topic --> Wild2 + + Exact --> Result + Wild1 --> Result + Wild2 --> Result + + +```