From 3240f381257bab22ee8f14da1c93339f9da7d905 Mon Sep 17 00:00:00 2001 From: Peer Reynders <17050883+peerreynders@users.noreply.github.com> Date: Fri, 2 Feb 2024 22:52:43 -0500 Subject: [PATCH 01/21] Started Route Design section --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/app.tsx | 8 ++--- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1b2bbc3..e9af701 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,99 @@ It doesn't use a database but stores the notes via the [Unstorage Node.js Filesy The longer term goal is to eventually leverage island routing to maximum effect once it's more stable and documented ([nksaraf](https://github.com/nksaraf) already demonstrated that [capability](https://github.com/solidjs/solid-start/tree/3f086d7660a6e29dea649e80ea5a7d2fc1ff5910/archived_examples/notes) ([live demo](https://notes-server-components.vinxi.workers.dev/)) with a non-standard branch of SolidStart). + +--- + +## Route Design +The original's demo routing is managed by inside a single context ([`route.js`](https://github.com/reactjs/server-components-demo/blob/95fcac10102d20722af60506af3b785b557c5fd7/src/framework/router.js)) managing the following data: +- A content cache +- `location` consisting of: + - `selectedId` + - `isEditing` + - `searchText` + +This triple is used as a key to cache server content for that `location`. + +- `refresh(response)` purges/reinitializes the content cache within a [transition](https://react.dev/reference/react/startTransition); while the next rendering has been initiated with fresh data from the server, the existing UI remains intact, fully capable of interrupting the current render with another state update. + +- `navigate(location)` updates the `location` state within a transition. + +- The `useMutation` hook sends the `payload` associated with `location` to the `endpoint` then using response to `refresh` the content cache. The hook state reflects the status of the fetch (`isSaving`) and stores the last error. + +It needs to be explicitly stated: the RSC demo *does **not** support SSR*. + +Any keys necessary for SSR need to appear in the path. So the path-based routing becomes: + +* `/?search=`**`:searchText`** i.e. `{selectedId: undefined, isEditing: false, searchText?}` +* `/notes/`**`:selectedId`**`?search=`**`:searchText`** i.e. `{selectedId, isEditing: false, searchText?}` +* `/notes/`**`:selectedId`**`\edit?search=`**`:searchText`** i.e. `{selectedId, isEditing: true, searchText?}` + +Note that `:selectedId` and `:searchText` can vary independently. In a typical usage scenario `:selectedId` will come from a `:searchText` search result but once `:selectedId` is referenced in the path, `:searchText` is free to change and return a result that does **not** include `:selectedId`. +Consequently the server functions are separate: +- `getBriefs`: fetches the note briefs that match `:searchText`. +- `getNote`: fetches the details of the `:selectedId` note. + +```TypeScript +// file: src/api.ts +import { action, cache, redirect, revalidate } from '@solidjs/router'; +// … +import { + deleteNote as deleteNt, + getBriefs as getBf, + getNote as getNt, + upsertNote as upsertNt, +} from './server/api'; +// … +import type { NoteBrief, Note } from './types'; +// … +const getBriefs = cache< + (search: string | undefined) => Promise, + Promise +>(async (search: string | undefined) => getBf(search), NAME_GET_BRIEFS); + +const getNote = cache< + (noteId: string) => Promise, + Promise +>(async (noteId: string) => getNt(noteId), NAME_GET_NOTE); +// … +export { getBriefs, getNote, editAction }; +``` + +Both of these functions are wrapped in [`solid-router`](https://github.com/solidjs/solid-router)'s [`cache()`](https://github.com/solidjs/solid-router?tab=readme-ov-file#cache). The page is fully server rendered on initial load but all subsequent updates are purely client rendered. +But the router's `cache()` tracks the currently loaded `:noteId` and `:search` keys; so rather than running **both** `getBriefs` and `getNote` server fetches the router will only use the one whose key has actually changed (or both it both have changed). + +Consequently only the portion of the page that needs to change is updated on the client for `navigate()` even when the path changes. +The `search` parameter affects the content of the `"],bs=["",""];const Ss=e=>E(ws,T(),e.search?`Couldn't find any notes titled "${g(e.search)}"`:"No notes created yet!");function Es(e){if(!(e instanceof HTMLButtonElement&&e.classList.contains("js:c-brief__open")))return;const n=e.closest("div.js\\:c-brief");return n instanceof HTMLDivElement?n.dataset.noteId:void 0}function Rs(){return S?at():at(Intl.DateTimeFormat().resolvedOptions())}function Ts(e){const[n,t]=wn([]);return[async function(){const o=await es(e());return t(yn(o)),n},{initialValue:n}]}function $s(e,n){const t="";return()=>{const r=n();switch(r?.[0]){case void 0:case"delete":return t;case"update":return r[1];case"new":{const o=e();return typeof o=="string"&&o.length>0?o:t}default:return t}}}const Ns=([e,n],t)=>e===t?n:0;function As(e){const n=Qn(),t=()=>n.noteId??"",r=ye(),[o,s]=A({noteId:t(),pathname:r.pathname}),a=I(()=>{const d=o();if(r.pathname!==d.pathname&&d.noteId.length>0)return[d.noteId,1];const f=t();return f.length>0?[f,2]:["",0]}),i=Z(),l=d=>{const f=Es(d.target);if(!f)return;const w=jr(r,f);s({noteId:f,pathname:w.pathname}),i(w.href)},{lastEdit:u}=ms(),c=I($s(t,u)),h=Ct(...Ts(()=>e.searchText)),m=Rs();let y;return qe(()=>{y instanceof HTMLElement&&y.addEventListener("click",l)}),E(vs,T(),g(p(U,{get when(){return h().length},get fallback(){return p(Ss,{get search(){return e.searchText}})},get children(){return E(ys,T(),g(p(pn,{get each(){return h()},children:d=>E(bs,T(),g(p(us,{get noteId(){return d.id},get title(){return d.title},get summary(){return d.summary},get updatedAt(){return d.updatedAt},get active(){return Ns(a(),d.id)},get flushed(){return c()===d.id},format:m})))})))}})))}var xs=["
',"
"];const _s=S?globalThis.ssrSupport.mdToHtml:e=>"client";console.log("YYY",globalThis.ssrSupport,S);const zt=e=>E(xs,T(),e.body?_s(e.body):"");var Cs=["Delete'],Is=["
Preview

',"

","
"];const Ls=e=>typeof e=="string"&&e.length>0?e:void 0;function Yt(e){const n=()=>!!Ls(e.noteId),[t,r]=A(n()?"update":"insert"),[o,s]=A(!1),[a,i]=A(e.initialTitle),l=()=>{const d=a();return d!==void 0?d:e.initialTitle??""},[u,c]=A(e.initialBody),h=()=>{const d=u();return d!==void 0?d:e.initialBody??""},m=ye(),{sendLastEdit:y}=gs();return y(void 0),E(Is,T(),R("action",g(ns,!0),!1),R("value",g(e.noteId,!0)??"",!1),R("value",g(kr(m),!0),!1),R("value",g(t(),!0),!1),R("value",g(l(),!0),!1),R("value",g(h(),!0),!1),R("disabled",o(),!0),g(p(U,{get when(){return n()},get children(){return E(Cs,T(),R("disabled",o(),!0))}})),g(l()),g(p(zt,{get body(){return h()}})))}var Ps=["",""],Os=["

','

",""];const Xt=S?st():st(Intl.DateTimeFormat().resolvedOptions());function ks(e,n){const t=({id:r,title:o,body:s,updatedAt:a})=>{const[i,l]=Xt(a);return{id:r,title:o,body:s,updatedAt:a,updatedISO:l,updated:i}};return function(o){if(o)return t(o);n(qr(e),{replace:!0})}}function Ds(e){const n=(r,o="")=>e.note?.[r]??o;let t;return qe(()=>{Wt(Xt,t)}),[p(te,{get children(){return ne(e.noteId)}}),E(Os,T(),g(n("title")),g(p(pe,{get children(){return E(Ps,T()+R("datetime",g(n("updatedISO"),!0),!1),g(n("updated")))}})),g(p(kt,{kind:"update",children:"Edit"})),g(p(zt,{get body(){return n("body")}})))]}function it(e){const n=()=>e.edit,t=()=>e.params.noteId,r=Z(),o=ks(e.location,r),s=Ct(()=>ts(t()).then(o),{deferStream:!0});return[p(te,{get children(){return ne(n()?`Edit ${t()}`:t())}}),p(U,{get when(){return n()},get fallback(){return p(Ds,{get noteId(){return t()},get note(){return s()}})},get children(){return p(Yt,{get noteId(){return t()},get initialTitle(){return s()?.title},get initialBody(){return s()?.body}})}})]}function Fs(){return[p(te,{get children(){return ne("New Note")}}),p(Yt,{noteId:void 0,initialTitle:"Untitled",initialBody:""})]}var Hs=["Click a note on the left to view something! 🥺'];function qs(){return[p(te,{get children(){return ne()}}),E(Hs,T())]}function Ms(e){e.forEach(n=>{if(!n.attrs.href)return;let t=document.head.querySelector(`link[href="${n.attrs.href}"]`);t||(t=document.createElement("link"),t.setAttribute("rel","preload"),t.setAttribute("as","style"),t.setAttribute("href",n.attrs.href),document.head.appendChild(t))})}var js=" ";const Bs={style:e=>Ee("style",e.attrs,()=>g(e.children),!0),link:e=>Ee("link",e.attrs,void 0,!0),script:e=>e.attrs.src?Ee("script",an(()=>e.attrs,{get id(){return e.key}}),()=>E(js),!0):null};function Fe(e){let{tag:n,attrs:{key:t,...r}={key:void 0},children:o}=e;return Bs[n]({attrs:r,key:t,children:o})}function Us(e,n,t,r="default"){return mn(async()=>{{const s=(await e.import())[r],i=(await n.inputs?.[e.src].assets()).filter(u=>u.tag==="style"||u.attrs.rel==="stylesheet");return typeof window<"u"&&Ms(i),{default:u=>[...i.map(c=>Fe(c)),mt(s,u)]}}})}const B={NORMAL:0,WILDCARD:1,PLACEHOLDER:2};function Ws(e={}){const n={options:e,rootNode:Gt(),staticRoutesMap:{}},t=r=>e.strictTrailingSlash?r:r.replace(/\/$/,"")||"/";if(e.routes)for(const r in e.routes)ct(n,t(r),e.routes[r]);return{ctx:n,lookup:r=>Ks(n,t(r)),insert:(r,o)=>ct(n,t(r),o),remove:r=>zs(n,t(r))}}function Ks(e,n){const t=e.staticRoutesMap[n];if(t)return t.data;const r=n.split("/"),o={};let s=!1,a=null,i=e.rootNode,l=null;for(let u=0;ue.$component));function Gs(e){function n(t,r,o,s){const a=Object.values(t).find(i=>o.startsWith(i.id+"/"));return a?(n(a.children||(a.children=[]),r,o.slice(a.id.length)),t):(t.push({...r,id:o,path:o.replace(/\/\([^)/]+\)/g,"")}),t)}return e.sort((t,r)=>t.path.length-r.path.length).reduce((t,r)=>n(t,r,r.path,r.path),[])}function Vs(e,n){const t=Qs.lookup(e);if(t){const r=t.route[`$${n}`];return r===void 0?void 0:{handler:r,params:t.params}}}function Js(e){return e.$GET||e.$POST||e.$PUT||e.$PATCH||e.$DELETE}const Qs=Ws({routes:Vt.reduce((e,n)=>{if(!Js(n))return e;let t=n.path.replace(/\(.*\)\/?/g,"").replace(/\*([^/]*)/g,(r,o)=>`**:${o}`);if(/:[^/]*\?/g.test(t))throw new Error(`Optional parameters are not supported in API routes: ${t}`);if(e[t])throw new Error(`Duplicate API routes for "${t}" found at "${e[t].route.path}" and "${n.path}"`);return e[t]={route:n},e},{})});function Zs(){function e(t){return{...t,...t.$$route?t.$$route.require().route:void 0,info:{...t.$$route?t.$$route.require().route.info:{},filesystem:!0},component:Us(t.$component,globalThis.MANIFEST.client,globalThis.MANIFEST.ssr),children:t.children?t.children.map(e):void 0}}return Xs.map(e)}function Jt(e){if(S){const n=L();n.response.status=e.code,n.response.statusText=e.text,H(()=>!n.nativeEvent.handled&&(n.response.status=200))}return null}var ea=["',"",'

Page Not Found

'];function ta(){return E(ea,T(),g(p(te,{get children(){return ne("Not Found")}})),g(p(Jt,{code:404})))}var na=["
Solid Notes
",'
',"
"];function ra(e){const[n]=Tt();return p(xr,{get children(){return E(na,T(),g(p(Gr,{})),g(p(kt,{kind:"new",children:"New"})),g(p(Ge,{get children(){return p(As,{get searchText(){return n.search}})}})),g(p(Ge,{get children(){return e.children}})))}})}function oa(){const e=t=>it($e({edit:!0},t)),n=t=>it($e({edit:!1},t));return p(Tr,{root:ra,get children(){return[p(K,{path:"/new",component:Fs}),p(K,{path:"/notes/:noteId/edit",component:e}),p(K,{path:"/notes/:noteId",component:n}),p(K,{path:"/",component:qs}),p(K,{path:"*404",component:ta})]}})}function sa(e){return p(gn,{get fallback(){return p(Jt,{code:500})},get children(){return e.children}})}var aa=["","<\/script>"],ia=["<\/script>"];const ca=E("");function Qt(e,n,t=[]){for(let r=0;r{if(n.router&&n.router.matches){const r=[...n.router.matches];for(;r.length&&(!r[0].info||!r[0].info.filesystem);)r.shift();const o=r.length&&Qt(r,n.routes);if(o)for(let s=0;s[r.attrs.key,r])).values()].filter(r=>r.attrs.rel==="modulepreload"&&!n.assets.find(o=>o.attrs.key===r.attrs.key))}),dt(()=>t.length?t.map(r=>Fe(r)):void 0),p(pe,{get children(){return[ca,p(e.document,{get assets(){return[p(cn,{}),n.assets.map(r=>Fe(r))]},get scripts(){return[E(aa,T(),`window.manifest = ${JSON.stringify(n.manifest)}`),E(ia,T(),R("src",g(globalThis.MANIFEST.client.inputs[globalThis.MANIFEST.client.handler].output.path,!0),!1))]},get children(){return p(ln,{get children(){return p(sa,{get children(){return p(oa,{})}})}})}})]}})}function ua(e){const n=lo(e,"flash");if(!n)return;let t=JSON.parse(n);if(!t||!t.result)return[];const r=[...t.input.slice(0,-1),new Map(t.input[t.input.length-1])];return uo(e,"flash","",{maxAge:0}),{url:t.url,result:t.error?new Error(t.result):t.result,input:r}}async function Zt(e){const n=globalThis.MANIFEST.client;return globalThis.MANIFEST.ssr,e.response.headers.set("Content-Type","text/html"),Object.assign(e,{manifest:await n.json(),assets:[...await n.inputs[n.handler].assets()],router:{submission:ua(e)},routes:Zs(),$islands:new Set})}function da(e,n={}){return On({onRequest:n.onRequest,onBeforeResponse:n.onBeforeResponse,handler:t=>{const r=bo(t);return wt(r,async()=>{const o=Vs(new URL(r.request.url).pathname,r.request.method);if(o){const h=(await o.handler.import())[r.request.method];r.params=o.params||{},C.context={event:r};const m=await h(r);if(m!==void 0)return m;if(r.request.method!=="GET")throw new Error(`API handler for ${r.request.method} "${r.request.url}" did not return a response.`)}const s=await Zt(r);let a={...n};if(a.onCompleteAll){const c=a.onCompleteAll;a.onCompleteAll=h=>{ut(s)(h),c(h)}}else a.onCompleteAll=ut(s);if(a.onCompleteShell){const c=a.onCompleteShell;a.onCompleteShell=h=>{lt(s,t)(),c(h)}}else a.onCompleteShell=lt(s,t);const i=un(()=>(C.context.event=s,e(s)),a);if(s.response&&s.response.headers.get("Location"))return co(r,s.response.headers.get("Location"));const{writable:l,readable:u}=new TransformStream;return i.pipeTo(l),u})}})}function lt(e,n){return()=>{e.response&&e.response.headers.get("Location")&&(Pe(n,302),fo(n,"Location",e.response.headers.get("Location")))}}function ut(e){return({write:n})=>{const t=e.response&&e.response.headers.get("Location");t&&n(` + + + + + + + + + + + + + + + + + + + + + diff --git a/moq/reference/public/style.css b/moq/reference/public/style.css new file mode 100644 index 0000000..34443b7 --- /dev/null +++ b/moq/reference/public/style.css @@ -0,0 +1,704 @@ +/* -------------------------------- CSSRESET --------------------------------*/ +/* CSS Reset adapted from https://dev.to/hankchizljaw/a-modern-css-reset-6p3 */ +/* Box sizing rules */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* Remove default padding */ +ul[class], +ol[class] { + padding: 0; +} + +/* Remove default margin */ +body, +h1, +h2, +h3, +h4, +p, +ul[class], +ol[class], +li, +figure, +figcaption, +blockquote, +dl, +dd { + margin: 0; +} + +/* Set core body defaults */ +body { + min-height: 100vh; + scroll-behavior: smooth; + text-rendering: optimizeSpeed; + line-height: 1.5; +} + +/* Remove list styles on ul, ol elements with a class attribute */ +ul[class], +ol[class] { + list-style: none; +} + +/* A elements that don't have a class get default styles */ +a:not([class]) { + text-decoration-skip-ink: auto; +} + +/* Make images easier to work with */ +img { + max-width: 100%; + display: block; +} + +/* Natural flow and rhythm in articles by default */ +article > * + * { + margin-block-start: 1em; +} + +/* Inherit fonts for inputs and buttons */ +input, +button, +textarea, +select { + font: inherit; +} + +/* Remove all animations and transitions for people that prefer not to see them */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* -------------------------------- /CSSRESET --------------------------------*/ + +:root { + /* Colors */ + --main-border-color: #ddd; + --primary-border: #037dba; + --gray-20: #404346; + --gray-60: #8a8d91; + --gray-70: #bcc0c4; + --gray-80: #c9ccd1; + --gray-90: #e4e6eb; + --gray-95: #f0f2f5; + --gray-100: #f5f7fa; + --primary-blue: #037dba; + --secondary-blue: #0396df; + --tertiary-blue: #c6efff; + --flash-blue: #4cf7ff; + --outline-blue: rgba(4, 164, 244, 0.6); + --navy-blue: #035e8c; + --red-25: #bd0d2a; + --secondary-text: #65676b; + --white: #fff; + --yellow: #fffae1; + + --outline-box-shadow: 0 0 0 2px var(--outline-blue); + --outline-box-shadow-contrast: 0 0 0 2px var(--navy-blue); + + /* Fonts */ + --sans-serif: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, + Ubuntu, Helvetica, sans-serif; + --monospace: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, + monospace; +} + +html { + font-size: 100%; +} + +body { + font-family: var(--sans-serif); + background: var(--gray-100); + font-weight: 400; + line-height: 1.75; +} + +h1, +h2, +h3, +h4, +h5 { + margin: 0; + font-weight: 700; + line-height: 1.3; +} + +h1 { + font-size: 3.052rem; +} +h2 { + font-size: 2.441rem; +} +h3 { + font-size: 1.953rem; +} +h4 { + font-size: 1.563rem; +} +h5 { + font-size: 1.25rem; +} +small, +.text_small { + font-size: 0.8rem; +} +pre, +code { + font-family: var(--monospace); + border-radius: 6px; +} +pre { + background: var(--gray-95); + padding: 12px; + line-height: 1.5; +} +code { + background: var(--yellow); + padding: 0 3px; + font-size: 0.94rem; + word-break: break-word; +} +pre code { + background: none; +} +a { + color: var(--primary-blue); +} + +.text-with-markdown h1, +.text-with-markdown h2, +.text-with-markdown h3, +.text-with-markdown h4, +.text-with-markdown h5 { + margin-block: 2rem 0.7rem; + margin-inline: 0; +} + +.text-with-markdown blockquote { + font-style: italic; + color: var(--gray-20); + border-left: 3px solid var(--gray-80); + padding-left: 10px; +} + +hr { + border: 0; + height: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.3); +} + +/* ---------------------------------------------------------------------------*/ +.main { + display: flex; + height: 100vh; + width: 100%; + overflow: hidden; +} + +.col { + height: 100%; +} +.col:last-child { + flex-grow: 1; +} + +.logo { + height: 20px; + width: 22px; + margin-inline-end: 10px; +} + +.edit-button { + border-radius: 100px; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 6px 20px 8px; + cursor: pointer; + font-weight: 700; + outline-style: none; +} +.edit-button--solid { + background: var(--primary-blue); + color: var(--white); + border: none; + margin-inline-start: 6px; + transition: all 0.2s ease-in-out; +} +.edit-button--solid:hover { + background: var(--secondary-blue); +} +.edit-button--solid:focus { + box-shadow: var(--outline-box-shadow-contrast); +} +.edit-button--outline { + background: var(--white); + color: var(--primary-blue); + border: 1px solid var(--primary-blue); + margin-inline-start: 12px; + transition: all 0.1s ease-in-out; +} +.edit-button--outline:disabled { + opacity: 0.5; +} +.edit-button--outline:hover:not([disabled]) { + background: var(--primary-blue); + color: var(--white); +} +.edit-button--outline:focus { + box-shadow: var(--outline-box-shadow); +} + +ul.notes-list { + padding: 16px 0; +} +.notes-list > li { + padding: 0 16px; +} +.notes-empty { + padding: 16px; +} + +.sidebar { + background: var(--white); + box-shadow: + 0px 8px 24px rgba(0, 0, 0, 0.1), + 0px 2px 2px rgba(0, 0, 0, 0.1); + overflow-y: scroll; + z-index: 1000; + flex-shrink: 0; + max-width: 350px; + min-width: 250px; + width: 30%; +} +.sidebar-header { + letter-spacing: 0.15em; + text-transform: uppercase; + padding: 36px 16px 16px; + display: flex; + align-items: center; +} +.sidebar-menu { + padding: 0 16px 16px; + display: flex; + justify-content: space-between; +} +.sidebar-menu > .search { + position: relative; + flex-grow: 1; +} +.sidebar-note-list-item { + position: relative; + margin-bottom: 12px; + padding: 16px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + max-height: 100px; + transition: max-height 250ms ease-out; + transform: scale(1); +} +.sidebar-note-list-item.note-expanded { + max-height: 300px; + transition: max-height 0.5s ease; +} +.sidebar-note-list-item.flash { + animation-name: flash; + animation-duration: 0.6s; +} + +.sidebar-note-open { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + z-index: 0; + border: none; + border-radius: 6px; + text-align: start; + background: var(--gray-95); + cursor: pointer; + outline-style: none; + color: transparent; + font-size: 0px; +} +.sidebar-note-open:focus { + box-shadow: var(--outline-box-shadow); +} +.sidebar-note-open:hover { + background: var(--gray-90); +} +.sidebar-note-header { + z-index: 1; + max-width: 85%; + pointer-events: none; +} +.sidebar-note-header > strong { + display: block; + font-size: 1.25rem; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sidebar-note-toggle-expand { + z-index: 2; + border-radius: 50%; + height: 24px; + border: 1px solid var(--gray-60); + cursor: pointer; + flex-shrink: 0; + visibility: hidden; + opacity: 0; + cursor: default; + transition: + visibility 0s linear 20ms, + opacity 300ms; + outline-style: none; + /* Firefox user agent styles button padding-inline is only 4px, not 6px */ + padding-left: 6px; + padding-right: 6px; +} +.sidebar-note-toggle-expand:focus { + box-shadow: var(--outline-box-shadow); +} +.sidebar-note-open:hover + .sidebar-note-toggle-expand, +.sidebar-note-open:focus + .sidebar-note-toggle-expand, +.sidebar-note-toggle-expand:hover, +.sidebar-note-toggle-expand:focus { + visibility: visible; + opacity: 1; + transition: + visibility 0s linear 0s, + opacity 300ms; +} +.sidebar-note-toggle-expand img { + width: 10px; + height: 10px; +} + +.sidebar-note-excerpt { + pointer-events: none; + z-index: 2; + flex: 1 1 250px; + color: var(--secondary-text); + position: relative; + animation: slideIn 100ms; +} + +.search input { + padding: 0 16px; + border-radius: 100px; + border: 1px solid var(--gray-90); + width: 100%; + height: 100%; + outline-style: none; +} +.search input:focus { + box-shadow: var(--outline-box-shadow); +} +.search .spinner { + position: absolute; + right: 10px; + top: 10px; +} + +.note-viewer { + display: flex; + align-items: center; + justify-content: center; +} +.note { + background: var(--white); + box-shadow: + 0px 0px 5px rgba(0, 0, 0, 0.1), + 0px 0px 1px rgba(0, 0, 0, 0.1); + border-radius: 8px; + height: 95%; + width: 95%; + min-width: 400px; + padding: 8%; + overflow-y: auto; +} +.note--empty-state { + margin-inline: 20px 20px; +} +.note-text--empty-state { + font-size: 1.5rem; +} +.note-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap-reverse; + margin-inline-start: -12px; +} +.note-menu { + display: flex; + justify-content: space-between; + align-items: center; + flex-grow: 1; +} +.note-title { + line-height: 1.3; + flex-grow: 1; + overflow-wrap: break-word; + margin-inline-start: 12px; +} +.note-updated-at { + color: var(--secondary-text); + white-space: nowrap; + margin-inline-start: 12px; +} +.note-preview { + margin-block-start: 50px; +} + +.note-editor { + background: var(--white); + display: flex; + height: 100%; + width: 100%; + padding: 58px; + overflow-y: auto; +} +.note-editor .label { + margin-bottom: 20px; +} +.note-editor-form { + display: flex; + flex-direction: column; + width: 400px; + flex-shrink: 0; + position: sticky; + top: 0; +} +.note-editor-form input, +.note-editor-form textarea { + background: none; + border: 1px solid var(--gray-70); + border-radius: 2px; + font-family: var(--monospace); + font-size: 0.8rem; + padding: 12px; + outline-style: none; +} +.note-editor-form input:focus, +.note-editor-form textarea:focus { + box-shadow: var(--outline-box-shadow); +} +.note-editor-form input { + height: 44px; + margin-bottom: 16px; +} +.note-editor-form textarea { + height: 100%; + max-width: 400px; +} +.note-editor-menu { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 12px; +} +.note-editor-preview { + margin-inline-start: 40px; + width: 100%; +} +.note-editor-done, +.note-editor-delete { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 100px; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 6px 20px 8px; + cursor: pointer; + font-weight: 700; + margin-inline-start: 12px; + outline-style: none; + transition: all 0.2s ease-in-out; +} +.note-editor-done:disabled, +.note-editor-delete:disabled { + opacity: 0.5; +} +.note-editor-done { + border: none; + background: var(--primary-blue); + color: var(--white); +} +.note-editor-done:focus { + box-shadow: var(--outline-box-shadow-contrast); +} +.note-editor-done:hover:not([disabled]) { + background: var(--secondary-blue); +} +.note-editor-delete { + border: 1px solid var(--red-25); + background: var(--white); + color: var(--red-25); +} +.note-editor-delete:focus { + box-shadow: var(--outline-box-shadow); +} +.note-editor-delete:hover:not([disabled]) { + background: var(--red-25); + color: var(--white); +} +/* Hack to color our svg */ +.note-editor-delete:hover:not([disabled]) img { + filter: grayscale(1) invert(1) brightness(2); +} +.note-editor-done > img { + width: 14px; +} +.note-editor-delete > img { + width: 10px; +} +.note-editor-done > img, +.note-editor-delete > img { + margin-inline-end: 12px; +} +.note-editor-done[disabled], +.note-editor-delete[disabled] { + opacity: 0.5; +} + +.label { + display: inline-block; + border-radius: 100px; + letter-spacing: 0.05em; + text-transform: uppercase; + font-weight: 700; + padding: 4px 14px; +} +.label--preview { + background: rgba(38, 183, 255, 0.15); + color: var(--primary-blue); +} + +.text-with-markdown p { + margin-bottom: 16px; +} +.text-with-markdown img { + width: 100%; +} + +/* https://codepen.io/mandelid/pen/vwKoe */ +.spinner { + display: inline-block; + transition: opacity linear 0.1s 0.2s; + width: 20px; + height: 20px; + border: 3px solid rgba(80, 80, 80, 0.5); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; + opacity: 0; +} +.spinner--active { + opacity: 1; +} + +.skeleton::after { + content: 'Loading...'; +} +.skeleton { + height: 100%; + background-color: #eee; + background-image: linear-gradient(90deg, #eee, #f5f5f5, #eee); + background-size: 200px 100%; + background-repeat: no-repeat; + border-radius: 4px; + display: block; + line-height: 1; + width: 100%; + animation: shimmer 1.2s ease-in-out infinite; + color: transparent; +} +.skeleton:first-of-type { + margin: 0; +} +.skeleton--button { + border-radius: 100px; + padding: 6px 20px 8px; + width: auto; +} +.v-stack + .v-stack { + margin-block-start: 0.8em; +} + +.offscreen { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + width: 1px; + position: absolute; +} + +/* ---------------------------------------------------------------------------*/ +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes shimmer { + 0% { + background-position: -200px 0; + } + 100% { + background-position: calc(200px + 100%) 0; + } +} + +@keyframes slideIn { + 0% { + top: -10px; + opacity: 0; + } + 100% { + top: 0; + opacity: 1; + } +} + +@keyframes flash { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.9; + } + 100% { + transform: scale(1); + opacity: 1; + } +} diff --git a/moq/reference/rollup.config.mjs b/moq/reference/rollup.config.mjs new file mode 100644 index 0000000..b34c38c --- /dev/null +++ b/moq/reference/rollup.config.mjs @@ -0,0 +1,12 @@ +import nodeResolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import terser from '@rollup/plugin-terser'; + +export default { + input: 'src/entry.js', + output: { + file: 'public/main.js', + format: 'iife', + }, + plugins: [nodeResolve(), commonjs(), terser()], +}; diff --git a/moq/reference/seed-data.js b/moq/reference/seed-data.js new file mode 100644 index 0000000..6bc1958 --- /dev/null +++ b/moq/reference/seed-data.js @@ -0,0 +1,128 @@ +// file: seed-data.js +import { nanoid } from 'nanoid'; +import { marked } from 'marked'; +import sanitizeHtml from 'sanitize-html'; + +const optionsSanitized = { + allowedTags: sanitizeHtml.defaults.allowedTags.concat([ + 'img', + 'h1', + 'h2', + 'h3', + ]), + allowedAttributes: Object.assign( + {}, + sanitizeHtml.defaults.allowedAttributes, + { + img: ['alt', 'src'], + } + ), +}; + +/** @param { string } rawHtml */ +const sanitizedFromRaw = (rawHtml) => sanitizeHtml(rawHtml, optionsSanitized); + +const WORD_COUNT = 20; + +const optionsPlain = { + allowedTags: [], + allowedAttributes: {}, +}; + +/** @param { string } rawHtml */ +function excerptFromRaw(rawHtml) { + // - strip tags from HTML + // - only use the first 20 words + const plain = sanitizeHtml(rawHtml, optionsPlain); + const words = plain.trim().split(/\s+/); + const begin = words.slice(0, WORD_COUNT).join(' '); + return words.length > WORD_COUNT ? begin + '…' : begin; +} + +/** + * @param { number } size + * @param { number } low + * @param { number } high + */ +function makeEpochMsBetween(size, low, high) { + const [start, end] = low < high ? [low, high] : [high, low]; + const interval = end - start; + /** @param {number} value */ + const interpolate = (value) => + start + Math.floor((interval * value) / 0xffff_ffff); + const values = new Uint32Array(size); + crypto.getRandomValues(values); + values.sort((a, b) => b - a); + // Borrow Array's `map` to get regular array instead of TypedArray + return /** @type { number[] } */ ( + Array.prototype.map.call(values, interpolate) + ); +} + +/** @param { number } timestamp */ +function startOfYear(timestamp) { + const date = new Date(timestamp); + return new Date(date.getFullYear(), 0, 1).getTime(); +} + +/** typedef { [title: string, body: string] } ContentTuple */ +/** + * @typedef {{ + * id: string, + * title: string, + * body: string, + * excerpt: string, + * html: string, + * createdAt: number, + * updatedAt: number + * }} Note + */ + +/** @param { ContentTuple[] } content */ +function makeCollectContent(content) { + /** + * @param { Note[] } collect + * @param { number } createdAt + * @param { number } index + */ + return function (collected, createdAt, index) { + const [title, body] = content[index]; + const rawHtml = /** @type { string } */ ( + marked.parse(body, { async: false }) + ); + collected.push({ + id: nanoid(), + title, + body, + excerpt: excerptFromRaw(rawHtml), + html: sanitizedFromRaw(rawHtml), + createdAt, + updatedAt: createdAt, + }); + return collected; + }; +} + +/** @type {ContentTuple[]} */ +const content = [ + ['Meeting Notes', 'This is an example note. It contains **Markdown**!'], + [ + 'Make a thing', + `It's very easy to make some words **bold** and other words *italic* with +Markdown. You can even [link to React's website!](https://www.reactjs.org).`, + ], + [ + 'A note with a very long title because sometimes you need more words', + `You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing) +notes in this app! These note live on the server in the \`notes\` folder. + +![This app is powered by React](https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/React_Native_Logo.png/800px-React_Native_Logo.png)`, + ], + ['I wrote this note today', 'It was an excellent note.'], +]; + +const now = Date.now(); +const created = makeEpochMsBetween(content.length, startOfYear(now), now); +const json = JSON.stringify(created.reduce(makeCollectContent(content), [])); + +console.log(json); diff --git a/moq/reference/src/components/app.js b/moq/reference/src/components/app.js new file mode 100644 index 0000000..3b8947a --- /dev/null +++ b/moq/reference/src/components/app.js @@ -0,0 +1,38 @@ +// @ts-check +// file: src/components/app.js +import { contentById } from '../template'; + +/** @param { Document } document */ +function makeDefinition(document) { + const appSource = contentById(document, 'app'); + + /** @param {{ + * newButton: HTMLElement; + * note: HTMLElement; + * noteList: HTMLElement; + * searchField: HTMLElement; + * }} param0 + */ + return function makeApp({ newButton, note, noteList, searchField }) { + const content = /** @type {HTMLElement} */ (appSource.cloneNode(true)); + + const menu = content.querySelector('.sidebar-menu'); + if (!menu) + throw new Error('Failed to find ".sidebar-menu" in "app" content'); + menu.append(searchField); + menu.append(newButton); + + const nav = content.querySelector('nav'); + if (!nav) throw new Error('Failed to find "nav" in "app" content'); + nav.append(noteList); + + const noteView = content.querySelector('.note-viewer'); + if (!noteView) + throw new Error('Failed to find "note-viewer" in "app" content'); + noteView.append(note); + + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/edit-button.js b/moq/reference/src/components/edit-button.js new file mode 100644 index 0000000..44c52b9 --- /dev/null +++ b/moq/reference/src/components/edit-button.js @@ -0,0 +1,35 @@ +// @ts-check +// file: src/components/edit-button.js +import { contentById } from '../template'; + +/** @param { Document } document */ +function makeDefinition(document) { + const buttonSource = contentById(document, 'edit-button'); + + /** + * @param { string } label + * @param { boolean } [isDraft = false] + * @param { boolean } [isDisabled = false] + */ + return function makeEditButton(label, isDraft = false, isDisabled = false) { + const content = /** @type {HTMLElement} */ (buttonSource.cloneNode(true)); + + const child = document.createTextNode(label); + content.appendChild(child); + + if (isDraft) + content.classList.replace('edit-button--solid', 'edit-button--outline'); + + if (isDisabled) content.setAttribute('disabled', ''); + + if (content instanceof HTMLButtonElement) + document.addEventListener( + 'moq-busy', + (event) => (content.disabled = event.detail) + ); + + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/note-edit.js b/moq/reference/src/components/note-edit.js new file mode 100644 index 0000000..9e5c998 --- /dev/null +++ b/moq/reference/src/components/note-edit.js @@ -0,0 +1,68 @@ +// @ts-check +// file: src/components/note-edit.js +import { contentById } from '../template'; + +/** @typedef { import('../internal.d.ts').NotePersist } NotePersist */ +/** @typedef { ReturnType } MakeNotePreview */ + +const INITIAL_TITLE = 'Untitled'; +const INITIAL_BODY = ''; + +/** + * @param { Document } document + * @param { MakeNotePreview } makeNotePreview + */ +function makeDefinition(document, makeNotePreview) { + const editSource = contentById(document, 'note-edit'); + const editSkeletonSource = contentById(document, 'note-edit__skeleton'); + + /** + * @param { NotePersist | undefined } note + * @param { boolean } [isLoading=false] + */ + return function makeNoteEdit(note, isLoading) { + if (isLoading) { + return /** @type {HTMLElement} */ (editSkeletonSource.cloneNode(true)); + } + + const content = /** @type {HTMLElement} */ (editSource.cloneNode(true)); + const titleInput = content.querySelector('#note-title-input'); + if (titleInput instanceof HTMLInputElement) { + titleInput.value = note ? note.title : INITIAL_TITLE; + } + const title = content.querySelector('.note-title'); + if (title instanceof HTMLElement) { + title.append(document.createTextNode(note ? note.title : INITIAL_TITLE)); + title.insertAdjacentElement( + 'afterend', + makeNotePreview(note ? note.html : INITIAL_BODY) + ); + } + + const deleteButton = content.querySelector('.note-editor-delete'); + const doneButton = content.querySelector('.note-editor-done'); + if (note) { + const body = content.querySelector('#note-body-input'); + if (body instanceof HTMLTextAreaElement) body.textContent = note.body; + } + + if ( + doneButton instanceof HTMLButtonElement && + deleteButton instanceof HTMLButtonElement + ) { + if (!note) deleteButton.remove(); + + /** @type {(event: CustomEvent) => void} disabled */ + const listener = note + ? (event) => + void (doneButton.disabled = deleteButton.disabled = event.detail) + : (event) => void (doneButton.disabled = event.detail); + + document.addEventListener('moq-busy', listener); + } + + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/note-list.js b/moq/reference/src/components/note-list.js new file mode 100644 index 0000000..9e86308 --- /dev/null +++ b/moq/reference/src/components/note-list.js @@ -0,0 +1,80 @@ +// @ts-check +// file: src/components/note-list.js +import { contentById, contentItemPairById } from '../template'; +import { makeDefinition as defineSidebarNote } from './sidebar-note'; + +/** @typedef { import('../internal.d.ts').NotePersist } NotePersist */ + +/** + * @param { string | undefined } noteId + * @param { boolean } flush + * @param { string | undefined } nextId + * @returns { (id: string) => Exclude>[1], undefined> } + */ +function makeBriefFlags(noteId, flush, nextId) { + if (noteId) { + const noteFlags = flush ? 3 : 1; + return nextId + ? (id) => (nextId === id ? 2 : noteId === id ? noteFlags : 0) + : (id) => (id === noteId ? noteFlags : 0); + } + return nextId ? (id) => (id === nextId ? 2 : 0) : (_id) => 0; +} + +/** @param { Document } document */ +function makeDefinition(document) { + const makeSidebarNote = defineSidebarNote(document); + const skeletonSource = contentById(document, 'note-list__skeleton'); + const emptySource = contentById(document, 'note-list__empty'); + const [contentSource, itemSource] = contentItemPairById( + document, + 'note-list' + ); + + /** + * @param { NotePersist[] | string | undefined } notes + * @param { string | undefined } noteId + * @param { string | undefined } nextId + * @param { boolean } [loading=false] + * @param { boolean } [flush=false] + */ + return function makeNoteList( + notes, + noteId, + nextId, + loading = false, + flush = false + ) { + if (loading) + return /** @type {HTMLElement} */ (skeletonSource.cloneNode(true)); + + const [list, search] = + typeof notes === 'string' + ? [undefined, notes.length > 0 ? notes : undefined] + : [ + Array.isArray(notes) && notes.length > 0 ? notes : undefined, + undefined, + ]; + + if (!list) { + const content = /** @type {HTMLElement} */ (emptySource.cloneNode(true)); + content.textContent = search + ? `Couldn't find any notes titled "${search}"` + : 'No notes created yet!'; + return content; + } + + const content = /** @type {HTMLElement} */ (contentSource.cloneNode(true)); + const toFlags = makeBriefFlags(noteId, flush, nextId); + for (const info of list) { + const listItem = /** @type {HTMLLIElement} */ ( + itemSource.cloneNode(true) + ); + listItem.append(makeSidebarNote(info, toFlags(info.id))); + content.append(listItem); + } + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/note-preview.js b/moq/reference/src/components/note-preview.js new file mode 100644 index 0000000..cb04b1f --- /dev/null +++ b/moq/reference/src/components/note-preview.js @@ -0,0 +1,20 @@ +// @ts-check +// file: src/components/note-preview.js +import { contentById } from '../template'; + +/** @param { Document } document */ +function makeDefinition(document) { + const previewSource = contentById(document, 'note-preview'); + + /** @param { string } noteHtml */ + return function makeNotePreview(noteHtml) { + const content = /** @type {HTMLElement} */ (previewSource.cloneNode(true)); + const wrapper = content.querySelector('.text-with-markdown'); + if (wrapper instanceof HTMLElement) { + wrapper.innerHTML = noteHtml; + } + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/note.js b/moq/reference/src/components/note.js new file mode 100644 index 0000000..9edf195 --- /dev/null +++ b/moq/reference/src/components/note.js @@ -0,0 +1,72 @@ +// @ts-check +// file: src/components/edit-button.js +import { contentById } from '../template'; +import { makeNoteDateFormat } from '../date-time'; +import { makeDefinition as defineNoteEdit } from './note-edit'; +import { makeDefinition as defineNotePreview } from './note-preview'; + +/** @typedef { import('../internal.d.ts').NotePersist } NotePersist */ +/** @typedef { ReturnType } MakeButton */ + +const updateFormat = makeNoteDateFormat( + Intl.DateTimeFormat().resolvedOptions() +); + +const makeNotePreview = defineNotePreview(document); +const makeNoteEdit = defineNoteEdit(document, makeNotePreview); + +/** @typedef { 0 | 1 | 2 | 3 } NoteFlags */ + +/** @param { NoteFlags } flags */ +const isLoading = (flags) => (flags & 0x2) === 0x2; +/** @param { NoteFlags } flags */ +const isEditing = (flags) => (flags & 0x1) === 0x1; + +/** + * @param { Document } document + * @param { MakeButton } makeButton + */ +function makeDefinition(document, makeButton) { + const noteSource = contentById(document, 'note'); + const noteSkeletonSource = contentById(document, 'note__skeleton'); + const noteNoneSource = contentById(document, 'note__none'); + + /** + * @param { NotePersist | undefined } note + * @param { NoteFlags } flags + */ + return function makeNote(note, flags = 0) { + const loading = isLoading(flags); + if (isEditing(flags)) return makeNoteEdit(note, loading); + + if (loading) + return /** @type {HTMLElement} */ (noteSkeletonSource.cloneNode(true)); + + if (!note) + return /** @type {HTMLElement} */ (noteNoneSource.cloneNode(true)); + + // Note view + const content = /** @type {HTMLElement} */ (noteSource.cloneNode(true)); + const title = content.querySelector('.note-title'); + if (title instanceof HTMLElement) { + const child = document.createTextNode(note.title); + title.appendChild(child); + } + const updated = content.querySelector('.note-updated-at'); + if (updated instanceof HTMLElement) { + const text = updated.firstChild; + if (text instanceof Text) { + const [updatedOn] = updateFormat(note.updatedAt); + text.nodeValue = text.nodeValue + ' ' + updatedOn; + } + } + const menu = content.querySelector('.note-menu'); + if (menu instanceof HTMLElement) { + menu.append(makeButton('Edit')); + } + content.append(makeNotePreview(note.html)); + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/search-field.js b/moq/reference/src/components/search-field.js new file mode 100644 index 0000000..2c9831c --- /dev/null +++ b/moq/reference/src/components/search-field.js @@ -0,0 +1,39 @@ +// @ts-check +// file: src/components/search-field.js +import { contentById } from '../template'; + +/** @param { Document } document */ +function makeDefinition(document) { + const spinnerSource = contentById(document, 'spinner'); + const searchFieldSource = contentById(document, 'search-field'); + + /** + * @param { string | undefined } search + * @param { boolean } [loading = false] + */ + return function makeSearchField(search, loading = false) { + const spinner = /** @type {HTMLElement} */ (spinnerSource.cloneNode(true)); + const content = /** @type {HTMLElement} */ ( + searchFieldSource.cloneNode(true) + ); + + if (search) { + const input = content.querySelector('#sidebar-search-input'); + if (!(input instanceof HTMLInputElement)) + throw new Error( + 'Failed to find "#sidebar-search-input" input in "search-field" content' + ); + input.value = search; + } + + if (loading) { + spinner.classList.add('spinner--active'); + spinner.setAttribute('aria-busy', 'true'); + } + + content.append(spinner); + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/components/sidebar-note.js b/moq/reference/src/components/sidebar-note.js new file mode 100644 index 0000000..44f19c1 --- /dev/null +++ b/moq/reference/src/components/sidebar-note.js @@ -0,0 +1,135 @@ +// @ts-check +// file: src/components/note-list.js +import { contentById } from '../template'; +import { makeBriefDateFormat } from '../date-time'; + +/** @typedef { import('../internal.d.ts').NotePersist } NotePersist */ + +const NAME_FLUSH = 'flash'; + +const updateFormat = makeBriefDateFormat( + Intl.DateTimeFormat().resolvedOptions() +); + +/** @typedef { 0 | 1 | 2 | 3 } BriefFlags */ + +/** @param { BriefFlags } flags */ +const isActive = (flags) => (flags & 0b01) === 0b01; +/** @param { BriefFlags } flags */ +const isNext = (flags) => flags === 2; +/** @param { BriefFlags } flags */ +const isFlush = (flags) => flags === 3; + +/** @param { Document } document */ +function makeDefinition(document) { + const briefSource = contentById(document, 'sidebar-note'); + + const sendBusyStart = () => + document.dispatchEvent(new CustomEvent('moq-busy', { detail: true })); + const sendBusyEnd = () => + document.dispatchEvent(new CustomEvent('moq-busy', { detail: false })); + + /** + * @param { NotePersist } note + * @param { BriefFlags } [flags] + */ + return function makeSidebarNote(note, flags = 0) { + const content = /** @type {HTMLElement} */ (briefSource.cloneNode(true)); + + // fill header fields + const header = content.querySelector('header'); + if (header instanceof HTMLElement) { + const title = content.querySelector('strong'); + if (title instanceof HTMLElement) { + const child = document.createTextNode(note.title); + title.appendChild(child); + } + + const updated = content.querySelector('small'); + if (updated instanceof HTMLElement) { + const [timeOrDate] = updateFormat(note.updatedAt); + const child = document.createTextNode(timeOrDate); + updated.appendChild(child); + } + } + // Fill excerpt + const excerpt = content.querySelector('.sidebar-note-excerpt'); + if (excerpt instanceof HTMLElement) { + const child = document.createTextNode(note.excerpt); + excerpt.replaceChildren(child); + } + + // Expand button & expanded children + const buttonExpand = content.querySelector('.sidebar-note-toggle-expand'); + /** @type { HTMLElement[] } */ + const childrenExpand = []; + if (buttonExpand instanceof HTMLButtonElement) { + const expand = buttonExpand.querySelector('[alt="expand" i]'); + const collapse = buttonExpand.querySelector('[alt="collapse" i]'); + if (expand instanceof HTMLElement) { + childrenExpand[0] = expand; + if (collapse instanceof HTMLElement) { + childrenExpand[1] = collapse; + } + } + } + + if (excerpt && buttonExpand && childrenExpand.length === 2) { + buttonExpand.removeChild(childrenExpand[1]); + content.removeChild(excerpt); + + buttonExpand.addEventListener('click', () => { + if (!excerpt.parentNode) { + content.classList.add('note-expanded'); + content.append(excerpt); + buttonExpand.replaceChild(childrenExpand[1], childrenExpand[0]); + return; + } + buttonExpand.replaceChild(childrenExpand[0], childrenExpand[1]); + content.removeChild(excerpt); + content.classList.remove('note-expanded'); + }); + } + + // Open button + const buttonOpen = content.querySelector('.sidebar-note-open'); + if (buttonOpen instanceof HTMLButtonElement) { + const active = isActive(flags); + const bgColor = isNext(flags) + ? 'var(--gray-80)' + : active + ? 'var(--tertiary-blue)' + : undefined; + const border = active ? '1px solid var(--primary-border)' : undefined; + + if (bgColor) buttonOpen.style.backgroundColor = bgColor; + if (border) buttonOpen.style.border = border; + } + + // add flushing-note-to-server animation + if (isFlush(flags)) { + function startFlush() { + content.classList.add(NAME_FLUSH); + sendBusyStart(); + } + + /** @param { AnimationEvent } event */ + function endListener(event) { + if (event.animationName === NAME_FLUSH) { + content.classList.remove(NAME_FLUSH); + sendBusyEnd(); + + setTimeout(startFlush, 3000); + return; + } + } + + content.addEventListener('animationend', endListener); + startFlush(); + } + + return content; + }; +} + +export { makeDefinition }; diff --git a/moq/reference/src/data.js b/moq/reference/src/data.js new file mode 100644 index 0000000..c74168c --- /dev/null +++ b/moq/reference/src/data.js @@ -0,0 +1,37 @@ +// @ts-check +// file: src/data.js + +/** @typedef { import('./internal.d.ts').NotePersist } NotePersist */ + +const jsonContent = document.getElementById('note-data'); +if (!(jsonContent instanceof HTMLScriptElement)) + throw new Error('Unable to locate "note-data" JSON content.'); +const noteData = /** @type { NotePersist[] } */ (JSON.parse(jsonContent.text)); + +(function () { + if (!Array.isArray(noteData) || noteData.length < 1) return; + + noteData[0].updatedAt = Date.now() - 7200000; // -2hrs +})(); + +/** + * @param { Map} collect + * @param { NotePersist } note + */ +const collectNote = (collect, note) => collect.set(note.id, note); +const noteIndexById = noteData.reduce(collectNote, new Map()); + +/** @param { string } id */ +const noteById = (id) => noteIndexById.get(id); + +/** @param { string | undefined } part */ +function notesWithTitle(part) { + if (!part) return noteData; + + const text = part.toLowerCase(); + /** @param { NotePersist } note */ + const predicate = (note) => note.title.toLowerCase().includes(text); + return noteData.filter(predicate); +} + +export { noteById, notesWithTitle }; diff --git a/moq/reference/src/date-time.js b/moq/reference/src/date-time.js new file mode 100644 index 0000000..dc2e5bf --- /dev/null +++ b/moq/reference/src/date-time.js @@ -0,0 +1,96 @@ +// @ts-check +// file: src/date-time.js + +/** @type { Intl.ResolvedDateTimeFormatOptions } */ +const defaultOptionsBriefDateFormat = { + locale: 'en-GB', + timeZone: 'UTC', + numberingSystem: 'latn', + calendar: 'gregory', + hour12: false, +}; + +/** @typedef { (epochTimestamp: number) => [local: string, utcIso: string] } FormatFn */ + +function makeNoteDateFormat({ + locale, + timeZone, + hour12, +} = defaultOptionsBriefDateFormat) { + const display = Intl.DateTimeFormat(locale, { + timeZone, + hour12, + dateStyle: 'medium', + timeStyle: 'short', + }); + + /** @type { FormatFn } */ + const format = function format(epochTimestamp) { + const dateTime = new Date(epochTimestamp); + return [display.format(dateTime), dateTime.toISOString()]; + }; + return format; +} + +function makeBriefDateFormat({ + locale, + timeZone, + hour12, +} = defaultOptionsBriefDateFormat) { + const dateOnly = Intl.DateTimeFormat(locale, { + timeZone, + hour12, + dateStyle: 'short', + }); + const timeOnly = Intl.DateTimeFormat(locale, { + timeZone, + hour12, + timeStyle: 'short', + }); + let today = new Date(); + + /** @type { FormatFn } */ + const format = function format(epochTimestamp, resetToday = false) { + if (resetToday) today = new Date(); + + const dateTime = new Date(epochTimestamp); + const display = + dateTime.getDate() === today.getDate() && + dateTime.getMonth() === today.getMonth() && + dateTime.getFullYear() === today.getFullYear() + ? timeOnly.format(dateTime) + : dateOnly.format(dateTime); + + return [display, dateTime.toISOString()]; + }; + + return format; +} + +/** + * @param { FormatFn } format + * @param { HTMLElement | undefined } timeAncestor + * @returns void + */ +/* +function localizeFormat( + format, + timeAncestor +) { + if (!(timeAncestor instanceof HTMLElement)) + throw new Error('Unsuitable ancestor element'); + + const time = timeAncestor.querySelector('time'); + if (!(time instanceof HTMLTimeElement)) + throw new Error('Unable to locate time element under specified ancestor'); + + const current = time.textContent; + if (!current) return; + // i.e. nothing to do (CSR waiting for async content) + + const epochTimestamp = Date.parse(time.dateTime); + const [local] = format(epochTimestamp); + if (current !== local) time.textContent = local; +} +*/ +export { /* localizeFormat, */ makeBriefDateFormat, makeNoteDateFormat }; diff --git a/moq/reference/src/entry.js b/moq/reference/src/entry.js new file mode 100644 index 0000000..d464d9e --- /dev/null +++ b/moq/reference/src/entry.js @@ -0,0 +1,54 @@ +// @ts-check +// file: src/entry.js +import { noteById, notesWithTitle } from './data'; +import { urlToParams } from './params'; +import { makeDefinition as defineApp } from './components/app.js'; +import { makeDefinition as defineEditButton } from './components/edit-button.js'; +import { makeDefinition as defineNote } from './components/note.js'; +import { makeDefinition as defineNoteList } from './components/note-list.js'; +import { makeDefinition as defineSearchField } from './components/search-field.js'; + +/** @typedef { import('./internal.d.ts').NotePersist } NotePersist */ + +const makeApp = defineApp(document); +const makeEditButton = defineEditButton(document); +const makeNote = defineNote(document, makeEditButton); +const makeNoteList = defineNoteList(document); +const makeSearchField = defineSearchField(document); + +/** + * @param { boolean } isEditing + * @param { string | undefined } nextId + * @returns { Exclude[1], undefined> } + */ +const toNoteFlags = (isEditing, nextId) => + isEditing ? (nextId ? 3 : 1) : nextId ? 2 : 0; + +/** @param {ReturnType} param0 */ +function assemble({ + flushNote, + isEditing, + listLoading, + nextId, + noteId, + search, +}) { + const notes = listLoading + ? undefined + : ((r) => (r.length > 0 ? r : search))(notesWithTitle(search)); + const note = noteId ? noteById(noteId) : undefined; + + const loading = + listLoading || (typeof nextId === 'string' && nextId.length > 0); + + return makeApp({ + searchField: makeSearchField(search, loading), + newButton: makeEditButton('New'), + noteList: makeNoteList(notes, note?.id, nextId, listLoading, flushNote), + note: makeNote(note, toNoteFlags(isEditing, nextId)), + }); +} + +document.title = 'React Notes'; +const body = /** @type {HTMLBodyElement} */ (document.querySelector('body')); +body.prepend(assemble(urlToParams(new URL(document.location.href)))); diff --git a/moq/reference/src/internal.d.ts b/moq/reference/src/internal.d.ts new file mode 100644 index 0000000..68785c2 --- /dev/null +++ b/moq/reference/src/internal.d.ts @@ -0,0 +1,23 @@ +interface CustomEventMap { + 'moq-busy': CustomEvent; +} + +declare global { + interface Document { + addEventListener( + type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void + ): void; + dispatchEvent(ev: CustomEventMap[K]): void; + } +} + +export type NotePersist = { + id: string; + title: string; + body: string; + excerpt: string; + html: string; + createdAt: number; + updatedAt: number; +}; diff --git a/moq/reference/src/params.js b/moq/reference/src/params.js new file mode 100644 index 0000000..3813449 --- /dev/null +++ b/moq/reference/src/params.js @@ -0,0 +1,29 @@ +// @ts-check +// file: src/params.js + +/** @param { URL } url */ +function urlToParams(url) { + const searchParams = url.searchParams; + const nextNoteId = searchParams.get('moqnextid') ?? undefined; + const locationJson = searchParams.get('location') ?? undefined; + const location = locationJson ? JSON.parse(locationJson) : {}; + + const nextId = nextNoteId && nextNoteId.length > 0 ? nextNoteId : undefined; + const noteId = + typeof location.selectedId === 'string' ? location.selectedId : undefined; + + return { + flushNote: searchParams.has('moqflush'), + isEditing: + typeof location.isEditing === 'boolean' ? location.isEditing : false, + listLoading: searchParams.has('moqlistload'), + nextId: nextId && nextId !== noteId ? nextId : undefined, + noteId, + search: + typeof location.searchText === 'string' && location.searchText.length > 0 + ? location.searchText + : undefined, + }; +} + +export { urlToParams }; diff --git a/moq/reference/src/template.js b/moq/reference/src/template.js new file mode 100644 index 0000000..8624a1c --- /dev/null +++ b/moq/reference/src/template.js @@ -0,0 +1,34 @@ +// @ts-check +// file: src/template.js +/** + * @param { Document } document + * @param { string } id + */ +function contentById(document, id) { + const template = document.querySelector(`#${id}`); + if (!(template instanceof HTMLTemplateElement)) + throw new Error(`Can't find template for ID "${id}"`); + + const content = template.content.firstElementChild; + if (!(content instanceof HTMLElement)) + throw new Error(`Invalid root for template ID "${id}"`); + + return content; +} + +/** + * @param { Document } document + * @param { string } id + */ +function contentItemPairById(document, id) { + const content = contentById(document, id); + + const item = content.firstElementChild; + if (!(item instanceof HTMLLIElement)) + throw new Error(`Template ID "${id}" does not contain list item`); + + content.removeChild(item); + return [content, item]; +} + +export { contentById, contentItemPairById }; diff --git a/moq/reference/src/tsconfig.json b/moq/reference/src/tsconfig.json new file mode 100644 index 0000000..e81ec1f --- /dev/null +++ b/moq/reference/src/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "files": ["entry.js"] +} diff --git a/moq/reference/tsconfig.base.json b/moq/reference/tsconfig.base.json new file mode 100644 index 0000000..75d9d30 --- /dev/null +++ b/moq/reference/tsconfig.base.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "skipLibCheck": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "checkJs": true, + "allowJs": true, + "declaration": true, + "outDir": "types" + } +} diff --git a/moq/reference/tsconfig.json b/moq/reference/tsconfig.json new file mode 100644 index 0000000..10abe41 --- /dev/null +++ b/moq/reference/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowSyntheticDefaultImports": true + }, + "files": ["index.js"] +} diff --git a/src/server/seed.ts b/src/server/seed.ts index 7dbb9c4..a8ca76e 100644 --- a/src/server/seed.ts +++ b/src/server/seed.ts @@ -20,8 +20,7 @@ Markdown. You can even [link to SolidStart's website!](https://start.solidjs.com ], [ 'A note with a very long title because sometimes you need more words', - `You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing) -notes in this app! These notes live on the server in the \`.data/notes\` file. + `You can write all kinds of [amazing](https://en.wikipedia.org/wiki/The_Amazing) notes in this app! These notes live on the server in the \`.data/notes\` file. ![This app is powered by SolidStart](https://assets.solidjs.com/banner?project=Start&type=core)`, ], ['I wrote this note today', 'It was an excellent note.'], From ea9ed12a4b642e5295537f49b2613d460c21e6be Mon Sep 17 00:00:00 2001 From: Peer Reynders <17050883+peerreynders@users.noreply.github.com> Date: Thu, 7 Mar 2024 00:09:24 -0500 Subject: [PATCH 20/21] Added moq --- moq/.gitignore | 9 + moq/.prettierignore | 4 + moq/.prettierrc.json | 9 + moq/index.js | 41 ++ moq/package.json | 36 ++ moq/pnpm-lock.yaml | 752 +++++++++++++++++++++++++++++ moq/public/checkmark.svg | 3 + moq/public/cross.svg | 3 + moq/public/favicon.ico | Bin 0 -> 15406 bytes moq/public/logo.svg | 1 + moq/public/main.js | 1 + moq/public/map.html | 45 ++ moq/public/moq-page.html | 149 ++++++ moq/public/style.css | 674 ++++++++++++++++++++++++++ moq/reference/README.md | 2 +- moq/reference/public/main.js | 361 +++++++++++++- moq/rollup.config.mjs | 12 + moq/seed-data.js | 126 +++++ moq/src/components/app.js | 36 ++ moq/src/components/brief-list.js | 61 +++ moq/src/components/brief.js | 120 +++++ moq/src/components/edit-button.js | 30 ++ moq/src/components/note-edit.js | 64 +++ moq/src/components/note-preview.js | 20 + moq/src/components/note.js | 61 +++ moq/src/components/search-field.js | 37 ++ moq/src/data.js | 37 ++ moq/src/date-time.js | 96 ++++ moq/src/entry.js | 37 ++ moq/src/event-bus.js | 13 + moq/src/internal.d.ts | 23 + moq/src/params.js | 80 +++ moq/src/template.js | 34 ++ moq/src/tsconfig.json | 8 + moq/tsconfig.base.json | 17 + moq/tsconfig.json | 9 + 36 files changed, 3009 insertions(+), 2 deletions(-) create mode 100644 moq/.gitignore create mode 100644 moq/.prettierignore create mode 100644 moq/.prettierrc.json create mode 100644 moq/index.js create mode 100644 moq/package.json create mode 100644 moq/pnpm-lock.yaml create mode 100644 moq/public/checkmark.svg create mode 100644 moq/public/cross.svg create mode 100644 moq/public/favicon.ico create mode 100644 moq/public/logo.svg create mode 100644 moq/public/main.js create mode 100644 moq/public/map.html create mode 100644 moq/public/moq-page.html create mode 100644 moq/public/style.css create mode 100644 moq/rollup.config.mjs create mode 100644 moq/seed-data.js create mode 100644 moq/src/components/app.js create mode 100644 moq/src/components/brief-list.js create mode 100644 moq/src/components/brief.js create mode 100644 moq/src/components/edit-button.js create mode 100644 moq/src/components/note-edit.js create mode 100644 moq/src/components/note-preview.js create mode 100644 moq/src/components/note.js create mode 100644 moq/src/components/search-field.js create mode 100644 moq/src/data.js create mode 100644 moq/src/date-time.js create mode 100644 moq/src/entry.js create mode 100644 moq/src/event-bus.js create mode 100644 moq/src/internal.d.ts create mode 100644 moq/src/params.js create mode 100644 moq/src/template.js create mode 100644 moq/src/tsconfig.json create mode 100644 moq/tsconfig.base.json create mode 100644 moq/tsconfig.json diff --git a/moq/.gitignore b/moq/.gitignore new file mode 100644 index 0000000..1dcbbc1 --- /dev/null +++ b/moq/.gitignore @@ -0,0 +1,9 @@ +# dependencies +/node_modules + +# Temp +gitignore + +# System Files +.DS_Store + diff --git a/moq/.prettierignore b/moq/.prettierignore new file mode 100644 index 0000000..7ca48a3 --- /dev/null +++ b/moq/.prettierignore @@ -0,0 +1,4 @@ +package-lock.json +pnpm-lock.yaml + +public/main.js diff --git a/moq/.prettierrc.json b/moq/.prettierrc.json new file mode 100644 index 0000000..2e97b3b --- /dev/null +++ b/moq/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": true +} diff --git a/moq/index.js b/moq/index.js new file mode 100644 index 0000000..0cc5614 --- /dev/null +++ b/moq/index.js @@ -0,0 +1,41 @@ +// @ts-check +// file: index.js +import polka from 'polka'; +import sirv from 'sirv'; +import { parse } from 'regexparam'; + +/** @typedef { import('node:fs').Stats } Stats */ +/** @typedef { import('node:http').ServerResponse } ServerResponse */ + +const pathNoStore = ['/', '/map(.html)?', '/main.js', '/style.css']; +const patternNoStore = pathNoStore.map((uriTemplate) => parse(uriTemplate)); + +/** + * @param { ServerResponse } res + * @param { string } pathname + * @param { Stats } _stats + */ +function setHeaders(res, pathname, _stats) { + const value = patternNoStore.some((route) => route.pattern.test(pathname)) + ? 'no-store' + : 'public,max-age=31536000,immutable'; + res.setHeader('Cache-Control', value); +} + +const assets = sirv('public', { + single: 'moq-page.html', + setHeaders, +}); + +const port = 3030; + +polka() + .use(assets) + .listen(port, () => { + console.log(`> Running on localhost:${port}`); + }); + +/* + * http://localhost:3030/map (map.html) shows the links to the mapped out variations + * for everything else page.html does the work + */ diff --git a/moq/package.json b/moq/package.json new file mode 100644 index 0000000..e96a6dc --- /dev/null +++ b/moq/package.json @@ -0,0 +1,36 @@ +{ + "name": "reference", + "version": "0.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "seed": "node seed-data.js", + "serve": "node index.js", + "build": "./node_modules/.bin/rollup -c rollup.config.mjs", + "format": "prettier . --write", + "lint:types": "./node_modules/.bin/tsc --noEmit -p src/tsconfig.json", + "server:types": "./node_modules/.bin/tsc --noEmit -p tsconfig.json" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "polka": "^0.5.2", + "regexparam": "^3.0.0", + "sirv": "^2.0.4" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "@types/node": "^20.11.24", + "@types/polka": "^0.5.7", + "@types/sanitize-html": "^2.11.0", + "marked": "^12.0.1", + "prettier": "^3.2.5", + "rollup": "^4.12.1", + "sanitize-html": "^2.12.1", + "typescript": "^5.3.3" + } +} diff --git a/moq/pnpm-lock.yaml b/moq/pnpm-lock.yaml new file mode 100644 index 0000000..c2d60c6 --- /dev/null +++ b/moq/pnpm-lock.yaml @@ -0,0 +1,752 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + polka: + specifier: ^0.5.2 + version: 0.5.2 + regexparam: + specifier: ^3.0.0 + version: 3.0.0 + sirv: + specifier: ^2.0.4 + version: 2.0.4 + +devDependencies: + '@rollup/plugin-commonjs': + specifier: ^25.0.7 + version: 25.0.7(rollup@4.12.1) + '@rollup/plugin-node-resolve': + specifier: ^15.2.3 + version: 15.2.3(rollup@4.12.1) + '@rollup/plugin-terser': + specifier: ^0.4.4 + version: 0.4.4(rollup@4.12.1) + '@types/node': + specifier: ^20.11.24 + version: 20.11.24 + '@types/polka': + specifier: ^0.5.7 + version: 0.5.7 + '@types/sanitize-html': + specifier: ^2.11.0 + version: 2.11.0 + marked: + specifier: ^12.0.1 + version: 12.0.1 + prettier: + specifier: ^3.2.5 + version: 3.2.5 + rollup: + specifier: ^4.12.1 + version: 4.12.1 + sanitize-html: + specifier: ^2.12.1 + version: 2.12.1 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + /@arr/every@1.0.1: + resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} + engines: {node: '>=4'} + dev: false + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/source-map@0.3.5: + resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@polka/url@0.5.0: + resolution: {integrity: sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==} + dev: false + + /@polka/url@1.0.0-next.24: + resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} + dev: false + + /@rollup/plugin-commonjs@25.0.7(rollup@4.12.1): + resolution: {integrity: sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.12.1) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.30.8 + rollup: 4.12.1 + dev: true + + /@rollup/plugin-node-resolve@15.2.3(rollup@4.12.1): + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.12.1) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.8 + rollup: 4.12.1 + dev: true + + /@rollup/plugin-terser@0.4.4(rollup@4.12.1): + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + rollup: 4.12.1 + serialize-javascript: 6.0.2 + smob: 1.4.1 + terser: 5.29.1 + dev: true + + /@rollup/pluginutils@5.1.0(rollup@4.12.1): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 4.12.1 + dev: true + + /@rollup/rollup-android-arm-eabi@4.12.1: + resolution: {integrity: sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.12.1: + resolution: {integrity: sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.12.1: + resolution: {integrity: sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.12.1: + resolution: {integrity: sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.12.1: + resolution: {integrity: sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.12.1: + resolution: {integrity: sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.12.1: + resolution: {integrity: sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.12.1: + resolution: {integrity: sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.12.1: + resolution: {integrity: sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.12.1: + resolution: {integrity: sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.12.1: + resolution: {integrity: sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.12.1: + resolution: {integrity: sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.12.1: + resolution: {integrity: sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.11.24 + dev: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.11.24 + dev: true + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/express-serve-static-core@4.17.43: + resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} + dependencies: + '@types/node': 20.11.24 + '@types/qs': 6.9.12 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.43 + '@types/qs': 6.9.12 + '@types/serve-static': 1.15.5 + dev: true + + /@types/http-errors@2.0.4: + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + dev: true + + /@types/mime@1.3.5: + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + dev: true + + /@types/mime@3.0.4: + resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} + dev: true + + /@types/node@20.11.24: + resolution: {integrity: sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/polka@0.5.7: + resolution: {integrity: sha512-TH8CDXM8zoskPCNmWabtK7ziGv9Q21s4hMZLVYK5HFEfqmGXBqq/Wgi7jNELWXftZK/1J/9CezYa06x1RKeQ+g==} + dependencies: + '@types/express': 4.17.21 + '@types/express-serve-static-core': 4.17.43 + '@types/node': 20.11.24 + '@types/trouter': 3.1.4 + dev: true + + /@types/qs@6.9.12: + resolution: {integrity: sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==} + dev: true + + /@types/range-parser@1.2.7: + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + dev: true + + /@types/resolve@1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + dev: true + + /@types/sanitize-html@2.11.0: + resolution: {integrity: sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==} + dependencies: + htmlparser2: 8.0.2 + dev: true + + /@types/send@0.17.4: + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.11.24 + dev: true + + /@types/serve-static@1.15.5: + resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} + dependencies: + '@types/http-errors': 2.0.4 + '@types/mime': 3.0.4 + '@types/node': 20.11.24 + dev: true + + /@types/trouter@3.1.4: + resolution: {integrity: sha512-4YIL/2AvvZqKBWenjvEpxpblT2KGO6793ipr5QS7/6DpQ3O3SwZGgNGWezxf3pzeYZc24a2pJIrR/+Jxh/wYNQ==} + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: true + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: true + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /hasown@2.0.1: + resolution: {integrity: sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: true + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.1 + dev: true + + /is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: true + + /is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.5 + dev: true + + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /marked@12.0.1: + resolution: {integrity: sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==} + engines: {node: '>= 18'} + hasBin: true + dev: true + + /matchit@1.1.0: + resolution: {integrity: sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==} + engines: {node: '>=6'} + dependencies: + '@arr/every': 1.0.1 + dev: false + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /polka@0.5.2: + resolution: {integrity: sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==} + dependencies: + '@polka/url': 0.5.0 + trouter: 2.0.1 + dev: false + + /postcss@8.4.35: + resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /regexparam@3.0.0: + resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} + engines: {node: '>=8'} + dev: false + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /rollup@4.12.1: + resolution: {integrity: sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.12.1 + '@rollup/rollup-android-arm64': 4.12.1 + '@rollup/rollup-darwin-arm64': 4.12.1 + '@rollup/rollup-darwin-x64': 4.12.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.12.1 + '@rollup/rollup-linux-arm64-gnu': 4.12.1 + '@rollup/rollup-linux-arm64-musl': 4.12.1 + '@rollup/rollup-linux-riscv64-gnu': 4.12.1 + '@rollup/rollup-linux-x64-gnu': 4.12.1 + '@rollup/rollup-linux-x64-musl': 4.12.1 + '@rollup/rollup-win32-arm64-msvc': 4.12.1 + '@rollup/rollup-win32-ia32-msvc': 4.12.1 + '@rollup/rollup-win32-x64-msvc': 4.12.1 + fsevents: 2.3.3 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /sanitize-html@2.12.1: + resolution: {integrity: sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==} + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.35 + dev: true + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.24 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: false + + /smob@1.4.1: + resolution: {integrity: sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /terser@5.29.1: + resolution: {integrity: sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.5 + acorn: 8.11.3 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: false + + /trouter@2.0.1: + resolution: {integrity: sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==} + engines: {node: '>=6'} + dependencies: + matchit: 1.1.0 + dev: false + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true diff --git a/moq/public/checkmark.svg b/moq/public/checkmark.svg new file mode 100644 index 0000000..fde2dfb --- /dev/null +++ b/moq/public/checkmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/moq/public/cross.svg b/moq/public/cross.svg new file mode 100644 index 0000000..3a10858 --- /dev/null +++ b/moq/public/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/moq/public/favicon.ico b/moq/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d80eeb8413f5ddb4e63c7fc002cb71714abcc1ed GIT binary patch literal 15406 zcmeI33A9vo9moI8z4OpE&C>Ry#YEdElT0*8%F;B`qiH*#$B@iSg>2DWOH)E9G-7#g z?!EIM^k`z3=0X^135q622}-CKAn(0d?#z1+!Rhn;&kPLnX1VjkvgMugJ9FpGfBEg( z|No46nQ_eSyBj`-nsKf%BaAUahZesNdyO%(c{hA`>GyXVbMKzU3}Xyam{EMsN6o*i zXWm=5#SE$WV}zF(FZR7Kh)qrh=D?nIvwLB>iAR{WZAciH(V_4BBMh7kxxl$O=f^I~ z1+j72ggL3tGoMN)%!shvyrFQRiEW!y@lZ&b*D!xU7`V>^1^el{n|YTBlg>ugeKPdj zhrsbC`Gk3M#nf$&IWKk^V{)bWTBE@CO|0<;;ErtI+>{BNcIX>bn1?W`{&K!~BXgGR zFh6iJ?0qZHo=rM4LeH7Snv+=PxPoV1QMm-PwPl0Y1^m9XY7NnpXa5xI2ztV#`?sv; zoXi}gzWq;f%=+$M_`S>r(Xlz_J2z(i*cs3`4q2U+@yr-xb>sjXpeyuZXE7(N!r?i1 zu!+5&=5r%F?c#GSYfW6!Zg$y@fiVT-&$wSRe@oRG;zcJkPASRvB4{{q<23Wi%K3T@ z6XyN!;ZMx3ezzVuGwzGbUx;ppCHYD| zqOF_nXP3v;`;^{mpFDa4&!s~TvgQWnZ)VLk_2$)jueEw+n!U5W8HGNM3H{j5YK?2< zo&3yDU=$$#RPQg-X?$1Ujd?NO%VzSO?jgB3=1u<&9MG9>VxO1cht5Nuhj-03 zZ>_eU7nA-}!u5a*?S#& zo0TugVdpt~V>TH(GwgIfolBSx_Il>+VW)Y$bRPSDt^B*@3P0JCVpZY9fj+ny?^aDU zLnH(2=YDMGIpBHhIR_pa;K^!mdzj}X__sU3?>fFG@P|{Fb7?OzF53}1opGPf1ZJe- z2)=F?@v0!TkMO6skt0U3#x&ONW&dKFDaWj0{E~0zxBlMp$63qw$AIPFy@dCR6pL76 zI(l?Y#y1D2;E()OGZcg;IJ`3BJEubDQ>-Z-RN_$kS@EP8Q-oU)e7BG1zp~bJ;>j_E zS>{zyaggq#1Giuc#Ds?XKM&1{8~8eBJ#p+G7SJEQuZ0hf6Hk`m6Mxb@-MkH1><|y@ z@uQEJL4NTAc<^+Ip4xQ7A7YK-%q;l*bH+~L`;?6DUJmakqO)gYlFlizE9tBBD+&+R zUuj-}+zti*`&cvIfCpRHcQrh|DdV|U!LN&f3)8-PzG5%Bc}~uAe;5Ywv*62TQ@;6- zbfF&ImG9Btw?Y5R5)bOpFZ&=DaMpmU_;6P~>D*BP*ok`|uo!*53A>pIUZ-U{%zNh5 z$dM}dFU&N1SUn8fKR2LXcx~6YV$4+Btc3FKdWYS(_lO_XJ{#!6a`G*F-oDV3WS(qM z>(t|e-fu-0iQiTiu^rh@5uly?Qg-S*0o-qO1)Y$sRnlDfEL+^j?`iTaWcmQ>H?&3Q z!e6>~0^e26l24EelpyCh$B;990$+JZI%!5^=bHWGJDcU<@<*u-a{w`5GV&$2+#%np z=0!g`wwiG_Wc=79?B^o#xC;g3mKS4(m&y(^lwM_>KZLQAu~@v*iK#AGn@q z^{YDn9w-mVC{xnezv7ct3gG`L-bo%CY`%|Oqm%c7&-X&#e4r5y!bkBhm&A^&3A-!AY}!N`MY*WW6QG=+tnAd3__lAUO$7#xFq5-K^V87dJyU zb&Y#JxZD#aV@vTz)E>@V?EQNSs)tl>c+OX>9Gc`@T~p1giA_hd#x2m+%pR~ccfD z02xdj0KR**)k}1%Z?1V~l$^%K=S2L2B|rN&bV4$&mVf1U$w^S@kBZMp;cso~cKJEt z({5#28+{fQ8aGVFhqL!3d8^x27L|L}d)^`+f!_77uWB{Hv+(C>_WvdEX5U8pR{o~E zCez+FEFZWNp=~Mpu(?EIy}k8bar&-|=XluuHNZ6aLHxoc=;N2Vsp%^BX!eXO+S2W2 zA8O^XtaBY}tKO-r6X-%Ocs`0ACh=z%qA&E7kUCFa8mH&$-}m8@Q++fIqf&BE8>{ zFXW6R8(PGergd%xpOUfK5E>EaWL@cnDhN)3c)=5<3SOos^m59-5uo zwjVM1Eopq0dipj7s86S!h-3v=nNjO31thzL$|=h7+llt2HH&qv^9ypp<>08C&E^Bt z7Ss*#wd6+RwaxnF|0s!Pe6KSy0e{%1bfn=~KqrEin= z3^|Y)E}KFZhpKK-UZQ<>hJ2*Y+1X})Y~cIw{WgHJv28V>@+@j`dsdYy$wvLLXBf8* zSWSNRV7}9l?%mGs^Yfk)P?P;CuO61~{xq9#&sX0;xpk(Kdd-i2C_iV{w^|)sifl?U z87wW*U-j6@eUG76Hi^7wB{&bv4~zL?IZrFfC+YSxR&}(_?bIh;1I~|1??MkdqTa=> z?qtkw`RzK!&F1+kbn!fB{W{}5Ne^V7?%U071!x%z4e}%EGxtii(uKfT%(|)t>(Nk| z_lgdie=~=(H@A-*IM1`r7DHR3sZUU>)4EvuSt~osBjlB&ZpEh1z4xkSftQ0iE0K?v z9jY(Hdh^&DUN-DCR?^VwvuMt<{yOA+KfW)4ex0d2z~(W7j5o3a*%`9E3jV1lpmR9w z6&*j-1HRQM^;z?>8|vDy4B%XniF!G1w`>jkSMk|Ne*YQUyJ^tFYNlWE@5JD7$apR? zS1-R6{GSIW>Uqv|bzbQ^=sh}83BL0?kGC$8uf4v{dSru%@AAv?(doeXGJKLR6+hL_h@wOA-`17X(6)|(uP*(~ zVjrH*QZHV868gA0PfNznM7M~G@$vM@kEa*@F=FZa^7G7_pZb~EwH}}Ay_YXl+$6S+ zMERr~xx%xeQG;A~np3j2!YUV$}+-3OG z2WkPf{xIVoWy}hGm(RExx^5NSp&$Qtiu2A5P5Q9|@FyPFGf83r>$C%FBI94ANA@C+ zFWL$XbwR(ELF1UCSc1MfPhvy!d7h|vU64&g%r|5=%FA?*23woJ8V|t};S)^;1ILx= ziTsKDtRF*{ywHn}+3d$Zpng&{uj_qR4@_rlJ)C`z)7{E?4ByS!Ol&FR7vq~av&441_$m20@sB9)tA}s(d)a_|5%+EmV%^E?&AQy9 zX*C}B_IyBd=b|6yXA|*nfX`_Iz>l8_ufK+^eF5B0;GFVUa`vNolJ3X)IUlmMp!zf| z%Z{NlNr^)F60{NVh|>E?Y8 z{MT8b&aXA54CH%G3OPN&ySuRME2y;&vvPp9we+UeJ93k~z;`tHH3b|4%$WN#qDgktSIJy6b@zfa0=z>PqnEeg=0}xUa^$`^vyO-FsMw9jPaB zrTQUVLEHZJ{t@|3EqrUeQ*P8xj!2z60e;*JJjuGU#N)Bk{?#Iq$&o5^>+>t%WBL7?Rr}!h~Q$3vh;*Z87hr8KN zwj4D-l|Pc*lOvFmc=5SBpQn3*#K^7 zFlSWixO1(nv zK&e*OIdd!gmriNCXh{3!UC92c^5yVwCHO?G_sjlNGNaGrtY@9l-6ZLMZD4)w@N(Y_ zy+uYBWjdVCs(+?4lv=vp=$-6fEpaVuj}Pa*!C3CUB+-lI!UvmcEiROOuI4oYbgpPO z&Xd@)Z228gR zKkqU&p8M_seERY9`1Y>Gt(EVJ&AE>F0r-!x&@dDJ>&#g8P^s@xpH=^7S?h6h{z2>t zziIn=>g|O*n?;^+2z08w>{ z^IvB@_$>nWK>mTXu3(+Z;r)-LGr;+_Znk}7b8t6(uWFnI^Ifqd>q(a!`nU8he0M&) ze2o3LuVejd6dD*~d&lHQ&aLWUq!aGN+{K#+&flcJd5UWM{2ce-O~j!*_j%PX8VtVG zbJX|KnXb;5^FjO|dOD|r3q7RRBk+;`ld!t0+OUx>iALQe)!Fz8@V^lrOab=`?0ws$ zGbZghN2!lpaW^~C9m;Ba>wQ=MUwMY@Ivavt0Ng|=V7n=|mChc~-tRS(Bz?`0FJKZ`YVd;~H$8yx1q(?`Is3t8x1zx;o_ zyhCyj9r8oEqp(>t6=f*@>D-nMxU=HLKPYQ@@P@wjvpF`f{oCMB4#Yd9=;iq(7 zRqPdI#ADvcMjFv%@ADx~-65O_FMbdemlrb5)~&<}_5hp5Hq=vFgulK1h2Yi7T&q($E25St+<-iSw)YkN3B6qI`CETO z%z_qj5$Y{!4CqvSlY&m_G8>c2^5I-gcV^rbt>Cb;#>zIj=b7DfZ&-I1RGZVgKLVV; zpgI>lzmn%p@V|q+XbHILj6i(=$wxHEwz+@j_UtU2Ug$c?ZRJlgvxe*oexntyu!$cc zkIBljk \ No newline at end of file diff --git a/moq/public/main.js b/moq/public/main.js new file mode 100644 index 0000000..ff62cc2 --- /dev/null +++ b/moq/public/main.js @@ -0,0 +1 @@ +!function(){"use strict";const e=document.getElementById("note-data");if(!(e instanceof HTMLScriptElement))throw new Error('Unable to locate "note-data" JSON content.');const t=JSON.parse(e.text);!Array.isArray(t)||t.length<1||(t[0].updatedAt=Date.now()-72e5);const n=t.reduce(((e,t)=>e.set(t.id,t)),new Map);const o="SolidStart Notes",i=e=>e?`${e} - ${o}`:o;function r(e){throw new Error(`Invalid path "${e}"`)}function c(e,t){const n=e.querySelector(`#${t}`);if(!(n instanceof HTMLTemplateElement))throw new Error(`Can't find template for ID "${t}"`);const o=n.content.firstElementChild;if(!(o instanceof HTMLElement))throw new Error(`Invalid root for template ID "${t}"`);return o}function d(e){document.addEventListener("moq-busy",(t=>e(t.detail)))}const s=e=>document.dispatchEvent(new CustomEvent("moq-busy",{detail:e}));const l={locale:"en-GB",timeZone:"UTC",numberingSystem:"latn",calendar:"gregory",hour12:!1};const a="Untitled",u="";const f=function({locale:e,timeZone:t,hour12:n}=l){const o=Intl.DateTimeFormat(e,{timeZone:t,hour12:n,dateStyle:"medium",timeStyle:"short"});return function(e){const t=new Date(e);return[o.format(t),t.toISOString()]}}(Intl.DateTimeFormat().resolvedOptions());const m="js:c-brief--flash",p=e=>!(1&~e),h=e=>2===e,E=e=>3===e;const y=function({locale:e,timeZone:t,hour12:n}=l){const o=Intl.DateTimeFormat(e,{timeZone:t,hour12:n,dateStyle:"short"}),i=Intl.DateTimeFormat(e,{timeZone:t,hour12:n,timeStyle:"short"});let r=new Date;return function(e,t=!1){t&&(r=new Date);const n=new Date(e);return[n.getDate()===r.getDate()&&n.getMonth()===r.getMonth()&&n.getFullYear()===r.getFullYear()?i.format(n):o.format(n),n.toISOString()]}}(Intl.DateTimeFormat().resolvedOptions());const T=function(e){const t=c(e,"app");return function({briefList:e,newButton:n,note:o,searchField:i}){const r=t.cloneNode(!0),c=r.querySelector(".c-sidebar__menu");if(!c)throw new Error('Failed to find ".c-sidebar__menu" in "app" content');c.append(i),c.append(n),c.insertAdjacentElement("afterend",e);const d=r.querySelector(".c-note-view");if(!d)throw new Error('Failed to find "c-note-view" in "app" content');return d.append(o),r}}(document),g=function(e){const t=c(e,"edit-button");return function(n,o){const i=t.cloneNode(!0),r=e.createTextNode(n);return i.appendChild(r),i.classList.add("new"===o?"js:c-edit-button--new":"js:c-edit-button--update"),i instanceof HTMLButtonElement&&d((e=>i.disabled=e)),i}}(document),w=function(e,t){const n=c(e,"note"),o=c(e,"note-none"),i=function(e){const t=c(e,"note-preview");return function(e){const n=t.cloneNode(!0),o=n.querySelector(".o-from-markdown");return o instanceof HTMLElement&&(o.innerHTML=e),n}}(e),r=function(e,t){const n=c(e,"note-edit");return function(o){const i=n.cloneNode(!0),r=i.querySelector("#note-edit__title");r instanceof HTMLInputElement&&(r.value=o?o.title:a);const c=i.querySelector(".c-note-edit__note-title");c instanceof HTMLElement&&(c.append(e.createTextNode(o?o.title:a)),c.insertAdjacentElement("afterend",t(o?o.html:u)));const s=i.querySelector(".c-note-edit__delete"),l=i.querySelector(".c-note-edit__done");if(o){const e=i.querySelector("#note-edit__body");e instanceof HTMLTextAreaElement&&(e.textContent=o.body)}return l instanceof HTMLButtonElement&&s instanceof HTMLButtonElement&&(o||s.remove(),d(o?e=>{l.disabled=s.disabled=e}:e=>{l.disabled=e})),i}}(e,i);return function(c,d){if(d)return r(c);if(!c)return o.cloneNode(!0);const s=n.cloneNode(!0),l=s.querySelector("h1");l instanceof HTMLElement&&l.appendChild(e.createTextNode(c.title));const a=s.querySelector(".c-note__menu");if(a instanceof HTMLElement){const n=s.querySelector("time");if(n instanceof HTMLTimeElement){const[t,o]=f(c.updatedAt);n.dateTime=o,n.appendChild(e.createTextNode(t))}a.append(t("Edit","edit"))}return s.append(i(c.html)),s}}(document,g),L=function(e){const t=function(e){const t=c(e,"brief");return function(n,o=0){const i=t.cloneNode(!0);i.dataset.noteId=n.id;const r=i.querySelector("header");if(r instanceof HTMLElement){const u=r.querySelector("strong");u instanceof HTMLElement&&u.appendChild(e.createTextNode(n.title));const f=r.querySelector("time");if(f instanceof HTMLTimeElement){const[T,g]=y(n.updatedAt);f.dateTime=g,f.appendChild(e.createTextNode(T))}}const c=i.querySelector(".c-brief__summary");c instanceof HTMLElement&&n.excerpt&&c.replaceChildren(e.createTextNode(n.excerpt));const d=i.querySelector("input"),l=i.querySelector("label");d instanceof HTMLInputElement&&l instanceof HTMLLabelElement&&(d.id=d.id+n.id,l.htmlFor=d.id);const a=i.querySelector(".c-brief__open");if(a instanceof HTMLButtonElement){const w=h(o)?"c-brief--pending":p(o)?"c-brief--active":void 0;w&&a.classList.add(w)}if(E(o)){function L(){i.classList.add(m),s(!0)}function S(e){if("flash"===e.animationName)return i.classList.remove(m),s(!1),void setTimeout(L,3e3)}i.addEventListener("animationend",S),L()}return i}}(e),n=c(e,"brief-list__empty"),[o,i]=function(e,t){const n=c(e,t),o=n.querySelector("li");if(!(o instanceof HTMLLIElement))throw new Error(`Template ID "${t}" does not contain list item`);return o.remove(),[n,o]}(e,"brief-list");return function(r,c,d,s=!1){const l=o.cloneNode(!0),[a,u]="string"==typeof r?[void 0,r.length>0?r:void 0]:[Array.isArray(r)&&r.length>0?r:void 0,void 0];if(!a){const t=n.cloneNode(!0);return t.appendChild(e.createTextNode(u?`Couldn't find any notes titled "${u}"`:"No notes created yet!")),l.replaceChildren(t),l}const f=l.firstElementChild;if(f){const e=function(e,t,n){if(e){const o=t?3:1;return n?t=>n===t?2:e===t?o:0:t=>t===e?o:0}return n?e=>e===n?2:0:e=>0}(c,s,d);for(const n of a){const o=i.cloneNode(!0);o.append(t(n,e(n.id))),f.append(o)}}return l}}(document),S=function(e){const t=c(e,"search-field");return function(e,n=!1){const o=t.cloneNode(!0);if(e){const t=o.querySelector("#search-field__search");if(!(t instanceof HTMLInputElement))throw new Error('Failed to find "#search-field__search" input in "search-field" content');t.value=e}const i=o.querySelector(".c-spinner");return i instanceof HTMLElement&&n&&(i.classList.add("c-spinner--active"),i.setAttribute("aria-busy","true")),o}}(document);const v=function(e){const{isEditing:t,noteId:n,title:o}=function(e){const t=e.split("/");if(2===t.length&&0===t[1].length)return{noteId:void 0,isEditing:!1,title:i()};if(t.length>2&&"note"===t[1]){if(3===t.length&&"new"===t[2])return{noteId:void 0,isEditing:!0,title:i("New Note")};if(t.length<5&&t[2].length>0){if(3===t.length)return{noteId:t[2],isEditing:!1,title:i(t[2])};if("edit"===t[3])return{noteId:t[2],isEditing:!0,title:i(`Edit ${t[2]}`)};r(e)}r(e)}r(e)}(e.pathname),c=e.searchParams,d=c.get("search")??void 0,s=c.get("moqnextid")??void 0;return{flushNote:c.has("moqflush"),isEditing:t,nextId:s&&s.length>0?s:void 0,noteId:n,search:"string"==typeof d&&d.length>0?d:void 0,title:o}}(new URL(document.location.href));document.title=v.title;document.querySelector("body").prepend(function({flushNote:e,isEditing:o,nextId:i,noteId:r,search:c}){const d=function(e){if(!e)return t;const n=e.toLowerCase();return t.filter((e=>e.title.toLowerCase().includes(n)))}(c),s=d.length>0?d:c,l=r?(a=r,n.get(a)):void 0;var a;const u=Boolean(i);return T({briefList:L(s,l?.id,i,e),newButton:g("New","new"),note:w(l,o),searchField:S(c,u)})}(v))}(); diff --git a/moq/public/map.html b/moq/public/map.html new file mode 100644 index 0000000..7001c90 --- /dev/null +++ b/moq/public/map.html @@ -0,0 +1,45 @@ + + + + + + + Map to reference views + + + + + + diff --git a/moq/public/moq-page.html b/moq/public/moq-page.html new file mode 100644 index 0000000..5f42a1a --- /dev/null +++ b/moq/public/moq-page.html @@ -0,0 +1,149 @@ + + + + + + + SolidJS Notes CSS moq + + + + + + + + + + + + + + + + + + + diff --git a/moq/public/style.css b/moq/public/style.css new file mode 100644 index 0000000..c60ff4a --- /dev/null +++ b/moq/public/style.css @@ -0,0 +1,674 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} +ul[class], +ol[class] { + padding: 0; +} +body, +h1, +h2, +h3, +h4, +p, +ul[class], +ol[class], +li, +figure, +figcaption, +blockquote, +dl, +dd { + margin: 0; +} +body { + min-height: 100vh; + scroll-behavior: smooth; + text-rendering: optimizeSpeed; + line-height: 1.5; +} +ul[class], +ol[class] { + list-style: none; +} +a:not([class]) { + text-decoration-skip-ink: auto; +} +img { + max-width: 100%; + display: block; +} +article > * + * { + margin-block-start: 1em; +} +input, +button, +textarea, +select { + font: inherit; +} +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +:root { + --main-border-color: #ddd; + --primary-border: #037dba; + --gray-20: #404346; + --gray-60: #8a8d91; + --gray-70: #bcc0c4; + --gray-80: #c9ccd1; + --gray-90: #e4e6eb; + --gray-95: #f0f2f5; + --gray-100: #f5f7fa; + --primary-blue: #037dba; + --secondary-blue: #0396df; + --tertiary-blue: #c6efff; + --flash-blue: #4cf7ff; + --outline-blue: rgba(4, 164, 244, 0.6); + --navy-blue: #035e8c; + --red-25: #bd0d2a; + --secondary-text: #65676b; + --white: #fff; + --yellow: #fffae1; + --outline-box-shadow: 0 0 0 2px var(--outline-blue); + --outline-box-shadow-contrast: 0 0 0 2px var(--navy-blue); + --sans-serif: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, + Ubuntu, Helvetica, sans-serif; + --monospace: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, + monospace; +} +html { + font-size: 100%; +} +body { + font-family: var(--sans-serif); + background: var(--gray-100); + font-weight: 400; + line-height: 1.75; +} +.c-main { + display: flex; + height: 100vh; + width: 100%; + overflow: hidden; +} +.c-main__column { + height: 100%; +} +.c-main__column:last-child { + flex-grow: 1; +} +.c-note-view { + display: flex; + align-items: center; + justify-content: center; +} +.c-note-none { + font-size: 1.5rem; + margin-inline: 20px 20px; +} +.c-note-skeleton-title.c-note-skeleton-title, +.c-note-edit__note-title, +.c-note__header h1 { + line-height: 1.3; + flex-grow: 1; + overflow-wrap: break-word; + margin-inline-start: 12px; +} +.c-note-skeleton-preview, +.c-note-preview { + margin-block-start: 50px; +} +.c-note-skeleton-display__header, +.c-note__header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap-reverse; + margin-inline-start: -12px; +} +.c-note-skeleton-display, +.c-note { + background: var(--white); + box-shadow: + 0 0 5px #0000001a, + 0 0 1px #0000001a; + border-radius: 8px; + height: 95%; + width: 95%; + min-width: 400px; + padding: 8%; + overflow-y: auto; +} +.c-note__menu { + display: flex; + justify-content: space-between; + align-items: center; + flex-grow: 1; +} +.c-note__updated { + color: var(--secondary-text); + white-space: nowrap; + margin-inline-start: 12px; +} +.o-from-markdown h1, +.o-from-markdown h2, +.o-from-markdown h3, +.o-from-markdown h4, +.o-from-markdown h5 { + margin-block: 2rem 0.7rem; + margin-inline: 0; +} +.o-from-markdown blockquote { + font-style: italic; + color: var(--gray-20); + border-left: 3px solid var(--gray-80); + padding-left: 10px; +} +.o-from-markdown p { + margin-bottom: 16px; +} +.o-from-markdown img { + width: 100%; +} +.c-note-skeleton-edit, +.c-note-edit { + background: var(--white); + display: flex; + height: 100%; + width: 100%; + padding: 58px; + overflow-y: auto; + margin-bottom: 20px; +} +.c-note-skeleton-edit__form, +.c-note-edit__form { + display: flex; + flex-direction: column; + width: 400px; + flex-shrink: 0; + position: sticky; + top: 0; +} +.c-note-skeleton-edit__preview, +.c-note-edit__preview { + margin-inline-start: 40px; + width: 100%; +} +.c-note-skeleton-edit__menu, +.c-note-edit__menu { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 12px; +} +.c-note-edit__form input, +.c-note-edit__form textarea { + background: none; + border: 1px solid var(--gray-70); + border-radius: 2px; + font-family: var(--monospace); + font-size: 0.8rem; + padding: 12px; + outline-style: none; +} +.c-note-edit__form input:focus, +.c-note-edit__form textarea:focus { + box-shadow: var(--outline-box-shadow); +} +.c-note-edit__form input { + height: 44px; + margin-bottom: 16px; +} +.c-note-edit__form textarea { + height: 100%; + max-width: 400px; +} +.c-note-edit__done, +.c-note-edit__delete { + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 100px; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 6px 20px 8px; + cursor: pointer; + font-weight: 700; + margin-inline-start: 12px; + outline-style: none; + transition: all 0.2s ease-in-out; +} +.c-note-edit__done:disabled, +.c-note-edit__delete:disabled { + opacity: 0.5; +} +.c-note-edit__done[disabled], +.c-note-edit__delete[disabled] { + opacity: 0.5; +} +.c-note-edit__done > img, +.c-note-edit__delete > img { + margin-inline-end: 12px; +} +.c-note-edit__done { + border: none; + background: var(--primary-blue); + color: var(--white); +} +.c-note-edit__done:focus { + box-shadow: var(--outline-box-shadow-contrast); +} +.c-note-edit__done:hover:not([disabled]) { + background: var(--secondary-blue); +} +.c-note-edit__done > img { + width: 14px; +} +.c-note-edit__delete { + border: 1px solid var(--red-25); + background: var(--white); + color: var(--red-25); +} +.c-note-edit__delete:focus { + box-shadow: var(--outline-box-shadow); +} +.c-note-edit__delete:hover:not([disabled]) { + background: var(--red-25); + color: var(--white); +} +.c-note-edit__delete:hover:not([disabled]) img { + filter: grayscale(1) invert(1) brightness(2); +} +.c-note-edit__delete > img { + width: 10px; +} +.c-note-edit__label-preview { + display: inline-block; + border-radius: 100px; + letter-spacing: 0.05em; + text-transform: uppercase; + font-weight: 700; + padding: 4px 14px; + margin-bottom: 20px; + background: rgba(38, 183, 255, 0.15); + color: var(--primary-blue); +} +.c-logo { + height: 20px; + width: 22px; + margin-inline-end: 10px; +} +.c-sidebar { + background: var(--white); + box-shadow: + 0 8px 24px #0000001a, + 0 2px 2px #0000001a; + overflow-y: scroll; + z-index: 1000; + flex-shrink: 0; + max-width: 350px; + min-width: 250px; + width: 30%; +} +.c-sidebar__header { + letter-spacing: 0.15em; + text-transform: uppercase; + padding: 36px 16px 16px; + display: flex; + align-items: center; +} +.c-sidebar__menu { + padding: 0 16px 16px; + display: flex; + justify-content: space-between; +} +.c-sidebar__menu > .c-search-field { + position: relative; + flex-grow: 1; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.c-spinner { + display: inline-block; + transition: opacity linear 0.1s 0.2s; + width: 20px; + height: 20px; + border: 3px solid rgba(80, 80, 80, 0.5); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; + opacity: 0; +} +.c-spinner--active { + opacity: 1; +} +.c-search-field .c-spinner { + position: absolute; + right: 10px; + top: 10px; +} +.c-search-field input { + padding: 0 16px; + border-radius: 100px; + border: 1px solid var(--gray-90); + width: 100%; + height: 100%; + outline-style: none; +} +.c-search-field input:focus { + box-shadow: var(--outline-box-shadow); +} +.c-edit-button { + border-radius: 100px; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 6px 20px 8px; + cursor: pointer; + font-weight: 700; + outline-style: none; +} +.js\:c-edit-button--new { + background: var(--primary-blue); + color: var(--white); + border: none; + margin-inline-start: 6px; + transition: all 0.2s ease-in-out; +} +.js\:c-edit-button--new:hover { + background: var(--secondary-blue); +} +.js\:c-edit-button--new:focus { + box-shadow: var(--outline-box-shadow-contrast); +} +.js\:c-edit-button--update { + background: var(--white); + color: var(--primary-blue); + border: 1px solid var(--primary-blue); + margin-inline-start: 12px; + transition: all 0.1s ease-in-out; +} +.js\:c-edit-button--update:disabled { + opacity: 0.5; +} +.js\:c-edit-button--update:hover:not([disabled]) { + background: var(--primary-blue); + color: var(--white); +} +.js\:c-edit-button--update:focus { + box-shadow: var(--outline-box-shadow); +} +.c-note-skeleton-edit__done, +.c-note-skeleton-edit__delete, +.c-note-skeleton-edit__title, +.c-note-skeleton-edit__body, +.c-note-skeleton-display__done, +.c-note-skeleton-preview div, +.c-note-skeleton-title.c-note-skeleton-title, +.c-brief-list-skeleton__brief { + height: 100%; + background-color: #eee; + background-image: linear-gradient(90deg, #eee, #f5f5f5, #eee); + background-size: 200px 100%; + background-repeat: no-repeat; + border-radius: 4px; + display: block; + line-height: 1; + width: 100%; + animation: shimmer 1.2s ease-in-out infinite; + color: transparent; +} +.c-note-skeleton-edit__done:after, +.c-note-skeleton-edit__delete:after, +.c-note-skeleton-edit__title:after, +.c-note-skeleton-edit__body:after, +.c-note-skeleton-display__done:after, +.c-note-skeleton-preview div:after, +.c-note-skeleton-title:after, +.c-brief-list-skeleton__brief:after { + content: 'Loading...'; +} +.c-note-skeleton-edit__done:first-of-type, +.c-note-skeleton-edit__delete:first-of-type, +.c-note-skeleton-edit__title:first-of-type, +.c-note-skeleton-edit__body:first-of-type, +.c-note-skeleton-display__done:first-of-type, +.c-note-skeleton-preview div:first-of-type, +.c-note-skeleton-title:first-of-type, +.c-brief-list-skeleton__brief:first-of-type { + margin: 0; +} +.c-note-skeleton-edit__done, +.c-note-skeleton-edit__delete, +.c-note-skeleton-display__done { + border-radius: 100px; + padding: 6px 20px 8px; + width: auto; +} +ul.c-brief-list, +ul.c-brief-list-skeleton { + padding: 16px 0; +} +.c-brief-list > li, +.c-brief-list-skeleton > li { + padding: 0 16px; +} +.c-brief-list-skeleton > * + * { + margin-block-start: 0.8em; +} +.c-brief-list-skeleton__brief { + position: relative; + margin-bottom: 12px; + padding: 16px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + max-height: 100px; + height: 5em; +} +.c-brief-empty { + padding: 16px; +} +.c-brief-list > * + * { + margin-block-start: 0.8em; +} +.c-brief { + position: relative; + margin-bottom: 12px; + padding: 16px; + width: 100%; + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + max-height: 100px; + transition: max-height 0.25s ease-out; + transform: scale(1); +} +.c-brief:has(.c-brief__summary-state:checked) { + max-height: 300px; + transition: max-height 0.5s ease; +} +.c-brief.js\:c-brief--flash { + animation-name: flash; + animation-duration: 0.6s; +} +.c-brief header { + z-index: 1; + width: 85%; + pointer-events: none; +} +.c-brief strong { + display: block; + font-size: 1.25rem; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.c-brief time { + font-size: 0.8rem; +} +.c-brief__open { + position: absolute; + inset: 0; + width: 100%; + z-index: 0; + border: 1px solid transparent; + border-radius: 6px; + text-align: start; + background-color: var(--gray-95); + cursor: pointer; + outline-style: none; + color: transparent; + font-size: 0; +} +.c-brief__open:focus { + box-shadow: var(--outline-box-shadow); +} +.c-brief__open:hover { + background-color: var(--gray-90); +} +.c-brief__open.c-brief--active { + background-color: var(--tertiary-blue); + border: 1px solid var(--primary-border); +} +.c-brief__open.c-brief--pending { + background-color: var(--gray-80); +} +.c-brief__no-content { + font-style: italic; +} +.c-brief__summary-state { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.c-brief__summary-state:checked ~ .c-brief__summary-toggle svg { + transform: rotate(-180deg); +} +.c-brief__summary-state:checked ~ .c-brief__summary { + transform: translateY(5px); + opacity: 1; +} +.c-brief__summary-toggle { + z-index: 2; + border-radius: 50%; + height: 24px; + border: 1px solid var(--gray-60); + flex-shrink: 0; + visibility: hidden; + opacity: 0; + cursor: pointer; + transition: + visibility 0s linear 20ms, + opacity 0.3s; + outline-style: none; +} +.c-brief__summary-state:focus ~ .c-brief__summary-toggle { + box-shadow: var(--outline-box-shadow); +} +.c-brief__open:hover + .c-brief__summary-toggle, +.c-brief__open:focus + .c-brief__summary-toggle, +.c-brief__summary-toggle:hover, +.c-brief__summary-toggle:focus { + visibility: visible; + opacity: 1; + transition: + visibility 0s linear 0s, + opacity 0.3s; +} +.c-brief__summary-toggle svg { + opacity: 0.8; + margin: 0.15rem 0.3rem; + transition: all 0.25s ease-in-out; + font-size: 0.8rem; +} +.c-brief__summary { + pointer-events: none; + z-index: 2; + flex: 1 1 250px; + position: relative; + overflow: hidden; + opacity: 0; + top: -5px; + transition: + transform 0.25s ease-out, + opacity 0.25s ease-in; +} +.c-note-skeleton-title.c-note-skeleton-title { + height: 3rem; + width: 65%; + margin-inline: 12px 1em; +} +.c-note-skeleton-preview > * + * { + margin-block-start: 0.8em; +} +.c-note-skeleton-preview div { + height: 1.5em; +} +.c-note-skeleton-display__done { + width: 8em; + height: 2.5em; +} +.c-note-skeleton-edit__form > * + * { + margin-block-start: 0.8em; +} +.c-note-skeleton-edit__title { + height: 3rem; +} +.c-note-skeleton-edit__body { + height: 100%; +} +.c-note-skeleton-edit__done, +.c-note-skeleton-edit__delete { + width: 8em; + height: 2.5em; +} +.c-note-skeleton-edit__delete { + margin-inline: 12px 0; +} +@keyframes shimmer { + 0% { + background-position: -200px 0; + } + to { + background-position: calc(200px + 100%) 0; + } +} +@keyframes flash { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.9; + } + to { + transform: scale(1); + opacity: 1; + } +} +.u-offscreen { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + width: 1px; + position: absolute; +} diff --git a/moq/reference/README.md b/moq/reference/README.md index e9d6406..9a7b8fc 100644 --- a/moq/reference/README.md +++ b/moq/reference/README.md @@ -3,7 +3,7 @@ A moq (mock-up/maquette) page to explore the [React Server Components Demo](http It serves as the visual reference implementation for the port's styling with [Sass](https://sass-lang.com/). Rather than launching the full React application this is just a page that cobbles together the DOM client side from [`

Visit start.solidjs.com to learn how to build SolidStart apps.