From 86f3c2cd59432a00121af015c505499a57edf568 Mon Sep 17 00:00:00 2001 From: Benjamin Auder <benjamin.auder@somewhere> Date: Sat, 13 Nov 2021 13:23:14 +0100 Subject: [PATCH] Better styles --- app.js | 148 +++++++++++++++-------- common.css | 344 +++++++++++++++++++++++++++++++++++++++++++++++++---- index.html | 158 ++++++++++++++---------- 3 files changed, 512 insertions(+), 138 deletions(-) diff --git a/app.js b/app.js index 21d205c..56ba25a 100644 --- a/app.js +++ b/app.js @@ -30,6 +30,22 @@ if (!localStorage.getItem("name")) const sid = localStorage.getItem("sid"); $.getElementById("myName").value = localStorage.getItem("name"); +// "Material" input field name +let inputName = document.getElementById("myName"); +let formField = document.getElementById("ng-name"); +const setActive = (active) => { + if (active) formField.classList.add("form-field--is-active"); + else { + formField.classList.remove("form-field--is-active"); + inputName.value === "" ? + formField.classList.remove("form-field--is-filled") : + formField.classList.add("form-field--is-filled"); + } +}; +inputName.onblur = () => setActive(false); +inputName.onfocus = () => setActive(true); +inputName.focus(); + ///////// // Utils @@ -39,10 +55,15 @@ function setName() { // Turn a "tab" on, and "close" all others function toggleVisible(element) { - for (elt of document.querySelectorAll('body > div')) { + for (elt of document.querySelectorAll('main > div')) { if (elt.id != element) elt.style.display = "none"; else elt.style.display = "block"; } + if (element.id == "newGame") { + // Workaround "superposed texts" effect + inputName.focus(); + inputName.blur(); + } } let seek_vname; @@ -89,47 +110,64 @@ function toggleStyle(e, word) { let options; function prepareOptions(Rules) { options = {}; - let optHtml = ""; - for (let select of Rules.Options.select) { - optHtml += ` - <label for="var_${select.variable}">${select.label}</label> - <select id="var_${select.variable}" data-numeric="1">`; - for (option of select.options) { - const defaut = option.value == select.defaut; - optHtml += `<option value="${option.value}"`; - if (defaut) optHtml += 'selected="true"'; - optHtml += `>${option.label}</option>`; + let optHtml = Rules.Options.select.map(select => { return ` + <div class="option-select"> + <label for="var_${select.variable}">${select.label}</label> + <div class="select"> + <select id="var_${select.variable}" data-numeric="1">` + + select.options.map(option => { return ` + <option + value="${option.value}" + ${option.value == select.defaut ? " selected" : ""} + > + ${option.label} + </option>`; + }).join("") + ` + </select> + <span class="focus"></span> + </div> + </div>`; + }).join(""); + optHtml += Rules.Options.check.map(check => { + return ` + <div class="option-check"> + <label class="checkbox"> + <input id="var_${check.variable}" + type="checkbox"${check.defaut ? " checked" : ""}/> + <span class="spacer"></span> + <span>${check.label}</span> + </label> + </div>`; + }).join(""); + if (Rules.Options.styles.length >= 1) { + optHtml += '<div class="words">'; + let i = 0; + const stylesLength = Rules.Options.styles.length; + while (i < stylesLength) { + optHtml += '<div class="row">'; + for (let j=i; j<i+4; j++) { + if (j == stylesLength) break; + const style = Rules.Options.styles[j]; + optHtml += + `<span onClick="toggleStyle(event, '${style}')">${style}</span>`; + } + optHtml += "</div>"; + i += 4; } - optHtml += '</select>'; - } - for (let check of Rules.Options.check) { - optHtml += ` - <label for="var_${check.variable}">${check.label}</label> - <input id="var_${check.variable}" - type="checkbox"`; - if (check.defaut) optHtml += 'checked="true"'; - optHtml += '/>'; + optHtml += "</div>"; } - if (Rules.Options.styles.length >= 1) optHtml += '<p class="words">'; - for (let style of Rules.Options.styles) { - optHtml += ` - <span class="word" onClick="toggleStyle(event, '${style}')"> - ${style} - </span>`; - } - if (Rules.Options.styles.length >= 1) optHtml += "</p>"; $.getElementById("gameOptions").innerHTML = optHtml; } function getGameLink() { const vname = $.getElementById("selectVariant").value; const color = $.getElementById("selectColor").value; - for (const select of $.querySelectorAll("#gameOptions > select")) { + for (const select of $.querySelectorAll("#gameOptions select")) { let value = select.value; if (select.attributes["data-numeric"]) value = parseInt(value, 10); options[ select.id.split("_")[1] ] = value; } - for (const check of $.querySelectorAll("#gameOptions > input")) + for (const check of $.querySelectorAll("#gameOptions input")) options[ check.id.split("_")[1] ] = check.checked; send("creategame", { vname: vname, @@ -143,28 +181,36 @@ const fillGameInfos = (gameInfos, oppIndex) => { .then(res => res.text()) .then(txt => { let htmlContent = ` - <p> - <strong>${gameInfos.vdisp}</strong> - <span>vs. ${gameInfos.players[oppIndex].name}</span> - </p> - <hr> - <p>`; - htmlContent += - Object.entries(gameInfos.options).map(opt => { - return ( - '<span class="option">' + - (opt[1] === true ? opt[0] : `${opt[0]}:${opt[1]}`) + - '</span>' - ); - }) - .join(", "); + <div class="players-info"> + <p> + <span class="bold">${gameInfos.vdisp}</span> + <span>vs. ${gameInfos.players[oppIndex].name}</span> + </p> + </div>`; + const options = Object.entries(gameInfos.options); + if (options.length > 0) { + htmlContent += '<div class="options-info">'; + let i = 0; + while (i < options.length) { + htmlContent += '<div class="row">'; + for (let j=i; j<i+4; j++) { + if (j == options.length) break; + const opt = options[j]; + htmlContent += + '<span class="option">' + + (opt[1] === true ? opt[0] : `${opt[0]}:${opt[1]}`) + " " + + '</span>'; + } + htmlContent += "</div>"; + i += 4; + } + htmlContent += "</div>"; + } htmlContent += ` - </p> - <hr> - <div class="rules"> - ${txt} - </div> - <button onClick="toggleGameInfos()">Back to game</button>`; + <div class="rules">${txt}</div> + <div class="btn-wrap"> + <button onClick="toggleGameInfos()">Back to game</button> + </div>`; $.getElementById("gameInfos").innerHTML = htmlContent; }); }; diff --git a/common.css b/common.css index 638a588..abaf91f 100644 --- a/common.css +++ b/common.css @@ -1,29 +1,125 @@ +/* CSS reset */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + body { margin: 0; - text-align: center; + /*text-align: center;*/ background-color: #f8f8f8; font-family: Arial, Verdana, Tahoma, sans-serif; } -#gameInfos, #boardContainer, #gameStopped, #pendingSeek, #pendingRematch, #newGameForm { +main { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex-wrap: nowrap; + font-size: 1.25rem; +} + +main > div { + margin-top: 25vh; + min-height: 500px; + min-width: 320px; + /*max-width: 800px;*/ /*unnecessary*/ +} + +@media (max-height: 800px) { + main > div { + margin-top: 30px; + } +} + +#boardContainer { + margin: 0; + padding: 0; + border: none; +} + +h1 { + font-size: 2rem; + font-weight: bold; + text-align: center; + display: block; + margin: 10px 0; +} + +#gameInfos, +#boardContainer, +#gameStopped, +#pendingSeek, +#pendingRematch, +#newGameForm { display: none; } +.bold { + font-weight: bold; +} + +#gameInfos > .players-info { + text-align: center; +} + +#gameInfos > .options-info { + text-align: center; + color: #757575; + margin-bottom: 15px; +} + +#gameInfos > div { + margin: 10px 0; +} + +#gameInfos > .rules { + color: #732E6C; +} + +#gameInfos > .rules > p, +#gameInfos > .rules > ul, +#gameInfos > .rules > ol { + margin: 10px 0; +} + #gameStopped > h1 { margin-bottom: 10px; } -/* Sticky footer */ -footer { +/* "Sticky footer" */ +#footer { position: absolute; bottom: 0; left: 0; right: 0; height: 50px; + text-align: center; +} + +a.left-link { + margin-right: 25px; +} +a.right-link { + margin-left: 25px; +} + +#footer a > img { + height: 1.2em; + display: inline-block; + transform: translateY(3px); } button { - background-color: green; + background-color: #757575; border: none; color: white; padding: 10px 15px; @@ -36,14 +132,15 @@ button { margin: 15px 0; } -button:hover { - background-color: darkblue; +button:hover, button.block-btn:hover { + background-color: #b11adc; } button.block-btn { display: block; - margin: 30px auto 20px auto; - font-size: 1.5em; + background-color: #01786F; + margin: 0 auto 20px auto; + font-size: 2rem; padding: 15px 32px; } @@ -58,25 +155,32 @@ button.block-btn { left: calc(100% - 25px); top: 0; } - #upLeftInfos > img, #upRightStop > img { width: 25px; cursor: pointer; } -#ng-select { - margin-bottom: 20px; +@media (max-width: 767px) { + #upRightStop { + left: calc(100% - 35px); + } + #upLeftInfos > img, #upRightStop > img { + width: 35px; + } } -#ng-name { - /* TODO */ +#ng-select { + margin-bottom: 20px; } /* Options when starting custom game */ -p.words { +.words { line-height: 0.9em; } -.word { +.words > .row { + margin: 0; +} +.words span { cursor: pointer; padding: 3px; display: inline-block; @@ -86,15 +190,41 @@ p.words { background-color: lightblue; } +#gameOptions { + text-align: center; +} + +.option-select, .option-check { + margin: 15px 0; +} + +.btn-wrap { + text-align: center; +} + +#gameLink { + width: inherit; + text-align: center; +} + +a { + text-decoration: none; +} + /* Game link div + custom game "button" */ -#gameLink span, #gameLink a, #playCustom { - text-decoration: underline; - color: blue; +#gameLink span, #gameLink a, #footer a { + padding-bottom: 1px; + border-bottom: 1px dotted darkgrey; + color: darkred; +} + +#gameLink span { + display: inline-box; cursor: pointer; } -#selectVariant { - margin-right: 15px; +#gameLink > p { + margin: 10px 0; } /* Board container (without reserves) */ @@ -164,11 +294,181 @@ piece { cursor: pointer; } + +/* https://moderncss.dev/custom-select-styles-with-pure-css/ */ +:root { + --select-border: #777; + --select-focus: #b11adc; + --select-arrow: var(--select-border); +} + +select { + appearance: none; + background-color: transparent; + border: none; + padding: 0 1em 0 0; + margin: 0; + width: 100%; + font-family: inherit; + font-size: inherit; + cursor: inherit; + line-height: inherit; + z-index: 1; + outline: none; +} + +.select { + display: grid; + grid-template-areas: "select"; + align-items: center; + position: relative; + min-width: 15ch; + max-width: 30ch; + border: 1px solid var(--select-border); + border-radius: 0.25em; + padding: 0.25em 0.5em; + font-size: 1.25rem; + cursor: pointer; + line-height: 1.1; + background-color: #fff; + background-image: linear-gradient(to top, #f9f9f9, #fff 33%); + width: 100%; + margin: auto; +} + +select, .select::after { + grid-area: select; +} + +.select::after { + content: ""; + justify-self: end; + width: 0.8em; + height: 0.5em; + background-color: var(--select-arrow); + clip-path: polygon(100% 0%, 0 0%, 50% 100%); +} + +select:focus + .focus { + position: absolute; + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + border: 2px solid var(--select-focus); + border-radius: inherit; +} + + +/* https://auralinna.blog/post/2018/how-to-create-material-design-like-form-text-fields/ */ +.form-field { + display: block; + margin-bottom: 16px; +} +.form-field--is-active .form-field__control::after { + border-bottom: 2px solid #b11adc; + transform: scaleX(150); +} +.form-field--is-active .form-field__label { + color: #b11adc; + font-size: 0.75rem; + transform: translateY(-14px); +} +.form-field--is-filled .form-field__label { + font-size: 0.75rem; + transform: translateY(-14px); +} +.form-field__label { + display: block; + font-size: 1.2rem; + font-weight: normal; + left: 0; + margin: 0; + padding: 18px 12px 0; + position: absolute; + top: 0; + transition: all 0.4s; + width: 100%; +} +.form-field__control { + background: #eee; + border-radius: 8px 8px 0 0; + overflow: hidden; + position: relative; + width: 100%; +} +.form-field__control::after { + border-bottom: 2px solid #b11adc; + bottom: 0; + content: ""; + display: block; + left: 0; + margin: 0 auto; + position: absolute; + right: 0; + transform: scaleX(0); + transition: all 0.4s; + width: 1%; +} +.form-field__input { + appearance: none; + background: transparent; + border: 0; + border-bottom: 1px solid #999; + color: #333; + display: block; + font-size: 1.2rem; + margin-top: 24px; + outline: 0; + padding: 0 12px 10px 12px; + width: 100%; +} + + +/* https://dev.to/kallmanation/styling-a-checkbox-with-only-css-3o3p */ +label.checkbox > input[type="checkbox"] { + display: none; +} +label.checkbox > input[type="checkbox"] + *::before { + content: ""; + display: inline-block; + vertical-align: bottom; + margin-bottom: 3px; + width: 1.1rem; + height: 1.1rem; + border-radius: 10%; + border-style: solid; + border-width: 0.1rem; + border-color: gray; +} + +label.checkbox > input[type="checkbox"]:checked + *::before { + content: "â"; + font-size: 1.1rem; + /*padding:10px;*/ + color: white; + text-align: center; + background: teal; + border-color: teal; +} +label.checkbox > input[type="checkbox"]:checked + * { + color: teal; +} + +/*label.checkbox { + color: teal; +}*/ +label.checkbox > span.spacer { + width: 10px; + content: " "; +} + + /* https://theanam.github.io/css-only-loaders/ ("hour-glass") */ :root{ --loader-width: 70px; --loader-height: 70px; - --loader-color-primary: #141D58; + --loader-color-primary: #01786F; --loader-color-secondary: #EEE; --line-width: 3px; --animation-duration: 3s; diff --git a/index.html b/index.html index 12253ed..b30f6cf 100644 --- a/index.html +++ b/index.html @@ -7,76 +7,104 @@ <meta name="viewport" content="width=device-width, initial-scale=1"/> <link id="_common_css" - rel="stylesheet" href="./common.css"/> + rel="stylesheet" href="/common.css"/> </head> <body> - <div id="gameInfos"></div> - <div id="boardContainer"></div> - <div id="gameStopped"> - <h1>Game over</h1> - <button id="rematchBtn" - onClick="sendRematch()"> - Rematch - </button> - <button class="cancel-something" - onClick="cancelRematch()"> - Close - </button> - </div> - <div id="pendingRematch"> - <div class="loader hour-glass"></div> - <button class="cancel-something" - onClick="cancelRematch()"> - Cancel - </button> - </div> - <div id="newGame"> - <button id="seekGame" - class="block-btn" - onClick="seekGame()"> - Play! - </button> - <div id="ng-select"> - <select id="selectVariant"></select> - <span id="playCustom" - onClick="showNewGameForm()"> - Customize - </span> + <main> + <div id="gameInfos"></div> + <div id="boardContainer"></div> + <div id="gameStopped"> + <h1>Game over</h1> + <div class="btn-wrap"> + <button id="rematchBtn" + onClick="sendRematch()"> + Rematch + </button> + <button class="cancel-something" + onClick="cancelRematch()"> + Close + </button> + </div> </div> - <div id="ng-name"> - <label for="myName">Name:</label> - <input id="myName" - type="text" - onChange="setName()"/> + <div id="pendingRematch"> + <div class="loader hour-glass"></div> + <div class="btn-wrap"> + <button class="cancel-something" + onClick="cancelRematch()"> + Cancel + </button> + </div> </div> - <footer> - <a href="https://discord.gg/QC7Aa5WMYp">Discord</a> - / - <a href="https://github.com/yagu0/xogo">GitHub</a> - </footer> - </div> - <div id="pendingSeek"> - <div class="loader hour-glass"></div> - <button class="cancel-something" - onClick="cancelSeek()"> - Cancel - </button> - </div> - <div id="newGameForm"> - <fieldset> - <label for="selectColor">Play as</label> - <select id="selectColor"> - <option value=""></option> - <option value="w">Player 1</option> - <option value="b">Player 2</option> - </select> - </fieldset> - <fieldset id="gameOptions"></fieldset> - <button onClick="getGameLink()">Get link</button> - <button onClick="backToNormalSeek()">Cancel</button> - <div id="gameLink"></div> - </div> + <div id="newGame"> + <button id="seekGame" + class="block-btn" + onClick="seekGame()"> + Play! + </button> + <div id="ng-select"> + <div class="select"> + <select id="selectVariant"></select> + <span class="focus"></span> + </div> + <div class="btn-wrap"> + <button id="playCustom" + onClick="showNewGameForm()"> + Customize + </button> + </div> + </div> + <div id="ng-name" class="form-field"> + <div class="form-field__control"> + <label for="myName" class="form-field__label">Name</label> + <input id="myName" + type="text" + required + class="form-field__input" + onChange="setName()"/> + </div> + </div> + <div id="footer"> + <p> + <a class="left-link" href="https://discord.gg/QC7Aa5WMYp"> + <img src="/assets/discord.svg"/> + Discord + </a> + <a class="right-link" href="https://github.com/yagu0/xogo"> + GitHub + <img src="/assets/github.svg"/> + </a> + </p> + </div> + </div> + <div id="pendingSeek"> + <div class="loader hour-glass"></div> + <div class="btn-wrap"> + <button class="cancel-something" + onClick="cancelSeek()"> + Cancel + </button> + </div> + </div> + <div id="newGameForm"> + <fieldset> + <div class="select"> + <select id="selectColor"> + <option value="">Any color</option> + <option value="w">Player 1</option> + <option value="b">Player 2</option> + </select> + <span class="focus"></span> + </div> + </fieldset> + <fieldset id="gameOptions"></fieldset> + <div class="btn-wrap"> + <button onClick="getGameLink()">Get link</button> + <button onClick="backToNormalSeek()">Cancel</button> + </div> + <div id="gameLink"></div> + </div> + </main> <script src="/parameters.js"></script> <script src="/variants.js"></script> -- 2.44.0