diff --git a/.jules/architect.md b/.jules/architect.md index 3fb6e9a0..68c65ded 100644 --- a/.jules/architect.md +++ b/.jules/architect.md @@ -13,3 +13,9 @@ * **Pattern:** Extract repeated `embedMessage.edit()` and typing indicator loops into a helper function (e.g. `_animate_update`). * **Why:** The sequential reveal of complex Discord embeds clutters the main logic loop. * **Rule:** When multiple sequential embeds updates are tied to typing animations, move the boilerplate into a standalone helper method. + +### Guard Clauses in roleUi.ts +* **Date:** 2026-06-26 +* **Pattern:** Use early return guard clauses for toggling state rather than deep nesting (e.g. `if (state.has) { ... return; } else { ... }`). +* **Why:** Deep nesting inside Discord UI interactions makes code harder to parse. +* **Rule:** If an `if` condition represents a complete action that finishes the handler's work (like untoggling a role), early. diff --git a/packages/bot/src/core/roleUi.ts b/packages/bot/src/core/roleUi.ts index e9c6e229..a4822f22 100644 --- a/packages/bot/src/core/roleUi.ts +++ b/packages/bot/src/core/roleUi.ts @@ -69,6 +69,16 @@ export interface ViewData { // -- Role button callback logic (pure, testable) -- +/** + * Handles toggling a specific role button in a role selection view. + * Mutates the provided state and button views directly. + * + * @param state - The current role selection state (selected roles set). + * @param viewButtons - The buttons for the current active view layer. + * @param roleName - The unique ID of the role being toggled. + * @param isMainSpec - Whether the view is operating in "Main Spec" mode + * (where only one selection is allowed at a time). + */ export function handleRoleButtonClick( state: RoleSelectionState, viewButtons: ButtonData[], @@ -83,25 +93,27 @@ export function handleRoleButtonClick( if (state.selectedRoles.has(roleName)) { state.selectedRoles.delete(roleName); btn.style = 'secondary'; - } else { - if (isMainSpec) { - for (const item of viewButtons) { - if (isRoleButton(item) && item.roleName !== roleName) { - state.selectedRoles.delete(item.roleName); - item.style = 'secondary'; - } - } - } - state.selectedRoles.add(roleName); - btn.style = 'primary'; + return; + } - // Deselect NoneButton sibling if present + if (isMainSpec) { for (const item of viewButtons) { - if (isNoneButton(item)) { + if (isRoleButton(item) && item.roleName !== roleName) { + state.selectedRoles.delete(item.roleName); item.style = 'secondary'; } } } + + state.selectedRoles.add(roleName); + btn.style = 'primary'; + + // Deselect NoneButton sibling if present + for (const item of viewButtons) { + if (isNoneButton(item)) { + item.style = 'secondary'; + } + } } export function handleNoneButtonClick(