Claude-skill-registry add-card
Create a new card part within a section with factory/class pattern, Card wrapper, and proper cleanup
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/add-card" ~/.claude/skills/majiayu000-claude-skill-registry-add-card && rm -rf "$T"
manifest:
skills/data/add-card/SKILL.mdsource content
Add Card Skill
Usage
/add-card <SectionName>/<CardName>
Example:
/add-card Pets/InventoryCard or /add-card Alerts/WeatherCard
What is a Card?
A Card is a self-contained UI part within a Section. It:
- Wraps content in the
component (expandable/collapsible container)Card - Has a specific purpose (logs viewer, team manager, item list, etc.)
- Lives in
src/ui/sections/<SectionName>/parts/<cardName>/ - Can be either class-based (complex state) or factory-based (simpler)
Cards vs Components
| Aspect | Card (Part) | Component |
|---|---|---|
| Location | | |
| Scope | Section-specific | Reusable across sections |
| State | Often section-specific state | Props-driven, minimal internal state |
| Complexity | Higher (orchestrates components) | Lower (single purpose) |
Step 1: Ask Questions
1. Parent Section
Which section will contain this card? → Must exist in src/ui/sections/
2. Card Purpose
Brief description (1 sentence): What does this card display/manage?
3. Pattern Type
A) Factory pattern (recommended for simpler cards) - createXxxCard(options): XxxCardHandle - Better for cards with minimal internal state - Example: ShopsCard, PublicCard B) Class pattern (for complex cards) - class XxxCardPart { build(), destroy(), render() } - Better for cards with complex state, multiple modes, drag handlers - Example: TeamCard, TrackerCard, AbilityLogsCard
4. Card Features
[ ] Expandable (collapsible header with chevron) [ ] Filterable (search bar, selects) [ ] Scrollable list (virtual scrolling if >50 items) [ ] Modes toggle (SegmentedControl for overview/manage/etc.) [ ] Refresh capability (manual refresh button)
5. Data Sources
A) Globals (reactive) → subscribe + cleanup B) MGData (static) → MGData.get('plants') C) Feature state → feature.getState() D) API fetch → async loading E) Section state → state.ts F) Combination of above
6. Child Components
Check existing components before creating new: Layout: [ ] Card (always used - the wrapper) Inputs/Controls: [ ] SearchBar - text filtering [ ] Select - dropdown filter [ ] SegmentedControl - mode toggle [ ] Checkbox - multi-select [ ] Button - actions Display: [ ] Table - tabular data with sorting [ ] Badge - status indicators [ ] ProgressBar - progress display [ ] TeamListItem - team rows Utility: [ ] Modal - popups [ ] SoundPicker - audio selection
7. Sprites
Does it display game sprites? → MGSprite.toCanvas()
Step 2: Create Structure
src/ui/sections/<SectionName>/parts/<cardName>/ ├── <CardName>.ts # Main card logic (class or factory) ├── <cardName>.css.ts # Scoped styles (optional) ├── index.ts # Barrel exports └── <cardName>Data.ts # Data processing helpers (optional, for complex data) └── <cardName>Table.ts # Table configuration (optional, if using Table)
Read existing cards for templates:
- Factory pattern:
src/ui/sections/Alerts/parts/shop/shopsCard.ts - Factory pattern:
src/ui/sections/Room/parts/public.ts - Class pattern:
src/ui/sections/Pets/parts/ability/AbilityLogsCard.ts - Class pattern:
src/ui/sections/Pets/parts/team/TeamCard.ts
Step 3: File Templates
Factory Pattern (Recommended for simpler cards)
<CardName>.ts
<CardName>.ts/** * <CardName> Card Part * <Brief description> */ import { Card } from "../../../../components/Card/Card"; import { element } from "../../../../styles/helpers"; // Import needed components // import { SearchBar } from "../../../../components/SearchBar/SearchBar"; // import { Select } from "../../../../components/Select/Select"; // Import data sources // import { getMyInventory } from "../../../../../globals/variables/myInventory"; // import { MGData } from "../../../../../modules"; /* ─────────────────────────── Types ─────────────────────────── */ export interface <CardName>Options { defaultExpanded?: boolean; onExpandChange?: (expanded: boolean) => void; // Add card-specific options } export interface <CardName>Handle { root: HTMLElement; refresh?(): void; destroy(): void; } /* ─────────────────────────── Factory ─────────────────────────── */ export function create<CardName>(options: <CardName>Options = {}): <CardName>Handle { const { defaultExpanded = true, onExpandChange } = options; // Internal state let root: HTMLElement | null = null; const cleanups: (() => void)[] = []; // Component references (for cleanup) // let searchHandle: SearchBarHandle | null = null; /** * Build the card UI */ function buildCard(): HTMLElement { const content = element("div", { style: "display: flex; flex-direction: column; gap: 12px;", }) as HTMLDivElement; // Add filters, lists, content... // const searchBar = SearchBar({ ... }); // content.appendChild(searchBar.root); // cleanups.push(() => searchBar.destroy?.()); root = Card( { title: "<Card Title>", subtitle: "<Card subtitle description>", expandable: true, defaultExpanded, padding: "md", onExpandChange, }, content ); return root; } /** * Refresh data (optional) */ function refresh(): void { // Reload data, update UI } /** * Cleanup all resources */ function destroy(): void { cleanups.forEach(fn => fn()); cleanups.length = 0; root = null; } return { root: buildCard(), refresh, destroy, }; }
Class Pattern (For complex cards with state)
<CardName>.ts
<CardName>.ts/** * <CardName> Card Part * <Brief description> * * Per .claude/rules/ui/sections.md */ import { Card } from "../../../../components/Card/Card"; import { element } from "../../../../styles/helpers"; // Import needed components // import { SegmentedControl, SegmentedControlHandle } from "../../../../components/SegmentedControl/SegmentedControl"; // Import data sources // import { Globals } from "../../../../../globals"; // import { MGData } from "../../../../../modules"; /* ─────────────────────────── Types ─────────────────────────── */ export interface <CardName>PartOptions { // Card-specific options onSomeEvent?: () => void; } /* ─────────────────────────── Class ─────────────────────────── */ export class <CardName>Part { private card: HTMLDivElement | null = null; private content: HTMLDivElement | null = null; private options: <CardName>PartOptions; private cleanups: (() => void)[] = []; // Component references // private modeControl: SegmentedControlHandle | null = null; // Internal state // private mode: "overview" | "manage" = "overview"; constructor(options: <CardName>PartOptions = {}) { this.options = options; } /* ───────────────────── Public API ───────────────────── */ build(): HTMLDivElement { if (this.card) return this.card; return this.createCard(); } destroy(): void { this.cleanups.forEach(fn => fn()); this.cleanups.length = 0; // Destroy child components // this.modeControl?.destroy(); // this.modeControl = null; this.card = null; this.content = null; } render(): void { if (!this.card) return; this.renderContent(); } /* ───────────────────── Card Setup ───────────────────── */ private createCard(): HTMLDivElement { const wrapper = element("div", { className: "<card-name>-wrapper", }); this.content = element("div", { className: "<card-name>__content", }); wrapper.appendChild(this.content); this.card = Card( { title: "<Card Title>", subtitle: "<Card subtitle description>", expandable: true, defaultExpanded: true, }, wrapper ); return this.card; } /* ───────────────────── Rendering ───────────────────── */ private renderContent(): void { if (!this.content) return; this.content.replaceChildren(); // Build UI based on state... } /* ───────────────────── Event Handlers ───────────────────── */ // private handleSomeAction(): void { ... } }
index.ts
(Barrel exports)
index.ts/** * <CardName> Card Parts - Barrel exports */ // Factory pattern: export { create<CardName> } from "./<CardName>"; export type { <CardName>Options, <CardName>Handle } from "./<CardName>"; // OR Class pattern: export { <CardName>Part } from "./<CardName>"; export type { <CardName>PartOptions } from "./<CardName>"; // Optional CSS export export { <cardName>CardCss } from "./<cardName>.css";
<cardName>.css.ts
(Optional)
<cardName>.css.ts/** * <CardName> Card styles */ export const <cardName>CardCss = /* css */` /* Scoped to card */ .<card-name>-wrapper { display: flex; flex-direction: column; gap: 12px; } .<card-name>__content { /* Content styles */ } .<card-name>__list { max-height: 400px; overflow-y: auto; } .<card-name>__empty { padding: 24px; text-align: center; color: color-mix(in oklab, var(--fg) 60%, #9ca3af); font-size: 14px; } `;
Step 4: Register in Section
Update parts/index.ts
parts/index.ts// <CardName> parts export { create<CardName> } from "./<cardName>/<CardName>"; export type { <CardName>Options, <CardName>Handle } from "./<cardName>/<CardName>"; // OR for class pattern: export { <CardName>Part } from "./<cardName>/<CardName>"; export type { <CardName>PartOptions } from "./<cardName>/<CardName>";
Use in section.ts
section.tsimport { create<CardName> } from "./parts"; // OR import { <CardName>Part } from "./parts"; // In build(): // Factory pattern: const card = create<CardName>({ defaultExpanded: true, onExpandChange: (expanded) => { ... }, }); container.appendChild(card.root); cleanups.push(() => card.destroy()); // OR Class pattern: const cardPart = new <CardName>Part({ ... }); container.appendChild(cardPart.build()); cardPart.render(); cleanups.push(() => cardPart.destroy());
Step 5: Validate
Structure
- Card file named
(PascalCase)<CardName>.ts - CSS file named
(camelCase)<cardName>.css.ts -
exports card and typesindex.ts - Card lives in
folderparts/<cardName>/
API
- Factory returns
OR class has{ root, destroy, refresh? }
,build()
,destroy()render() - Options interface defined for configuration
- Handle/Options types exported
Card Wrapper
- Uses
component fromCardcomponents/Card/Card - Has title and subtitle
- Has
if collapsibleexpandable: true - Has
optiondefaultExpanded
Cleanup
- All subscriptions unsubscribed in
destroy() - All child components'
calleddestroy() - All event listeners removed
-
array used for trackingcleanups[]
Styling
- Uses CSS variables (no hardcoded colors)
- Classes scoped with card name prefix
- Touch-friendly (min 44px targets)
- Responsive (flexible widths)
Data (if applicable)
- Globals subscribed with cleanup
- MGData for static game data
- Section state for persisted preferences
- Loading states handled
Components (if applicable)
- Reuses existing components from
src/ui/components/ - Child components tracked in
cleanups[]
Existing Cards Reference
Factory Pattern (simpler)
- Table with filtersAlerts/parts/shop/shopsCard.ts
- Weather alertsAlerts/parts/weather/weatherCard.ts
- Rooms list with API fetchRoom/parts/public.ts
Class Pattern (complex)
- Virtual scrolling listPets/parts/ability/AbilityLogsCard.ts
- CRUD with drag-dropPets/parts/team/TeamCard.ts
- Expansion panelsPets/parts/teamDetails/TeamDetailsCard.ts
- Team list with expansionTrackers/parts/TrackerCard.ts
Common Patterns
Filters Row
const filters = element("div", { className: "<card>-filters", style: "display: flex; gap: 8px; margin-bottom: 12px;", }); const select = Select({ options: [...], onChange: (value) => applyFilters(), }); const search = SearchBar({ placeholder: "Search...", onSearch: (value) => applyFilters(), }); filters.append(select.root, search.root);
Scrollable List
const list = element("div", { className: "<card>__list", style: "max-height: 400px; overflow-y: auto;", });
Empty State
if (items.length === 0) { const empty = element("div", { className: "<card>__empty", style: "padding: 24px; text-align: center; color: color-mix(in oklab, var(--fg) 60%, #9ca3af);", }, "No items yet"); content.appendChild(empty); return; }
Mode Toggle
const modeControl = SegmentedControl({ segments: [ { id: "simple", label: "Simple" }, { id: "detailed", label: "Detailed" }, ], selected: "simple", onChange: (id) => { mode = id; renderContent(); }, });
Subscribe to Globals
const unsub = getMyInventory().subscribe((inventory) => { items = inventory.items; renderList(); }); cleanups.push(unsub);
References
- Rules:
.claude/rules/ui/sections.md - Card component:
src/ui/components/Card/Card.ts - Existing cards:
src/ui/sections/*/parts/*/ - UI Components:
src/ui/components/ - Globals:
src/globals/variables/ - Section workflow:
.claude/workflows/ui/section/add-section.md