# Plan 010: Create Shared Component Library **Priority**: MEDIUM **Effort**: 1-2 weeks **Status**: NOT STARTED **Risk Level**: LOW - Code organization --- ## Problem Statement SBA and PD frontends have duplicate component implementations. When a component is updated in one frontend, it must be manually synchronized to the other. Current state: - `frontend-sba/components/` - Full implementation (~20 components) - `frontend-pd/components/` - Minimal (mostly stubs) ## Impact - **DRY Violation**: Same code in two places - **Maintenance**: Updates require changes in both places - **Consistency**: Risk of drift between implementations - **Testing**: Need to test same component twice ## Proposed Structure ``` strat-gameplay-webapp/ ├── packages/ │ └── shared-ui/ # Shared component library │ ├── package.json │ ├── nuxt.config.ts # Nuxt module config │ ├── components/ │ │ ├── Game/ │ │ ├── Decisions/ │ │ ├── Gameplay/ │ │ ├── Substitutions/ │ │ └── UI/ │ ├── composables/ │ ├── types/ │ └── styles/ ├── frontend-sba/ # SBA-specific │ └── nuxt.config.ts # Extends shared-ui └── frontend-pd/ # PD-specific └── nuxt.config.ts # Extends shared-ui ``` ## Implementation Steps ### Step 1: Create Package Structure (1 hour) ```bash cd /mnt/NV2/Development/strat-gameplay-webapp # Create shared package mkdir -p packages/shared-ui/{components,composables,types,styles} # Initialize package cd packages/shared-ui ``` Create `packages/shared-ui/package.json`: ```json { "name": "@strat-gameplay/shared-ui", "version": "0.1.0", "private": true, "type": "module", "exports": { ".": { "import": "./index.ts" }, "./components/*": { "import": "./components/*" }, "./composables/*": { "import": "./composables/*" }, "./types/*": { "import": "./types/*" } }, "dependencies": { "vue": "^3.4.0" }, "devDependencies": { "@nuxt/kit": "^3.14.0", "typescript": "^5.3.0" }, "peerDependencies": { "nuxt": "^3.14.0" } } ``` ### Step 2: Create Nuxt Module (2 hours) Create `packages/shared-ui/nuxt.config.ts`: ```typescript import { defineNuxtModule, addComponentsDir, addImportsDir } from '@nuxt/kit' import { fileURLToPath } from 'url' import { dirname, join } from 'path' export default defineNuxtModule({ meta: { name: '@strat-gameplay/shared-ui', configKey: 'sharedUi', }, defaults: { prefix: '', // No prefix for components }, setup(options, nuxt) { const runtimeDir = dirname(fileURLToPath(import.meta.url)) // Add components addComponentsDir({ path: join(runtimeDir, 'components'), prefix: options.prefix, pathPrefix: false, }) // Add composables addImportsDir(join(runtimeDir, 'composables')) // Add types nuxt.hook('prepare:types', ({ tsConfig }) => { tsConfig.include = tsConfig.include || [] tsConfig.include.push(join(runtimeDir, 'types/**/*.d.ts')) }) }, }) ``` Create `packages/shared-ui/index.ts`: ```typescript // Export module export { default } from './nuxt.config' // Export types export * from './types/game' export * from './types/player' export * from './types/websocket' // Export composables export { useWebSocket } from './composables/useWebSocket' export { useGameActions } from './composables/useGameActions' ``` ### Step 3: Move Shared Components (4 hours) Identify components that are league-agnostic: **Fully Shared (move to packages/shared-ui):** - `components/UI/` - All UI primitives - `components/Game/ScoreBoard.vue` - League-agnostic - `components/Game/GameBoard.vue` - League-agnostic - `components/Game/CurrentSituation.vue` - League-agnostic - `components/Gameplay/DiceRoller.vue` - League-agnostic - `components/Gameplay/PlayResult.vue` - League-agnostic **League-Specific (keep in frontend-*):** - `components/Decisions/` - Different for SBA vs PD - Player cards - Different data structures - Scouting views - PD only Move shared components: ```bash # Move UI components cp -r frontend-sba/components/UI packages/shared-ui/components/ # Move Game components cp -r frontend-sba/components/Game packages/shared-ui/components/ # Move Gameplay components cp -r frontend-sba/components/Gameplay packages/shared-ui/components/ # Move composables cp frontend-sba/composables/useWebSocket.ts packages/shared-ui/composables/ cp frontend-sba/composables/useGameActions.ts packages/shared-ui/composables/ # Move types cp frontend-sba/types/game.ts packages/shared-ui/types/ cp frontend-sba/types/player.ts packages/shared-ui/types/ cp frontend-sba/types/websocket.ts packages/shared-ui/types/ ``` ### Step 4: Create Theme System (2 hours) Create `packages/shared-ui/styles/themes.ts`: ```typescript export interface LeagueTheme { primary: string secondary: string accent: string background: string surface: string text: string textMuted: string } export const sbaTheme: LeagueTheme = { primary: '#1e40af', // Blue secondary: '#3b82f6', accent: '#f59e0b', // Amber background: '#0f172a', surface: '#1e293b', text: '#f8fafc', textMuted: '#94a3b8', } export const pdTheme: LeagueTheme = { primary: '#7c3aed', // Purple secondary: '#a78bfa', accent: '#10b981', // Emerald background: '#0f172a', surface: '#1e293b', text: '#f8fafc', textMuted: '#94a3b8', } ``` Create `packages/shared-ui/composables/useTheme.ts`: ```typescript import { inject, provide, ref, readonly } from 'vue' import type { LeagueTheme } from '../styles/themes' import { sbaTheme } from '../styles/themes' const THEME_KEY = Symbol('theme') export function provideTheme(theme: LeagueTheme) { const themeRef = ref(theme) provide(THEME_KEY, readonly(themeRef)) return themeRef } export function useTheme(): LeagueTheme { const theme = inject(THEME_KEY) if (!theme) { console.warn('No theme provided, using SBA default') return sbaTheme } return theme } ``` ### Step 5: Update Frontends to Use Shared Package (2 hours) Update `frontend-sba/package.json`: ```json { "dependencies": { "@strat-gameplay/shared-ui": "workspace:*" } } ``` Update `frontend-sba/nuxt.config.ts`: ```typescript export default defineNuxtConfig({ modules: [ '@strat-gameplay/shared-ui', ], sharedUi: { prefix: '', }, // Override theme runtimeConfig: { public: { league: 'sba', }, }, }) ``` Update `frontend-sba/app.vue`: ```vue ``` ### Step 6: Setup Workspace (1 hour) Create/update root `package.json`: ```json { "name": "strat-gameplay-webapp", "private": true, "workspaces": [ "packages/*", "frontend-sba", "frontend-pd" ], "scripts": { "dev:sba": "cd frontend-sba && npm run dev", "dev:pd": "cd frontend-pd && npm run dev", "build": "npm run build --workspaces", "test": "npm run test --workspaces" } } ``` Install dependencies: ```bash cd /mnt/NV2/Development/strat-gameplay-webapp npm install # or bun install ``` ### Step 7: Update Imports in Frontends (2 hours) Update components to import from shared package: **Before:** ```typescript // frontend-sba/pages/game/[id].vue import ScoreBoard from '~/components/Game/ScoreBoard.vue' import { useWebSocket } from '~/composables/useWebSocket' ``` **After:** ```typescript // frontend-sba/pages/game/[id].vue // ScoreBoard auto-imported from shared-ui module // useWebSocket auto-imported from shared-ui module ``` ### Step 8: Add Shared Tests (2 hours) Create `packages/shared-ui/tests/`: ```typescript // packages/shared-ui/tests/components/ScoreBoard.spec.ts import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import ScoreBoard from '../components/Game/ScoreBoard.vue' describe('ScoreBoard', () => { it('renders team scores', () => { const wrapper = mount(ScoreBoard, { props: { homeScore: 5, awayScore: 3, homeTeamName: 'Home Team', awayTeamName: 'Away Team', }, }) expect(wrapper.text()).toContain('5') expect(wrapper.text()).toContain('3') }) it('applies theme colors', () => { // Test theme integration }) }) ``` ### Step 9: Documentation (1 hour) Create `packages/shared-ui/README.md`: ```markdown # @strat-gameplay/shared-ui Shared Vue 3 components for Strat Gameplay frontends. ## Usage Add to your Nuxt config: ```typescript export default defineNuxtConfig({ modules: ['@strat-gameplay/shared-ui'], }) ``` ## Components ### Game Components - `ScoreBoard` - Displays game score, inning, count - `GameBoard` - Diamond visualization - `CurrentSituation` - Batter/pitcher matchup ### UI Components - `ActionButton` - Styled action button - `ToggleSwitch` - Boolean toggle - `ButtonGroup` - Button group container ## Composables - `useWebSocket` - WebSocket connection management - `useGameActions` - Type-safe game action emitters - `useTheme` - Theme injection/consumption ## Theming Provide theme at app root: ```vue ``` ``` ## Verification Checklist - [ ] Shared package builds without errors - [ ] SBA frontend works with shared components - [ ] PD frontend works with shared components - [ ] Themes apply correctly per league - [ ] Tests pass for shared components - [ ] No duplicate component code remains ## Migration Strategy 1. **Phase 1**: Create package, move UI primitives 2. **Phase 2**: Move game display components 3. **Phase 3**: Move composables and types 4. **Phase 4**: Refactor league-specific components ## Rollback Plan If issues arise: 1. Remove shared-ui from module list 2. Restore original component imports 3. Keep shared package for future use ## Dependencies - None (can be implemented independently) ## Notes - Consider Storybook for component documentation - May want to publish to private npm registry eventually - Future: Add design tokens for full design system