Browse Source

with tool tip ?

tripeur 1 week ago
parent
commit
dbbdf7676d

+ 6 - 0
index.html

@@ -4,6 +4,12 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="preconnect" href="https://fonts.googleapis.com" />
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap"
+      rel="stylesheet"
+    />
     <title>factorio-clockmaster</title>
   </head>
   <body>

File diff suppressed because it is too large
+ 625 - 31
package-lock.json


+ 3 - 0
package.json

@@ -13,6 +13,9 @@
     "factorio-process": "ts-node -r tsconfig-paths/register scripts/factorio-process-data.ts"
   },
   "dependencies": {
+    "@emotion/react": "^11.14.0",
+    "@emotion/styled": "^11.14.1",
+    "@mui/material": "^7.3.5",
     "react": "^19.2.0",
     "react-dom": "^19.2.0"
   },

BIN
public/assets/close.png


BIN
public/assets/search.png


+ 0 - 0
src/assets/data/2.0/icon_128.webp → public/data/2.0/icon_128.webp


+ 0 - 0
src/assets/data/2.0/icon_56.webp → public/data/2.0/icon_56.webp


+ 0 - 0
src/assets/data/2.0/icon_64.webp → public/data/2.0/icon_64.webp


BIN
public/snapshot.png


+ 0 - 1
public/vite.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 21 - 2
scripts/factorio-process-data.ts

@@ -138,12 +138,31 @@ export async function processData(outputFolder: string) {
       order: vs.order,
     });
   }
+  for (let quality of qualityLevels) {
+    signals.push({
+      name: quality.name,
+      subgroup: quality.subgroup ?? "",
+      icon: quality.icon,
+      order: quality.order,
+    });
+  }
   const signalGroup: Array<Group<Signal>> = groupPrototypes(signals, data, false);
 
-  const output: ProcessedData = { qualityLevels, machines, modules, recipeCategories, recipeGroup, signalGroup };
+  const output: ProcessedData = {
+    qualityLevels,
+    machines,
+    modules,
+    recipeCategories,
+    recipeGroup,
+    signalGroup,
+  };
   fs.mkdirSync(outputFolder, { recursive: true });
 
-  fs.writeFileSync(path.resolve(outputFolder, "data.json"), JSON.stringify(output, null, 2), "utf-8");
+  fs.writeFileSync(
+    path.resolve(outputFolder, "data.json"),
+    JSON.stringify(output, null, 2),
+    "utf-8"
+  );
   const icons = new Set<string>();
   walkForIcons(output, icons);
   const iconsList = Array.from(icons).sort();

+ 596 - 27
src/App.css

@@ -4,39 +4,608 @@
   padding: 2rem;
   text-align: center;
 }
+.panel-button {
+  border: none;
+  cursor: pointer;
+  position: relative;
+  background-color: #313031;
+  height: 24px;
+  aspect-ratio: 1;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px 8px 4px -8px #fff,
+    inset 0px -8px 2px -8px #432400,
+    0px 0px 4px 0px #000;
+}
+.panel-button:active,
+.panel-button.active {
+  box-shadow:
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px -9px 2px -8px #fff,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+  filter: none;
+  outline: 0;
+}
 
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-  transition: filter 300ms;
+.panel-button.hover,
+.panel-button:focus,
+.panel-button:hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px -8px 4px -8px hsl(36, 94%, 20%),
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
 }
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
+input:focus {
+  border: none;
+  filter: drop-shadow(0 0 2px #f9b44b);
 }
-.logo.react:hover {
-  filter: drop-shadow(0 0 2em #61dafbaa);
+.button {
+  background-color: #8e8e8e;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #e3e3e3,
+    inset 0px 10px 2px -8px #282828,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: left;
 }
-
-@keyframes logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
+.button.icon {
+  height: fit-content;
+  min-width: auto;
+  padding: 0;
+  margin: 0;
 }
-
-@media (prefers-reduced-motion: no-preference) {
-  a:nth-of-type(2) .logo {
-    animation: logo-spin infinite 20s linear;
-  }
+.flex-column .button {
+  margin-right: 0;
 }
-
-.card {
-  padding: 2em;
+.button i {
+  margin-right: 0px;
+}
+.button:hover,
+.button.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.button:active,
+.button.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px 9px 2px -8px #563a10,
+    inset 0px -9px 2px -8px #563a10,
+    inset 0px -8.5px 0px -8px #563a10,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+  filter: none;
+  outline: 0;
+}
+.button.disabled,
+.button.disabled:hover,
+.button.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #3d3d3d;
+  color: #818181;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #818181,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+.button-green {
+  background-color: #5eb663;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #95df99,
+    inset 0px 10px 2px -8px #163218,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: left;
+}
+.flex-column .button-green {
+  margin-right: 0;
+}
+.button-green i {
+  margin-right: 0px;
+}
+.button-green:hover,
+.button-green:focus,
+.button-green.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #cdf1cf,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #34be3c;
+  background-color: #92e897;
+  filter: drop-shadow(0 0 2px #34be3c);
+}
+.button-green:active,
+.button-green.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #3f5024,
+    inset 8px 0px 4px -8px #3f5024,
+    inset -8px 0px 4px -8px #3f5024,
+    inset -8px 0px 4px -8px #3f5024,
+    inset 0px 9px 2px -8px #3f5024,
+    inset 0px -9px 2px -8px #3f5024,
+    inset 0px -8.5px 0px -8px #3f5024,
+    0px 0px 4px 0px #000;
+  background-color: #cfdf93;
+  filter: none;
+  outline: 0;
+}
+.button-green.disabled,
+.button-green.disabled:hover,
+.button-green.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #002b02;
+  color: #376d3b;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #376d3b,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+.button-green-right {
+  background-color: #5eb663;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #95df99,
+    inset 0px 10px 2px -8px #163218,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: right;
+}
+.button-green-right::after {
+  content: "";
+  position: absolute;
+  height: 100%;
+  width: 24px;
+  right: -10px;
+  top: 0;
+  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAABACAMAAADCiszgAAABDlBMVEUAAAASMBURLxJJgUkAKxNKiU1Ih01msGpMjVFNjlFLik9ToFdLik9Lik9Lik5Yq1wIHgoOKhGAyoVNjVFipmY3cDpQkVVrsHB2vHlXmFyK1Y5/yoRvuHNcs2E7eT8bQB1doGFAe0IJIQxNjFJDgkhjrGgGHgxetmNcs2FasF9RllVVpllXqlxRnlVQm1RYrV0cQh5SoVcUNRdQk1VLllA8e0AXOhpQkFM0bDcqXC4jTiVRmFVFiUkIHgpTpFhNmVJEh0hNj1EtYTCI1I16w35utnJXn1tNk1FJkk1HjUs/fkI3czsyajYpWSsZPhxrsW9ZnV1XmVo6dz41bzgmUygkUCYiTSUTMRUPKxEOKRFdNbmJAAAAJ3RSTlMAVisODG41Bvb75Zfc1dGXlwb98/7y8fDS0JeXl5eXl25sVjU1MSucyRWzAAABMElEQVQ4y23M51LCUBiE4QWRKFFB7L37EUJTithI6FXs7f5vxDgok3Oy+/OdZxbGdrOZ3DyDOmMn2WptHM5odbfdflzdOtGq2ek8Pa/Pqtg0nVK3eLUUVWrcvsil08UVFaeszG/WsKQsT+tYJJW3s7caFpl8FOf8WKZ40YeFYqFYKBaKhWLx44V/LBQLxaLg+T8sFAvFouFYdFIZFoqFYqFYArjuYQngeuwUEsB19wii4lqu2x+s6dXO9QavDa1atuMOG2+Bh1658VFQq5XJutX3cQjaQal8XbgMQ6N31ZtKKAJKQSkoBaWgFJSCUlAKSkEpKAWloBSUglJQCkpBKSgFpdiz8rUARTxec9yRRmGYTv/lQaMw9ocjL6oUywfjyufXdxhqTdx7O46QmjhX4w9Fq5Q/daB+bwAAAABJRU5ErkJggg==");
+  background-repeat: no-repeat;
+  background-position: 100% 0%;
+  background-size: contain;
+}
+.button-green-right i {
+  margin-right: 0px;
+}
+.button-green-right:hover,
+.button-green-right:focus,
+.button-green-right.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  filter: brightness(1.3) drop-shadow(0 0 2px #34be3c);
+}
+.button-green-right:active,
+.button-green-right.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #3f5024,
+    inset 8px 0px 4px -8px #3f5024,
+    inset 0px 9px 2px -8px #3f5024,
+    inset 0px -9px 2px -8px #3f5024,
+    inset 0px -8.5px 0px -8px #3f5024,
+    0px 0px 4px 0px #000;
+  background-color: #cfdf93;
+  filter: none;
+  outline: 0;
+}
+.button-green-right:active::after,
+.button-green-right.active::after {
+  content: "";
+  position: absolute;
+  height: 100%;
+  width: 24px;
+  right: -10px;
+  top: 0;
+  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAABACAMAAADCiszgAAACBFBMVEUAAACWq1clPgwnPRChwmOhv18iOQ4iOhEdLwwhNBAkOA8jOQ4iNBAkPBSJolSUs1t1ij4kNA4MHwAhLwwhMw0hMQ+Fn0+Jp16Xt14aLA0kOxQlPBR5jkAnOBMqOhYVJQYOIgMcLgsLHQIoPRgZKQoiOBEkOxF4jj6SsFkLGwMlORdOYCRleS4/UB9ohU8xRR3P35PD5ovO3pHM3ZDK243A1IXG2Iq4zX7N4JLM4ZHL4pDJ4o+ev2nH44/F5I6guGoxRxitw3WBmlNxiUZOZS294YW12H0+VCKqzXOcvGWSqmBedjmevmSRqV9geDoQIQIJGgLJ346v0nquxHUmMSMEEQTF4Y3B44mtzom+3IegwoSVt3+etWlwj2KKpld+llJ/mVFuhUQ6TiLH2oq704nA24e21Ya73oW73oOz1X210n2oy3Klxm6lvW6Lq22YuWWaumSUs2KYt2CQpl6Rr1yLqFuNplqBnFF0jEN2kEJeci9MYi1LYCtBVCU9UyItQBguQxccKBLK3Y3C2onD1YfC2IWrzYS22YC6z32bvny1ynu10HigwnaoyHOpx22brmmjv2WWtWGMq2GPr196lV2AnFprh1iSrlZ4k1ZuilaLp1F4kEpyikhmfUF1jEBdcD5acjdUZzdtgjZacDNNYi5HWy0zQyg4TCYoNx4pOxsmMRogMA46/ea2AAAAMHRSTlMADhT7YTLolBhR/O2XTTk1Ev779vKcbWxlVk5MPv76+Ovpp6CclpRzZ15aVU5JRxpOaUmtAAAB4klEQVQ4y23Q5VuTcRTG8VtRBKQEAxS7++BgM6YOwYBJbOo6AQnp7rK7u5v6J3keHsbO2fW7X36vz4tzHVRryzmIhNVoy9mVArkb2mp25ENuZm5qqnr7ngT849e/6dn5namyvugJf/trS0tfKWqxpa0/OjqZnCFq0d3HgbB9PGmfwCeKiksqS8tsyVt4JZPZcv6C/XLSGo5JiUmJKY4zWFViUmJSYlrCFRpeu5lXA1cxTEpMAu9dwiRxarzGcezPtIyvVXwts603MAk8npaeYlSGS0cmDUwcR+y16zbomDiujI5O5ObrleNA+KeBieOSnujIRO4hrTJcZwlEqsa27j6uV3HGlz+1246CeK572NY/NDx2WNbb9568/Px9+Iio5Lh56/mHod8Fop7UamtocH+2qKcc3mbrwGABwOlpl9fZFdmYDXB6xtX4rG8gEwCnZ90PrCGdgtNzbmdXSKfgtLCptffACr1yWtjc0ZsFfZxebHmVt0jB6SVrt0HB6ZXOVQaFoOWrYYzT8hgFp1djFEoKJYWSQkmhpFBSKCmUFCaTw+VuaukUFGbzHW+js6NbUNTXNzQ4rW/yBIXH47n/9HVfFsR8Pt+j9nebJIXf729/+zETcsFg8P2nRIrr2v4fg9wCPnjQTyTwM1IAAAAASUVORK5CYII=");
+  background-repeat: no-repeat;
+  background-position: 100% 0%;
+  background-size: contain;
+}
+.button-green-right.disabled,
+.button-green-right.disabled:hover,
+.button-green-right.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #002b02;
+  color: #376d3b;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #376d3b,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+.button-red {
+  background-color: #fe5a5a;
+  padding: 10px 12px 10px 12px;
+  font-size: 100%;
+  text-align: left;
+  color: #000;
+  font-weight: 600;
+  display: inline-block;
+  vertical-align: baseline;
+  min-width: 128px;
+  border: none;
+  line-height: inherit;
+  white-space: nowrap;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 10px 2px -8px #fda1a1,
+    inset 0px 10px 2px -8px #8b0101,
+    inset 0px -9px 2px -8px #000,
+    0px 0px 4px 0px #000;
+  position: relative;
+  margin-right: 14px;
+  cursor: pointer;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  height: 36px;
+  text-align: left;
+}
+.flex-column .button-red {
+  margin-right: 0;
+}
+.button-red i {
+  margin-right: 0px;
+}
+.button-red:hover,
+.button-red:focus,
+.button-red.hover {
+  color: #000;
+  text-decoration: none;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #f8eaea,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #c35353;
+  background-color: #ff9b9b;
+  filter: drop-shadow(0 0 2px #c35353);
+}
+.button-red:active,
+.button-red.active {
+  position: relative;
+  padding-top: 12px;
+  padding-bottom: 8px;
+  vertical-align: -2px;
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #642323,
+    inset 8px 0px 4px -8px #642323,
+    inset -8px 0px 4px -8px #642323,
+    inset -8px 0px 4px -8px #642323,
+    inset 0px 9px 2px -8px #642323,
+    inset 0px -9px 2px -8px #642323,
+    inset 0px -8.5px 0px -8px #642323,
+    0px 0px 4px 0px #000;
+  background-color: #fca8a8;
+  filter: none;
+  outline: 0;
+}
+.button-red.disabled,
+.button-red.disabled:hover,
+.button-red.disabled:active {
+  padding-top: 10px;
+  padding-bottom: 10px;
+  cursor: default;
+  vertical-align: 0;
+  background-color: #571f1f;
+  color: #8c4e4e;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -6px 4px -8px #8c4e4e,
+    inset 0px -8px 4px -8px #000,
+    0px 0px 4px 0px #000;
+  filter: none;
+}
+input[type="text"],
+input[type="password"],
+input[type="email"],
+input[type="url"],
+input[type="search"],
+textarea {
+  vertical-align: baseline;
+  font-family: inherit;
+  line-height: 1.2;
+  font-size: 105%;
+  height: 36px;
+  max-width: 100%;
+  background: #8e8e8e;
+  border-radius: 4px;
+  padding: 6px;
+  border: none;
+  box-shadow:
+    inset 0px 4px 1px -2px #000,
+    inset 0px -4px 1px -2px #c5c5c5,
+    inset 2px 0px 1px 0px #5f5f5f,
+    inset -2px 0px 1px 0px #5f5f5f,
+    inset 0px -2px 2px 0px #5f5f5f,
+    0px 0px 4px 1px #2e2521;
+}
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus,
+input[type="search"]:focus,
+textarea:focus {
+  outline: none;
+  background: #f0dab4;
+  box-shadow:
+    inset 0px 4px 2px -2px #000,
+    inset 0px -1px 1px 0px #74624b,
+    inset 0px -4px 2px -2px #e0e0e0,
+    inset 2px 0px 2px 0px #a6885c,
+    inset -2px 0px 2px 0px #a6885c,
+    0px 0px 4px 1px #2e2521;
+}
+textarea {
+  width: 100%;
+  height: 8.1em;
 }
 
-.read-the-docs {
-  color: #888;
+.checkbox-label {
+  margin-top: 8px;
+  margin-bottom: 8px;
+  display: flex;
+  align-content: center;
+  position: relative;
+  cursor: pointer;
+  line-height: 1.2;
+  break-inside: avoid;
+}
+.checkbox-label > div {
+  width: 100%;
+  padding-left: 8px;
+}
+.checkbox-label input {
+  position: relative;
+  top: -1px;
+  left: 0px;
+  width: 16px;
+  height: 15px;
+  padding: 6px;
+  position: absolute;
+  opacity: 0;
+  cursor: pointer;
+}
+.checkbox-label .checkbox {
+  margin: auto;
+  position: relative;
+  top: -1px;
+  left: 0px;
+  width: 16px;
+  height: 15px;
+  padding: 6px;
+  background: #8e8e8e;
+  box-shadow:
+    inset 0px 4px 2.5px -2.5px #111,
+    inset 2px 0px 2px 0px #323232,
+    inset -2px 0px 2px 0px #323232,
+    inset 0px -1px 1px 0px #fff,
+    inset 0px -4px 2px -2px #8e8e8e,
+    0px 0px 4px 1px #2e2521;
+}
+.checkbox-label .checkbox:hover,
+.checkbox-label .checkbox.hover,
+.checkbox-label .checkbox:active,
+.checkbox-label .checkbox.active,
+.checkbox-label input:focus ~ .checkbox {
+  background: #e39827;
+  box-shadow:
+    inset 0px 4px 2px -2px #412a07,
+    inset 2px 0px 2px 0px #5a3c10,
+    inset -2px 0px 2px 0px #5a3c10,
+    inset 0px -2px 2px 0px #d0ae79,
+    inset 0px -2px 4px 0px #c78627,
+    0px 0px 4px 1px #786b4f;
+}
+.checkbox-label .checkbox:active,
+.checkbox-label .checkbox.active {
+  background: #f3c98e;
+  box-shadow:
+    inset 0px 4px 2px -2px #412a07,
+    inset 2px 0px 2px 0px #5a3c10,
+    inset -2px 0px 2px 0px #5a3c10,
+    inset 0px -2px 2px 0px #d0ae79,
+    inset 0px -2px 4px 0px #c78627,
+    inset 0px 1px 2.5px 4.5px #e39827,
+    0px 0px 4px 1px #786b4f;
+}
+.checkbox-label .checkbox:disabled,
+.checkbox-label .checkbox.disabled {
+  background: #313031;
+  box-shadow:
+    inset 0px 4px 2.5px -2.5px #111,
+    inset 2px 0px 2px 0px #323232,
+    inset -2px 0px 2px 0px #323232,
+    inset 0px -1px 1px 0px #666,
+    inset 0px -4px 2px -2px #8e8e8e,
+    0px 0px 4px 1px #2e2521;
+}
+.checkbox-label input:checked ~ .checkbox {
+  background: #e39827;
+}
+.checkbox-label input:checked ~ .checkbox:active,
+.checkbox-label input:checked ~ .checkbox.active {
+  background: #f3c98e;
+}
+.checkbox-label .checkbox::after {
+  position: absolute;
+  content: "";
+}
+.checkbox-label input:checked ~ .checkbox::after {
+  -webkit-transform: rotate(45deg) scale(1);
+  -ms-transform: rotate(45deg) scale(1);
+  transform: rotate(45deg) scale(1);
+  opacity: 1;
+  left: 6px;
+  top: 3px;
+  width: 2px;
+  height: 6px;
+  border: solid #282728;
+  border-width: 0 2.5px 2.5px 0;
+  background-color: transparent;
+  border-radius: 0;
+}
+.disable-checkbox-label input[type="checkbox"] {
+  position: relative;
+  opacity: 1;
+  -webkit-appearance: none;
+  appearance: none;
+  margin: 0;
+  margin-top: 2px;
+  width: 1.2em;
+  height: 1.2em;
+  cursor: pointer;
+  background-color: transparent;
+  background-repeat: no-repeat;
+  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNhNmE2YTZ9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
+}
+.disable-checkbox-label input[type="checkbox"]:hover {
+  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNmZmI1MzN9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
+}
+.disable-checkbox-label input[type="checkbox"]:checked {
+  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjQuMCBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZSAoQ29tbWVyY2lhbCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMyBGb250aWNvbnMsIEluYy4gLS0+PHN0eWxlPnN2Z3tmaWxsOiNmZmEyMDB9PC9zdHlsZT48cGF0aCBkPSJNMzY3LjIgNDEyLjVMOTkuNSAxNDQuOEM3Ny4xIDE3Ni4xIDY0IDIxNC41IDY0IDI1NmMwIDEwNiA4NiAxOTIgMTkyIDE5MmM0MS41IDAgNzkuOS0xMy4xIDExMS4yLTM1LjV6bTQ1LjMtNDUuM0M0MzQuOSAzMzUuOSA0NDggMjk3LjUgNDQ4IDI1NmMwLTEwNi04Ni0xOTItMTkyLTE5MmMtNDEuNSAwLTc5LjkgMTMuMS0xMTEuMiAzNS41TDQxMi41IDM2Ny4yek0wIDI1NmEyNTYgMjU2IDAgMSAxIDUxMiAwQTI1NiAyNTYgMCAxIDEgMCAyNTZ6Ii8+PC9zdmc+");
 }

+ 6 - 25
src/App.tsx

@@ -1,35 +1,16 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import "./App.css";
+import SelectMenu from "./assets/SelectRecipeMenu";
 
 function App() {
-  const [count, setCount] = useState(0)
-
   return (
     <>
-      <div>
-        <a href="https://vite.dev" target="_blank">
-          <img src={viteLogo} className="logo" alt="Vite logo" />
-        </a>
-        <a href="https://react.dev" target="_blank">
-          <img src={reactLogo} className="logo react" alt="React logo" />
-        </a>
-      </div>
       <h1>Vite + React</h1>
       <div className="card">
-        <button onClick={() => setCount((count) => count + 1)}>
-          count is {count}
-        </button>
-        <p>
-          Edit <code>src/App.tsx</code> and save to test HMR
-        </p>
+        <SelectMenu></SelectMenu>
       </div>
-      <p className="read-the-docs">
-        Click on the Vite and React logos to learn more
-      </p>
+      <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
     </>
-  )
+  );
 }
 
-export default App
+export default App;

+ 154 - 0
src/assets/SelectRecipeMenu.css

@@ -0,0 +1,154 @@
+.select-menu {
+  width: 382px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 8px;
+  background: #646464;
+  background-color: #313031;
+}
+
+.select-menu-header {
+  display: flex;
+  flex-direction: row;
+}
+.select-menu-header-title {
+  font-weight: 900;
+  font-stretch: 100%;
+  font-size: 19.2px;
+  color: #ffe6c0;
+}
+.select-menu-header-spacer {
+  flex-grow: 1;
+  cursor: grab;
+  margin: 2px 6px;
+  background: repeating-linear-gradient(
+    90deg,
+    #2b2b2b,
+    #2b2b2b 3px,
+    rgb(56, 55, 56) 3px,
+    rgb(56, 55, 56) 6px
+  );
+  box-shadow: inset 0 -4px 5px -2px #313031;
+}
+.select-menu-header-action {
+  display: inline-flex;
+  gap: 4px;
+  font-size: 12px;
+}
+.select-menu-header-action input[type="search"] {
+  font-size: 12px;
+  line-height: 1.1rem;
+  height: 1.5rem;
+}
+.select-menu-header-action img {
+  height: 18px;
+  aspect-ratio: 1;
+}
+.select-menu-group {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+.select-menu-group-icon {
+  background-color: rgb(140, 140, 140);
+  border: 1px solid #646464;
+}
+.select-menu-group-icon.active {
+  background-color: #f1be64;
+}
+.select-menu-group-icon:hover {
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.select-menu-content {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  background-color: #242324;
+  padding: 2px;
+  max-height: calc(40px * 5);
+  overflow-y: auto;
+}
+.select-menu-content-row {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 2px;
+}
+.select-menu-slot {
+  position: relative;
+  height: 36px;
+  width: 36px;
+  display: inline-flex;
+}
+.select-menu-icon {
+  background-color: #373737;
+  border-radius: 4px;
+  height: 36px;
+  width: 36px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow:
+    inset 0px -4px 0.5px -2px #121212,
+    inset 5px 0px 2px -2.5px #1f1f1f,
+    inset -3px 0px 1px -1.5px #292929,
+    inset 0px 4px 1px -2px #515050;
+}
+.select-menu-icon:hover,
+.select-menu-icon.hover {
+  color: #000;
+  outline: 0;
+  box-shadow:
+    inset 8px 0px 4px -8px #000,
+    inset -8px 0px 4px -8px #000,
+    inset 0px 9px 2px -8px #fff,
+    inset 0px 8px 4px -8px #000,
+    inset 0px -8px 4px -8px #000,
+    inset 0px -9px 2px -8px #432400,
+    0px 0px 4px 0px #000,
+    inset 0px 0px 4px 2px #f9b44b;
+  background-color: #e39827;
+  filter: drop-shadow(0 0 2px #f9b44b);
+}
+.select-menu-icon.active {
+  box-shadow:
+    inset 0px 10px 2px -8px #000,
+    inset 0px 9px 2px -8px #000,
+    inset 8px 0px 4px -8px #563a10,
+    inset 8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset -8px 0px 4px -8px #563a10,
+    inset 0px 9px 2px -8px #563a10,
+    inset 0px -9px 2px -8px #563a10,
+    inset 0px -8.5px 0px -8px #563a10,
+    0px 0px 4px 0px #000;
+  background-color: #f1be64;
+}
+
+.select-menu-slot-empty {
+  position: relative;
+  z-index: 0;
+  width: 100%;
+  margin: 8px;
+  background-color: #242324;
+  box-shadow: 0px 0px 2px #131112;
+}
+.select-menu-slot-empty::before {
+  position: relative;
+  content: "";
+  display: block;
+  width: 100%;
+  height: 0px;
+  box-shadow: 0px -0.5px 1.5px 1px #555250;
+}
+.select-menu-slot-empty::after {
+  position: relative;
+  content: "";
+  display: block;
+  width: 100%;
+  height: 0px;
+  margin-top: 100%;
+  box-shadow: 0px 0.5px 1.5px 1px #161414;
+}

+ 104 - 0
src/assets/SelectRecipeMenu.tsx

@@ -0,0 +1,104 @@
+import { useState, type CSSProperties, useMemo } from "react";
+import data from "./data/2.0/data.json";
+import Icon from "./icon";
+import "./SelectRecipeMenu.css";
+import Tooltip from "./Tooltip";
+type SelectMenuProps = {
+  className?: string;
+  style?: CSSProperties;
+};
+
+export type SubGroup<T> = {
+  name: string;
+  order?: string;
+  children: Array<T>;
+};
+const ITEM_PER_ROW = 10;
+
+interface SubGroupProps {
+  subgroup: SubGroup<{ name: string; icon: string }>;
+  selectedItem: string;
+  onSelectItem: (name: string) => void;
+}
+function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
+  const emptySlotsCounts = useMemo(() => {
+    const lastRowSlots = subgroup.children.length % ITEM_PER_ROW;
+    return lastRowSlots > 0 ? ITEM_PER_ROW - lastRowSlots : 0;
+  }, [subgroup]);
+  return (
+    <div className="select-menu-content-row">
+      {subgroup.children.map((o) => (
+        <Tooltip title={o.name} key={o.name}>
+          <div
+            className={"select-menu-icon " + (o.name == selectedItem ? "active" : "")}
+            onClick={() => onSelectItem(o.name)}
+          >
+            <Icon iconName={o.icon} size={30}></Icon>
+          </div>
+        </Tooltip>
+      ))}
+      {Array(emptySlotsCounts)
+        .fill(0)
+        .map((_, i) => (
+          <div className="select-menu-slot" key={i}>
+            <div className="select-menu-slot-empty"></div>
+          </div>
+        ))}
+    </div>
+  );
+}
+function SelectMenu({ style, className }: SelectMenuProps) {
+  const [category, setCategory] = useState(data.signalGroup[0].name);
+  const [item, selectItem] = useState("");
+  const [showSearch, setShowSearch] = useState(false);
+  const [search, setSearch] = useState("");
+  const content = useMemo(() => data.signalGroup.find((r) => r.name == category), [category]);
+  return (
+    <div style={style} className={"select-menu " + className}>
+      <div className="select-menu-header">
+        <div className="select-menu-header-title">Title</div>
+        <div className="select-menu-header-spacer"></div>
+        <div className="select-menu-header-action">
+          {showSearch && (
+            <input
+              type="search"
+              onChange={(evt) => setSearch(evt.target.value)}
+              value={search}
+            ></input>
+          )}
+          <button
+            className={"panel-button " + (showSearch ? "active" : "")}
+            onClick={() => setShowSearch(!showSearch)}
+          >
+            <img src="/assets/search.png" />
+          </button>
+          <button className="panel-button">
+            <img src="/assets/close.png" />
+          </button>
+        </div>
+      </div>
+      <div className="select-menu-group">
+        {data.signalGroup.map((g) => (
+          <div
+            className={"select-menu-group-icon " + (g.name == category ? "active" : "")}
+            onClick={() => setCategory(g.name)}
+            key={g.name}
+          >
+            <Icon iconName={g.icon} size={62}></Icon>
+          </div>
+        ))}
+      </div>
+      <div className="select-menu-content">
+        {content?.subGroup.map((subgroup) => (
+          <SubGroupRow
+            subgroup={subgroup}
+            selectedItem={item}
+            onSelectItem={selectItem}
+            key={subgroup.name}
+          ></SubGroupRow>
+        ))}
+      </div>
+    </div>
+  );
+}
+export default SelectMenu;

+ 19 - 0
src/assets/Tooltip.css

@@ -0,0 +1,19 @@
+.panel-tooltip-title {
+  background-color: #ffe6c0;
+  color: black;
+  font-weight: bold;
+  font-size: 107%;
+  word-wrap: normal;
+  word-break: keep-all;
+  border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYRpQ0NQSUNDIHByb2ZpbGUAACiRfZE9SMNAHMVfW6VSWhzaQcQhQ3Wy4BfiqFUoQoVQK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxNHJSdFFSvxfUmgR48FxP97de9y9A/zNKlPNnjFA1Swjk0oKufyqEHxFCFFEMI6oxEx9ThTT8Bxf9/Dx9S7Bs7zP/TkiSsFkgE8gnmW6YRFvEE9vWjrnfeIYK0sK8TnxqEEXJH7kuuzyG+eSw36eGTOymXniGLFQ6mK5i1nZUImniOOKqlG+P+eywnmLs1qts/Y9+QvDBW1lmes0h5DCIpYgQoCMOiqowkKCVo0UExnaT3r4Bx2/SC6ZXBUwciygBhWS4wf/g9/dmsXJCTcpnAR6X2z7YxgI7gKthm1/H9t26wQIPANXWsdfawIzn6Q3Olr8COjfBi6uO5q8B1zuAANPumRIjhSg6S8Wgfcz+qY8EL0FQmtub+19nD4AWeoqfQMcHAIjJcpe93h3X3dv/55p9/cDgjtyrWdvGYQAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmChMNMzGpuES2AAAA7VBMVEX/5sD8473437r13Ljy2bXs07Dr0q/oz6ziyafcw6PZwaHZwKDTvJzSu5zQuZrLtJbLtJXErpHAqo29qIu7pYi5o4a1ooi0oYexnoSynICxm3+tmoGsmX+ql36olXylk3ylk3qkk3uikXqij3agj3ifjXSii3KdjHWcjHabi3Wai3WdiHGZiXObiG+Zh3GXh3KYhGyXhG2UhG+Xg2yRf2mRfWaOfWiHdmCHclyCcFyCblp5aVV7aFN4Z1R1ZVJzY1ByYE5eUEBEPDFDOzFBOS8/Ny09Niw+NSo9NSs/NCohGxYQDgsIBgUBAQEAAAB0PBllAAAAAWJLR0RPbmZBSQAAAJxJREFUeF5lTkFOA0EMix3vUlClXjjzD/7/JCiI3RkHhVJVUF+cWHYcvM4xp6siAGYqBY+v6XIEwXyg5HHe57x4MgclfXxut9SaR+n0NvarZ1lOkj3GNRUYNmuzq9xo2ko9NCKiyWb8x70iEo3oywBIYSUN/nSBXCFSqvj9R73i6WV732cruRzXA3Two5/r4gFYFETcisol/ulH+hsBOWLTe3a33AAAAABJRU5ErkJggg==")
+    8/4px repeat;
+  margin: 0;
+  padding: 4px;
+}
+.panel-tooltip {
+  background-color: #313031;
+  border-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYRpQ0NQSUNDIHByb2ZpbGUAACiRfZE9SMNAHMVfW6VSWhzaQcQhQ3Wy4BfiqFUoQoVQK7TqYHLpFzRpSFJcHAXXgoMfi1UHF2ddHVwFQfADxNHJSdFFSvxfUmgR48FxP97de9y9A/zNKlPNnjFA1Swjk0oKufyqEHxFCFFEMI6oxEx9ThTT8Bxf9/Dx9S7Bs7zP/TkiSsFkgE8gnmW6YRFvEE9vWjrnfeIYK0sK8TnxqEEXJH7kuuzyG+eSw36eGTOymXniGLFQ6mK5i1nZUImniOOKqlG+P+eywnmLs1qts/Y9+QvDBW1lmes0h5DCIpYgQoCMOiqowkKCVo0UExnaT3r4Bx2/SC6ZXBUwciygBhWS4wf/g9/dmsXJCTcpnAR6X2z7YxgI7gKthm1/H9t26wQIPANXWsdfawIzn6Q3Olr8COjfBi6uO5q8B1zuAANPumRIjhSg6S8Wgfcz+qY8EL0FQmtub+19nD4AWeoqfQMcHAIjJcpe93h3X3dv/55p9/cDgjtyrWdvGYQAAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfmChMNNByjJo4EAAAAvVBMVEWBdGOBc2J/cmF7bV11aVt1aVpzZ1hxY1RsX1FhV01hV0xhVktfVUldUUVeUERWS0FJPDJHPDRDPThDPDhCOzZBOjQ6MSwxMDExMDAxLzAxLy8xLi8wLi4wLS0wLCwwLCswKyovKigzJyEsJyUsJiUuJSEsJSQoIR8lIiEnIR4lISEnIB0kIR8kIB4nHRojHRsjHBkjGxgjGhcgGhceFhQVDw0TDg0PDg0RDQwQDQwPDQ0MCQcFBAMDAgIAAAAndX8aAAAAAWJLR0Q/PmMwdQAAAIdJREFUeJxljrsOQkEUAoFlV43G1///o7E292CxV41KQTEFQzsCxBpKY28NVhAAIKguX29akknIposP7Z4P4Wlnj/MjlQCk2LudqtRKoKoYlSQVgEpScACsQ7OE3/wTc2o/bYgklXmHgilReP2RaG29SRUASJRpqpPLHG1JxYMC/DZVLH65lSerxj77wTd86AAAAABJRU5ErkJggg==")
+    8/4px repeat;
+  margin: 0;
+  padding: 4px;
+}

+ 48 - 0
src/assets/Tooltip.tsx

@@ -0,0 +1,48 @@
+import { styled } from "@mui/material/styles";
+import Tooltip, { tooltipClasses } from "@mui/material/Tooltip";
+import type { TooltipProps } from "@mui/material/Tooltip";
+import "./Tooltip.css";
+interface TooltipCustoProps {
+  title: string;
+  children: React.ReactElement;
+  content?: React.ReactNode;
+}
+const RawTooltip = styled(({ className, ...props }: TooltipProps) => (
+  <Tooltip {...props} classes={{ popper: className }} />
+))(({ theme }) => ({
+  [`& .${tooltipClasses.tooltip}`]: {
+    backgroundColor: "#f5f5f9",
+    color: "#fff",
+    maxWidth: 300,
+    fontSize: theme.typography.pxToRem(12),
+    border: "none",
+    padding: 0,
+    display: "flex",
+    flexDirection: "column",
+  },
+}));
+
+function TooltipFull({ title, children, content }: TooltipCustoProps) {
+  return (
+    <>
+      <RawTooltip
+        title={
+          <>
+            <div className="panel-tooltip-title">{title}</div>
+            {content && <div className="panel-tooltip">{content}</div>}
+          </>
+        }
+      >
+        {children}
+      </RawTooltip>
+    </>
+  );
+}
+export function TooltipSimple({ title, children }: TooltipCustoProps) {
+  return (
+    <>
+      <RawTooltip title={title}>{children}</RawTooltip>
+    </>
+  );
+}
+export default TooltipFull;

+ 42 - 0
src/assets/icon.tsx

@@ -0,0 +1,42 @@
+import { useMemo, type CSSProperties } from "react";
+import iconMap from "./data/2.0/iconMap.json";
+
+type IconProps = {
+  style?: CSSProperties;
+  className?: string;
+  iconName: string;
+  size: number;
+};
+const iconCountPerSize = iconMap.reduce((acc, icon) => {
+  acc.set(icon.size, (acc.get(icon.size) ?? 0) + 1);
+  return acc;
+}, new Map<number, number>());
+const mapSizePerSize = new Map<number, number>();
+for (let [size, count] of iconCountPerSize.entries()) {
+  mapSizePerSize.set(size, Math.floor(Math.sqrt(count)) + 1);
+}
+
+function Icon({ style, className, iconName, size = 64 }: IconProps) {
+  const icon = useMemo(() => iconMap.find((o) => o.name == iconName), [iconName]);
+  const backgroundMap = `/data/2.0/icon_${icon?.size}.webp`;
+  const bgsize = useMemo(() => {
+    if (icon) return (mapSizePerSize.get(icon.size) ?? 1) * size;
+    return size;
+  }, [icon]);
+  return (
+    <i
+      className={className}
+      style={{
+        ...style,
+        backgroundImage: `url("${backgroundMap}")`,
+        backgroundPositionX: (-(icon?.x ?? 0) * size) / (icon?.size ?? 1),
+        backgroundPositionY: (-(icon?.y ?? 0) * size) / (icon?.size ?? 1),
+        backgroundSize: `${bgsize}px`,
+        height: size,
+        width: size,
+        display: "inline-block",
+      }}
+    ></i>
+  );
+}
+export default Icon;

File diff suppressed because it is too large
+ 0 - 0
src/assets/react.svg


+ 1 - 48
src/index.css

@@ -1,25 +1,10 @@
 :root {
-  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+  font-family: "Titillium Web", sans-serif;
   line-height: 1.5;
   font-weight: 400;
 
   color-scheme: light dark;
   color: rgba(255, 255, 255, 0.87);
-  background-color: #242424;
-
-  font-synthesis: none;
-  text-rendering: optimizeLegibility;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-a {
-  font-weight: 500;
-  color: #646cff;
-  text-decoration: inherit;
-}
-a:hover {
-  color: #535bf2;
 }
 
 body {
@@ -34,35 +19,3 @@ h1 {
   font-size: 3.2em;
   line-height: 1.1;
 }
-
-button {
-  border-radius: 8px;
-  border: 1px solid transparent;
-  padding: 0.6em 1.2em;
-  font-size: 1em;
-  font-weight: 500;
-  font-family: inherit;
-  background-color: #1a1a1a;
-  cursor: pointer;
-  transition: border-color 0.25s;
-}
-button:hover {
-  border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
-  outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
-  :root {
-    color: #213547;
-    background-color: #ffffff;
-  }
-  a:hover {
-    color: #747bff;
-  }
-  button {
-    background-color: #f9f9f9;
-  }
-}

Some files were not shown because too many files changed in this diff