refactor: NewHelpMenu → Components V2 mega cog (complete rewrite)

Replaces embed-based help with full Components V2 system:
- converter.py: Embed → Container conversion engine
- formatter.py: CV2 help page builder + paginator views
- newhelpmenu.py: Main cog with settings, monkey-patching, public API
- 1,747 lines across 4 files, default off
This commit is contained in:
everestmcarthur 2026-05-07 22:41:31 +00:00
parent e1718139d4
commit 324524fc77
5 changed files with 1684 additions and 1558 deletions

View file

@ -1,13 +1,7 @@
"""New Help Menu — Fully customisable interactive help system for Red-DiscordBot."""
from .newhelpmenu import NewHelpMenu
__red_end_user_data_statement__ = (
"This cog stores Discord user IDs and guild IDs for per-user favourite commands "
"and per-guild help menu configuration."
)
async def setup(bot):
cog = NewHelpMenu(bot)
await bot.add_cog(cog)
await cog.initialize()

256
newhelpmenu/converter.py Normal file
View file

@ -0,0 +1,256 @@
"""
Embed → Components V2 converter.
Translates discord.Embed objects into discord.ui LayoutView trees
using Containers, Sections, TextDisplays, Thumbnails, MediaGalleries,
Separators, and ActionRows.
"""
from __future__ import annotations
import re
from typing import Any, Dict, List, Optional, Tuple, Union
import discord
from discord import ui
# ──────────────────────────── helpers ────────────────────────────
def _truncate(text: str, limit: int = 4000) -> str:
"""Truncate to Discord's TextDisplay limit."""
if len(text) <= limit:
return text
return text[: limit - 3] + "..."
def _field_to_md(field: Any) -> str:
"""Convert an embed field to markdown."""
name = getattr(field, "name", "") or ""
value = getattr(field, "value", "") or ""
if name:
return f"**{name}**\n{value}"
return value
# ──────────────────────────── core ────────────────────────────
def embed_to_container(
embed: discord.Embed,
*,
accent_color: Optional[int] = None,
show_thumbnail: bool = True,
show_footer: bool = True,
compact: bool = False,
) -> ui.Container:
"""Convert a ``discord.Embed`` into a ``discord.ui.Container``.
Parameters
----------
embed:
The embed to convert.
accent_color:
Override accent colour. Falls back to embed colour, then None.
show_thumbnail:
Whether to render the thumbnail as a Thumbnail accessory.
show_footer:
Whether to render the footer line.
compact:
If True, merges fields into fewer TextDisplays.
"""
# Resolve colour
color = accent_color
if color is None and embed.color and embed.color.value:
color = embed.color.value
if color is None:
color = discord.Color.blurple().value
container = ui.Container(accent_colour=discord.Colour(color))
# ── Author line ──
author_text = ""
if embed.author and embed.author.name:
author_name = embed.author.name
if embed.author.url:
author_text = f"[{author_name}]({embed.author.url})"
else:
author_text = author_name
# ── Title + URL ──
title_text = ""
if embed.title:
title = embed.title
if embed.url:
title_text = f"### [{title}]({embed.url})"
else:
title_text = f"### {title}"
# ── Description ──
desc_text = embed.description or ""
# Build the header block — combine author, title, description
header_parts = []
if author_text:
header_parts.append(author_text)
if title_text:
header_parts.append(title_text)
if desc_text:
header_parts.append(desc_text)
header_md = "\n".join(header_parts)
# ── Thumbnail? → use Section with accessory ──
thumbnail_url = None
if show_thumbnail and embed.thumbnail and embed.thumbnail.url:
thumbnail_url = embed.thumbnail.url
if header_md:
if thumbnail_url:
# Section: header text + thumbnail accessory
section = ui.Section(
accessory=ui.Thumbnail(thumbnail_url),
)
section.add_item(ui.TextDisplay(_truncate(header_md)))
container.add_item(section)
else:
container.add_item(ui.TextDisplay(_truncate(header_md)))
# ── Fields ──
if embed.fields:
if header_md:
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
if compact:
# Merge all fields into one or two text displays
inline_row: List[str] = []
blocks: List[str] = []
for field in embed.fields:
md = _field_to_md(field)
if getattr(field, "inline", False):
inline_row.append(md)
if len(inline_row) >= 3:
blocks.append(" · ".join(inline_row))
inline_row = []
else:
if inline_row:
blocks.append(" · ".join(inline_row))
inline_row = []
blocks.append(md)
if inline_row:
blocks.append(" · ".join(inline_row))
combined = "\n\n".join(blocks)
container.add_item(ui.TextDisplay(_truncate(combined)))
else:
# Group inline fields, render non-inline as standalone
inline_buffer: List[str] = []
for field in embed.fields:
md = _field_to_md(field)
if getattr(field, "inline", False):
inline_buffer.append(md)
if len(inline_buffer) >= 3:
joined = " · ".join(inline_buffer)
container.add_item(ui.TextDisplay(_truncate(joined)))
inline_buffer = []
else:
if inline_buffer:
joined = " · ".join(inline_buffer)
container.add_item(ui.TextDisplay(_truncate(joined)))
inline_buffer = []
container.add_item(ui.TextDisplay(_truncate(md)))
if inline_buffer:
joined = " · ".join(inline_buffer)
container.add_item(ui.TextDisplay(_truncate(joined)))
# ── Image ──
if embed.image and embed.image.url:
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
container.add_item(
ui.MediaGallery(
ui.MediaGalleryItem(embed.image.url)
)
)
# ── Footer + timestamp ──
if show_footer:
footer_parts: List[str] = []
if embed.footer and embed.footer.text:
footer_parts.append(embed.footer.text)
if embed.timestamp:
footer_parts.append(
f"<t:{int(embed.timestamp.timestamp())}:R>"
)
if footer_parts:
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
footer_md = "-# " + " · ".join(footer_parts)
container.add_item(ui.TextDisplay(_truncate(footer_md, 4000)))
return container
def embeds_to_layout(
embeds: List[discord.Embed],
content: Optional[str] = None,
*,
accent_color: Optional[int] = None,
show_thumbnail: bool = True,
show_footer: bool = True,
compact: bool = False,
existing_action_rows: Optional[List[ui.ActionRow]] = None,
) -> ui.LayoutView:
"""Convert a list of embeds (and optional content) to a full LayoutView.
Parameters
----------
embeds:
List of embeds to convert.
content:
Optional text content to add before embeds.
accent_color:
Global accent colour override.
show_thumbnail:
Whether to render thumbnails.
show_footer:
Whether to render footers.
compact:
Compact field rendering.
existing_action_rows:
Pre-built action rows (buttons/selects) to append.
"""
layout = ui.LayoutView()
# Optional leading text
if content:
layout.add_item(ui.TextDisplay(_truncate(content)))
# Convert each embed to a container
for i, embed in enumerate(embeds):
if i > 0:
layout.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
container = embed_to_container(
embed,
accent_color=accent_color,
show_thumbnail=show_thumbnail,
show_footer=show_footer,
compact=compact,
)
layout.add_item(container)
# Append any action rows (buttons, selects)
if existing_action_rows:
for row in existing_action_rows:
layout.add_item(row)
return layout
def view_items_to_action_rows(view: ui.View) -> List[ui.ActionRow]:
"""Extract action rows from a regular View for re-use in LayoutView."""
rows: List[ui.ActionRow] = []
for child in view.children:
if isinstance(child, ui.ActionRow):
rows.append(child)
elif isinstance(child, (ui.Button, ui.Select)):
row = ui.ActionRow()
row.add_item(child)
rows.append(row)
return rows

554
newhelpmenu/formatter.py Normal file
View file

@ -0,0 +1,554 @@
"""
Components V2 Help Formatter for Red-DiscordBot.
Replaces the default embed-based help with a fully Components V2
layout featuring containers, sections, buttons, and select menus.
"""
from __future__ import annotations
import asyncio
from collections import defaultdict
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
import discord
from discord import ui
from redbot.core import commands
from redbot.core.commands import Command, Group
from redbot.core.utils.chat_formatting import humanize_list, pagify
# ──────────────────────────── constants ────────────────────────────
MAX_FIELDS_PER_PAGE = 12
MAX_COMPONENTS = 38 # Leave room for nav action row + safety margin
# ──────────────────────────── layout builders ────────────────────────────
def _make_header_container(
title: str,
description: str = "",
*,
accent_color: int = 0x5865F2,
thumbnail_url: Optional[str] = None,
bot_name: str = "Bot",
) -> ui.Container:
"""Build the top header container for a help page."""
container = ui.Container(accent_colour=discord.Colour(accent_color))
header_md = f"## {title}"
if description:
header_md += f"\n{description}"
if thumbnail_url:
section = ui.Section(accessory=ui.Thumbnail(thumbnail_url))
section.add_item(ui.TextDisplay(header_md))
container.add_item(section)
else:
container.add_item(ui.TextDisplay(header_md))
return container
def _make_category_container(
category_name: str,
commands_list: List[Tuple[str, str]],
*,
accent_color: int = 0x5865F2,
emoji: str = "📂",
) -> ui.Container:
"""Build a container for a category with its command listing."""
container = ui.Container(accent_colour=discord.Colour(accent_color))
container.add_item(
ui.TextDisplay(f"### {emoji} {category_name}")
)
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
lines: List[str] = []
for cmd_name, cmd_short in commands_list:
short = cmd_short[:80] if cmd_short else "No description"
lines.append(f"`{cmd_name}` — {short}")
text = "\n".join(lines)
for chunk in pagify(text, delims=["\n"], page_length=3900):
container.add_item(ui.TextDisplay(chunk))
return container
def _make_command_container(
cmd: commands.Command,
prefix: str,
*,
accent_color: int = 0x5865F2,
) -> ui.Container:
"""Build a detailed container for a single command."""
container = ui.Container(accent_colour=discord.Colour(accent_color))
if isinstance(cmd, commands.Group):
container.add_item(ui.TextDisplay(f"## 📁 {prefix}{cmd.qualified_name}"))
else:
container.add_item(ui.TextDisplay(f"## 🔹 {prefix}{cmd.qualified_name}"))
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
# Usage
sig = cmd.signature or ""
usage_line = f"**Usage:** `{prefix}{cmd.qualified_name} {sig}`".strip()
container.add_item(ui.TextDisplay(usage_line))
# Description
help_text = cmd.help or cmd.brief or cmd.short_doc or "No description provided."
container.add_item(ui.TextDisplay(help_text[:3900]))
# Aliases
if cmd.aliases:
aliases = ", ".join(f"`{a}`" for a in cmd.aliases)
container.add_item(ui.TextDisplay(f"**Aliases:** {aliases}"))
# Cooldown
if cmd.cooldown:
cd = cmd.cooldown
container.add_item(
ui.TextDisplay(
f"**Cooldown:** {cd.rate} uses per {cd.per:.0f}s ({cd.type.name})"
)
)
# Subcommands for groups
if isinstance(cmd, commands.Group) and cmd.commands:
container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
container.add_item(ui.TextDisplay("### Subcommands"))
sub_lines = []
for sub in sorted(cmd.commands, key=lambda c: c.name):
short = sub.short_doc or "No description"
sub_lines.append(f"`{prefix}{sub.qualified_name}` — {short[:60]}")
sub_text = "\n".join(sub_lines)
for chunk in pagify(sub_text, delims=["\n"], page_length=3900):
container.add_item(ui.TextDisplay(chunk))
return container
# ──────────────────────────── paginator view ────────────────────────────
class HelpPaginatorView(ui.LayoutView):
"""A LayoutView that supports paginated help with button navigation."""
def __init__(
self,
pages: List[List[Any]],
*,
author_id: int,
timeout: float = 180.0,
category_options: Optional[List[discord.SelectOption]] = None,
):
super().__init__(timeout=timeout)
self.pages = pages
self.current_page = 0
self.author_id = author_id
self.category_options = category_options
self.message: Optional[discord.Message] = None
self._build_page()
def _build_page(self):
"""Populate the view with the current page's components."""
self.clear_items()
for component in self.pages[self.current_page]:
self.add_item(component)
if len(self.pages) > 1:
self.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
nav_row = ui.ActionRow()
nav_row.add_item(ui.Button(
emoji="⏮️",
style=discord.ButtonStyle.secondary,
custom_id="help_first",
disabled=self.current_page == 0,
))
nav_row.add_item(ui.Button(
emoji="◀️",
style=discord.ButtonStyle.primary,
custom_id="help_prev",
disabled=self.current_page == 0,
))
nav_row.add_item(ui.Button(
label=f"{self.current_page + 1}/{len(self.pages)}",
style=discord.ButtonStyle.secondary,
custom_id="help_page_indicator",
disabled=True,
))
nav_row.add_item(ui.Button(
emoji="▶️",
style=discord.ButtonStyle.primary,
custom_id="help_next",
disabled=self.current_page >= len(self.pages) - 1,
))
nav_row.add_item(ui.Button(
emoji="⏭️",
style=discord.ButtonStyle.secondary,
custom_id="help_last",
disabled=self.current_page >= len(self.pages) - 1,
))
self.add_item(nav_row)
close_row = ui.ActionRow()
close_row.add_item(ui.Button(
label="Close",
emoji="🗑️",
style=discord.ButtonStyle.danger,
custom_id="help_close",
))
if self.category_options and len(self.category_options) > 1:
close_row.add_item(ui.Select(
placeholder="Jump to category...",
options=self.category_options[:25],
custom_id="help_category_select",
))
self.add_item(close_row)
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.author_id:
await interaction.response.send_message(
"This help menu isn't yours!", ephemeral=True
)
return False
return True
async def on_timeout(self):
if self.message:
try:
self.clear_items()
for component in self.pages[self.current_page]:
self.add_item(component)
self.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
self.add_item(ui.TextDisplay("-# Help menu timed out. Run the help command again."))
await self.message.edit(view=self)
except (discord.NotFound, discord.HTTPException):
pass
async def _handle_nav(self, interaction: discord.Interaction, page: int):
self.current_page = max(0, min(page, len(self.pages) - 1))
self._build_page()
await interaction.response.edit_message(view=self)
async def handle_interaction(self, interaction: discord.Interaction):
"""Route interactions to the correct handler."""
custom_id = interaction.data.get("custom_id", "") if interaction.data else ""
if custom_id == "help_first":
await self._handle_nav(interaction, 0)
elif custom_id == "help_prev":
await self._handle_nav(interaction, self.current_page - 1)
elif custom_id == "help_next":
await self._handle_nav(interaction, self.current_page + 1)
elif custom_id == "help_last":
await self._handle_nav(interaction, len(self.pages) - 1)
elif custom_id == "help_close":
self.stop()
if self.message:
try:
await self.message.delete()
except (discord.NotFound, discord.HTTPException):
pass
elif custom_id == "help_category_select":
values = interaction.data.get("values", []) if interaction.data else []
if values:
try:
page_idx = int(values[0])
await self._handle_nav(interaction, page_idx)
except (ValueError, IndexError):
pass
# ──────────────────────────── menu paginator ────────────────────────────
class CV2MenuPaginator(ui.LayoutView):
"""General-purpose Components V2 paginator to replace Red's menu system."""
def __init__(
self,
pages: List[Any],
*,
author_id: int,
timeout: float = 120.0,
accent_color: int = 0x5865F2,
):
super().__init__(timeout=timeout)
self.raw_pages = pages
self.current_page = 0
self.author_id = author_id
self.accent_color = accent_color
self.message: Optional[discord.Message] = None
self._build_page()
def _convert_page(self, page: Any) -> List[Any]:
"""Convert a raw page into components."""
from .converter import embed_to_container
if isinstance(page, discord.Embed):
return [embed_to_container(page, accent_color=self.accent_color)]
elif isinstance(page, str):
container = ui.Container(accent_colour=discord.Colour(self.accent_color))
for chunk in pagify(page, page_length=3900):
container.add_item(ui.TextDisplay(chunk))
return [container]
elif isinstance(page, (ui.Container, ui.TextDisplay)):
return [page]
elif isinstance(page, list):
return page
else:
return [ui.TextDisplay(str(page)[:3900])]
def _build_page(self):
self.clear_items()
components = self._convert_page(self.raw_pages[self.current_page])
for comp in components:
self.add_item(comp)
if len(self.raw_pages) > 1:
self.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
nav_row = ui.ActionRow()
nav_row.add_item(ui.Button(
emoji="◀️",
style=discord.ButtonStyle.primary,
custom_id="cv2menu_prev",
disabled=self.current_page == 0,
))
nav_row.add_item(ui.Button(
label=f"{self.current_page + 1}/{len(self.raw_pages)}",
style=discord.ButtonStyle.secondary,
custom_id="cv2menu_indicator",
disabled=True,
))
nav_row.add_item(ui.Button(
emoji="▶️",
style=discord.ButtonStyle.primary,
custom_id="cv2menu_next",
disabled=self.current_page >= len(self.raw_pages) - 1,
))
nav_row.add_item(ui.Button(
label="Close",
emoji="🗑️",
style=discord.ButtonStyle.danger,
custom_id="cv2menu_close",
))
self.add_item(nav_row)
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.author_id:
await interaction.response.send_message(
"This menu isn't yours!", ephemeral=True
)
return False
return True
async def on_timeout(self):
if self.message:
try:
self.clear_items()
components = self._convert_page(self.raw_pages[self.current_page])
for comp in components:
self.add_item(comp)
self.add_item(ui.TextDisplay("-# Menu timed out."))
await self.message.edit(view=self)
except (discord.NotFound, discord.HTTPException):
pass
async def handle_interaction(self, interaction: discord.Interaction):
custom_id = interaction.data.get("custom_id", "") if interaction.data else ""
if custom_id == "cv2menu_prev":
self.current_page = max(0, self.current_page - 1)
self._build_page()
await interaction.response.edit_message(view=self)
elif custom_id == "cv2menu_next":
self.current_page = min(len(self.raw_pages) - 1, self.current_page + 1)
self._build_page()
await interaction.response.edit_message(view=self)
elif custom_id == "cv2menu_close":
self.stop()
if self.message:
try:
await self.message.delete()
except (discord.NotFound, discord.HTTPException):
pass
# ──────────────────────────── help builder ────────────────────────────
async def build_bot_help_pages(
ctx: commands.Context,
bot: commands.Bot,
*,
categories: Dict[str, List[str]],
category_emojis: Dict[str, str],
accent_color: int,
show_hidden: bool = False,
blacklisted_cogs: List[str] = None,
blacklisted_commands: List[str] = None,
) -> Tuple[List[List[Any]], List[discord.SelectOption]]:
"""Build paginated help pages for the full bot.
Returns (pages, category_select_options).
"""
blacklisted_cogs = blacklisted_cogs or []
blacklisted_commands = blacklisted_commands or []
prefix = ctx.clean_prefix
# Gather all visible cogs and their commands
cog_commands: Dict[str, List[commands.Command]] = defaultdict(list)
for cmd in sorted(bot.commands, key=lambda c: c.qualified_name):
if cmd.hidden and not show_hidden:
continue
if cmd.qualified_name in blacklisted_commands:
continue
try:
if not await cmd.can_run(ctx):
continue
except Exception:
continue
cog_name = cmd.cog_name or "No Category"
if cog_name in blacklisted_cogs:
continue
cog_commands[cog_name].append(cmd)
# Build category mappings
cog_to_category: Dict[str, str] = {}
for cat_name, cog_list in categories.items():
for cog_name in cog_list:
cog_to_category[cog_name] = cat_name
# Group by category
category_data: Dict[str, List[Tuple[str, str]]] = defaultdict(list)
for cog_name, cmds in cog_commands.items():
cat = cog_to_category.get(cog_name, cog_name)
for cmd in cmds:
short = cmd.short_doc or cmd.brief or "No description"
category_data[cat].append((f"{prefix}{cmd.qualified_name}", short))
sorted_cats = sorted(category_data.keys())
# ── Build pages ──
pages: List[List[Any]] = []
select_options: List[discord.SelectOption] = []
# Page 0: Overview
bot_user = bot.user
bot_name = bot_user.display_name if bot_user else "Bot"
bot_avatar = bot_user.display_avatar.url if bot_user else None
overview_components: List[Any] = []
header = _make_header_container(
f"{bot_name} — Help",
f"Use `{prefix}help <command>` for details on a command.\nUse `{prefix}help <category>` for a category overview.\n\n**{len(sorted_cats)} categories · {sum(len(v) for v in category_data.values())} commands**",
accent_color=accent_color,
thumbnail_url=bot_avatar,
bot_name=bot_name,
)
overview_components.append(header)
summary_lines: List[str] = []
for i, cat in enumerate(sorted_cats):
emoji = category_emojis.get(cat, "📂")
cmd_count = len(category_data[cat])
summary_lines.append(f"{emoji} **{cat}** — {cmd_count} command{'s' if cmd_count != 1 else ''}")
select_options.append(
discord.SelectOption(
label=cat[:100],
value=str(i + 1),
emoji=emoji if len(emoji) <= 2 else None,
description=f"{cmd_count} commands",
)
)
summary_container = ui.Container(accent_colour=discord.Colour(accent_color))
summary_container.add_item(ui.TextDisplay("### 📚 Categories"))
summary_container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
summary_text = "\n".join(summary_lines)
for chunk in pagify(summary_text, delims=["\n"], page_length=3900):
summary_container.add_item(ui.TextDisplay(chunk))
overview_components.append(summary_container)
pages.append(overview_components)
# Category pages
for cat in sorted_cats:
cmds = category_data[cat]
emoji = category_emojis.get(cat, "📂")
for chunk_start in range(0, len(cmds), MAX_FIELDS_PER_PAGE):
chunk = cmds[chunk_start: chunk_start + MAX_FIELDS_PER_PAGE]
page_components: List[Any] = []
container = _make_category_container(
cat, chunk, accent_color=accent_color, emoji=emoji
)
page_components.append(container)
pages.append(page_components)
return pages, select_options
async def build_command_help_page(
ctx: commands.Context,
cmd: commands.Command,
*,
accent_color: int,
) -> List[Any]:
"""Build a single-page help view for a specific command."""
prefix = ctx.clean_prefix
return [_make_command_container(cmd, prefix, accent_color=accent_color)]
async def build_cog_help_page(
ctx: commands.Context,
cog: commands.Cog,
*,
accent_color: int,
show_hidden: bool = False,
) -> List[Any]:
"""Build a help page for a specific cog."""
prefix = ctx.clean_prefix
components: List[Any] = []
cog_name = cog.qualified_name
cog_doc = cog.help or cog.__doc__ or "No description."
cmds: List[Tuple[str, str]] = []
for cmd in sorted(cog.get_commands(), key=lambda c: c.qualified_name):
if cmd.hidden and not show_hidden:
continue
try:
if not await cmd.can_run(ctx):
continue
except Exception:
continue
short = cmd.short_doc or "No description"
cmds.append((f"{prefix}{cmd.qualified_name}", short))
header = ui.Container(accent_colour=discord.Colour(accent_color))
header.add_item(ui.TextDisplay(f"## 📂 {cog_name}"))
header.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small))
header.add_item(ui.TextDisplay(cog_doc[:3900]))
components.append(header)
if cmds:
cmd_container = _make_category_container(
"Commands", cmds, accent_color=accent_color, emoji="🔹"
)
components.append(cmd_container)
return components

View file

@ -1,30 +1,15 @@
{
"author": ["everestmcarthur"],
"install_msg": "Thanks for installing **New Help Menu v1.0.0** — the ultimate customisable help system!\n\nThe cog automatically replaces your default help command with an interactive, button-driven menu.\n\nGet started:\n• `[p]helpmenu theme <name>` — choose a theme (default, minimal, compact, dark)\n• `[p]helpmenu category create <name> <emoji> <description>` — build custom categories\n• `[p]helpmenu category addcog <category> <CogName>` — assign cogs to categories\n• `[p]helpmenu settings` — view all current settings\n• `[p]helpmenu preview` — preview the menu\n\nFull docs: `[p]help helpmenu`",
"install_msg": "**New Help Menu** installed!\n\nThis cog replaces Red's help system and optionally converts ALL embeds & menus to Discord Components V2.\n\nGet started:\n- `[p]cv2 toggle` — Enable Components V2 (off by default)\n- `[p]cv2 settings` — View all current settings\n- `[p]cv2 color <hex>` — Set accent colour\n- `[p]cv2 category add <name> <cog1> <cog2>...` — Group cogs into categories\n\nComponents V2 is **off by default** — enable it when you're ready!",
"name": "NewHelpMenu",
"disabled": false,
"short": "Fully customisable interactive help menu with categories, buttons, select menus, themes, search & favourites.",
"description": "New Help Menu v1.0.0 completely replaces Red's default help formatter with a modern, interactive experience. Features include: custom categories with emoji/colour/thumbnail/image per category, 4 built-in themes (default/minimal/compact/dark) plus full custom mode, button and select-menu navigation, paginated command lists, command search via modal, per-user favourite commands, role-gated category visibility, quick-link buttons (URLs), custom home page fields/images/tagline, per-command detail views with subcommand browsing, cooldown/permission/alias display toggles, DM help mode, ephemeral mode, auto-delete, hidden cogs/commands blacklists, uncategorised section control, and 50+ admin configuration commands.",
"tags": [
"help",
"menu",
"buttons",
"select",
"interactive",
"categories",
"customise",
"customize",
"theme",
"embed",
"search",
"favourites",
"navigation",
"ui",
"paginator"
],
"type": "COG",
"end_user_data_statement": "This cog stores Discord user IDs and guild IDs for per-user favourite commands and per-guild help menu configuration including categories, themes, and display preferences.",
"min_bot_version": "3.5.0",
"short": "Components V2 help menu + global embed/menu override",
"description": "A mega cog that replaces Red's entire help system with Discord Components V2 (containers, sections, text displays, thumbnails, media galleries, separators) and optionally converts ALL bot embeds and menus into Components V2 layouts. Fully customisable with categories, accent colours, layout modes, per-server settings. Toggle on/off with default off.",
"end_user_data_statement": "This cog stores per-guild configuration. No personal user data is stored.",
"min_bot_version": "3.5.21",
"min_python_version": [3, 9, 0],
"requirements": []
"requirements": [],
"tags": ["help", "menu", "components", "components-v2", "embed", "ui", "layout", "customization", "override"],
"type": "COG",
"disabled": false,
"hidden": false
}

File diff suppressed because it is too large Load diff