diff options
author | Alexis Hovorka <[email protected]> | 2024-02-16 23:48:02 -0700 |
---|---|---|
committer | Alexis Hovorka <[email protected]> | 2024-02-16 23:48:02 -0700 |
commit | c5aa364e0e372d0e29063a27bd17811446db8b6a (patch) | |
tree | 5c5b10471f7356b66b74277071e61e5e0447edc5 /app | |
parent | 773e7f747fa096031be427b22257137cf894d587 (diff) |
[feat] Flesh out basic UI
Diffstat (limited to 'app')
-rw-r--r-- | app/public/icons.svg | 1 | ||||
-rw-r--r-- | app/public/index.html | 71 | ||||
-rw-r--r-- | app/public/logo-icon.svg | 96 | ||||
-rw-r--r-- | app/public/main.js | 119 | ||||
-rw-r--r-- | app/public/style.css | 414 |
5 files changed, 653 insertions, 48 deletions
diff --git a/app/public/icons.svg b/app/public/icons.svg new file mode 100644 index 0000000..47e13fb --- /dev/null +++ b/app/public/icons.svg @@ -0,0 +1 @@ +<svg width="96" height="72" viewBox="0 0 25.4 19.05" xmlns="http://www.w3.org/2000/svg"><g style="fill:#000;stroke-width:2"><g style="stroke-width:2"><path fill="none" style="stroke-width:4" d="M0 0h24v24H0z" transform="matrix(.26458 0 0 .26458 6.35 0)"/><path style="fill:#1ab21a;fill-opacity:1;stroke-width:2" d="M7.333 6h9.333C17.4 6 18 6.6 18 7.334v9.333C18 17.4 17.4 18 16.666 18H7.333C6.6 18 6 17.4 6 16.666V7.333c0-.733.6-1.332 1.333-1.332zM5 3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" transform="matrix(.26458 0 0 .26458 6.35 0)"/></g></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 12.7 0)"/><path d="M15.824 13.676 14.71 14l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28-.324 1.114 4.905 4.906c.41.41 1.225.265 1.812-.322s.732-1.402.322-1.812zM9.5 12.989A3.484 3.484 0 0 1 6.011 9.5 3.484 3.484 0 0 1 9.5 6.011 3.484 3.484 0 0 1 12.989 9.5 3.484 3.484 0 0 1 9.5 12.989z" style="fill:#1565c0;fill-opacity:1;stroke-width:2" transform="matrix(.26458 0 0 .26458 12.7 0)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 19.05 0)"/><g style="opacity:1"><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000;solid-opacity:1;vector-effect:none;fill:#fcc11c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000;stop-opacity:1" d="M12 3c-1.095 0-2 .905-2 2v5H5c-1.095 0-2 .905-2 2s.905 2 2 2h5v5c0 1.095.905 2 2 2s2-.905 2-2v-5h5c1.095 0 2-.905 2-2s-.905-2-2-2h-5V5c0-1.095-.905-2-2-2Z" transform="matrix(.26458 0 0 .26458 19.05 0)"/></g></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 0 6.35)"/><path d="M3 17.46v3.04c0 .28.22.5.5.5h3.04c.13 0 .26-.05.35-.15L17.81 9.94l-3.75-3.75L3.15 17.1c-.1.1-.15.22-.15.36ZM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z" style="fill:red;stroke-width:2" transform="matrix(.26458 0 0 .26458 0 6.35)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 19.05 6.35)"/><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" style="fill:red;stroke-width:2" transform="matrix(.26458 0 0 .26458 19.05 6.35)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:4" transform="matrix(.26458 0 0 .26458 6.35 6.35)"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" style="fill:red;stroke-width:2" transform="matrix(.26458 0 0 .26458 6.35 6.35)"/></g><g style="fill:#000;stroke-width:2"><g style="stroke-width:2" fill="none"><path style="stroke-width:4" d="M0 0h24v24H0z" transform="matrix(.26458 0 0 .26458 12.7 6.35)"/><path style="stroke-width:4" d="M0 0h24v24H0z" transform="matrix(.26458 0 0 .26458 12.7 6.35)"/></g><g style="stroke-width:2"><path d="M19 12.87c0-.47-.34-.85-.8-.98A2.997 2.997 0 0 1 16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.38-.93 2.54-2.2 2.89-.46.13-.8.51-.8.98V13c0 .55.45 1 1 1h4.98l.02 7c0 .55.45 1 1 1s1-.45 1-1l-.02-7H18c.55 0 1-.45 1-1z" fill-rule="evenodd" style="fill:red;stroke-width:4" transform="matrix(.26458 0 0 .26458 12.7 6.35)"/></g></g><path style="fill:#fff;fill-opacity:1;stroke:none;stroke-width:.264999;stroke-linecap:round;stroke-linejoin:round" d="M12.196 179.652a.53.53 0 0 0-.517.53.53.53 0 0 0 .53.528h3.704a.53.53 0 0 0 .529-.529.53.53 0 0 0-.53-.529H12.21a.53.53 0 0 0-.013 0zm0 1.852a.53.53 0 0 0-.517.53.53.53 0 0 0 .53.529h3.704a.53.53 0 0 0 .529-.53.53.53 0 0 0-.53-.529H12.21a.53.53 0 0 0-.013 0zm0 1.852a.53.53 0 0 0-.517.53.53.53 0 0 0 .53.529h3.704a.53.53 0 0 0 .529-.53.53.53 0 0 0-.53-.529H12.21a.53.53 0 0 0-.013 0z" transform="translate(-10.886 -178.858)"/><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:2" transform="matrix(.26458 0 0 .26458 0 12.7)"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2Zm-8.29 13.29a.996.996 0 0 1-1.41 0L5.71 12.7a.996.996 0 1 1 1.41-1.41L10 14.17l6.88-6.88a.996.996 0 1 1 1.41 1.41z" style="stroke-width:2;fill:#1565c0;fill-opacity:1" transform="matrix(.26458 0 0 .26458 0 12.7)"/></g><g style="fill:#000;stroke-width:2"><path d="M0 0h24v24H0Z" fill="none" style="stroke-width:2" transform="matrix(.26458 0 0 .26458 6.35 12.7)"/><path d="M18 19H6c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1zm1-16H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2Z" style="stroke-width:2;fill:red" transform="matrix(.26458 0 0 .26458 6.35 12.7)"/></g></svg>
\ No newline at end of file diff --git a/app/public/index.html b/app/public/index.html index c7b44c7..593e6b3 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -56,26 +56,59 @@ <link rel="stylesheet" href="style.css"> </head> <body> - <h1>Notes</h1> + <dialog id="sign-in-dialog"> + <form id="sign-in-form" class="card" method="dialog"> + <h1 class="logo"><img src="logo-icon.svg" alt="" class="logo-icon"> Notes</h1> + <p id="sign-in-error" class="error-message"></p> + <div> + <input type="text" id="sign-in-username" name="username" placeholder="Username" autocomplete="username" autocorrect="off" autocapitalize="off" spellcheck="false" required autofocus> + <input type="password" id="sign-in-password" name="password" placeholder="Password" autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false" required> + <label> + <input type="checkbox" id="sign-in-keep-session" name="keepSession"> + <span class="checkbox-icon"></span> Keep me signed in + </label> + <button id="sign-in-basic-go"><span class="loading-spinner"></span>Sign in</button> + </div> + <!--div> + <input type="text" inputmode="numeric" id="sign-in-otp" name="otp" placeholder="OTP Code" autocomplete="one-time-code" autocorrect="off" autocapitalize="off" spellcheck="false" pattern="\d{6,6}" required> + <button id="sign-in-otp-go">Sign in</button> + </div> + <div> + <input type="password" id="sign-in-new-password" name="newPassword" placeholder="New password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" minlength=8 maxlength=512 required> + <input type="password" id="sign-in-new-password-confirm" name="newPasswordConfirm" placeholder="Confirm new password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" minlength=8 maxlength=512 required> + <button id="sign-in-change-password-go">Change password</button> + </div--> + </form> + </dialog> - <form> - <input type="text" id="username" name="username"> - <input type="password" id="password" name="password"> - <input type="checkbox" id="keep-session" name="keepSession"> - <button id="login">Log In</button> - </form> - <br> - <input type="password" id="new-password" name="newPassword"> - <button id="change-password">Change Password</button> - <br> - <input type="text" id="new-username" name="newUsername"> - <button id="change-username">Change Username</button> - <br> - <input type="text" id="uid" name="uid"> - <button id="logout">Log Out</button> - <button id="logout-everywhere">Log Out Everywhere</button> - <div id="notes"></div> - <button id="new-note">New Note</button> + <div id="menu" class="card-shadow"> + <div class="card"> + <input type="password" id="new-password" name="newPassword" placeholder="New password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false"> + <button id="change-password">Change password</button> + <br> + <input type="text" id="new-username" name="newUsername" placeholder="New username" autocomplete="none" autocorrect="off" autocapitalize="off" spellcheck="false"> + <button id="change-username">Change username</button> + <br> + <button id="log-out">Log out</button> + <button id="log-out-everywhere">Log out everywhere</button> + <br> + <!--input type="text" inputmode="search" id="search" name="search" placeholder="Search…" list="search-hints"--> + <input type="search" id="search" name="search" placeholder="Search…" list="search-hints" incremental> + <datalist id="search-hints"> + <option value="#hash"></option> + <option value="#tag"></option> + </datalist> + </div> + </div> + + <div id="notes" class="notes-column"></div> + <div id="toolbar"> + <button class="toolbar-btn" id="menu-btn"></button> + <button class="toolbar-btn" id="window-btn"></button> + <button class="toolbar-btn" id="search-btn"></button> + <button class="toolbar-btn" id="conflict-btn"></button> + <button class="toolbar-fab" id="add-btn"></button> + </div> <!--script src="sock.js"></script--> <script src="main.js"></script> diff --git a/app/public/logo-icon.svg b/app/public/logo-icon.svg new file mode 100644 index 0000000..d66678f --- /dev/null +++ b/app/public/logo-icon.svg @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + width="96" + height="96" + viewBox="0 0 25.4 25.4" + version="1.1" + id="svg1753" + inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" + sodipodi:docname="logo-icon.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> + <defs + id="defs1747" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="6.2441455" + inkscape:cx="123.67585" + inkscape:cy="47.999999" + inkscape:document-units="mm" + inkscape:current-layer="text902" + inkscape:document-rotation="0" + showgrid="false" + units="px" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:showpageshadow="2" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:window-width="1368" + inkscape:window-height="890" + inkscape:window-x="-6" + inkscape:window-y="-6" + inkscape:window-maximized="1" /> + <metadata + id="metadata1750"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(645.044,-144.5381)"> + <rect + style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-linecap:round;stroke-linejoin:round" + id="rect2383" + width="16.933332" + height="16.933332" + x="-640.81067" + y="148.77144" /> + <g + style="fill:#000000;stroke-width:0.5" + id="g898" + transform="matrix(1.0583333,0,0,1.0583333,-645.044,144.5381)"> + <g + id="g896" + style="stroke-width:0.5"> + <rect + fill="none" + height="24" + width="24" + id="rect892" + x="0" + y="0" + style="stroke-width:0.25" /> + <path + d="M 19,3 H 5 C 3.9,3 3,3.9 3,5 v 14 c 0,1.1 0.9,2 2,2 h 14 c 1.1,0 2,-0.9 2,-2 V 5 C 21,3.9 20.1,3 19,3 Z M 13,17 H 8 C 7.45,17 7,16.55 7,16 7,15.45 7.45,15 8,15 h 5 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 z m 3,-4 H 8 C 7.45,13 7,12.55 7,12 7,11.45 7.45,11 8,11 h 8 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 z M 16,9 H 8 C 7.45,9 7,8.55 7,8 7,7.45 7.45,7 8,7 h 8 c 0.55,0 1,0.45 1,1 0,0.55 -0.45,1 -1,1 z" + id="path894" + style="fill:#fbbc04;fill-opacity:1;stroke-width:0.5" /> + </g> + </g> + <g + aria-label="Notes" + id="text902" + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:14.9352px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:0.87;stroke:none;stroke-width:0.264583" + transform="translate(-0.91684937)" /> + </g> +</svg> diff --git a/app/public/main.js b/app/public/main.js index 5dc9531..5746142 100644 --- a/app/public/main.js +++ b/app/public/main.js @@ -28,13 +28,25 @@ function debounce(fn, delay) { //} function drawNote(note) { - const el = m(`<div> - <h3>${note.id}</h3> - <textarea>${note.content}</textarea> - </div>`); + const el = m(`<div class="card-shadow"><div class="card"> + <!--h3>${note.id}</h3--> + <textarea>${note.content.trim()}</textarea> + </div></div>`); - $("textarea", el).addEventListener("input", - debounce(() => { saveNote(note.id, $("textarea", el).value); }, 500)); + const ta = $("textarea", el) + + ta.addEventListener("input", + debounce(() => { saveNote(note.id, ta.value); }, 500)); + + function resizeTextarea() { // TODO simplify + el.style.height = (el.scrollHeight) + "px"; + ta.style.height = ""; ta.style.height = (ta.scrollHeight) + "px"; + el.style.height = ""; + } + ta.addEventListener("input", resizeTextarea); + ta.addEventListener("focus", resizeTextarea); + window.addEventListener("resize", resizeTextarea); + setTimeout(resizeTextarea); $("#notes").appendChild(el); } @@ -51,9 +63,7 @@ async function getList() { const list = await res.json(); console.log(list); - for (let i=0;i<list.length;i++) { - await getNote(list[i]); - } + return Promise.all(list.map(id => getNote(id))); } async function saveNote(id, content) { @@ -77,13 +87,12 @@ async function newNote() { drawNote(note); } -$("#new-note").addEventListener("click", newNote); +$("#add-btn").addEventListener("click", newNote); async function getUserData() { const res = await fetch(`/user`); const user = await res.json(); console.log(user); - if (user.uid) $("#uid").value = user.uid; } async function getUserSessions() { @@ -92,26 +101,75 @@ async function getUserSessions() { console.log(sessions); } -$("#login").addEventListener("click", async () => { +function showSignIn() { + const hash = location.hash.slice(2).replace(/^sign-in(\/to\/)?/,""); + history.replaceState("sign-in","","#/sign-in"+(hash?"/to/"+hash:"")); + $("#sign-in-dialog").showModal(); +} + +$("#sign-in-dialog").addEventListener("cancel", e => e.preventDefault()); +$("#sign-in-username").addEventListener("input", e => e.target.classList.remove("error")); +$("#sign-in-password").addEventListener("input", e => e.target.classList.remove("error")); +$("#sign-in-basic-go").addEventListener("click", async e => { + e.preventDefault(); + $("#sign-in-error").textContent = ""; + + const inputs = [ + $("#sign-in-username"), + $("#sign-in-password"), + $("#sign-in-keep-session"), + $("#sign-in-basic-go"), + ]; + const validatedInputs = inputs.slice(0,2); + + if (validatedInputs.map(e => { + if (!e.checkValidity()) { + e.classList.add("error"); + return true; } + }).reduce((a,e) => a||e, false)) return; + + inputs.forEach(e => e.disabled = true); + $("#sign-in-form").classList.add("loading"); + const res = await fetch("/login", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ - username: $("#username").value, - password: $("#password").value, - keepSession: $("#keep-session").checked, + username: $("#sign-in-username").value, + password: $("#sign-in-password").value, + keepSession: $("#sign-in-keep-session").checked, }) }); - // TODO check return code + inputs.forEach(e => e.disabled = false); + $("#sign-in-form").classList.remove("loading"); + if (!res.ok) { + $("#sign-in-error").textContent = res.status+" "+res.statusText; + return; + } const json = await res.json(); console.log(json); - // TODO "Must Change Password" flow + if (!json.success) { + $("#sign-in-error").textContent = json.msg; + return; + } + + if (json.mustChangePassword) { + alert("Must change password (TODO)"); + // TODO + return; + } + + const hash = location.hash.slice(9); + history.replaceState("","","#/"+hash); + // TODO load location getUserData(); getList(); + + $("#sign-in-dialog").close(); }); $("#change-password").addEventListener("click", async () => { @@ -119,8 +177,8 @@ $("#change-password").addEventListener("click", async () => { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ - username: $("#username").value, - password: $("#password").value, + username: $("#username").value, // TODO + password: $("#password").value, // TODO newPassword: $("#new-password").value, keepSession: $("#keep-session").checked, }) @@ -138,7 +196,7 @@ $("#change-username").addEventListener("click", async () => { headers: {"Content-Type": "application/json"}, body: JSON.stringify({ newUsername: $("#new-username").value, - password: $("#password").value, + password: $("#password").value, // TODO }) }); @@ -148,7 +206,7 @@ $("#change-username").addEventListener("click", async () => { getUserData(); }); -$("#logout").addEventListener("click", async () => { +$("#log-out").addEventListener("click", async () => { const res = await fetch("/logout", { method: "POST", }); @@ -156,19 +214,28 @@ $("#logout").addEventListener("click", async () => { //const json = await res.json(); //console.log(json); console.log("Logged out"); - $("#uid").value = ""; + $("#sign-in-dialog").showModal(); }); -$("#logout-everywhere").addEventListener("click", async () => { +$("#log-out-everywhere").addEventListener("click", async () => { const res = await fetch("/deauth-all", { method: "POST", }); console.log("Logged out everywhere"); - $("#uid").value = ""; + showSignIn(); }); -try { getUserData(); getList(); getUserSessions(); } catch(e) {} +try { await getUserData(); await getList(); await getUserSessions(); } catch(e) { showSignIn(); } + +(() => { + let prevScroll = 0; + window.addEventListener("scroll", () => { + document.querySelector("#toolbar").classList[ + (window.scrollY && window.scrollY > prevScroll)?"add":"remove"]("hide"); + prevScroll = window.scrollY; + }); +})(); // TODO make sure to save unsynced delta even if token has expired, then sync when logged in // TODO Cookie consent on signup; "only necessary for maintaining user session and cached data" @@ -190,4 +257,6 @@ try { getUserData(); getList(); getUserSessions(); } catch(e) {} // https://github.com/cure53/DOMPurify/blob/main/src/attrs.js // https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html +// TODO note kebab menu could be a <dialog> element? + }); diff --git a/app/public/style.css b/app/public/style.css index 655e181..e7dd049 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -3,12 +3,418 @@ padding: 0; box-sizing: border-box; font-family: "Roboto", "Noto Sans", sans-serif; - transition-timing-function: ease-in-out; + transition-timing-function: cubic-bezier(.4,0,.2,1); } -body { - user-select: none; - -webkit-user-select: none; +/* TODO + - normalize text size + - put all colors in variables + - checklists using ::marker? + - links + - CSS scroll snap for columns + - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap + - box-decoration-break: clone; -webkit-box-decoration-break: clone; + - overscroll-behavior: contain; + - user-select: text; +*/ + +:root, ::backdrop { + --bg-color: #FFF; + --page-bg-color: #EEE; + --text-color: rgba(0,0,0,.87); + --error-triple: 176, 0, 32; + --error-color: rgb(var(--error-triple)); + --icon-opacity-active: .87; + --icon-opacity-inactive: .6; + --icon-opacity-disabled: .38; + --icon-inactive-filter: brightness(0); + --icon-checkbox-active-filter: brightness(1); + --icon-add-hover-filter: brightness(1); + --icon-menu-hover-filter: brightness(1); + --icon-search-hover-filter: brightness(1); + --icon-window-hover-filter: brightness(1); + --menu-button-hover-bg-color: #FBBC04; + --button-hover-bg-color: #FFF; + --button-hover-shadow: 0 2px 16px rgba(0,0,0,.1), 0 2px 4px rgba(0,0,0,.2); + --fab-bg: #FFF; + --fab-shadow: 0 2px 12px rgba(0,0,0,.15), 0 2px 6px rgba(0,0,0,.2); + --fab-shadow-hover: 0 4px 16px rgba(0,0,0,.2), 0 3px 6px rgba(0,0,0,.3); + --toolbar-shadow-mobile: 0 8px 8px 8px rgba(0,0,0,.2); + --toolbar-hide-distance-mobile: calc(-56px - 8px); + --toolbar-button-hover-bg-mobile: rgba(0,0,0,.1); +} + +/*@media (prefers-color-scheme: dark) { :root, ::backdrop { + --bg-color: #000; + --page-bg-color: #000; + --text-color: rgba(255,255,255,.87); + --error-triple: 245, 0, 45; + --error-color: rgb(var(--error-triple)); + --icon-opacity-active: .87; + --icon-opacity-inactive: .6; + --icon-opacity-disabled: .38; + --icon-inactive-filter: brightness(0) invert(1) hue-rotate(180deg); + --icon-add-hover-filter: brightness(1.4) invert(.1) hue-rotate(0); + --icon-menu-hover-filter: brightness(1) invert(1) hue-rotate(180deg); + --icon-search-hover-filter: brightness(1) invert(1) hue-rotate(180deg); + --icon-window-hover-filter: brightness(.75) invert(1) hue-rotate(180deg); + --menu-button-hover-bg-color: #FBBC04; + --button-hover-bg-color: #181818; + --button-hover-shadow: 0 0 0 transparent; + --fab-bg: #333; + --fab-shadow: 0 2px 12px rgba(0,0,0,.2), 0 2px 6px rgba(0,0,0,.24); + --fab-shadow-hover: 0 2px 12px rgba(0,0,0,.2), 0 2px 6px rgba(0,0,0,.24); + --toolbar-shadow-mobile: 0 8px 8px 8px #000, 0 56px 56px -56px rgba(255,255,255,.25) inset; + --toolbar-hide-distance-mobile: calc(-56px - 8px); + --toolbar-button-hover-bg-mobile: rgba(255,255,255,.1); +}}*/ + +html { + background: var(--page-bg-color); + color: var(--text-color); + overscroll-behavior: contain; +} + +body, dialog { /* TODO */ + user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } + +#toolbar { position: fixed; font-size: 0; } +#toolbar > * { font-size: 16px; text-align: center; } +.toolbar-btn { height: 48px; width: 48px; } +.toolbar-btn, .toolbar-fab { + position: relative; + border-radius: 100%; + background: transparent; + border: none; + outline: none; +} + +.toolbar-btn:not(#conflict-btn)::after, .toolbar-fab::after { content: ""; display: block; height: 24px; width: 24px; background-image: url("icons.svg"); filter: var(--icon-inactive-filter); opacity: var(--icon-opacity-active); position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); } +#menu-btn::after { background-position: 0 0; } +#window-btn::after { background-position: -24px 0; } +#search-btn::after { background-position: -48px 0; } +#add-btn::after { background-position: -72px 0; } + +#conflict-btn { display: none; vertical-align: bottom; } +#conflict-btn::after { content: ""; position: absolute; top: 50%; left: 50%; display: block; height: 8px; width: 8px; border-radius: 4px; background: rgba(var(--error-triple), .8); transform: translate(-50%,-50%); } + +@media (hover:hover) { + .toolbar-btn, .toolbar-fab { cursor: pointer; transition: background-color .2s, box-shadow .2s; } + .toolbar-btn:not(#conflict-btn)::after, .toolbar-fab::after { transition: opacity .1s, filter .1s; filter: var(--icon-inactive-filter); } + .toolbar-btn:not(#conflict-btn):hover::after, .toolbar-fab:hover::after { opacity: 1; } + + #menu-btn:hover::after { filter: var(--icon-menu-hover-filter); } + #window-btn:hover::after { filter: var(--icon-window-hover-filter); } + #search-btn:hover::after { filter: var(--icon-search-hover-filter); } + #add-btn:hover::after { filter: var(--icon-add-hover-filter); } + + #conflict-btn::after { transition: box-shadow .1s; } + #conflict-btn:hover::after { box-shadow: 0 0 0 4px rgba(var(--error-triple),.25); } +} + +@media (hover:hover) and (min-width:601px) { + .notes-column { padding: 8px 80px 16px; } + #toolbar { top: 16px; left: 50%; transform: translate(-50%,0); width: 100%; max-width: calc(7in + 16px); pointer-events: none; } + #toolbar > * { pointer-events: auto; } + .toolbar-fab { height: 48px; width: 48px; } + .toolbar-btn, .toolbar-fab { display: block; margin: 0 16px; } + .toolbar-btn:not(#conflict-btn):hover, .toolbar-fab:hover { background-color: var(--button-hover-bg-color); box-shadow: var(--button-hover-shadow); } + .toolbar-btn:not(#conflict-btn)::after, .toolbar-fab::after { opacity: var(--icon-opacity-inactive) } + .toolbar-btn:not(#conflict-btn):hover::after, .toolbar-fab:hover::after { opacity: var(--icon-opacity-active) } + + #menu-btn:hover { background-color: var(--menu-button-hover-bg-color) !important; } + #search-btn { position: absolute; top: 0; right: 0; } + #add-btn { position: absolute; top: 48px; right: 0; } + #conflict-btn { height: 42px; } + #conflict-btn.show { display: block; } +} + +@media (hover:none), (max-width:600px) { + .notes-column { padding: 0 16px 16px; } + #toolbar { bottom: 0; left: 0; right: 0; width: 100%; box-shadow: var(--toolbar-shadow-mobile); padding: 0 4px; background: var(--bg-color); transition: bottom .25s; /* reentrance speed */ } + .toolbar-btn { display: inline-block; } + .toolbar-fab { position: absolute; display: block; right: 16px; top: -28px; width: 56px; height: 56px; box-shadow: var(--fab-shadow); background: var(--fab-bg); transition: top .25s; } + #conflict-btn { width: 36px; } + #conflict-btn.show { display: inline-block; } + + #toolbar.hide { bottom: var(--toolbar-hide-distance-mobile); transition: bottom .2s; /* exit speed */ } + #toolbar.hide .toolbar-fab { top: calc(var(--toolbar-hide-distance-mobile) - 16px); transition: top .2s; } + + @media (orientation:portrait) { + .toolbar-btn { margin: 4px 0; } + } + + @media (orientation:landscape) { + .toolbar-btn { margin: 0; } + } +} + +@media (hover:hover) and (max-width:600px) { + .toolbar-btn:not(#conflict-btn)::before { content: ""; display: block; position: absolute; top: 50%; left: 50%; height: 40px; width: 40px; border-radius: 20px; transform: translate(-50%,-50%); transition: background-color .2s; } + .toolbar-btn:not(#conflict-btn):hover::before { background-color: var(--toolbar-button-hover-bg-mobile); } + #menu-btn:hover::before { background-color: var(--menu-button-hover-bg-color) !important; } + .toolbar-fab { transition: top .25s, box-shadow .2s; } + #toolbar.hide .toolbar-fab { transition: top .2s, box-shadow .2s; } + .toolbar-fab:hover { box-shadow: var(--fab-shadow-hover); } + #add-btn::after { transition: opacity .15s, filter .15s, transform .2s; } + #add-btn:hover::after { filter: var(--icon-add-hover-filter); transform: translate(-50%,-50%) scale(1.25); } +} + +.logo { + font-weight: 500; + word-spacing: -.15em; +} +.logo-icon { + vertical-align: -.45em; + height: 1.6em; +} + +#sign-in-dialog { + margin: auto; + width: auto; + max-width: min(5.5in, 100vw - 32px); + outline: none; + border: none; + border-radius: 24px; + box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.1); + transition: box-shadow .2s; +} +#sign-in-dialog::after { + content: ""; + position: absolute; + inset: 0; + box-shadow: 0 0 0 1px rgba(255,255,255,.2) inset; + border-radius: 24px; + pointer-events: none; + transition: box-shadow .2s; +} +#sign-in-dialog::backdrop { + background: var(--page-bg-color); +} +#sign-in-form { + display: block; + padding: 16px 16px 40px; + + & .logo { + margin: 16px 0; + padding: .1em .3em .1em 0; + font-size: min(48px, 12vw); + text-align: center; + } + + & .error-message { + text-align: center; + color: var(--error-color); + } + + & input[type=text], + & input[type=password], + & label, + & button { + display: block; + width: 100%; + max-width: 3.5in; + margin: 12px auto; + font-size: inherit; + letter-spacing: .3px; + color: var(--text-color); + border-radius: 16px; + } + + & input[type=text], + & input[type=password], + & button { + padding: 12px 18px; + outline: 0; + } + & input[type=text], + & input[type=password] { + position: relative; + background-color: #FFF; + border: 1px solid #FFF; + border-bottom: 1px solid rgba(0,0,0,.15); + box-shadow: 0 2px 1px -1px rgba(0,0,0,.1); + transition: border .2s, box-shadow .2s, opacity .2s; + } + & input[type=text]:hover:not(:disabled), + & input[type=password]:hover:not(:disabled), + & input[type=text]:focus, + & input[type=password]:focus { + border: 1px solid rgba(0,0,0,.07); + border-bottom: 1px solid rgba(0,0,0,.18); + box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.15); + } + & input[type=text]::placeholder, + & input[type=password]::placeholder { + color: rgba(0,0,0,.6); + } + & input[type=text]:autofill, + & input[type=password]:autofill { + background-color: rgb(232,240,254); + border: 1px solid rgb(179,207,255); + } + & input[type=text].error, + & input[type=password].error { + background-color: rgb(254,232,232); + border: 1px solid rgb(255,191,191); + } + & input[type=text].error:focus, + & input[type=password].error:focus { + box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.15), 0 0 1px 2px rgb(255,191,191) inset; + } + + & label { + width: fit-content; + padding: 8px 12px 8px 8px; + text-align: center; + cursor: pointer; + transition: background-color .2s, opacity .2s; + } + & label:focus-within:has(:focus-visible) { + background-color: rgb(232,240,254); + text-align: center; + cursor: pointer; + } + & input[type=checkbox] { + display: inline-block; + height: 0; + width: 0; + opacity: 0; + } + .checkbox-icon { + display: inline-block; + height: 24px; + width: 24px; + vertical-align: -.36em; + background-image: url("icons.svg"); + background-position: -24px -48px; + filter: var(--icon-inactive-filter); + opacity: var(--icon-opacity-inactive); + } + input[type=checkbox]:checked + .checkbox-icon { + background-position: 0 -48px; + filter: var(--icon-checkbox-active-filter); + opacity: var(--icon-opacity-active); + } + + & button { + position: relative; + font-weight: 500; + background-color: rgba(0,0,0,.06); + border: none; + cursor: pointer; + transition: background-color .2s, box-shadow .2s; + } + & button:hover, + & button:focus, + &.loading button { + background-color: #FBBC04; + } + & button:focus-visible { + box-shadow: 0 0 0 4px rgba(251,188,4,.4); + } + & input + button { + margin-top: 32px; + } + &.loading button { + color: transparent; + } + + & button .loading-spinner { + inset: 50%; + transform: translate(-50%,calc(-50% - 1em)); + display: none; + } + &.loading button .loading-spinner { + display: inline-block; + } + & .loading-spinner, + & .loading-spinner::before, + & .loading-spinner::after { + position: absolute; + border-radius: 50%; + width: .7em; + height: .7em; + animation-fill-mode: both; + animation: loading-spinner 1.8s infinite ease-in-out; + } + & .loading-spinner { + animation-delay: -0.16s; + } + & .loading-spinner::before, + & .loading-spinner::after { + content: ''; + top: 0; + } + & .loading-spinner::before { + left: -1.3em; + animation-delay: -0.32s; + } + & .loading-spinner::after { + left: 1.3em; + } + + &.loading input, + &.loading label { + opacity: .5; + } +} +@keyframes loading-spinner { + 0%, 80%, 100% { box-shadow: 0 1em 0 -1.3em #fff; } + 40% { box-shadow: 0 1em 0 0 #fff; } +} + +.notes-column { + overflow-y: auto; +} + +/* TODO :target? */ +.card-shadow { + position: relative; + margin: 16px auto; + max-width: 5.5in; + border-radius: 24px; + box-shadow: 0 2px 16px rgba(0,0,0,.05), 0 2px 4px rgba(0,0,0,.1); + transition: box-shadow .2s; +} +.card-shadow:hover, +.card-shadow:focus-within { + box-shadow: 0 2px 16px rgba(0,0,0,.15), 0 2px 4px rgba(0,0,0,.15); +} +.card-shadow::after { + content: ""; + position: absolute; + inset: 0; + box-shadow: 0 0 0 1px rgba(255,255,255,.2) inset; + border-radius: 24px; + pointer-events: none; + transition: box-shadow .2s; +} +.card-shadow:hover::after, +.card-shadow:focus-within::after { + box-shadow: 0 0 0 2px rgba(255,255,255,.25) inset; +} +.card { + /*padding-bottom: 16px;*/ + border-radius: 24px; + background-color: var(--bg-color); + color: var(--text-color); + overflow: hidden; +} + +.card > textarea { /* TODO */ + display: block; + width: 100%; + padding: 16px 24px; + border: none; + resize: none; + outline: none; + font-size: 16px; + line-height: 24px; + letter-spacing: .3px; + background: transparent; + color: inherit; +} |