|
|
@@ -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>
|