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