Create a reusable UI component with factory pattern, theme compatibility, and proper cleanup
/add-component <ComponentName>
Does an existing component cover ~80% of the need?
ā Check src/ui/components/ before creating new
Existing: ArcadeButton, BasePetCard, GeminiIconButton, Modal, ProgressBar, SegmentedControl, SeeMore, SoundPicker, Tab, TeamListItem
Brief description (1 sentence):
What options does it need?
- Required: ___________
- Optional with defaults: ___________
- Event handlers: ___________
Does it display game sprites? ā MGSprite.toCanvas()
Does it need reactive updates?
A) Manual setters only (setLabel, setDisabled, setValue, etc.)
B) Reactive to Globals (subscribe to myInventory, currentTile, weather, etc.)
C) Both
If B/C: Component subscribes to Globals and auto-updates UI
ā Remember to unsubscribe in destroy()!
Does this component need sub-components?
ā REUSE existing components, never recreate!
Available:
- ArcadeButton, GeminiIconButton ā Buttons
- Modal ā Dialogs/popups
- ProgressBar ā Progress indicators
- SegmentedControl ā Tab-like selection
- Tab ā Tabs
- SoundPicker ā Audio selection
- BasePetCard, TeamListItem ā List items
- SeeMore ā Expandable content
Example: A "SettingsPanel" component might use:
- SegmentedControl for sections
- Toggle for on/off settings
- ArcadeButton for actions
src/ui/components/<ComponentName>/
āāā <ComponentName>.ts # Logic + factory function
āāā <componentName>.css.ts # Styles (CSS string)
āāā index.ts # Re-exports
Read existing components for templates: src/ui/components/*/
export interface <ComponentName>Options {
// Required (no default)
label: string;
// Optional (have defaults)
variant?: 'primary' | 'secondary';
disabled?: boolean;
// Event handlers
onClick?: () => void;
}
export interface <ComponentName>Handle {
root: HTMLElement; // REQUIRED
set<Property>(value): void; // Public setters (minimal)
destroy(): void; // REQUIRED - cleanup
}
export function create<ComponentName>(options: <ComponentName>Options): <ComponentName>Handle
/* NO hardcoded colors */
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
min-height: 44px; /* Touch-friendly */
width: 100%; /* Flexible, not fixed */
max-width: 300px; /* Constraint if needed */
/* Prefix all classes with component name */
.component-name { }
.component-name__label { }
.component-name--variant { }
src/ui/components/index.tsexport { create<ComponentName> } from './<ComponentName>/<ComponentName>';
export type { <ComponentName>Options, <ComponentName>Handle } from './<ComponentName>/<ComponentName>';
root: HTMLElement in Handledestroy() removes ALL listeners/observers/intervals:focus-visible)root, not document.headdestroy() called in parent's destroy()MGSprite.toCanvas() (never hardcoded paths)destroy() (memory leak otherwise!)import { createProgressBar, createArcadeButton } from '../index';
// In factory:
const progressBar = createProgressBar({ ... });
root.appendChild(progressBar.root);
// In destroy:
destroy() {
progressBar.destroy();
root.remove();
}
import { getMyInventory } from '../../../globals/variables/myInventory';
// In factory:
const unsub = getMyInventory().subscribe((inventory) => {
// Update UI when inventory changes
updateItemCount(inventory.items.length);
});
// In destroy - CRITICAL!
destroy() {
unsub(); // Unsubscribe from Global
root.remove();
}
.claude/rules/ui/components.mdsrc/ui/components/*/src/ui/theme/.claude/workflows/ui/component/reuse-existing-component.md