Browse Source

use css module

clovis 1 tháng trước cách đây
mục cha
commit
09dc0fd7e9
3 tập tin đã thay đổi với 67 bổ sung35 xóa
  1. 0 0
      src/assets/SelectRecipeMenu.module.css
  2. 62 31
      src/assets/SelectRecipeMenu.tsx
  3. 5 4
      vite.config.ts

+ 0 - 0
src/assets/SelectRecipeMenu.css → src/assets/SelectRecipeMenu.module.css


+ 62 - 31
src/assets/SelectRecipeMenu.tsx

@@ -1,6 +1,6 @@
 import { useState, type CSSProperties, useMemo } from "react";
 import Icon from "./icon";
-import "./SelectRecipeMenu.css";
+import styles from "./SelectRecipeMenu.module.css";
 import Tooltip from "./Tooltip";
 import SimpleBar from "simplebar-react";
 
@@ -15,6 +15,11 @@ export type SubGroup<T> = {
   order?: string;
   children: Array<T>;
 };
+/**
+ * Represents a single selectable item.
+ * @property {string} name – Display name of the item.
+ * @property {string} icon – Identifier of the icon to render.
+ */
 export type MenuItem = { name: string; icon: string };
 
 interface SubGroupProps {
@@ -27,11 +32,14 @@ interface SubGroupProps {
 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
+ * Filters a {@link Category} based on a search string.
+ * Only sub‑groups and items that contain *all* search terms are
+ * retained. The function returns a **new** category object; the
+ * original data is never mutated.
+ *
+ * @param {Category<MenuItem>} category - Original category to filter.
+ * @param {string} search - User’s search query (space separated).
+ * @returns {Category<MenuItem>} - A filtered copy of the category.
  */
 function filterCategory(category: Category<MenuItem>, search: string): Category<MenuItem> {
   const searchKeys = search.toLowerCase().split(" ");
@@ -50,34 +58,57 @@ function filterCategory(category: Category<MenuItem>, search: string): Category<
   };
 }
 
+/**
+ * Renders a single row of icons representing a sub‑group’s items.
+ *
+ * Empty slots are rendered to keep the grid layout intact when the
+ * number of items is not a multiple of {@link ITEM_PER_ROW}.
+ *
+ * @component
+ * @param {SubGroupProps} props
+ */
 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">
+    <div className={styles.selectMenuContentRow}>
       {subgroup.children.map((o) => (
         <Tooltip title={o.name} key={o.name}>
           <div
-            className={"select-menu-icon " + (o.name == selectedItem ? "active" : "")}
+            className={`${styles.selectMenuIcon} ${o.name === selectedItem ? styles.active : ""}`}
             onClick={() => onSelectItem(o.name)}
           >
-            <Icon iconName={o.icon} size={30}></Icon>
+            <Icon iconName={o.icon} size={30} />
           </div>
         </Tooltip>
       ))}
       {Array(emptySlotsCounts)
         .fill(0)
         .map((_, i) => (
-          <div className="select-menu-slot" key={i}>
-            <div className="select-menu-slot-empty"></div>
+          <div className={styles.selectMenuSlot} key={i}>
+            <div className={styles.selectMenuSlotEmpty} />
           </div>
         ))}
     </div>
   );
 }
 
+/**
+ * The main dropdown menu component. It renders:
+ *   - A header with optional title and search bar
+ *   - Category icons for switching between categories
+ *   - A scrollable list of {@link SubGroupRow}s for the current
+ *     category
+ *
+ * The component accepts an optional {@link onSelectItem} callback;
+ * if omitted it simply updates internal state and returns the name
+ * of the selected item via the `selectItem` state hook.
+ *
+ * @component
+ * @param {SelectMenuProps} props
+ */
 type SelectMenuProps = {
   categories: Category<MenuItem>[];
   className?: string;
@@ -99,41 +130,41 @@ function SelectMenu({ style, className, title, categories, onClose, onSelectItem
     () => (search && activeCategory && showSearch ? filterCategory(activeCategory, search) : activeCategory),
     [activeCategory, search, showSearch]
   );
+  const rootClassName = `${styles.selectMenu} ${className ?? ""}`;
+  console.log(styles);
   return (
-    <div style={style} className={"select-menu " + className}>
-      <div className="select-menu-header">
-        <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)}>
-            <img src="/assets/search.png" />
+    <div style={style} className={rootClassName}>
+      {/* Header */}
+      <div className={styles.selectMenuHeader}>
+        <div className={styles.selectMenuHeaderTitle}>{title ?? "Title"}</div>
+        <div className={styles.selectMenuHeaderSpacer} />
+        <div className={styles.selectMenuHeaderAction}>
+          {showSearch && <input type="search" onChange={(evt) => setSearch(evt.target.value)} value={search} />}
+          <button className={`panel-button ${showSearch ? "active" : ""}`} onClick={() => setShowSearch(!showSearch)}>
+            <img src="/assets/search.png" alt="Search" />
           </button>
           <button className="panel-button" onClick={onClose}>
-            <img src="/assets/close.png" />
+            <img src="/assets/close.png" alt="Close" />
           </button>
         </div>
       </div>
-      <div className="select-menu-group">
+      {/* Category icons */}
+      <div className={styles.selectMenuGroup}>
         {categories.map((g) => (
           <div
-            className={"select-menu-group-icon " + (g.name == category ? "active" : "")}
-            onClick={() => setCategory(g.name)}
             key={g.name}
+            className={`${styles.selectMenuGroupIcon} ${g.name === category ? styles.active : ""}`}
+            onClick={() => setCategory(g.name)}
           >
-            <Icon iconName={g.icon} size={62}></Icon>
+            <Icon iconName={g.icon} size={62} />
           </div>
         ))}
       </div>
-      <div className="select-menu-content">
+      {/* Scrollable content */}
+      <div className={styles.selectMenuContent}>
         <SimpleBar style={{ maxHeight: 300 }}>
           {filteredContent?.subGroup.map((subgroup) => (
-            <SubGroupRow
-              subgroup={subgroup}
-              selectedItem={item}
-              onSelectItem={activeOnSelect}
-              key={subgroup.name}
-            ></SubGroupRow>
+            <SubGroupRow key={subgroup.name} subgroup={subgroup} selectedItem={item} onSelectItem={activeOnSelect} />
           ))}
         </SimpleBar>
       </div>

+ 5 - 4
vite.config.ts

@@ -1,13 +1,14 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
 
 // https://vite.dev/config/
 export default defineConfig({
   plugins: [
     react({
       babel: {
-        plugins: [['babel-plugin-react-compiler']],
+        plugins: [["babel-plugin-react-compiler"]],
       },
     }),
   ],
-})
+  css: { modules: { localsConvention: "camelCase" } },
+});