Sfoglia il codice sorgente

polish item selection menu

clovis 1 mese fa
parent
commit
9b4c9c9282
5 ha cambiato i file con 152 aggiunte e 53 eliminazioni
  1. 52 18
      package-lock.json
  2. 2 1
      package.json
  3. 23 1
      src/App.tsx
  4. 3 1
      src/assets/SelectRecipeMenu.css
  5. 72 32
      src/assets/SelectRecipeMenu.tsx

+ 52 - 18
package-lock.json

@@ -12,7 +12,8 @@
         "@emotion/styled": "^11.14.1",
         "@mui/material": "^7.3.5",
         "react": "^19.2.0",
-        "react-dom": "^19.2.0"
+        "react-dom": "^19.2.0",
+        "simplebar-react": "^3.3.2"
       },
       "devDependencies": {
         "@eslint/js": "^9.39.1",
@@ -61,6 +62,7 @@
       "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
       "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.27.1",
         "@babel/generator": "^7.28.5",
@@ -382,6 +384,7 @@
       "version": "11.14.0",
       "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
       "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+      "peer": true,
       "dependencies": {
         "@babel/runtime": "^7.18.3",
         "@emotion/babel-plugin": "^11.13.5",
@@ -422,6 +425,7 @@
       "version": "11.14.1",
       "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
       "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
+      "peer": true,
       "dependencies": {
         "@babel/runtime": "^7.18.3",
         "@emotion/babel-plugin": "^11.13.5",
@@ -1732,6 +1736,7 @@
       "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
       "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "undici-types": "~7.16.0"
       }
@@ -1750,6 +1755,7 @@
       "version": "19.2.5",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
       "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
+      "peer": true,
       "dependencies": {
         "csstype": "^3.0.2"
       }
@@ -1814,6 +1820,7 @@
       "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz",
       "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "8.46.4",
         "@typescript-eslint/types": "8.46.4",
@@ -2053,6 +2060,7 @@
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "dev": true,
+      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -2203,6 +2211,7 @@
           "url": "https://github.com/sponsors/ai"
         }
       ],
+      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.8.25",
         "caniuse-lite": "^1.0.30001754",
@@ -2467,6 +2476,7 @@
       "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
       "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/regexpp": "^4.12.1",
@@ -3078,6 +3088,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+      "license": "MIT"
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3425,6 +3447,7 @@
       "version": "19.2.0",
       "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
       "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
+      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -3433,6 +3456,7 @@
       "version": "19.2.0",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
       "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
+      "peer": true,
       "dependencies": {
         "scheduler": "^0.27.0"
       },
@@ -3605,6 +3629,28 @@
         "node": ">=8"
       }
     },
+    "node_modules/simplebar-core": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/simplebar-core/-/simplebar-core-1.3.2.tgz",
+      "integrity": "sha512-qKgTTuTqapjsFGkNhCjyPhysnbZGpQqNmjk0nOYjFN5ordC/Wjvg+RbYCyMSnW60l/Z0ZS82GbNltly6PMUH1w==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.21"
+      }
+    },
+    "node_modules/simplebar-react": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-3.3.2.tgz",
+      "integrity": "sha512-ZsgcQhKLtt5ra0BRIJeApfkTBQCa1vUPA/WXI4HcYReFt+oCEOvdVz6rR/XsGJcKxTlCRPmdGx1uJIUChupo+A==",
+      "license": "MIT",
+      "dependencies": {
+        "simplebar-core": "^1.3.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -3710,6 +3756,7 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -3815,6 +3862,7 @@
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
       "dev": true,
+      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -3915,6 +3963,7 @@
       "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
       "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.5.0",
@@ -4006,6 +4055,7 @@
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
+      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -4049,23 +4099,6 @@
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
       "dev": true
     },
-    "node_modules/yaml": {
-      "version": "2.8.2",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
-      "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
-      "dev": true,
-      "optional": true,
-      "peer": true,
-      "bin": {
-        "yaml": "bin.mjs"
-      },
-      "engines": {
-        "node": ">= 14.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/eemeli"
-      }
-    },
     "node_modules/yn": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
@@ -4092,6 +4125,7 @@
       "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
       "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
       "dev": true,
+      "peer": true,
       "funding": {
         "url": "https://github.com/sponsors/colinhacks"
       }

+ 2 - 1
package.json

@@ -17,7 +17,8 @@
     "@emotion/styled": "^11.14.1",
     "@mui/material": "^7.3.5",
     "react": "^19.2.0",
-    "react-dom": "^19.2.0"
+    "react-dom": "^19.2.0",
+    "simplebar-react": "^3.3.2"
   },
   "devDependencies": {
     "@eslint/js": "^9.39.1",

+ 23 - 1
src/App.tsx

@@ -1,12 +1,34 @@
+import { FormControl, InputLabel, MenuItem, Select, type SelectChangeEvent } from "@mui/material";
 import "./App.css";
+
+import data from "./assets/data/2.0/data.json";
 import SelectMenu from "./assets/SelectRecipeMenu";
+import { useMemo, useState } from "react";
 
 function App() {
+  const [group, setGroup] = useState("Signals");
+  const handleChange = (event: SelectChangeEvent) => {
+    setGroup(event.target.value);
+  };
+  const categories = useMemo(() => (group == "Signals" ? data.signalGroup : data.recipeGroup), [group]);
   return (
     <>
       <h1>Vite + React</h1>
       <div className="card">
-        <SelectMenu></SelectMenu>
+        <FormControl fullWidth>
+          <InputLabel id="demo-simple-select-label">Groups</InputLabel>
+          <Select
+            labelId="demo-simple-select-label"
+            id="demo-simple-select"
+            value={group}
+            label="Age"
+            onChange={handleChange}
+          >
+            <MenuItem value={"Signals"}>Signals</MenuItem>
+            <MenuItem value={"Recipes"}>Recipes</MenuItem>
+          </Select>
+        </FormControl>
+        <SelectMenu categories={categories} title={`Select ${group.toLowerCase()}`}></SelectMenu>
       </div>
       <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
     </>

+ 3 - 1
src/assets/SelectRecipeMenu.css

@@ -13,7 +13,7 @@
   flex-direction: row;
 }
 .select-menu-header-title {
-  font-weight: 900;
+  font-weight: 700;
   font-stretch: 100%;
   font-size: 19.2px;
   color: #ffe6c0;
@@ -51,6 +51,7 @@
   flex-wrap: wrap;
 }
 .select-menu-group-icon {
+  cursor: pointer;
   background-color: rgb(140, 140, 140);
   border: 1px solid #646464;
 }
@@ -83,6 +84,7 @@
   display: inline-flex;
 }
 .select-menu-icon {
+  cursor: pointer;
   background-color: #373737;
   border-radius: 4px;
   height: 36px;

+ 72 - 32
src/assets/SelectRecipeMenu.tsx

@@ -1,25 +1,56 @@
 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;
-};
+import SimpleBar from "simplebar-react";
+import "simplebar-react/dist/simplebar.min.css";
 
+// Utility type definitions
+export type Category<T> = {
+  name: string;
+  icon: string;
+  subGroup: SubGroup<T>[];
+};
 export type SubGroup<T> = {
   name: string;
   order?: string;
   children: Array<T>;
 };
-const ITEM_PER_ROW = 10;
+export type MenuItem = { name: string; icon: string };
 
 interface SubGroupProps {
-  subgroup: SubGroup<{ name: string; icon: string }>;
+  subgroup: SubGroup<MenuItem>;
   selectedItem: string;
   onSelectItem: (name: string) => void;
 }
+
+// Constants
+const ITEM_PER_ROW = 10;
+
+/**
+ * Return a new `Category` object that contains only
+ * sub‑groups and items that match the search string.
+ * @param category the original category
+ * @param search the user’s search query
+ * @returns a filtered copy of the category
+ */
+function filterCategory(category: Category<MenuItem>, search: string): Category<MenuItem> {
+  const searchKeys = search.toLowerCase().split(" ");
+
+  // Filter the children inside a single sub‑group
+  function filterSubgroupChildren(subgroup: SubGroup<MenuItem>): SubGroup<MenuItem> {
+    const children = subgroup.children.filter((item) => searchKeys.every((k) => item.name.includes(k)));
+    return {
+      ...subgroup,
+      children,
+    };
+  }
+  return {
+    ...category,
+    subGroup: category.subGroup.map(filterSubgroupChildren).filter((s) => s.children.length > 0),
+  };
+}
+
 function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
   const emptySlotsCounts = useMemo(() => {
     const lastRowSlots = subgroup.children.length % ITEM_PER_ROW;
@@ -47,38 +78,45 @@ function SubGroupRow({ subgroup, selectedItem, onSelectItem }: SubGroupProps) {
     </div>
   );
 }
-function SelectMenu({ style, className }: SelectMenuProps) {
-  const [category, setCategory] = useState(data.signalGroup[0].name);
+
+type SelectMenuProps = {
+  categories: Category<MenuItem>[];
+  className?: string;
+  title?: string;
+  style?: CSSProperties;
+  onClose?: () => void;
+  onSelectItem?: (itemName: string) => void;
+};
+function SelectMenu({ style, className, title, categories, onClose, onSelectItem }: SelectMenuProps) {
+  const [category, setCategory] = useState(categories[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]);
+
+  const activeCategory = useMemo(() => categories.find((r) => r.name == category), [category, categories]);
+  const activeOnSelect = useMemo(() => (onSelectItem ? onSelectItem : selectItem), [onSelectItem, selectItem]);
+  // Apply filtering only if a search is active
+  const filteredContent = useMemo(
+    () => (search && activeCategory && showSearch ? filterCategory(activeCategory, search) : activeCategory),
+    [activeCategory, search, showSearch]
+  );
   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-title">{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)}
-          >
+          {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">
+          <button className="panel-button" onClick={onClose}>
             <img src="/assets/close.png" />
           </button>
         </div>
       </div>
       <div className="select-menu-group">
-        {data.signalGroup.map((g) => (
+        {categories.map((g) => (
           <div
             className={"select-menu-group-icon " + (g.name == category ? "active" : "")}
             onClick={() => setCategory(g.name)}
@@ -89,14 +127,16 @@ function SelectMenu({ style, className }: SelectMenuProps) {
         ))}
       </div>
       <div className="select-menu-content">
-        {content?.subGroup.map((subgroup) => (
-          <SubGroupRow
-            subgroup={subgroup}
-            selectedItem={item}
-            onSelectItem={selectItem}
-            key={subgroup.name}
-          ></SubGroupRow>
-        ))}
+        <SimpleBar style={{ maxHeight: 300 }}>
+          {filteredContent?.subGroup.map((subgroup) => (
+            <SubGroupRow
+              subgroup={subgroup}
+              selectedItem={item}
+              onSelectItem={activeOnSelect}
+              key={subgroup.name}
+            ></SubGroupRow>
+          ))}
+        </SimpleBar>
       </div>
     </div>
   );