whose-turn 1.0.0-alpha.8 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import{d as v,e as w,v as s,F as o,L as a,H as c,K as t,N as f,z as p,O as u,M as h,I as k,b as g,_ as y,$ as N}from"./_plugin-vue_export-helper-BbgK5wvS.js";import{g as x,S as b,m as R,t as C,j as d}from"./bem-OtK9ToP0.js";const B={class:"app report-page"},L={key:0,class:"report-page__note"},S={class:"report-page__head"},T={class:"report-page__id"},V={class:"report-page__title"},E={class:"report-page__meta"},I={class:"report-page__section"},P={class:"report-page__text"},W={class:"report-page__section"},z={key:0,class:"report-page__unanswered"},A={key:1,class:"empty"},D={class:"report-page__answer-meta"},F={class:"report-page__text"},M=v({__name:"ReportPage",setup(U){const n=new URLSearchParams(location.search).get("id")||"",e=g(null),i=g(n?"Loading…":"No report id in the link.");return w(()=>{n&&(document.title=n+" · Your report · Whose Turn",x("/api/feedback?id="+encodeURIComponent(n)).then(_=>{e.value=_.report||null,i.value=_.report?"":"No report "+n+" was filed from this machine."}).catch(()=>{i.value=b+"; reload this page once it is back."}))}),(_,r)=>(s(),o("main",B,[e.value?(s(),o(c,{key:1},[t("header",S,[t("span",T,a(e.value.id),1),t("h1",V,a(e.value.title),1),t("span",{class:f(["pill",p(R)("pill",p(C)(e.value.outcome))])},a(e.value.state||"filed, not yet seen"),3)]),t("p",E,[u(a(e.value.kind||"report")+" · filed "+a(p(d)(e.value.filed_at)),1),e.value.changed_at?(s(),o(c,{key:0},[u(" · last change "+a(p(d)(e.value.changed_at)),1)],64)):h("",!0)]),t("section",I,[r[0]||(r[0]=t("h2",{class:"report-page__heading"},"What you reported",-1)),t("p",P,a(e.value.body||"(no text kept)"),1)]),t("section",W,[r[1]||(r[1]=t("h2",{class:"report-page__heading"},"Answers",-1)),e.value.unanswered?(s(),o("p",z," Closed without an answer. The maintainer owes you a reason; this page will show it once it is written. ")):e.value.answers.length?h("",!0):(s(),o("p",A,"No answer yet.")),(s(!0),o(c,null,k(e.value.answers,(l,m)=>(s(),o("article",{key:m,class:"report-page__answer"},[t("p",D,a(l.by||"maintainer")+" · "+a(p(d)(l.at)),1),t("p",F,a(l.text),1)]))),128))])],64)):(s(),o("p",L,a(i.value),1)),r[2]||(r[2]=t("a",{class:"report-page__back",href:"/"},"Back to the window",-1))]))}}),O=y(M,[["__scopeId","data-v-16ea9dee"]]);N(O).mount("#app");
@@ -0,0 +1 @@
1
+ import{w as N,a1 as J,E as Y,a2 as _,a3 as we,a4 as Pe,a5 as Ne,b as _e,D as ke,g as ne,a6 as Se,i as Fe,a7 as Le,e as We,a8 as Ie,a9 as ze,U as $e,Y as Be,c as O,z as qe,r as pe}from"./_plugin-vue_export-helper-BbgK5wvS.js";function P(e,t){return Le()?(Ie(e,t),!0):!1}const Z=new WeakMap,Ve=(...e)=>{var t;const a=e[0],n=(t=ne())===null||t===void 0?void 0:t.proxy,i=n??Le();if(i==null&&!Se())throw new Error("injectLocal must be called in setup");return i&&Z.has(i)&&a in Z.get(i)?Z.get(i)[a]:Fe(...e)},z=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const ta=e=>typeof e<"u",Je=e=>e!=null,Ke=Object.prototype.toString,Ue=e=>Ke.call(e)==="[object Object]",M=()=>{};function ie(...e){if(e.length!==1)return Pe(...e);const t=e[0];return typeof t=="function"?Ne(we(()=>({get:t,set:M}))):_e(t)}function oe(e,t){function a(...n){return new Promise((i,o)=>{Promise.resolve(e(()=>t.apply(this,n),{fn:t,thisArg:this,args:n})).then(i).catch(o)})}return"cancel"in e&&Object.assign(a,{cancel:e.cancel,flush:e.flush,isPending:e.isPending}),a}const Ee=e=>e();function je(e,t={}){let a,n,i=M,o=M;const s=_(!1),r=c=>{clearTimeout(c),i(),i=M};let l;return Object.assign(c=>{const u=Y(e),d=Y(t.maxWait);return a&&r(a),u<=0||d!==void 0&&d<=0?(n&&(r(n),n=void 0),s.value=!1,Promise.resolve(c())):(s.value=!0,new Promise((g,p)=>{i=t.rejectOnCancel?p:g,o=g,l=c,d&&!n&&(n=setTimeout(()=>{a&&r(a),n=void 0,s.value=!1,g(l())},d)),a=setTimeout(()=>{n&&r(n),n=void 0,s.value=!1,g(c())},u)}))},{cancel:()=>{a&&(r(a),a=void 0),n&&(r(n),n=void 0),s.value=!1,o=M},flush:()=>{if(s.value){a&&(clearTimeout(a),a=void 0),n&&(clearTimeout(n),n=void 0),s.value=!1;const c=o;i=M,o=M,c(l())}},isPending:J(s)})}function Ge(...e){let t=0,a,n=!0,i=M,o,s,r,l,h;!$e(e[0])&&typeof e[0]=="object"?{delay:s,trailing:r=!0,leading:l=!0,rejectOnCancel:h=!1}=e[0]:[s,r=!0,l=!0,h=!1]=e;const c=()=>{a&&(clearTimeout(a),a=void 0,i(),i=M)};return d=>{const g=Y(s),p=Date.now()-t,b=()=>o=d();return c(),g<=0?(t=Date.now(),b()):(p>g?(t=Date.now(),(l||!n)&&b()):r&&(o=new Promise((v,R)=>{i=h?R:v,a=setTimeout(()=>{t=Date.now(),n=!0,v(b()),c()},Math.max(0,g-p))})),!l&&!a&&(a=setTimeout(()=>n=!0,g)),n=!1,o)}}function Qe(e=Ee,t={}){const{initialState:a="active"}=t,n=ie(a==="active");function i(){n.value=!1}function o(){n.value=!0}const s=(...r)=>{n.value&&e(...r)};return{isActive:J(n),pause:i,resume:o,eventFilter:s}}function fe(e){return e.endsWith("rem")?Number.parseFloat(e)*16:Number.parseFloat(e)}function V(e){return Array.isArray(e)?e:[e]}function Ze(e){return ne()}function aa(e){if(!z)return e;let t=0,a,n;const i=()=>{t-=1,n&&t<=0&&(n.stop(),a=void 0,n=void 0)};return((...o)=>(t+=1,n||(n=ze(!0),a=n.run(()=>e(...o))),P(i),a))}function na(e,t=1e4){return we((a,n)=>{let i=Y(e),o;const s=()=>setTimeout(()=>{i=Y(e),n()},Y(t));return P(()=>{clearTimeout(o)}),{get(){return a(),i},set(r){i=r,n(),clearTimeout(o),o=s()}}})}function et(e,t=200,a={}){return oe(je(t,a),e)}function tt(e,t=200,a=!0,n=!0,i=!1){return oe(Ge(t,a,n,i),e)}function De(e,t,a={}){const{eventFilter:n=Ee,...i}=a;return N(e,oe(n,t),i)}function at(e,t,a={}){const{eventFilter:n,initialState:i="active",...o}=a,{eventFilter:s,pause:r,resume:l,isActive:h}=Qe(n,{initialState:i});return{stop:De(e,t,{...o,eventFilter:s}),pause:r,resume:l,isActive:h}}function se(e,t=!0,a){Ze()?We(e,a):t?e():ke(e)}function ia(e,t=1e3,a={}){const{immediate:n=!0,immediateCallback:i=!1}=a;let o=null;const s=_(!1);function r(){o&&(clearInterval(o),o=null)}function l(){s.value=!1,r()}function h(){const c=Y(t);c<=0||(s.value=!0,i&&e(),r(),s.value&&(o=setInterval(e,c)))}return n&&z&&h(),($e(t)||typeof t=="function")&&P(N(t,()=>{s.value&&z&&h()})),P(l),{isActive:J(s),pause:l,resume:h}}function oa(e,t,a={}){const{immediate:n=!0,immediateCallback:i=!1}=a,o=_(!1);let s;function r(){s&&(clearTimeout(s),s=void 0)}function l(){o.value=!1,r()}function h(...c){i&&e(),r(),o.value=!0,s=setTimeout(()=>{o.value=!1,s=void 0,e(...c)},Y(t))}return n&&(o.value=!0,z&&h()),P(l),{isPending:J(o),start:h,stop:l}}function sa(e,t,a={}){const{debounce:n=0,maxWait:i=void 0,...o}=a;return De(e,t,{...o,eventFilter:je(n,{maxWait:i})})}function nt(e,t,a){return N(e,t,{...a,immediate:!0})}const $=z?window:void 0,it=z?window.document:void 0;function C(e){var t;const a=Y(e);return(t=a==null?void 0:a.$el)!==null&&t!==void 0?t:a}function q(...e){const t=(n,i,o,s)=>(n.addEventListener(i,o,s),()=>n.removeEventListener(i,o,s)),a=O(()=>{const n=V(Y(e[0])).filter(i=>i!=null);return n.every(i=>typeof i!="string")?n:void 0});return nt(()=>{var n,i;return[(n=(i=a.value)===null||i===void 0?void 0:i.map(o=>C(o)))!==null&&n!==void 0?n:[$].filter(o=>o!=null),V(Y(a.value?e[1]:e[0])),V(qe(a.value?e[2]:e[1])),Y(a.value?e[3]:e[2])]},([n,i,o,s],r,l)=>{if(!(n!=null&&n.length)||!(i!=null&&i.length)||!(o!=null&&o.length))return;const h=Ue(s)?{...s}:s,c=n.flatMap(u=>i.flatMap(d=>o.map(g=>t(u,d,g,h))));l(()=>{c.forEach(u=>u())})},{flush:"post"})}function ot(){const e=_(!1),t=ne();return t&&We(()=>{e.value=!0},t),e}function re(e){const t=ot();return O(()=>(t.value,!!e()))}function st(e,t,a={}){const{window:n=$,...i}=a;let o;const s=re(()=>n&&"MutationObserver"in n),r=()=>{o&&(o.disconnect(),o=void 0)},l=O(()=>{const d=Y(e),g=V(d).map(C).filter(Je);return new Set(g)}),h=N(l,d=>{r(),s.value&&d.size&&(o=new MutationObserver(t),d.forEach(g=>o.observe(g,i)))},{immediate:!0,flush:"post"}),c=()=>o==null?void 0:o.takeRecords(),u=()=>{h(),r()};return P(u),{isSupported:s,stop:u,takeRecords:c}}function rt(e){return typeof e=="function"?e:typeof e=="string"?t=>t.key===e:Array.isArray(e)?t=>e.includes(t.key):()=>!0}function ra(...e){let t,a,n={};e.length===3?(t=e[0],a=e[1],n=e[2]):e.length===2?typeof e[1]=="object"?(t=!0,a=e[0],n=e[1]):(t=e[0],a=e[1]):(t=!0,a=e[0]);const{target:i=$,eventName:o="keydown",passive:s=!1,dedupe:r=!1}=n,l=rt(t);return q(i,o,c=>{c.repeat&&Y(r)||l(c)&&a(c)},s)}function lt(e,t={}){const{immediate:a=!0,fpsLimit:n=null,window:i=$,once:o=!1}=t,s=_(!1),r=O(()=>{const g=Y(n);return g?1e3/g:null});let l=0,h=null;function c(g){if(!s.value||!i)return;l||(l=g);const p=g-l;if(r.value&&p<r.value){h=i.requestAnimationFrame(c);return}if(l=g,e({delta:p,timestamp:g}),o){s.value=!1,h=null;return}h=i.requestAnimationFrame(c)}function u(){!s.value&&i&&(s.value=!0,l=0,h=i.requestAnimationFrame(c))}function d(){s.value=!1,h!=null&&i&&(i.cancelAnimationFrame(h),h=null)}return a&&u(),P(d),{isActive:J(s),pause:d,resume:u}}const dt=Symbol("vueuse-ssr-width");function ut(){const e=Se()?Ve(dt,null):null;return typeof e=="number"?e:void 0}function Oe(e,t={}){const{window:a=$,ssrWidth:n=ut()}=t,i=re(()=>a&&"matchMedia"in a&&typeof a.matchMedia=="function"),o=_(typeof n=="number"),s=_(),r=_(!1),l=h=>{r.value=h.matches};return Be(()=>{if(o.value){o.value=!i.value;const h=Y(e).split(",");r.value=h.some(c=>{const u=c.includes("not all"),d=c.match(/\(\s*min-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/),g=c.match(/\(\s*max-width:\s*(-?\d+(?:\.\d*)?[a-z]+\s*)\)/);let p=!!(d||g);return d&&p&&(p=n>=fe(d[1])),g&&p&&(p=n<=fe(g[1])),u?!p:p});return}i.value&&(s.value=a.matchMedia(Y(e)),r.value=s.value.matches)}),q(s,"change",l,{passive:!0}),O(()=>r.value)}const K=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},U="__vueuse_ssr_handlers__",ct=ht();function ht(){return U in K||(K[U]=K[U]||{}),K[U]}function gt(e,t){return ct[e]||t}function pt(e){return Oe("(prefers-color-scheme: dark)",e)}function ft(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}const bt={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},be="vueuse-storage";function Te(e,t,a,n={}){var i;const{flush:o="pre",deep:s=!0,listenToStorageChanges:r=!0,writeDefaults:l=!0,mergeDefaults:h=!1,shallow:c,window:u=$,eventFilter:d,onError:g=f=>{console.error(f)},initOnMounted:p}=n,b=(c?_:_e)(typeof t=="function"?t():t),v=O(()=>Y(e));if(!a)try{a=gt("getDefaultStorage",()=>$==null?void 0:$.localStorage)()}catch(f){g(f)}if(!a)return b;const R=Y(t),w=ft(R),X=(i=n.serializer)!==null&&i!==void 0?i:bt[w],{pause:k,resume:H}=at(b,f=>T(f),{flush:o,deep:s,eventFilter:d});N(v,()=>D(),{flush:o});let W=!1;const E=f=>{p&&!W||D(f)},j=f=>{p&&!W||F(f)};u&&r&&(a instanceof Storage?q(u,"storage",E,{passive:!0}):q(u,be,j)),p?se(()=>{W=!0,D()}):D();function y(f,m){if(u){const S={key:v.value,oldValue:f,newValue:m,storageArea:a};u.dispatchEvent(a instanceof Storage?new StorageEvent("storage",S):new CustomEvent(be,{detail:S}))}}function T(f){try{const m=a.getItem(v.value);if(f==null)y(m,null),a.removeItem(v.value);else{const S=X.write(f);m!==S&&(a.setItem(v.value,S),y(m,S))}}catch(m){g(m)}}function L(f){const m=f?f.newValue:a.getItem(v.value);if(m==null)return l&&R!=null&&a.setItem(v.value,X.write(R)),R;if(!f&&h){const S=X.read(m);return typeof h=="function"?h(S,R):w==="object"&&!Array.isArray(S)?{...R,...S}:S}else return typeof m!="string"?m:X.read(m)}function D(f){if(!(f&&f.storageArea!==a)){if(f&&f.key==null){b.value=R;return}if(!(f&&f.key!==v.value)){k();try{const m=X.write(b.value);(f===void 0||(f==null?void 0:f.newValue)!==m)&&(b.value=L(f))}catch(m){g(m)}finally{f?ke(H):H()}}}}function F(f){D(f.detail)}return b}function yt(e,t,a={}){const{window:n=$,...i}=a;let o;const s=re(()=>n&&"ResizeObserver"in n),r=()=>{o&&(o.disconnect(),o=void 0)},l=O(()=>{const u=Y(e);return Array.isArray(u)?u.map(d=>C(d)):[C(u)]}),h=N(l,u=>{if(r(),s.value&&n){o=new ResizeObserver(t);for(const d of u)d instanceof Element&&o.observe(d,i)}},{immediate:!0,flush:"post"}),c=()=>{r(),h()};return P(c),{isSupported:s,stop:c}}function la(e,t={width:0,height:0},a={}){const{window:n=$,box:i="content-box"}=a,o=O(()=>{var u;return(u=C(e))===null||u===void 0||(u=u.namespaceURI)===null||u===void 0?void 0:u.includes("svg")}),s=_(t.width),r=_(t.height),{stop:l}=yt(e,([u])=>{const d=i==="border-box"?u.borderBoxSize:i==="content-box"?u.contentBoxSize:u.devicePixelContentBoxSize;if(n&&o.value){const g=C(e);if(g){const p=g.getBoundingClientRect();s.value=p.width,r.value=p.height}}else if(d){const g=V(d);s.value=g.reduce((p,{inlineSize:b})=>p+b,0),r.value=g.reduce((p,{blockSize:b})=>p+b,0)}else s.value=u.contentRect.width,r.value=u.contentRect.height},a);se(()=>{const u=C(e);if(u&&"offsetWidth"in u)if(i==="content-box"&&n){const d=n.getComputedStyle(u),g=Number.parseFloat(d.paddingLeft)+Number.parseFloat(d.paddingRight),p=Number.parseFloat(d.paddingTop)+Number.parseFloat(d.paddingBottom),b=Number.parseFloat(d.borderLeftWidth)+Number.parseFloat(d.borderRightWidth),v=Number.parseFloat(d.borderTopWidth)+Number.parseFloat(d.borderBottomWidth);s.value=u.offsetWidth-g-b,r.value=u.offsetHeight-p-v}else s.value=u.offsetWidth,r.value=u.offsetHeight;else u&&(s.value=t.width,r.value=t.height)});const h=N(()=>C(e),u=>{s.value=u?t.width:0,r.value=u?t.height:0});function c(){l(),h()}return{width:s,height:r,stop:c}}function mt(e){return e===!0?{}:e}const vt="message";function da(e,t=[],a={}){const n=_(null),i=_(null),o=_("CONNECTING"),s=_(null),r=_(null),l=ie(e),h=_(null);let c=!1,u=0;const{withCredentials:d=!1,immediate:g=!0,autoConnect:p=!0,autoReconnect:b,serializer:v={read:k=>k}}=a,R=()=>{z&&s.value&&(s.value.close(),s.value=null,o.value="CLOSED",c=!0)},w=()=>{if(c||typeof l.value>"u")return;const k=new EventSource(l.value,{withCredentials:d});o.value="CONNECTING",s.value=k,k.onopen=()=>{o.value="OPEN",r.value=null},k.onerror=H=>{if(o.value="CLOSED",r.value=H,k.readyState===2&&!c&&b){k.close();const{retries:W=-1,delay:E=1e3,onFailed:j}=mt(b);u+=1,typeof W=="number"&&(W<0||u<W)||typeof W=="function"&&W()?setTimeout(w,E):j==null||j()}},t=t.length>0?t:[vt];for(const H of t)q(k,H,W=>{var E,j;n.value=H,i.value=(E=v.read(W.data))!==null&&E!==void 0?E:null,h.value=(j=W.lastEventId)!==null&&j!==void 0?j:null},{passive:!0})},X=()=>{z&&(R(),c=!1,u=0,w())};return g&&X(),p&&N(l,X),P(R),{eventSource:s,event:n,data:i,status:o,error:r,open:X,close:R,lastEventId:h}}function ua(e=null,t={}){const{baseUrl:a="",rel:n="icon",document:i=it}=t,o=ie(e),s=r=>{const l=i==null?void 0:i.head.querySelectorAll(`link[rel*="${n}"]`);if(!l||l.length===0){const h=i==null?void 0:i.createElement("link");h&&(h.rel=n,h.href=`${a}${r}`,h.type=`image/${r.split(".").pop()}`,i==null||i.head.append(h));return}l==null||l.forEach(h=>h.href=`${a}${r}`)};return N(o,(r,l)=>{typeof r=="string"&&r!==l&&s(r)},{immediate:!0}),o}const ye=1;function ca(e,t={}){const{throttle:a=0,idle:n=200,onStop:i=M,onScroll:o=M,offset:s={left:0,right:0,top:0,bottom:0},observe:r={mutation:!1},eventListenerOptions:l={capture:!1,passive:!0},behavior:h="auto",window:c=$,onError:u=y=>{console.error(y)}}=t,d=typeof r=="boolean"?{mutation:r}:r,g=_(0),p=_(0),b=O({get(){return g.value},set(y){R(y,void 0)}}),v=O({get(){return p.value},set(y){R(void 0,y)}});function R(y,T){var L,D,F,f;if(!c)return;const m=Y(e);if(!m)return;(L=m instanceof Document?c.document.body:m)===null||L===void 0||L.scrollTo({top:(D=Y(T))!==null&&D!==void 0?D:v.value,left:(F=Y(y))!==null&&F!==void 0?F:b.value,behavior:Y(h)});const S=(m==null||(f=m.document)===null||f===void 0?void 0:f.documentElement)||(m==null?void 0:m.documentElement)||m;b!=null&&(g.value=S.scrollLeft),v!=null&&(p.value=S.scrollTop)}const w=_(!1),X=pe({left:!0,right:!1,top:!0,bottom:!1}),k=pe({left:!1,right:!1,top:!1,bottom:!1}),H=y=>{w.value&&(w.value=!1,k.left=!1,k.right=!1,k.top=!1,k.bottom=!1,i(y))},W=et(H,a+n),E=y=>{var T;if(!c)return;const L=(y==null||(T=y.document)===null||T===void 0?void 0:T.documentElement)||(y==null?void 0:y.documentElement)||C(y),{display:D,flexDirection:F,direction:f}=c.getComputedStyle(L),m=f==="rtl"?-1:1,S=L.scrollLeft;k.left=S<g.value,k.right=S>g.value;const ue=Math.abs(S*m)<=(s.left||0),ce=Math.abs(S*m)+L.clientWidth>=L.scrollWidth-(s.right||0)-ye;D==="flex"&&F==="row-reverse"?(X.left=ce,X.right=ue):(X.left=ue,X.right=ce),g.value=S;let B=L.scrollTop;y===c.document&&!B&&(B=c.document.body.scrollTop),k.top=B<p.value,k.bottom=B>p.value;const he=Math.abs(B)<=(s.top||0),ge=Math.abs(B)+L.clientHeight>=L.scrollHeight-(s.bottom||0)-ye;D==="flex"&&F==="column-reverse"?(X.top=ge,X.bottom=he):(X.top=he,X.bottom=ge),p.value=B},j=y=>{var T;if(!c)return;const L=(T=y.target.documentElement)!==null&&T!==void 0?T:y.target;E(L),w.value=!0,W(y),o(y)};return q(e,"scroll",a?tt(j,a,!0,!1):j,l),se(()=>{try{const y=Y(e);if(!y)return;E(y)}catch(y){u(y)}}),d!=null&&d.mutation&&e!=null&&e!==c&&e!==document&&st(e,()=>{const y=Y(e);y&&E(y)},{attributes:!0,childList:!0,subtree:!0}),q(e,"scrollend",H,l),{x:b,y:v,isScrolling:w,arrivedState:X,directions:k,measure(){const y=Y(e);c&&y&&E(y)}}}function ha(e,t,a={}){const{window:n=$}=a;return Te(e,t,n==null?void 0:n.localStorage,a)}function ga(e={}){const{controls:t=!1,scheduler:a=lt}=e,n=_(new Date),o=a(()=>n.value=new Date);return t?{now:n,...o}:n}function pa(e){const t=Oe("(prefers-reduced-motion: reduce)",e);return O(()=>t.value?"reduce":"no-preference")}function fa(e,t,a={}){const{window:n=$}=a;return Te(e,t,n==null?void 0:n.sessionStorage,a)}const Yt=e=>{let t=e>>>0;return()=>{t=t+1831565813>>>0;let a=t;return a=Math.imul(a^a>>>15,a|1),a^=a+Math.imul(a^a>>>7,a|61),((a^a>>>14)>>>0)/4294967296}},ba=e=>{const t=Yt(e),a=(n,i)=>n+(i-n)*t();return{next:t,range:a,int:(n,i)=>Math.floor(a(n,i+1)),chance:n=>t()<n,vary:(n,i=.12)=>n*a(1-i,1+i)}},Ae=(e,t=0,a=1)=>Math.min(a,Math.max(t,e)),Me=(e,t,a)=>e+(t-e)*a,G=/\s*(?:(\d+\.?\d*(?:e[+-]?\d+)?|\.\d+)|([A-Za-z_]\w*)|(<=|>=|==|!=|[-+*/%<>(),[\]]))/iy,Rt=e=>{const t=e.trim(),a=[];for(G.lastIndex=0;G.lastIndex<t.length;){const n=G.exec(t);if(!n)throw new Error(`cannot read "${t.slice(G.lastIndex).trim()}" in expression "${t}"`);a.push(n[1]!==void 0?{number:Number(n[1])}:n[2]!==void 0?{name:n[2]}:{op:n[3]})}return a},ee={"<":[1,(e,t)=>e<t],">":[1,(e,t)=>e>t],"<=":[1,(e,t)=>e<=t],">=":[1,(e,t)=>e>=t],"==":[1,(e,t)=>e===t],"!=":[1,(e,t)=>e!==t],"+":[2,(e,t)=>e+t],"-":[2,(e,t)=>e-t],"*":[3,(e,t)=>e*t],"/":[3,(e,t)=>e/t],"%":[3,(e,t)=>e%t]},Xt={range:(e,t,a)=>e.random.range(t,a),int:(e,t,a)=>e.random.int(t,a),chance:(e,t)=>e.random.chance(t),vary:(e,t,a)=>e.random.vary(t,a),cos:(e,t)=>Math.cos(t),sin:(e,t)=>Math.sin(t),sqrt:(e,t)=>Math.sqrt(t),abs:(e,t)=>Math.abs(t),hypot:(e,...t)=>Math.hypot(...t),min:(e,...t)=>Math.min(...t),max:(e,...t)=>Math.max(...t),lerp:(e,t,a,n)=>Me(t,a,n),clamp:(e,t,a,n)=>Ae(t,a,n),len:(e,t)=>t.length},me=new Map,wt=e=>{const t=me.get(e);if(t)return t;const a=Rt(e);let n=0;const i=()=>a[n],o=()=>a[n++],s=d=>{const g=o();if(!g||g.op!==d)throw new Error(`expected "${d}" in expression "${e}"`)},r=()=>{const d=[];if(i()&&i().op===")")return o(),d;do d.push(c(0));while(i()&&i().op===","&&o());return s(")"),d},l=()=>{const d=o();if(!d)throw new Error(`expression "${e}" ends early`);const g=d.number;if(g!==void 0)return()=>g;if(d.op==="-"){const p=h();return(b,v)=>-p(b,v)}if(d.op==="("){const p=c(0);return s(")"),p}if(d.name!==void 0){const p=d.name;if(i()&&i().op==="("){o();const b=r();if(p==="if")return(R,w)=>b[0](R,w)?b[1](R,w):b[2](R,w);const v=Xt[p];if(!v)throw new Error(`unknown function "${p}" in expression "${e}"`);return(R,w)=>v(w,...b.map(X=>X(R,w)))}return p==="PI"?()=>Math.PI:b=>{if(!(p in b))throw new Error(`unknown name "${p}" in expression "${e}"`);return b[p]}}throw new Error(`unexpected "${d.op}" in expression "${e}"`)},h=()=>{let d=l();for(;i()&&i().op==="[";){o();const g=c(0);s("]");const p=d;d=(b,v)=>p(b,v)[g(b,v)]}return d},c=d=>{let g=h();for(;i()&&ee[i().op??""]&&ee[i().op??""][0]>=d;){const[p,b]=ee[o().op??""],v=c(p+1),R=g;g=(w,X)=>b(R(w,X),v(w,X))}return g},u=c(0);if(n<a.length)throw new Error(`unexpected "${JSON.stringify(a[n])}" in expression "${e}"`);return me.set(e,u),u},te=e=>Object.create(e),I=(e,t,a)=>{const n=Object.keys(e).filter(i=>!t.includes(i));if(n.length)throw new Error(`${a} has unknown key "${n[0]}"`)},x=(e,t,a)=>{if(typeof e=="number")return e;if(typeof e=="string")return wt(e)(t,a);if(Array.isArray(e))return e.map(n=>x(n,t,a));if(e&&typeof e=="object"&&"if"in e)return I(e,["if","then","else"],"value {if}"),x(x(e.if,t,a)?e.then:e.else,t,a);if(e&&typeof e=="object"&&e.map){const n=e.map;return I(n,["count","values","as","let","value"],"value {map}"),xe(n,t,a).map(i=>(le(n.let,i,a),x(n.value,i,a)))}throw new Error(`cannot evaluate ${JSON.stringify(e)}`)},le=(e,t,a)=>{Object.entries(e||{}).forEach(([n,i])=>{t[n]=x(i,t,a)})},xe=(e,t,a)=>(e.values!==void 0?x(e.values,t,a):Array.from({length:x(e.count,t,a)},(i,o)=>o)).map((i,o)=>{const s=te(t);return s.index=o,e.as&&(s[e.as]=i),s}),ve={ellipse:e=>{const[t,a,n,i,o=0]=e;return{kind:"ellipse",x:t,y:a,radiusX:n,radiusY:i,angle:o}},circle:e=>{const[t,a,n]=e;return{kind:"ellipse",x:t,y:a,radiusX:n,radiusY:n,angle:0}},polygon:e=>({kind:"polygon",points:e}),blob:e=>({kind:"blob",points:e}),tube:(e,t,a,n)=>({kind:"tube",points:e,radii:[].concat(x(t.radii,a,n))})},Q=(e,t,a,n)=>{e.forEach(i=>{const o=Object.keys(ve).find(s=>s in i);if(o){I(i,[o,"radii","cut","part"],`shape {${o}}`);const s=ve[o](x(i[o],t,a),i,t,a);n.push(i.cut?Object.assign({},s,{cut:!0}):s)}else if("let"in i)I(i,["let","part"],"statement {let}"),le(i.let,t,a);else if("repeat"in i){I(i,["repeat","part"],"statement {repeat}");const s=i.repeat;I(s,["count","values","as","body"],"repeat"),xe(s,t,a).forEach(r=>Q(s.body||[],r,a,n))}else if("if"in i)I(i,["if","then","else","part"],"statement {if}"),Q((x(i.if,t,a)?i.then:i.else)||[],te(t),a,n);else if("use"in i){I(i,["use","with","part"],"statement {use}");const s=a.defs[i.use];if(!s)throw new Error(`unknown def "${i.use}"`);const r=te(t);Object.entries(i.with||{}).forEach(([l,h])=>{r[l]=x(h,t,a)}),Q(s,r,a,n)}else throw new Error(`unknown statement ${JSON.stringify(i)}`)})},_t=(e,t)=>{const a={...e};return Object.entries(t||{}).forEach(([n,i])=>{const o=a[n]||{};a[n]={...o,...i,params:{...o.params,...i.params},drop:[...o.drop||[],...i.drop||[]],body:[...o.body||[],...i.body||[]]}}),a},Ye=new WeakMap,de=(e,t,a=[])=>{const n=Ye.get(e)||new Map;Ye.set(e,n);const i=n.get(t);if(i)return i;const o=e[t];if(!o)throw new Error(`unknown species "${t}"`);if(a.includes(t))throw new Error(`species "${t}" extends itself through ${a.join(" > ")}`);const s=o.extends?de(e,o.extends,[...a,t]):{params:{},defs:{},body:[],poses:{}},r={name:t,params:{...s.params,...o.params},defs:{...s.defs,...o.defs},body:[...s.body.filter(l=>!(o.drop||[]).includes(l.part??"")),...o.body||[]],poses:_t(s.poses,o.poses)};return n.set(t,r),r},He=e=>Object.entries(e.poses).map(([t,a])=>[t,a.weight===void 0?1:a.weight]).filter(([,t])=>t>0),kt=(e,t)=>{if(e.length<2)return e.length?e[0][0]:"";let a=t.next()*e.reduce((i,[,o])=>i+o,0);return(e.find(([,i])=>(a-=i,a<0))||e[e.length-1])[0]},ya=(e,t,a,n={})=>{const i=de(e,t),o=He(i),s=n.pose&&o.some(([d])=>d===n.pose)?n.pose:kt(o,a),r=i.poses[s]||{},l=new Set(r.drop||[]),h={random:a,defs:i.defs},c={},u=[];try{le({...i.params,...r.params},c,h),Q([...i.body.filter(d=>!l.has(d.part??"")),...r.body||[]],c,h,u)}catch(d){throw new Error(`species "${t}"${s?` (pose ${s})`:""}: ${d.message}`,{cause:d})}return{shapes:u,pose:s}},ma=e=>Object.keys(e).filter(t=>!e[t].abstract),va=(e,t)=>{const a=He(de(e,t)).map(([n])=>n);return a.length?a:[""]},A=256,ae=2.6,Re=A/ae,St=(e,t,a,n,i)=>{const o=i*i,s=o*i,r=l=>.5*(2*t[l]+(-e[l]+a[l])*i+(2*e[l]-5*t[l]+4*a[l]-n[l])*o+(-e[l]+3*t[l]-3*a[l]+n[l])*s);return[r(0),r(1)]},Xe=(e,t,a)=>{const n=e.length,i=r=>t?e[(r+n)%n]:e[Ae(r,0,n-1)],o=t?n:n-1,s=[];for(let r=0;r<o;r+=1)for(let l=0;l<a;l+=1)s.push(St(i(r-1),i(r),i(r+1),i(r+2),l/a));return t||s.push(e[n-1]),s},Lt=(e,t)=>{if(e.length===1)return e[0];const a=t*(e.length-1),n=Math.min(e.length-2,Math.floor(a));return Me(e[n],e[n+1],a-n)},Ce=(e,t)=>{if(e.globalCompositeOperation=t.cut?"destination-out":"source-over",e.beginPath(),t.kind==="ellipse")e.ellipse(t.x,t.y,t.radiusX,t.radiusY,t.angle,0,Math.PI*2),e.fill();else if(t.kind==="polygon"||t.kind==="blob")(t.kind==="blob"?Xe(t.points,!0,12):t.points).forEach(([n,i],o)=>o===0?e.moveTo(n,i):e.lineTo(n,i)),e.closePath(),e.fill();else if(t.kind==="tube"){const a=Xe(t.points,!1,24);a.forEach(([n,i],o)=>{const s=Lt(t.radii,o/Math.max(1,a.length-1));e.moveTo(n+s,i),e.arc(n,i,s,0,Math.PI*2)}),e.fill()}},Ya=(e,t)=>{const a=document.createElement("canvas");a.width=A,a.height=A;const n=a.getContext("2d",{willReadFrequently:!0});if(!n)return new Uint8Array(A*A);n.translate(A/2,A/2),n.scale(Re*t.facing*t.stretchX,Re*t.stretchY),n.rotate(t.tilt),n.fillStyle="#000",e.forEach(s=>Ce(n,s));const i=n.getImageData(0,0,A,A).data,o=new Uint8Array(A*A);for(let s=0;s<o.length;s+=1)o[s]=i[s*4+3]>127?1:0;return o},Ra=(e,t,a)=>{const n=e.getContext("2d");n&&(n.clearRect(0,0,e.width,e.height),n.save(),n.translate(e.width/2,e.height/2),n.scale(e.width/ae,e.width/ae),n.fillStyle=a,t.forEach(i=>Ce(n,i)),n.restore())},Wt=!0,$t={groundY:.8,bodyRadiusX:"range(0.4, 0.46)",bodyRadiusY:"range(0.27, 0.32)",bodyAngle:-.45,legLength:"range(0.28, 0.34)",bodyY:"groundY - legLength - bodyRadiusY",headRadius:"range(0.15, 0.18)",headX:"bodyRadiusX * 0.55",headY:"bodyY - bodyRadiusY * 0.95 - headRadius * 0.4",neckWidth:"headRadius * 0.85",beakLength:"range(0.17, 0.23)",beakHeight:.09,tailLength:"range(0.3, 0.4)",tailSpread:.14,tailDrop:.18,legWidth:.022,wingWidth:.12},Et=[{ellipse:[0,"bodyY","bodyRadiusX","bodyRadiusY","bodyAngle"],part:"body"},{tube:[["bodyRadiusX * 0.25","bodyY - bodyRadiusY * 0.4"],["headX","headY + headRadius * 0.4"]],radii:["bodyRadiusY * 0.7","neckWidth"],part:"neck"},{circle:["headX","headY","headRadius"],part:"head"},{polygon:[["headX + headRadius * 0.7","headY - beakHeight * 0.6"],["headX + headRadius + beakLength","headY + beakHeight * 0.3"],["headX + headRadius * 0.7","headY + beakHeight * 0.7"]],part:"head"},{circle:["headX + headRadius * 0.3","headY - headRadius * 0.15",.025],cut:!0,part:"eye"},{polygon:[["-bodyRadiusX * 0.6","bodyY - tailSpread * 0.4"],["-bodyRadiusX * 0.6 - tailLength","bodyY + tailDrop - tailSpread"],["-bodyRadiusX * 0.6 - tailLength * 0.95","bodyY + tailDrop + tailSpread * 0.4"],["-bodyRadiusX * 0.3","bodyY + tailSpread * 0.6"]],part:"tail"},{tube:[["-bodyRadiusX * 0.55","bodyY - bodyRadiusY * 0.15"],["-bodyRadiusX * 0.1","bodyY + bodyRadiusY * 0.3"],["bodyRadiusX * 0.35","bodyY - bodyRadiusY * 0.05"]],radii:.012,cut:!0,part:"wing"},{repeat:{values:[-.08,.1],as:"legShift",body:[{tube:[["legShift","bodyY + bodyRadiusY * 0.6"],["legShift","groundY"]],radii:"legWidth"},{ellipse:["legShift + 0.05","groundY",.07,.02]}]},part:"legs"}],jt={perched:{weight:3},sleeping:{weight:1,params:{headX:"bodyRadiusX * 0.35",headY:"bodyY - bodyRadiusY * 0.55",beakLength:.1,bodyAngle:-.2},drop:["neck","eye","wing"],body:[{tube:[["headX + headRadius * 0.4","headY - headRadius * 0.1"],["headX + headRadius * 0.65","headY - headRadius * 0.05"]],radii:.012,cut:!0}]},front:{weight:1,params:{headX:0,headY:"bodyY - bodyRadiusX * 0.8 - headRadius * 0.2"},drop:["body","neck","head","eye","tail","wing","legs"],body:[{ellipse:[0,"bodyY - 0.02","bodyRadiusY * 1.25","bodyRadiusX * 0.95"]},{circle:["headX","headY","headRadius * 1.1"]},{polygon:[["headX - beakHeight","headY + headRadius * 0.15"],["headX + beakHeight","headY + headRadius * 0.15"],["headX","headY + headRadius * 0.15 + beakLength * 0.6"]]},{repeat:{values:[-1,1],as:"side",body:[{circle:["headX + side * headRadius * 0.45","headY - headRadius * 0.2",.027],cut:!0},{ellipse:["side * bodyRadiusY * 1.15","bodyY + 0.02","wingWidth * 0.9","bodyRadiusX * 0.6","side * -0.15"]},{tube:[["side * 0.1","groundY - legLength"],["side * 0.1","groundY"]],radii:"legWidth"},{ellipse:["side * 0.12","groundY",.07,.02]}]}}]}},Dt={abstract:Wt,params:$t,body:Et,poses:jt},Ot=!0,Tt={groundY:.7,bodyRadiusX:"range(0.5, 0.56)",bodyRadiusY:"range(0.26, 0.31)",legLength:"range(0.4, 0.5)",legWidth:.07,stride:0,bodyY:"groundY - bodyRadiusY - legLength",headRadiusX:"range(0.19, 0.22)",headRadiusY:"range(0.16, 0.18)",headX:"bodyRadiusX * 0.95",headY:"bodyY - bodyRadiusY * 0.8",neckWidth:"headRadiusY * 0.9",snoutLength:.16,snoutRadius:.08,earHeight:"range(0.16, 0.22)",earWidth:.09,earSpread:"range(0.1, 0.3)",eyeSize:.028,tailLength:"range(0.35, 0.45)",tailLift:"range(0.05, 0.2)",tailWidth:.06},At={ears:[{repeat:{values:[-.5,.3],as:"shift",body:[{polygon:[["headX + headRadiusX * shift - earWidth","headY - headRadiusY * 0.5"],["headX + headRadiusX * shift - earWidth * 0.2","headY - headRadiusY * 0.75 - earHeight"],["headX + headRadiusX * shift + earWidth","headY - headRadiusY * 0.7"]]}]}}]},Mt=[{ellipse:[0,"bodyY","bodyRadiusX","bodyRadiusY"],part:"body"},{tube:[["bodyRadiusX * 0.5","bodyY - bodyRadiusY * 0.2"],["headX - headRadiusX * 0.3","headY + headRadiusY * 0.2"]],radii:["bodyRadiusY * 0.8","neckWidth"],part:"neck"},{ellipse:["headX","headY","headRadiusX","headRadiusY"],part:"head"},{ellipse:["headX + headRadiusX * 0.75","headY + headRadiusY * 0.35","snoutLength","snoutRadius"],part:"head"},{use:"ears",part:"head"},{circle:["headX + headRadiusX * 0.4","headY - headRadiusY * 0.15","eyeSize"],cut:!0,part:"eye"},{tube:[["-bodyRadiusX * 0.85","bodyY - bodyRadiusY * 0.2"],["-bodyRadiusX * 0.85 - tailLength * 0.5","bodyY - bodyRadiusY * 0.2 - tailLift * 0.5"],["-bodyRadiusX * 0.85 - tailLength","bodyY - bodyRadiusY * 0.2 - tailLift"]],radii:["tailWidth","tailWidth * 0.9","tailWidth * 0.6"],part:"tail"},{repeat:{values:[-.72,-.5,.4,.68],as:"along",body:[{let:{swing:"if(index % 2 == 0, stride, -stride)",legX:"along * bodyRadiusX",hipY:"bodyY + bodyRadiusY * 0.5"}},{tube:[["legX","hipY"],["legX + swing","(hipY + groundY) / 2"],["legX + swing * 1.2","groundY"]],radii:["legWidth * 1.25","legWidth * 0.9","legWidth * 0.8"]}]},part:"legs"}],xt={standing:{weight:3},walking:{weight:2,params:{stride:"range(0.06, 0.14)"}},sleeping:{weight:1,params:{legLength:.02,headX:"bodyRadiusX * 0.8",headY:"groundY - headRadiusY",tailLift:.02},drop:["legs","eye","neck"],body:[{ellipse:["bodyRadiusX * 0.55","groundY - 0.05","bodyRadiusX * 0.28",.06]},{tube:[["headX + headRadiusX * 0.2","headY - headRadiusY * 0.1"],["headX + headRadiusX * 0.4","headY"],["headX + headRadiusX * 0.6","headY - headRadiusY * 0.1"]],radii:.012,cut:!0}]},sitting:{weight:1,params:{headX:"bodyRadiusY * 0.5",headY:"groundY - bodyRadiusX * 1.55 - headRadiusY * 0.4"},drop:["body","neck","legs","tail"],body:[{ellipse:["bodyRadiusY * 0.2","groundY - bodyRadiusX * 0.85","bodyRadiusY * 1.05","bodyRadiusX * 0.9",.12]},{ellipse:["-bodyRadiusY * 0.55","groundY - bodyRadiusY * 0.55","bodyRadiusY * 1.2","bodyRadiusY * 0.62"]},{tube:[["bodyRadiusY * 0.75","groundY - bodyRadiusX * 1.0"],["bodyRadiusY * 0.8","groundY"]],radii:"legWidth * 1.1"},{tube:[["-bodyRadiusY * 1.2","groundY - 0.05"],["-bodyRadiusY * 1.2 - tailLength","groundY - 0.03"]],radii:["tailWidth","tailWidth * 0.6"]}]},front:{weight:1,params:{headX:0,headY:"groundY - bodyRadiusX * 1.1 - headRadiusY * 0.9"},drop:["body","neck","head","eye","legs","tail"],body:[{ellipse:[0,"groundY - bodyRadiusX * 0.7","bodyRadiusY * 1.25","bodyRadiusX * 0.75"]},{ellipse:["headX","headY","headRadiusX * 1.15","headRadiusY * 1.1"]},{ellipse:["headX","headY + headRadiusY * 0.45","snoutRadius * 1.5","snoutRadius"]},{repeat:{values:[-1,1],as:"side",body:[{polygon:[["headX + side * headRadiusX * 1.05","headY - headRadiusY * 0.3"],["headX + side * headRadiusX * (0.75 + earSpread)","headY - headRadiusY * 0.8 - earHeight"],["headX + side * headRadiusX * 0.3","headY - headRadiusY * 1.0"]]},{circle:["headX + side * headRadiusX * 0.5","headY - headRadiusY * 0.15","eyeSize * 1.2"],cut:!0},{tube:[["side * bodyRadiusY * 0.55","groundY - bodyRadiusX * 0.4"],["side * bodyRadiusY * 0.55","groundY"]],radii:"legWidth * 1.3"}]}},{circle:["headX","headY + headRadiusY * 0.3","snoutRadius * 0.35"],cut:!0}]}},Ht={abstract:Ot,params:Tt,defs:At,body:Mt,poses:xt},Ct=!0,Pt={length:"range(0.85, 0.95)",height:"range(0.36, 0.42)",arch:0,dorsalHeight:"range(0.2, 0.28)",finLength:.22,flukeSpan:.32,eyeX:.58,eyeSize:.035},Nt=[{blob:[["length","arch * 0.6"],["length * 0.55","-height * 0.8 - arch * 0.2"],["-length * 0.2","-height * 0.7 - arch * 0.2"],["-length * 0.75","-height * 0.12 + arch * 0.6"],["-length * 0.75","height * 0.12 + arch * 0.6"],["-length * 0.2","height * 0.8 - arch * 0.4"],["length * 0.5","height * 0.75 - arch * 0.2"]]},{polygon:[["-length * 0.7","arch * 0.6"],["-length - flukeSpan * 0.55","arch * 0.6 - flukeSpan"],["-length - flukeSpan * 0.3","arch * 0.6"],["-length - flukeSpan * 0.55","arch * 0.6 + flukeSpan"]],part:"fluke"},{polygon:[["length * 0.3","-height * 0.75 - arch * 0.2"],["-length * 0.2","-height * 0.7 - arch * 0.2 - dorsalHeight"],["-length * 0.35","-height * 0.65 - arch * 0.2"]]},{ellipse:["length * 0.2","height * 0.6 - arch * 0.3","finLength",.06,.6]},{circle:["length * eyeX","-height * 0.1 + arch * 0.4","eyeSize"],cut:!0}],Ft={cruising:{weight:3},leaping:{weight:0,params:{arch:.38}}},It={abstract:Ct,params:Pt,body:Nt,poses:Ft},zt=[{let:{humpHeight:"range(0.18, 0.3)",headDrop:"range(-0.08, 0.12)",legLength:"range(0.68, 0.8)"}},{ellipse:[-.1,0,"vary(0.62, 0.06)","vary(0.38, 0.06)"]},{ellipse:[.2,-.12,.32,"humpHeight + 0.1"]},{ellipse:[.62,"-0.02 + headDrop",.27,.23,.15]},{ellipse:[.82,"0.08 + headDrop",.12,.09,.5]},{circle:[.5,"-0.24 + headDrop",.1]},{circle:[.68,"-0.26 + headDrop",.09]},{circle:[-.72,-.08,.07]},{circle:[.7,"-0.04 + headDrop",.03],cut:!0},{repeat:{values:[-.55,-.3,.15,.36],as:"legX",body:[{tube:[["legX",.15],["legX + range(-0.04, 0.04)","legLength"]],radii:[.14,.12]},{ellipse:["legX + 0.06","legLength + 0.03",.14,.06]}]}}],Bt={body:zt},qt={sitting:{weight:.55,body:[{let:{headX:"range(0.12, 0.28)",headY:"range(-0.58, -0.45)",earHeight:"range(0.26, 0.4)",earTilt:"range(-0.08, 0.08)",curlsForward:"chance(0.5)",tailPoints:{if:"curlsForward",then:[[-.5,.78],[-.2,.92],[.25,.9],["range(0.45, 0.6)","range(0.7, 0.85)"]],else:[[-.55,.7],[-.88,.62],["-range(0.9, 1.05)",.3],["-range(0.75, 0.95)","range(-0.05, 0.15)"]]}}},{ellipse:[-.15,.45,"vary(0.48)","vary(0.4)"]},{ellipse:[.1,.05,"vary(0.3)",.5,-.15]},{ellipse:["headX","headY","vary(0.27, 0.08)","vary(0.23, 0.08)"]},{polygon:[["headX - 0.25","headY - 0.04"],["headX - 0.2 + earTilt","headY - 0.12 - earHeight"],["headX - 0.02","headY - 0.18"]]},{polygon:[["headX + 0.04","headY - 0.18"],["headX + 0.22 + earTilt","headY - 0.12 - earHeight"],["headX + 0.27","headY - 0.03"]]},{tube:[[.14,.3],[.16,.86]],radii:.07},{tube:[[.3,.3],[.33,.86]],radii:.065},{tube:"tailPoints",radii:[.075,.07,.065,.055]},{ellipse:["headX - 0.1","headY - 0.01",.045,.03],cut:!0},{ellipse:["headX + 0.1","headY - 0.01",.045,.03],cut:!0}]},walking:{weight:.45,body:[{let:{tailHeight:"range(0.6, 0.95)",tailHook:"range(-0.25, 0.15)",stride:"range(0.05, 0.16)",headY:"range(-0.32, -0.18)"}},{ellipse:[-.05,.05,"vary(0.58, 0.08)","vary(0.22)"]},{ellipse:[.62,"headY",.21,.19]},{polygon:[[.45,"headY - 0.08"],[.47,"headY - 0.38"],[.6,"headY - 0.17"]]},{polygon:[[.64,"headY - 0.17"],[.76,"headY - 0.38"],[.8,"headY - 0.06"]]},{tube:[[-.45,.1],["-0.45 - stride",.42],["-0.42 - stride",.68]],radii:[.08,.055,.05]},{tube:[[-.25,.12],["-0.22 + stride",.45],["-0.2 + stride",.68]],radii:[.07,.05,.05]},{tube:[[.3,.12],["0.3 + stride",.45],["0.32 + stride",.68]],radii:[.07,.05,.05]},{tube:[[.42,.12],["0.42 - stride",.45],["0.43 - stride",.68]],radii:[.07,.05,.05]},{tube:[[-.58,-.02],[-.85,-.25],[-.85,"-tailHeight + 0.2"],["-0.7 + tailHook","-tailHeight"]],radii:[.06,.055,.05,.045]},{ellipse:[.68,"headY - 0.02",.035,.022],cut:!0}]}},Vt={poses:qt},Jt={antler:[{let:{height:"range(0.42, 0.6)"}},{let:{beam:[["rootX","rootY"],["rootX - 0.1 * spread","rootY - height * 0.4"],["rootX - 0.06 * spread","rootY - height * 0.8"],["rootX + 0.08 * spread","rootY - height"]]}},{tube:"beam",radii:[.04,.036,.032,.026]},{repeat:{count:"int(2, 4)",body:[{let:{anchor:"beam[min(3, index + 1)]",length:"range(0.12, 0.2)"}},{tube:["anchor",["anchor[0] + length * 0.7 * spread","anchor[1] - length"]],radii:[.03,.024]}]}}]},Kt=[{let:{neckLean:"range(0.4, 0.55)",headY:"range(-0.55, -0.42)",legLength:"range(0.85, 0.95)",headX:"neckLean + 0.08"}},{ellipse:[-.1,.05,"vary(0.48, 0.06)","vary(0.21, 0.08)"]},{tube:[[.25,0],["neckLean - 0.05",-.25],["neckLean","headY + 0.05"]],radii:[.13,.09,.075]},{ellipse:["headX","headY",.14,.09,.4]},{ellipse:["headX + 0.13","headY + 0.07",.09,.06,.4]},{ellipse:["headX - 0.13","headY - 0.06",.1,.035,-.5]},{ellipse:[-.58,-.04,.07,.04,-.5]},{tube:[[-.45,.12],[-.5,.5],[-.42,"legLength"]],radii:[.055,.035,.03]},{tube:[[-.32,.14],[-.34,.5],[-.3,"legLength"]],radii:[.05,.035,.03]},{tube:[[.2,.15],[.22,.55],[.2,"legLength"]],radii:[.045,.035,.03]},{tube:[[.32,.12],[.36,.52],[.33,"legLength"]],radii:[.045,.035,.03]},{circle:["headX + 0.02","headY - 0.02",.025],cut:!0},{use:"antler",with:{rootX:"headX - 0.06",rootY:"headY - 0.06",spread:-.8}},{use:"antler",with:{rootX:"headX + 0.04",rootY:"headY - 0.06",spread:1.3}}],Ut={defs:Jt,body:Kt},Gt={length:"range(0.8, 0.88)",height:"range(0.2, 0.25)",dorsalHeight:.2,flukeSpan:.36,finLength:.2,eyeX:.62,eyeSize:.025},Qt=["fluke"],Zt=[{ellipse:["-length * 0.85","arch * 0.6 - flukeSpan * 0.45","flukeSpan * 0.55",.07,-.95],part:"fluke"},{ellipse:["-length * 0.85","arch * 0.6 + flukeSpan * 0.45","flukeSpan * 0.55",.07,.95],part:"fluke"},{ellipse:["length + 0.03","height * 0.35 + arch * 0.6",.15,.06,.12]}],e0={leaping:{weight:1}},t0={extends:"swimmer",params:Gt,drop:Qt,body:Zt,poses:e0},a0={bodyRadiusX:"range(0.5, 0.56)",bodyRadiusY:"range(0.27, 0.3)",bodyAngle:-.1,legLength:.16,headRadius:.15,headX:"bodyRadiusX * 0.7",headY:"bodyY - bodyRadiusY - 0.12",neckWidth:.1,beakLength:.17,beakHeight:.07,tailLength:.2,tailSpread:.1,tailDrop:-.12,wingWidth:.15},n0={extends:"bird",params:a0},i0=[{let:{earSize:"range(0.9, 1.15)",trunkCurl:"range(-0.1, 0.25)",headY:"range(-0.32, -0.2)",legLength:"range(0.76, 0.86)"}},{ellipse:[-.15,0,"vary(0.6, 0.06)","vary(0.4, 0.06)"]},{ellipse:[.45,"headY",.3,.3]},{tube:[[-.73,-.1],[-.8,.15],[-.8,.38]],radii:[.025,.02,.035]},{repeat:{values:[-.6,-.36,.05,.26],as:"legX",body:[{tube:[["legX",.2],["legX","legLength"]],radii:.12}]}},{tube:[[.6,"headY + 0.28"],[.78,"headY + 0.38"],[.92,"headY + 0.3"]],radii:.035,cut:!0},{tube:[[.68,"headY - 0.02"],[.84,"headY + 0.25"],[.86,"headY + 0.6"],["0.9 + trunkCurl","headY + 0.92"],["1.02 + trunkCurl","headY + 0.85"]],radii:[.12,.1,.075,.055,.045]},{let:{earPoints:{map:{values:[[.12,-.52],[.38,-.6],[.48,-.22],[.38,.16],[.14,.1],[.02,-.2]],as:"point",value:["0.27 + (point[0] - 0.27) * earSize","headY + 0.05 + (point[1] + 0.2) * earSize"]}}}},{blob:"earPoints"},{tube:["earPoints[1]","earPoints[2]","earPoints[3]"],radii:.022,cut:!0},{circle:[.58,"headY - 0.04",.035],cut:!0}],o0={body:i0},s0={height:"range(0.4, 0.48)",dorsalHeight:.22,eyeX:.62,eyeSize:.05},r0={extends:"swimmer",params:s0},l0=["neck"],d0={bodyRadiusX:"range(0.3, 0.34)",bodyRadiusY:.2,bodyAngle:-.12,legLength:"range(0.75, 0.85)",headRadius:.07,headX:"range(0.38, 0.48)",headY:"bodyY - bodyRadiusY - range(0.7, 0.8)",beakLength:.12,beakHeight:.05,tailLength:.12,tailSpread:.06,tailDrop:.02,legWidth:.026,wingWidth:.08},u0=[{tube:[["bodyRadiusX * 0.55","bodyY - bodyRadiusY * 0.4"],["bodyRadiusX * 0.9","bodyY - bodyRadiusY - 0.28"],["headX - 0.14","headY + 0.3"],["headX - 0.02","headY + 0.08"]],radii:[.075,.04,.035,.03],part:"neck"}],c0={sleeping:{weight:0},front:{weight:0}},h0={extends:"bird",drop:l0,params:d0,body:u0,poses:c0},g0=[{let:{earHeight:"range(0.28, 0.4)",snout:"range(0.95, 1.1)",tailDroop:"range(-0.15, 0.25)",tailBush:"range(0.13, 0.19)",legLength:"range(0.6, 0.72)"}},{ellipse:[-.05,.05,"vary(0.48, 0.08)","vary(0.19)"]},{ellipse:[.35,0,.18,.22]},{blob:[[.28,-.12],[.52,-.32],[.72,-.28],["snout",-.12],[.75,-.04],[.48,.14]]},{polygon:[[.48,-.26],[.5,"-0.28 - earHeight"],[.63,-.31]]},{polygon:[[.6,-.3],[.7,"-0.28 - earHeight * 0.95"],[.75,-.25]]},{tube:[[-.45,.02],[-.75,"0.12 + tailDroop * 0.5"],[-1,"0.08 + tailDroop"],[-1.15,"-0.06 + tailDroop"]],radii:[.06,"tailBush","tailBush * 0.85",.025]},{ellipse:[.7,-.17,.035,.022,-.3],cut:!0},{repeat:{values:[-.4,-.25,.28,.4],as:"legX",body:[{let:{step:"if(index % 2 == 0, range(-0.08, 0.08), 0)"}},{tube:[["legX",.12],["legX + step","legLength"]],radii:[.05,.035]}]}}],p0={body:g0},f0=[{let:{bodyY:"range(0.05, 0.15)",bodyRadiusX:"range(0.6, 0.72)",bodyRadiusY:"range(0.3, 0.38)",spikeCount:"int(11, 17)",spikeLength:"range(0.3, 0.45)",sweep:"range(0.2, 0.55)",frontAngle:"PI * range(1.62, 1.75)"}},{ellipse:[0,"bodyY","bodyRadiusX","bodyRadiusY"]},{repeat:{count:"spikeCount",body:[{let:{angle:"lerp(PI * 0.88, frontAngle, index / (spikeCount - 1)) + range(-0.04, 0.04)",baseX:"cos(angle) * bodyRadiusX * 0.9",baseY:"bodyY + sin(angle) * bodyRadiusY * 0.9",directionX:"cos(angle) - sweep",directionY:"sin(angle) * 1.2",directionLength:"hypot(directionX, directionY)",length:"spikeLength * range(0.7, 1.25)",halfWidth:"range(0.07, 0.1)",tangentX:"-directionY / directionLength",tangentY:"directionX / directionLength"}},{polygon:[["baseX - tangentX * halfWidth","baseY - tangentY * halfWidth"],["baseX + directionX / directionLength * (length + 0.08)","baseY + directionY / directionLength * (length + 0.08)"],["baseX + tangentX * halfWidth","baseY + tangentY * halfWidth"]]}]}},{let:{snoutX:"bodyRadiusX * 0.85",snoutReach:"range(0.28, 0.42)"}},{ellipse:["snoutX - 0.05","bodyY + 0.12",.3,.2,.3]},{polygon:[["snoutX - 0.05","bodyY - 0.02"],["snoutX + snoutReach","bodyY + 0.25"],["snoutX - 0.1","bodyY + 0.3"]]},{circle:["snoutX + snoutReach - 0.02","bodyY + 0.25",.055]},{repeat:{values:[-.42,-.15,.22,.45],as:"legX",body:[{tube:[["legX","bodyY + bodyRadiusY * 0.7"],["legX + 0.04","bodyY + bodyRadiusY + range(0.08, 0.14)"]],radii:.06}]}},{circle:["snoutX + 0.04","bodyY + 0.08",.04],cut:!0}],b0={body:f0},y0=["neck"],m0={bodyRadiusX:"range(0.6, 0.68)",bodyRadiusY:"range(0.3, 0.34)",legLength:"range(0.6, 0.7)",legWidth:.055,headRadiusX:.17,headRadiusY:.14,headX:"bodyRadiusX * 0.95 + 0.22",headY:"bodyY - bodyRadiusY - 0.32",neckWidth:.1,snoutLength:.24,snoutRadius:.09,earHeight:.1,earWidth:.05,tailLength:.3,tailLift:"range(-0.6, -0.4)",tailWidth:.07},v0=[{tube:[["bodyRadiusX * 0.55","bodyY - bodyRadiusY * 0.9"],["bodyRadiusX * 0.7","bodyY - bodyRadiusY - 0.2"],["headX - 0.06","headY - headRadiusY * 0.8"]],radii:[.1,.075,.045],part:"neck"}],Y0={sitting:{weight:0},front:{weight:0}},R0={extends:"quadruped",drop:y0,params:m0,body:v0,poses:Y0},X0=[{let:{mantleRadius:"range(0.38, 0.46)",mantleLift:"range(0.35, 0.5)",armCount:"int(7, 8)",sway:"range(0.2, 0.5)"}},{ellipse:[0,"-mantleLift","mantleRadius","mantleRadius * 1.25"]},{ellipse:[0,.05,"mantleRadius * 1.1",.3]},{repeat:{count:"armCount",body:[{let:{spread:"lerp(-0.65, 0.65, index / (armCount - 1))",wave:"if(index % 2 == 0, sway, -sway) * range(0.6, 1.2)",reach:"range(0.5, 0.7)"}},{tube:[["spread * 0.45",.1],["spread * 0.9 + wave * 0.3","reach * 0.5"],["spread * 1.2 + wave","reach"],["spread * 1.1 + wave * 1.4","reach + 0.18"]],radii:[.1,.075,.05,.02]}]}},{circle:["-mantleRadius * 0.4","-mantleLift + 0.15",.07],cut:!0},{circle:["mantleRadius * 0.4","-mantleLift + 0.15",.07],cut:!0}],w0={body:X0},_0={bodyRadiusX:"range(0.56, 0.62)",bodyRadiusY:"range(0.2, 0.24)",legLength:"range(0.16, 0.22)",legWidth:.075,headRadiusX:.17,headRadiusY:.15,headX:"bodyRadiusX + 0.02",headY:"bodyY - bodyRadiusY * 0.7",neckWidth:"bodyRadiusY * 0.8",snoutLength:.11,snoutRadius:.07,earHeight:.04,earWidth:.05,tailLength:"range(0.5, 0.62)",tailLift:"range(-0.1, 0.05)",tailWidth:.11},k0={extends:"quadruped",params:_0},S0=[{let:{tuftHeight:"range(0.3, 0.5)",eyeSize:"range(0.13, 0.17)",lookX:"range(-0.04, 0.04)"}},{ellipse:[0,.15,"vary(0.46, 0.08)","vary(0.6, 0.06)"]},{ellipse:[0,-.4,.44,.33]},{polygon:[[-.42,-.5],[-.5,"-0.5 - tuftHeight"],[-.16,-.66]]},{polygon:[[.42,-.5],[.5,"-0.5 - tuftHeight"],[.16,-.66]]},{ellipse:[-.42,.2,.15,.42,.18]},{ellipse:[.42,.2,.15,.42,-.18]},{tube:[[-1.15,.82],[0,.8],[1.15,"range(0.78, 0.88)"]],radii:.05},{tube:[[-.14,.7],[-.16,.82]],radii:.045},{tube:[[.14,.7],[.16,.82]],radii:.045},{circle:[-.2,-.4,"eyeSize"],cut:!0},{circle:[.2,-.4,"eyeSize"],cut:!0},{circle:["-0.2 + lookX",-.4,"eyeSize * 0.42"]},{circle:["0.2 + lookX",-.4,"eyeSize * 0.42"]},{polygon:[[-.05,-.27],[.05,-.27],[0,-.14]],cut:!0},{repeat:{count:3,as:"row",body:[{repeat:{values:[-1,0,1],as:"column",body:[{let:{featherX:"column * 0.16 + (row % 2) * 0.08 - 0.04",featherY:"0.05 + row * 0.15"}},{tube:[["featherX - 0.05","featherY"],["featherX","featherY + 0.04"],["featherX + 0.05","featherY"]],radii:.014,cut:!0}]}}]}}],L0={body:S0},W0=["wing"],$0={bodyRadiusX:"range(0.55, 0.62)",bodyRadiusY:"range(0.3, 0.34)",bodyAngle:"-PI / 2",legLength:.06,headRadius:.17,headX:.04,headY:"bodyY - bodyRadiusX * 0.85",neckWidth:.15,beakLength:.15,beakHeight:.06,tailLength:.12,tailSpread:.08,tailDrop:.1,wingWidth:.07},E0=[{ellipse:["-bodyRadiusY * 0.95","bodyY + 0.02",.08,"bodyRadiusX * 0.6",.15],part:"flipper"},{ellipse:["bodyRadiusY * 0.95","bodyY + 0.02",.08,"bodyRadiusX * 0.6",-.15],part:"flipper"}],j0={sleeping:{weight:0}},D0={extends:"bird",drop:W0,params:$0,body:E0,poses:j0},O0={bodyRadiusX:"range(0.56, 0.62)",bodyRadiusY:"range(0.36, 0.4)",legLength:"range(0.2, 0.26)",legWidth:.08,headRadiusX:.25,headRadiusY:.23,headX:"bodyRadiusX * 0.95",headY:"bodyY + 0.02",neckWidth:.22,snoutLength:.12,snoutRadius:.12,earHeight:.1,earWidth:.1,earSpread:.1,tailLength:.14,tailLift:"range(0.15, 0.25)",tailWidth:.035},T0={sitting:{weight:0}},A0={extends:"quadruped",params:O0,poses:T0},M0={ear:[{ellipse:["baseX + sin(angle) * length","baseY - cos(angle) * length",.085,"length","angle"]}]},x0=[{let:{earLength:"range(0.38, 0.52)",earLean:"range(-0.5, 0.05)",floppy:"chance(0.3)",headX:"range(0.36, 0.46)",headY:"range(-0.18, -0.05)"}},{ellipse:[-.2,.35,"vary(0.46, 0.08)","vary(0.38, 0.08)"]},{ellipse:[.08,.22,.38,.33]},{ellipse:["headX","headY",.24,.2,.2]},{use:"ear",with:{baseX:"headX - 0.06",baseY:"headY - 0.12",angle:"earLean",length:"earLength"}},{use:"ear",with:{baseX:"headX + 0.04",baseY:"headY - 0.1",angle:"earLean + if(floppy, 1.2, 0.25)",length:"earLength * if(floppy, 0.85, 0.95)"}},{circle:[-.68,.2,"range(0.11, 0.15)"]},{ellipse:[-.08,.72,.3,.07]},{tube:[[.3,.35],[.36,.72]],radii:.06},{circle:["headX + 0.06","headY - 0.04",.035],cut:!0}],H0={defs:M0,body:x0},C0=[{let:{hornLength:"range(0.62, 0.85)",hornLean:"range(-0.12, 0.02)",headDrop:"range(0, 0.12)",legLength:"range(0.68, 0.82)"}},{ellipse:[-.15,.05,"vary(0.62, 0.06)","vary(0.36, 0.08)"]},{ellipse:[.15,-.1,.34,.3]},{blob:[[.3,-.2],[.68,"-0.02 + headDrop"],[1.02,"0.24 + headDrop"],[.98,"0.42 + headDrop"],[.62,"0.42 + headDrop"],[.36,.3]]},{polygon:[[.8,"0.24 + headDrop"],[.84,"0.1 + headDrop"],["0.96 + hornLean","0.24 + headDrop - hornLength"],[1.05,"0.3 + headDrop"]]},{polygon:[[.66,"0.1 + headDrop"],["0.74 + hornLean","0.08 + headDrop - hornLength * 0.45"],[.82,"0.16 + headDrop"]]},{polygon:[[.34,-.14],["range(0.28, 0.34)",-.46],[.48,-.12]]},{tube:[[-.74,-.02],[-.84,.12],[-.83,.28]],radii:.025},{circle:[.6,"0.18 + headDrop",.03],cut:!0},{repeat:{values:[-.56,-.32,.08,.3],as:"legX",body:[{tube:[["legX",.2],["legX + range(-0.03, 0.03)","legLength"]],radii:.12}]}}],P0={body:C0},N0={length:"range(0.85, 0.92)",height:"range(0.2, 0.24)",dorsalHeight:"range(0.35, 0.42)",finLength:.3,flukeSpan:.4,eyeX:.68,eyeSize:.022},F0={extends:"swimmer",params:N0},I0=[{let:{shellRadius:"range(0.48, 0.6)",shellX:"range(-0.25, -0.1)",shellY:"0.55 - shellRadius - range(0.02, 0.08)",turns:"range(2.6, 3.4)",stalk:"range(0.3, 0.42)"}},{let:{spiral:{map:{count:49,as:"step",let:{angle:"step / 48 * turns * PI * 2",radius:"shellRadius * 0.86 * (step / 48)"},value:["shellX + cos(angle) * radius","shellY + sin(angle) * radius"]}}}},{blob:[[-.95,.66],[-.5,.56],[.4,.56],[.68,.38],[.82,.08],[.95,.02],[1.02,.18],[.96,.5],[.8,.7],[-.88,.76]]},{circle:["shellX","shellY","shellRadius"]},{tube:"spiral",radii:.026,cut:!0},{tube:[[.88,.08],["0.95 + range(0, 0.08)","0.08 - stalk"]],radii:.024},{circle:["0.95 + 0.04","0.08 - stalk - 0.02",.05]},{tube:[[.82,.1],[.78,"0.1 - stalk * 0.8"]],radii:.022},{circle:[.78,"0.1 - stalk * 0.8 - 0.02",.045]}],z0={body:I0},B0=[{let:{shellHeight:"range(0.45, 0.6)",shellWidth:"range(0.66, 0.76)",neckReach:"range(0.85, 1.0)",dome:{map:{count:25,as:"step",let:{angle:"PI + (step / 24) * PI"},value:["cos(angle) * shellWidth","0.25 + sin(angle) * shellHeight"]}},scuteY:"0.25 - shellHeight * 0.45"}},{polygon:"dome"},{ellipse:[0,.27,"shellWidth + 0.07",.08]},{tube:[["shellWidth - 0.1",.24],["neckReach - 0.15",.1]],radii:.09},{ellipse:["neckReach",.08,.17,.12,-.1]},{ellipse:["-shellWidth * 0.62",.42,.12,.16,.3]},{ellipse:["shellWidth * 0.6",.42,.12,.16,-.3]},{polygon:[["-shellWidth - 0.02",.22],["-shellWidth - 0.2",.3],["-shellWidth - 0.02",.3]]},{tube:[["-shellWidth * 0.95",.2],["-shellWidth * 0.5","scuteY"],["shellWidth * 0.5","scuteY"],["shellWidth * 0.95",.2]],radii:.015,cut:!0},{tube:[["-shellWidth * 0.25","scuteY"],["-shellWidth * 0.3",.2]],radii:.015,cut:!0},{tube:[["shellWidth * 0.25","scuteY"],["shellWidth * 0.3",.2]],radii:.015,cut:!0},{tube:[["-shellWidth * 0.2","scuteY"],[0,"0.25 - shellHeight * 0.95"]],radii:.014,cut:!0},{tube:[["shellWidth * 0.2","scuteY"],[0,"0.25 - shellHeight * 0.95"]],radii:.014,cut:!0},{circle:["neckReach + 0.06",.04,.028],cut:!0}],q0={body:B0},V0=[{let:{belly:"range(0.24, 0.34)",back:"range(-0.42, -0.32)",tailLift:"range(-0.35, 0.05)",spout:"chance(0.75)"}},{blob:[[.9,-.02],[.72,"back + 0.1"],[.3,"back"],[-.2,"back + 0.12"],[-.6,"-0.08 + tailLift * 0.4"],[-.88,"tailLift * 0.8"],[-.62,"0.12 + tailLift * 0.3"],[-.1,"belly"],[.5,"belly + 0.02"],[.84,.18]]},{ellipse:[-1,"tailLift - 0.16","vary(0.28)",.075,-.85]},{ellipse:[-1,"tailLift + 0.13","vary(0.28)",.075,.85]},{ellipse:[.22,"belly + 0.08","vary(0.24)",.065,.55]},{tube:[[.88,.1],[.6,.16],[.32,.12]],radii:.018,cut:!0},{circle:[.42,.04,.035],cut:!0},{if:"spout",then:[{tube:[[.55,"back - 0.06"],[.56,"back - 0.3"],[.44,"back - 0.5"]],radii:[.035,.03,.02]},{tube:[[.56,"back - 0.3"],[.7,"back - 0.48"]],radii:[.03,.02]},{circle:[.38,"back - 0.48",.03]},{circle:[.76,"back - 0.42",.025]},{circle:[.6,"back - 0.6",.025]}]}],J0={body:V0},K0={bodyRadiusX:"range(0.58, 0.66)",bodyRadiusY:"range(0.26, 0.3)",legLength:"range(0.52, 0.62)",legWidth:.06,headRadiusX:.2,headRadiusY:.16,snoutLength:.24,snoutRadius:.07,earHeight:"range(0.2, 0.26)",tailLength:"range(0.4, 0.5)",tailLift:"range(-0.25, -0.05)",tailWidth:.09},U0={extends:"quadruped",params:K0},G0=Object.assign({"./base/bird.json":Dt,"./base/quadruped.json":Ht,"./base/swimmer.json":It,"./bear.json":Bt,"./cat.json":Vt,"./deer.json":Ut,"./dolphin.json":t0,"./duck.json":n0,"./elephant.json":o0,"./fish.json":r0,"./flamingo.json":h0,"./fox.json":p0,"./hedgehog.json":b0,"./horse.json":R0,"./octopus.json":w0,"./otter.json":k0,"./owl.json":L0,"./penguin.json":D0,"./pig.json":A0,"./rabbit.json":H0,"./rhino.json":P0,"./shark.json":F0,"./snail.json":z0,"./turtle.json":q0,"./whale.json":J0,"./wolf.json":U0}),Xa=Object.fromEntries(Object.entries(G0).map(([e,t])=>[e.replace(/^.*\//,"").replace(/\.json$/,""),t])),Q0=pt(),Z0=()=>Q0.value,wa=O(Z0);export{yt as A,ca as B,la as C,sa as D,ua as E,Ra as F,va as G,A as M,et as a,ya as b,ba as c,Re as d,Ae as e,ae as f,wa as g,ha as h,Z0 as i,fa as j,na as k,Me as l,Yt as m,da as n,ga as o,Ya as p,ia as q,Xa as r,ma as s,ta as t,q as u,pa as v,ra as w,aa as x,oa as y,st as z};
@@ -0,0 +1 @@
1
+ import{d as V,Y as F,v as t,F as l,L as i,K as a,b as c,c as k,_ as C,e as I,w as T,O as d,V as h,a0 as U,z as $,H as g,I as f,W as M,M as w,x as L,Q as P,$ as z}from"./_plugin-vue_export-helper-BbgK5wvS.js";import{F as A,b as D,r as y,c as E,s as O,G as q,g as G}from"./scheme-DCqBrLhv.js";const H={class:"species-cell"},K={key:0,class:"species-cell__error"},Q={class:"species-cell__seed"},R=V({__name:"SpeciesCell",props:{name:{},pose:{},seed:{},ink:{}},setup(m){const n=m,o=c(null),r=k(()=>{try{return{shapes:D(y,n.name,E(n.seed),{pose:n.pose}).shapes,error:""}}catch(u){return{shapes:[],error:u.message}}});return F(()=>{o.value&&!r.value.error&&n.ink&&A(o.value,r.value.shapes,n.ink)}),(u,_)=>(t(),l("figure",H,[r.value.error?(t(),l("p",K,i(r.value.error),1)):(t(),l("canvas",{key:1,ref_key:"canvas",ref:o,class:"species-cell__canvas",width:"260",height:"260"},null,512)),a("figcaption",Q,i(m.seed),1)]))}}),W=C(R,[["__scopeId","data-v-220e2a5a"]]),Y={class:"sheet-page__head"},j={class:"sheet-page__field"},J={value:""},X=["value"],Z={class:"sheet-page__field"},ee={class:"sheet-page__field"},se={class:"sheet-page__label"},ae={key:0,class:"sheet-page__pose"},te=V({__name:"SheetPage",setup(m){const n=new URLSearchParams(location.search),o=c(Math.min(12,Math.max(1,Number(n.get("seeds"))||6))),r=c(Number(n.get("from"))||1),u=c(n.get("species")||""),_=O(y),N=k(()=>Array.from({length:o.value},(p,e)=>r.value+e)),B=k(()=>_.filter(p=>!u.value||p===u.value).flatMap(p=>q(y,p).map(e=>({name:p,pose:e,key:`${p}:${e}`})))),v=c(null),b=c(""),S=()=>{v.value&&(b.value=getComputedStyle(v.value).color)};return I(S),T(G,S),(p,e)=>(t(),l("main",{ref_key:"page",ref:v,class:"app sheet-page"},[a("header",Y,[e[6]||(e[6]=a("h1",{class:"sheet-page__title"},"Species sheet",-1)),a("label",j,[e[3]||(e[3]=d("Species ",-1)),h(a("select",{"onUpdate:modelValue":e[0]||(e[0]=s=>u.value=s)},[a("option",J,"all ("+i($(_).length)+")",1),(t(!0),l(g,null,f($(_),s=>(t(),l("option",{key:s,value:s},i(s),9,X))),128))],512),[[U,u.value]])]),a("label",Z,[e[4]||(e[4]=d("Seeds ",-1)),h(a("input",{"onUpdate:modelValue":e[1]||(e[1]=s=>o.value=s),class:"sheet-page__number",type:"number",min:"1",max:"12"},null,512),[[M,o.value,void 0,{number:!0}]])]),a("label",ee,[e[5]||(e[5]=d("From seed ",-1)),h(a("input",{"onUpdate:modelValue":e[2]||(e[2]=s=>r.value=s),class:"sheet-page__number",type:"number",min:"0"},null,512),[[M,r.value,void 0,{number:!0}]])])]),(t(!0),l(g,null,f(B.value,s=>(t(),l("section",{key:s.key,class:"sheet-page__row"},[a("h2",se,[d(i(s.name),1),s.pose?(t(),l("small",ae,i(s.pose),1)):w("",!0)]),a("div",{class:"sheet-page__cells",style:P({gridTemplateColumns:`repeat(${o.value}, minmax(0, 1fr))`})},[(t(!0),l(g,null,f(N.value,x=>(t(),L(W,{key:x,name:s.name,pose:s.pose,seed:x,ink:b.value},null,8,["name","pose","seed","ink"]))),128))],4)]))),128))],512))}}),le=C(te,[["__scopeId","data-v-b4daa75a"]]);z(le).mount("#app");
package/dist/index.html CHANGED
@@ -6,12 +6,12 @@
6
6
  <title>Whose Turn</title>
7
7
  <script src="/vendor/popper.min.js"></script>
8
8
  <script src="/vendor/tippy-bundle.umd.min.js"></script>
9
- <script type="module" crossorigin src="/assets/index-DsQ_5Y-x.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-DeQjDyQ9.js">
11
- <link rel="modulepreload" crossorigin href="/assets/scheme-BdlolNvz.js">
12
- <link rel="modulepreload" crossorigin href="/assets/bem-CST_YFrc.js">
13
- <link rel="stylesheet" crossorigin href="/assets/_plugin-vue_export-helper-aQHODzfZ.css">
14
- <link rel="stylesheet" crossorigin href="/assets/index-CEB3_bQv.css">
9
+ <script type="module" crossorigin src="/assets/index-C3jZLqLJ.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BbgK5wvS.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/scheme-DCqBrLhv.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/bem-OtK9ToP0.js">
13
+ <link rel="stylesheet" crossorigin href="/assets/_plugin-vue_export-helper-BneMU3KP.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-B6grE8kv.css">
15
15
  </head>
16
16
  <body class="page">
17
17
  <div id="app" class="page__mount"></div>
package/dist/report.html CHANGED
@@ -4,10 +4,10 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Your report · Whose Turn</title>
7
- <script type="module" crossorigin src="/assets/report-Bar3ULi6.js"></script>
8
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-DeQjDyQ9.js">
9
- <link rel="modulepreload" crossorigin href="/assets/bem-CST_YFrc.js">
10
- <link rel="stylesheet" crossorigin href="/assets/_plugin-vue_export-helper-aQHODzfZ.css">
7
+ <script type="module" crossorigin src="/assets/report-DUlemvzD.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BbgK5wvS.js">
9
+ <link rel="modulepreload" crossorigin href="/assets/bem-OtK9ToP0.js">
10
+ <link rel="stylesheet" crossorigin href="/assets/_plugin-vue_export-helper-BneMU3KP.css">
11
11
  <link rel="stylesheet" crossorigin href="/assets/report-D7II0AfP.css">
12
12
  </head>
13
13
  <body class="page page--scroll">
package/dist/sheet.html CHANGED
@@ -4,10 +4,10 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Species sheet · Whose Turn</title>
7
- <script type="module" crossorigin src="/assets/sheet-q2RQEgOf.js"></script>
8
- <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-DeQjDyQ9.js">
9
- <link rel="modulepreload" crossorigin href="/assets/scheme-BdlolNvz.js">
10
- <link rel="stylesheet" crossorigin href="/assets/_plugin-vue_export-helper-aQHODzfZ.css">
7
+ <script type="module" crossorigin src="/assets/sheet-Ctd_6q4a.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BbgK5wvS.js">
9
+ <link rel="modulepreload" crossorigin href="/assets/scheme-DCqBrLhv.js">
10
+ <link rel="stylesheet" crossorigin href="/assets/_plugin-vue_export-helper-BneMU3KP.css">
11
11
  <link rel="stylesheet" crossorigin href="/assets/sheet-DYpaJaXO.css">
12
12
  </head>
13
13
  <body class="page page--scroll">
package/lib/api.mjs CHANGED
@@ -4,8 +4,10 @@ import { join } from 'node:path';
4
4
  import { createStatic } from './static.mjs';
5
5
  import { ApiError, toApiError } from './errors.mjs';
6
6
  import { connectorCatalogue, secretTarget, secretsRequired } from '../connectors/index.mjs';
7
- import { desktopOf, entryShapeViolations, normaliseWorkspaces, WORKSPACE_FIELDS } from './config.mjs';
7
+ import { desktopOf, entryShapeViolations, normaliseWorkspaces, settingsViolations, GLOBAL_SETTINGS, WORKSPACE_FIELDS } from './config.mjs';
8
8
  import { FACTS } from './facts.mjs';
9
+ import { withHidden } from './registry.mjs';
10
+ import { HIDDEN_MODES } from './backoff.mjs';
9
11
  import { createFeedback, installOf } from './feedback.mjs';
10
12
  import { composeIssue, DEFAULT_COMPOSER } from './compose.mjs';
11
13
  import { readSize, saveSize, checkMode, spawnOpener } from './window.mjs';
@@ -117,7 +119,7 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
117
119
  throw new ApiError('NO_WORKSPACE');
118
120
  }
119
121
  if (!id) {
120
- return workspaces[0];
122
+ return workspaces.find((workspace) => !workspace.hidden) || workspaces[0];
121
123
  }
122
124
  const found = workspaces.find((workspace) => workspace.id === id);
123
125
  if (!found) {
@@ -140,13 +142,18 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
140
142
  write({ ws: workspace.id, version: workspace.store.state.version, polled_at: workspace.store.state.polled_at });
141
143
  }
142
144
  write({ update: updater.status() });
145
+ if (reports.hasNews?.()) {
146
+ write({ report_news: true });
147
+ }
143
148
  const unsubscribe = registry.subscribe(write);
144
149
  const unwatch = updater.subscribe(write);
150
+ const unwatchReports = reports.subscribe?.(write);
145
151
  const keepAlive = setInterval(() => response.write(': keep-alive\n\n'), 25000);
146
152
  response.on('close', () => {
147
153
  clearInterval(keepAlive);
148
154
  unsubscribe();
149
155
  unwatch();
156
+ unwatchReports?.();
150
157
  });
151
158
  };
152
159
 
@@ -170,7 +177,7 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
170
177
  }
171
178
  if (url.pathname === '/api/config') {
172
179
  const tables = Object.fromEntries(registry.workspaces.map((workspace) => [workspace.id, workspace.rulesTable]));
173
- return send(response, 200, { workspaces: registry.rawWorkspaces, notifications: registry.notifications, problems: registry.problems, connectors: connectorCatalogue(), fields: WORKSPACE_FIELDS, facts: FACTS, tables, table_warnings: normaliseWorkspaces(registry.rawWorkspaces).warnings, config_file: registry.configFile });
180
+ return send(response, 200, { workspaces: registry.rawWorkspaces, notifications: registry.notifications, settings: registry.settings, settings_fields: GLOBAL_SETTINGS, problems: registry.problems, connectors: connectorCatalogue(), fields: WORKSPACE_FIELDS, facts: FACTS, tables, table_warnings: normaliseWorkspaces(registry.rawWorkspaces).warnings, config_file: registry.configFile });
174
181
  }
175
182
  if (url.pathname === '/api/changelog') {
176
183
  if (!changelog) {
@@ -187,6 +194,7 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
187
194
  return send(response, 200, { report: await reports.detail(id) });
188
195
  }
189
196
  const list = await reports.states();
197
+ await reports.markSeen?.();
190
198
  return send(response, 200, { reports: list });
191
199
  }
192
200
  if (url.pathname === '/api/handoff') {
@@ -248,7 +256,19 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
248
256
  return { notifications: registry.notifications };
249
257
  };
250
258
 
259
+ const handleSettings = async (settings) => {
260
+ const violations = settingsViolations(settings);
261
+ if (violations.length) {
262
+ throw new ApiError('INVALID_CONFIG', { count: String(violations.length) }, violations);
263
+ }
264
+ await registry.saveSettings(settings);
265
+ return { settings: registry.settings, problems: registry.problems };
266
+ };
267
+
251
268
  const handleConfig = async (body) => {
269
+ if (body.settings !== undefined && body.workspaces === undefined) {
270
+ return handleSettings(body.settings);
271
+ }
252
272
  if (!Array.isArray(body.workspaces)) {
253
273
  throw new ApiError('INVALID_ARGUMENT', { detail: 'workspaces must be a list' });
254
274
  }
@@ -286,6 +306,22 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
286
306
  return { workspaces: registry.workspaces.map((workspace) => workspace.describe()), notifications: registry.notifications, problems: registry.problems, secrets_stored: stored.map(({ service, account }) => ({ service, account })) };
287
307
  };
288
308
 
309
+ const handleHidden = async ({ id, hidden }) => {
310
+ if (!registry.workspaces.some((workspace) => workspace.id === id)) {
311
+ throw new ApiError('NO_SUCH_WORKSPACE', { ws: String(id || '') });
312
+ }
313
+ if (hidden !== null && !HIDDEN_MODES.includes(hidden)) {
314
+ throw new ApiError('INVALID_ARGUMENT', { detail: 'hidden must be "parked", "paused" or null' }, [{ field: 'hidden', description: 'Send "parked", "paused" or null to show the workspace again.' }]);
315
+ }
316
+ const { violations } = normaliseWorkspaces(registry.rawWorkspaces.map((entry) => (entry.id === id ? withHidden(entry, hidden) : entry)));
317
+ if (violations.length) {
318
+ throw new ApiError('INVALID_CONFIG', { count: String(violations.length) }, violations);
319
+ }
320
+ const created = await registry.setHidden(id, hidden || '');
321
+ await Promise.all(created.map((workspace) => workspace.pollIfDue()));
322
+ return { workspaces: registry.workspaces.map((workspace) => workspace.describe()) };
323
+ };
324
+
289
325
  const handleSecret = async (body) => {
290
326
  const service = String(body.service || '').trim();
291
327
  const account = String(body.account || '').trim();
@@ -349,7 +385,9 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
349
385
  const composer = registry.feedback && registry.feedback.composer && registry.feedback.composer.command ? registry.feedback.composer : DEFAULT_COMPOSER;
350
386
  const shaped = remark ? await composeIssue(remark, { composer, bugWords: registry.feedback && registry.feedback.bug_words }) : { kind: body.kind, title: body.title, body: body.body, composed_by: body.from || 'caller' };
351
387
  const issue = await createFeedback({ http, reports, relayUrl: reports.relayUrl, version, sources, install: installOf(appDir), client: body.client }, { kind: shaped.kind, title: shaped.title, body: shaped.body });
352
- return { issue, composed_by: shaped.composed_by, reports: await reports.states() };
388
+ const list = await reports.states();
389
+ await reports.markSeen?.();
390
+ return { issue, composed_by: shaped.composed_by, reports: list };
353
391
  } catch (error) {
354
392
  lastRemark = { text: '', at: 0 };
355
393
  throw error;
@@ -369,6 +407,9 @@ export const startApi = async ({ port, appDir, registry, requestPoll, saveConfig
369
407
  if (url.pathname === '/api/config') {
370
408
  return send(response, 200, await handleConfig(body));
371
409
  }
410
+ if (url.pathname === '/api/workspaces/hidden') {
411
+ return send(response, 200, await handleHidden(body));
412
+ }
372
413
  if (url.pathname === '/api/notifications') {
373
414
  return send(response, 200, await handleNotifications(body));
374
415
  }
package/lib/backoff.mjs CHANGED
@@ -1,2 +1,15 @@
1
1
  /** The next poll delay in seconds: doubled up to 15 minutes while any source is rate limited, else the normal interval. */
2
2
  export const nextDelay = (outcomes, delay, pollSeconds) => (outcomes.some((outcome) => outcome.rateLimited) ? Math.min(delay * 2, 15 * 60) : pollSeconds);
3
+
4
+ export const PARKED_EVERY_MS = 15 * 60 * 1000;
5
+
6
+ /** The workspace's hide modes: parked is checked rarely and returns by itself, paused is never checked. */
7
+ export const HIDDEN_MODES = ['parked', 'paused'];
8
+
9
+ /** Whether a workspace is polled in this round: always when shown, every 15 minutes when parked (its first check after a start is due 15 minutes after the stored last check, at once when there is none or it is older), never when paused. */
10
+ export const isDue = (hidden, lastPollAt, now) => {
11
+ if (hidden === 'paused') {
12
+ return false;
13
+ }
14
+ return hidden !== 'parked' || now - lastPollAt >= PARKED_EVERY_MS;
15
+ };
package/lib/config.mjs CHANGED
@@ -8,6 +8,7 @@ import { statusGroups } from './facts.mjs';
8
8
  import { tableProblems, tableWarnings } from './table.mjs';
9
9
  import { DEFAULT_LIMITS } from './health.mjs';
10
10
  import { CONNECTORS } from '../connectors/index.mjs';
11
+ import { HIDDEN_MODES } from './backoff.mjs';
11
12
 
12
13
  export const DEFAULT_RULES = {
13
14
  release_branch_prefix: 'release/',
@@ -19,6 +20,7 @@ export const DEFAULT_RULES = {
19
20
  passed: ['passed'],
20
21
  code_review: ['code review', 'in review'],
21
22
  qa: ['ready for qa', 'in qa', 'testing'],
23
+ release: ['ready for release', 'ready to release', 'awaiting release'],
22
24
  on_hold: ['on hold', 'blocked'],
23
25
  backlog: ['backlog'],
24
26
  },
@@ -40,6 +42,7 @@ export const WORKSPACE_FIELDS = {
40
42
  theme: [
41
43
  { key: 'seed', label: 'Seed colour', hint: 'the scheme is derived from it; empty derives one from the workspace id', colour: true },
42
44
  { key: 'accent', label: 'Accent colour', hint: 'optional second hue', colour: true },
45
+ { key: 'backdrop', label: 'Backdrop animal', hint: 'draw the dotted animal behind the window', boolean: true, default: true },
43
46
  ],
44
47
  rules: [
45
48
  { key: 'approved_next', label: 'Next step when the PR is approved', hint: 'shown when a PR under review is approved' },
@@ -54,11 +57,37 @@ export const WORKSPACE_FIELDS = {
54
57
  { key: 'code_review', label: 'Review statuses', list: true },
55
58
  { key: 'qa', label: 'Testing statuses', list: true },
56
59
  { key: 'passed', label: 'Passed statuses', list: true },
60
+ { key: 'release', label: 'Release statuses', list: true },
57
61
  { key: 'on_hold', label: 'On hold statuses', list: true },
58
62
  { key: 'backlog', label: 'Backlog statuses', list: true },
59
63
  ],
60
64
  };
61
65
 
66
+ /** The monitor-wide numbers the settings sheet and Claude may edit, with their defaults: how often to poll, how far back the feed reaches, and when a health alert fires. */
67
+ export const GLOBAL_SETTINGS = {
68
+ poll_seconds: { fallback: 60, label: 'Poll every (seconds)', hint: 'how often every workspace is polled' },
69
+ history_days: { fallback: 14, label: 'History (days)', hint: 'how far back the feed and the mentions reach', whole: true },
70
+ stale_minutes: { fallback: DEFAULT_LIMITS.stale_minutes, label: 'Stale after (minutes)', hint: 'alert when no source has answered for this long' },
71
+ failing_polls: { fallback: DEFAULT_LIMITS.failing_polls, label: 'Failing polls in a row', hint: 'alert when one source fails this many polls in a row', whole: true },
72
+ };
73
+
74
+ /** Field violations for a `settings` object: unknown keys, and values that are not positive numbers (whole numbers where the key says so). */
75
+ export const settingsViolations = (settings) => {
76
+ if (!isObject(settings)) {
77
+ return [{ field: 'settings', description: 'settings must be an object.' }];
78
+ }
79
+ return Object.entries(settings).flatMap(([key, value]) => {
80
+ const spec = GLOBAL_SETTINGS[key];
81
+ if (!spec) {
82
+ return [{ field: `settings.${key}`, description: `${key} is not a setting; the settings are ${Object.keys(GLOBAL_SETTINGS).join(', ')}.` }];
83
+ }
84
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || (spec.whole && !Number.isInteger(value))) {
85
+ return [{ field: `settings.${key}`, description: `${key} must be a positive ${spec.whole ? 'whole ' : ''}number.` }];
86
+ }
87
+ return [];
88
+ });
89
+ };
90
+
62
91
  /** The per-platform directory that holds config.json and the state, unless WHOSE_TURN_DATA overrides it. */
63
92
  export const defaultDataDir = () => {
64
93
  if (process.platform === 'darwin') {
@@ -125,14 +154,21 @@ const normaliseTheme = (theme, problems, workspaceId) => {
125
154
  if (!theme) {
126
155
  return null;
127
156
  }
157
+ const backdrop = theme.backdrop === false ? { backdrop: false } : {};
158
+ if (theme.backdrop !== undefined && typeof theme.backdrop !== 'boolean') {
159
+ problems.push(`Workspace "${workspaceId}": theme.backdrop must be true or false; the backdrop stays on.`);
160
+ }
161
+ if (theme.seed === undefined && theme.accent === undefined) {
162
+ return Object.keys(backdrop).length ? backdrop : null;
163
+ }
128
164
  if (!isHex(theme.seed)) {
129
165
  problems.push(`Workspace "${workspaceId}": theme.seed must be a hex colour like #3d7a5c; the theme is ignored.`);
130
- return null;
166
+ return Object.keys(backdrop).length ? backdrop : null;
131
167
  }
132
168
  if (theme.accent !== undefined && !isHex(theme.accent)) {
133
169
  problems.push(`Workspace "${workspaceId}": theme.accent must be a hex colour like #c27a2c; the accent is ignored.`);
134
170
  }
135
- return { seed: theme.seed.toLowerCase(), ...(isHex(theme.accent) ? { accent: theme.accent.toLowerCase() } : {}) };
171
+ return { seed: theme.seed.toLowerCase(), ...(isHex(theme.accent) ? { accent: theme.accent.toLowerCase() } : {}), ...backdrop };
136
172
  };
137
173
 
138
174
  const normaliseLinks = (links) => (Array.isArray(links) ? links : []).map((entry) => {
@@ -158,6 +194,9 @@ const fieldProblems = (workspace, id, index) => {
158
194
  problems.push(violation(`${at}.key_pattern`, `${where}: key_pattern is not a valid regular expression: ${error.message}`));
159
195
  }
160
196
  }
197
+ if (workspace.hidden !== undefined && !HIDDEN_MODES.includes(workspace.hidden)) {
198
+ problems.push(violation(`${at}.hidden`, `${where}: hidden must be one of ${HIDDEN_MODES.join(', ')}, or left out.`));
199
+ }
161
200
  const rules = workspace.rules || {};
162
201
  const backlogChoices = WORKSPACE_FIELDS.rules.find((field) => field.key === 'backlog_rows').choices;
163
202
  if (rules.backlog_rows !== undefined && !backlogChoices.includes(rules.backlog_rows)) {
@@ -209,7 +248,7 @@ const normaliseWorkspace = (workspace, index, problems, warnings) => {
209
248
  if (tableNotes.length) {
210
249
  warnings[id] = tableNotes;
211
250
  }
212
- return {
251
+ return /** @type {any} */ ({
213
252
  id,
214
253
  links: normaliseLinks(workspace.links),
215
254
  name: String(workspace.name || workspace.id || `Workspace ${index + 1}`),
@@ -217,7 +256,8 @@ const normaliseWorkspace = (workspace, index, problems, warnings) => {
217
256
  rules,
218
257
  key_pattern: workspace.key_pattern || DEFAULT_KEY_PATTERN,
219
258
  theme: normaliseTheme(workspace.theme, problems, id),
220
- };
259
+ ...(HIDDEN_MODES.includes(workspace.hidden) ? { hidden: workspace.hidden } : {}),
260
+ });
221
261
  };
222
262
 
223
263
  export const loadConfig = (env = process.env) => {
@@ -226,7 +266,7 @@ export const loadConfig = (env = process.env) => {
226
266
  const raw = readJson(configFile);
227
267
  const problems = [];
228
268
  if (raw === null) {
229
- problems.push(`No workspace config yet: open Settings (the gear) to add a source. It will be written to ${configFile}.`);
269
+ problems.push(`No workspace config yet: add a source in the workspace settings (top right of the workspace). It will be written to ${configFile}.`);
230
270
  } else if (raw.parse_error) {
231
271
  problems.push(`Cannot parse ${configFile}: ${raw.parse_error}`);
232
272
  }
@@ -235,13 +275,17 @@ export const loadConfig = (env = process.env) => {
235
275
  const { workspaces: normalised, problems: shapeProblems, warnings } = normaliseWorkspaces(rawWorkspaces);
236
276
  const warningLines = Object.entries(warnings).flatMap(([id, lines]) => lines.map((line) => `Workspace "${id}": ${line}.`));
237
277
  const workspaces = normalised.filter((workspace) => WORKSPACE_ID.test(workspace.id));
278
+ const health = normaliseHealth(source, problems);
279
+ const pollSeconds = Number(source.poll_seconds || GLOBAL_SETTINGS.poll_seconds.fallback);
280
+ const historyDays = Number(source.history_days || GLOBAL_SETTINGS.history_days.fallback);
238
281
  return {
239
282
  dataDir,
240
283
  configFile,
241
284
  port: resolvePort(dataDir, env),
242
- pollSeconds: Number(source.poll_seconds || 60),
243
- historyDays: Number(source.history_days || 14),
244
- health: normaliseHealth(source, problems),
285
+ pollSeconds,
286
+ historyDays,
287
+ health,
288
+ settings: { poll_seconds: pollSeconds, history_days: historyDays, ...health },
245
289
  feedback: withoutRetiredFeedbackKeys(source.feedback),
246
290
  notifications: normaliseNotifications(source.notifications, problems),
247
291
  workspaces,
@@ -307,6 +351,9 @@ export const normaliseWorkspaces = (rawWorkspaces) => {
307
351
  problems.push(`Workspace "${workspace.id}" has two connectors with the same id; give each an "id".`);
308
352
  }
309
353
  }
354
+ if (workspaces.length && workspaces.every((workspace) => workspace.hidden)) {
355
+ problems.push('Keep at least one workspace in the rail: parked and paused workspaces are out of it.');
356
+ }
310
357
  const violations = problems.map(toViolation);
311
358
  return { workspaces, problems: violations.map((entry) => entry.description), violations, warnings };
312
359
  };
@@ -315,10 +362,17 @@ const RETIRED_FEEDBACK_KEYS = ['workspace', 'team', 'project', 'token_service',
315
362
 
316
363
  const withoutRetiredFeedbackKeys = (feedback) => (isObject(feedback) ? Object.fromEntries(Object.entries(feedback).filter(([key]) => !RETIRED_FEEDBACK_KEYS.includes(key))) : {});
317
364
 
365
+ const storedConfig = (configFile) => {
366
+ const current = readJson(configFile);
367
+ return current && !current.parse_error ? current : {};
368
+ };
369
+
370
+ /** Writes `patch` over the config file's top-level keys and leaves every other key as it is; an unparsable file is replaced, the previous one stays as config.json.bak. */
371
+ const mergeIntoConfig = (configFile, patch) => writeConfig(configFile, { ...storedConfig(configFile), ...patch });
372
+
318
373
  /** Writes the workspace list into the config file; the previous file stays as config.json.bak. */
319
374
  export const saveWorkspaces = (configFile, rawWorkspaces) => {
320
- const current = readJson(configFile);
321
- const source = current && !current.parse_error ? current : {};
375
+ const source = storedConfig(configFile);
322
376
  delete source.github;
323
377
  delete source.jira;
324
378
  delete source.rules;
@@ -332,10 +386,10 @@ export const saveWorkspaces = (configFile, rawWorkspaces) => {
332
386
  };
333
387
 
334
388
  /** Writes the validated notifications block into the config file. */
335
- export const saveNotifications = (configFile, notifications) => {
336
- const current = readJson(configFile);
337
- writeConfig(configFile, { ...(current && !current.parse_error ? current : {}), notifications });
338
- };
389
+ export const saveNotifications = (configFile, notifications) => mergeIntoConfig(configFile, { notifications });
390
+
391
+ /** Writes the validated monitor-wide numbers (poll_seconds, history_days, stale_minutes, failing_polls) into the config file, leaving every other key as it is. */
392
+ export const saveSettings = (configFile, settings) => mergeIntoConfig(configFile, settings);
339
393
 
340
394
  const writeConfig = (configFile, next) => {
341
395
  mkdirSync(dirname(configFile), { recursive: true });
@@ -44,6 +44,7 @@ export const defaultTable = (rules) => [
44
44
  },
45
45
  { when: { 'ticket.status': 'code_review' }, then: { whose: 'reviewer', next: 'Code review' } },
46
46
  { when: { 'ticket.status': 'qa' }, then: { whose: 'them', next: 'Testing' } },
47
+ { when: { 'ticket.status': 'release' }, then: { whose: 'release', next: 'the next release' } },
47
48
  { when: { 'ticket.status': 'on_hold', 'ticket.hold_reason': { present: true } }, then: { whose: 'them', next: 'On hold: {ticket.hold_reason}', hold: true } },
48
49
  { when: { 'ticket.status': 'on_hold' }, then: { whose: 'them', next: 'On hold' } },
49
50
  {
package/lib/errors.mjs CHANGED
@@ -9,7 +9,7 @@ const REASONS = {
9
9
  INVALID_BODY: { code: 400, status: 'INVALID_ARGUMENT', message: 'The request body is not valid JSON.' },
10
10
  INVALID_ARGUMENT: { code: 400, status: 'INVALID_ARGUMENT', message: 'A required value is missing or has the wrong shape.' },
11
11
  INVALID_CONFIG: { code: 400, status: 'INVALID_ARGUMENT', message: 'The workspace list has problems; nothing was saved.' },
12
- NO_WORKSPACE: { code: 412, status: 'FAILED_PRECONDITION', message: 'No workspace is configured; open Settings to add one.' },
12
+ NO_WORKSPACE: { code: 412, status: 'FAILED_PRECONDITION', message: 'No workspace is configured; add one with + in the rail.' },
13
13
  UPSTREAM_FAILED: { code: 502, status: 'UNAVAILABLE', message: 'The tracker did not answer; try again in a minute.' },
14
14
  RATE_LIMITED: { code: 429, status: 'RESOURCE_EXHAUSTED', message: 'The source is rate-limited; try again later.' },
15
15
  OPEN_TOO_SOON: { code: 429, status: 'RESOURCE_EXHAUSTED', message: 'The window was just opened; wait a few seconds before opening it again.' },
package/lib/facts.mjs CHANGED
@@ -4,7 +4,7 @@ import { DAY, oneline, newestFirst } from './text.mjs';
4
4
  export const shortDate = (iso) => new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
5
5
 
6
6
  const CATEGORY_GROUPS = { new: 'todo', indeterminate: 'in_progress', done: 'done' };
7
- const CHAIN_GROUPS = ['backlog', 'passed', 'code_review', 'qa', 'on_hold'];
7
+ const CHAIN_GROUPS = ['backlog', 'passed', 'code_review', 'qa', 'release', 'on_hold'];
8
8
 
9
9
  /** The closed fact dictionary a table row may test or quote, each with its plain words.
10
10
  * Rules: docs/mechanisms/rules-table.md, Facts. */