From feeb522dff050c3ffd28b121bd86f475964131cb Mon Sep 17 00:00:00 2001 From: prism Date: Wed, 10 Dec 2025 15:35:21 +0100 Subject: [PATCH] docs(HeaderBarAPI): add some comment and orgnaize code structure (#552) * feat(HeaderBarAPI): ability to add buttons to ChannelToolbar * cleanup, organization and some comment cuz why not * t * fix: add ref prop to HeaderBarButtonProps * fix: remove duplicate channel toolbar patch * we actually dont need this lol --- src/api/HeaderBar.tsx | 203 ++++++++++++++++++-------- src/equicordplugins/_api/headerBar.ts | 2 +- 2 files changed, 140 insertions(+), 65 deletions(-) diff --git a/src/api/HeaderBar.tsx b/src/api/HeaderBar.tsx index 41027c50c..3425bff97 100644 --- a/src/api/HeaderBar.tsx +++ b/src/api/HeaderBar.tsx @@ -8,80 +8,153 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Logger } from "@utils/Logger"; import { findComponentByCodeLazy } from "@webpack"; import { useEffect, useState } from "@webpack/common"; -import type { ComponentType, JSX, MouseEventHandler, ReactNode, RefObject } from "react"; +import type { ComponentType, JSX, MouseEventHandler, ReactNode } from "react"; const logger = new Logger("HeaderBarAPI"); -export type HeaderBarButtonFactory = () => JSX.Element | null; +const HeaderBarIcon = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"') as ComponentType; -export type HeaderBarButtonLocation = "headerbar" | "channeltoolbar"; - -export interface HeaderBarButtonData { - render: HeaderBarButtonFactory; +export interface HeaderBarButtonProps { + /** The icon component to render inside the button */ icon: ComponentType; - priority?: number; - location?: HeaderBarButtonLocation; + /** Tooltip text shown on hover. Pass null to disable tooltip */ + tooltip: ReactNode; + /** Called when the button is clicked */ + onClick?: MouseEventHandler; + /** Called when the button is right-clicked */ + onContextMenu?: MouseEventHandler; + /** Additional CSS class names */ + className?: string; + /** CSS class name for the icon element */ + iconClassName?: string; + /** Tooltip position relative to the button */ + position?: "top" | "bottom" | "left" | "right"; + /** Whether the button appears in a selected/active state */ + selected?: boolean; + /** Whether the button is disabled */ + disabled?: boolean; + /** Whether to show a notification badge */ + showBadge?: boolean; + /** Position of the notification badge */ + badgePosition?: "top" | "bottom"; + /** Size of the icon in pixels */ + iconSize?: number; + /** Ref to the button element */ + ref?: React.RefObject; } -interface HeaderBarButton { +export type HeaderBarButtonFactory = () => JSX.Element | null; + +export interface HeaderBarButtonData { + /** Function that renders the button component */ + render: HeaderBarButtonFactory; + /** Icon component used for settings UI display */ + icon: ComponentType; + /** Higher priority buttons appear further right. Default: 0 */ + priority?: number; + /** Where to render the button. Default: "headerbar" */ + location?: "headerbar" | "channeltoolbar"; +} + +interface ButtonEntry { render: HeaderBarButtonFactory; priority: number; } -export const buttons = new Map(); -export const channelToolbarButtons = new Map(); +/** + * Button component for the top header bar (title bar area). + * + * @example + * console.log("clicked")} + * /> + */ +export const HeaderBarButton = HeaderBarIcon; -const listeners = new Set<() => void>(); +/** + * Button component for the channel toolbar (below the search bar). + * Automatically handles selected state styling. + * + * @example + * setOpen(v => !v)} + * selected={isOpen} + * /> + */ +export function ChannelToolbarButton(props: HeaderBarButtonProps) { + return ; +} + +const headerBarButtons = new Map(); +const channelToolbarButtons = new Map(); + +const headerBarListeners = new Set<() => void>(); const channelToolbarListeners = new Set<() => void>(); -export interface HeaderBarButtonProps { - icon: ComponentType; - tooltip: ReactNode; - onClick?: MouseEventHandler; - onContextMenu?: MouseEventHandler; - className?: string; - iconClassName?: string; - position?: "top" | "bottom" | "left" | "right"; - selected?: boolean; - disabled?: boolean; - showBadge?: boolean; - badgePosition?: "top" | "bottom"; - iconSize?: number; - ref?: RefObject; -} - -export const HeaderBarButton = findComponentByCodeLazy(".HEADER_BAR_BADGE_TOP:", '.iconBadge,"top"') as ComponentType; - -export const ChannelToolbarButton = HeaderBarButton; - /** - * @param id Unique identifier for the button. - * @param render Function that renders the button component. - * @param priority Higher priority = more to the right. Default is 0. + * Adds a button to the header bar (title bar area). + * + * @param id - Unique identifier for the button (e.g., "my-plugin-button") + * @param render - Function that returns the button JSX + * @param priority - Higher values appear further right. Default: 0 + * + * @example + * addHeaderBarButton("my-button", () => ( + * + * )); */ export function addHeaderBarButton(id: string, render: HeaderBarButtonFactory, priority = 0) { - buttons.set(id, { render, priority }); - listeners.forEach(l => l()); -} - -export function removeHeaderBarButton(id: string) { - buttons.delete(id); - listeners.forEach(l => l()); + headerBarButtons.set(id, { render, priority }); + headerBarListeners.forEach(listener => listener()); } /** - * @param id Unique identifier for the button. - * @param render Function that renders the button component. - * @param priority Higher priority = more to the right. Default is 0. + * Removes a button from the header bar. + * + * @param id - The identifier used when adding the button + */ +export function removeHeaderBarButton(id: string) { + headerBarButtons.delete(id); + headerBarListeners.forEach(listener => listener()); +} + +/** + * Adds a button to the channel toolbar (below the search bar, next to pins/members). + * + * @param id - Unique identifier for the button (e.g., "my-plugin-toolbar") + * @param render - Function that returns the button JSX + * @param priority - Higher values appear further right. Default: 0 + * + * @example + * addChannelToolbarButton("my-toolbar", () => ( + * + * )); */ export function addChannelToolbarButton(id: string, render: HeaderBarButtonFactory, priority = 0) { channelToolbarButtons.set(id, { render, priority }); - channelToolbarListeners.forEach(l => l()); + channelToolbarListeners.forEach(listener => listener()); } +/** + * Removes a button from the channel toolbar. + * + * @param id - The identifier used when adding the button + */ export function removeChannelToolbarButton(id: string) { channelToolbarButtons.delete(id); - channelToolbarListeners.forEach(l => l()); + channelToolbarListeners.forEach(listener => listener()); } function HeaderBarButtons() { @@ -89,17 +162,17 @@ function HeaderBarButtons() { useEffect(() => { const listener = () => forceUpdate(n => n + 1); - listeners.add(listener); - return () => { listeners.delete(listener); }; + headerBarListeners.add(listener); + return () => { headerBarListeners.delete(listener); }; }, []); - const sorted = Array.from(buttons).sort(([, a], [, b]) => a.priority - b.priority); - - return sorted.map(([id, { render: Button }]) => ( - logger.error(`Failed to render ${id}`, e.error)}> -