iteration-deck
Version:
AI-first prototyping web components for comparing interactive UI variations. Perfect for designers working with AI coding agents to create multiple design iterations in prototypes.
1 lines • 103 kB
Source Map (JSON)
{"version":3,"file":"wc.cjs","sources":["../src/wc/utils/index.ts","../src/wc/store/iteration-store.ts","../shared/styles.ts","../src/wc/components/iteration-deck-toolbar/iteration-deck-toolbar.ts","../src/wc/components/iteration-deck/iteration-deck.ts","../src/wc/components/iteration-deck-slide/iteration-deck-slide.ts","../src/wc/components/iteration-confidence-bar/iteration-confidence-bar.ts"],"sourcesContent":["/**\n * Core utility functions for iteration-deck components\n * Framework-agnostic helpers for deck/slide management, environment detection, and more\n */\n\n// Simple types for utilities\nexport type Environment = 'development' | 'production';\nexport type NavigationDirection = 'prev' | 'next' | 'first' | 'last';\n\n/**\n * Environment detection utilities\n */\nexport const detectEnvironment = (): Environment => {\n // Check for common production environment indicators\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {\n return 'production';\n }\n \n // Check for development indicators (broader check)\n if (typeof window !== 'undefined') {\n const hostname = window.location.hostname;\n // More comprehensive localhost detection\n if (hostname === 'localhost' || \n hostname === '127.0.0.1' || \n hostname.startsWith('localhost:') ||\n hostname.startsWith('127.0.0.1:') ||\n // Development port patterns\n /^localhost:\\d+$/.test(window.location.host) ||\n /^127\\.0\\.0\\.1:\\d+$/.test(window.location.host)) {\n return 'development';\n }\n }\n \n // Fallback: assume development if we can't determine\n return 'development';\n};\n\n/**\n * Check if we're currently in a test environment\n */\nexport const isTestEnvironment = (): boolean => {\n // Check for common test environment indicators\n if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {\n return true;\n }\n \n // Check for Vitest environment\n if (typeof globalThis !== 'undefined' && 'vi' in globalThis) {\n return true;\n }\n \n // Check for Jest environment\n if (typeof globalThis !== 'undefined' && 'expect' in globalThis && 'test' in globalThis) {\n return true;\n }\n \n // Check for other test runners\n if (typeof window !== 'undefined' && window.location?.href?.includes('test')) {\n return true;\n }\n \n return false;\n};\n\n/**\n * Check if we're currently in production mode\n */\nexport const isProduction = (): boolean => {\n return detectEnvironment() === 'production';\n};\n\n/**\n * Check if we're currently in development mode\n */\nexport const isDevelopment = (): boolean => {\n return detectEnvironment() === 'development';\n};\n\n/**\n * Generate a unique slide ID based on deck ID and label\n */\nexport const generateSlideId = (deckId: string, label: string): string => {\n const safeLabel = label || 'slide';\n const sanitizedLabel = safeLabel.toLowerCase().replace(/[^a-z0-9]/g, '-');\n return `${deckId}-${sanitizedLabel}`;\n};\n\n/**\n * Validate deck ID format\n */\nexport const validateDeckId = (id: string): boolean => {\n return /^[a-z0-9-_]+$/i.test(id);\n};\n\n/**\n * Error logging for development\n */\nexport const errorLog = (message: string, error?: any): void => {\n // In test environments, suppress noisy validation errors\n if (isTestEnvironment() && (\n message.includes('not inside an iteration-deck element') ||\n message.includes('Cannot activate slide: no parent deck found')\n )) {\n return;\n }\n \n console.error(`[IterationDeck ERROR] ${message}`, error || '');\n};\n\n/**\n * Warning logging for development\n */\nexport const warnLog = (message: string, data?: any): void => {\n console.warn(`[IterationDeck WARN] ${message}`, data || '');\n};\n\n/**\n * Throttle function to limit how often a function can be called\n */\nexport const throttle = <T extends (...args: any[]) => any>(\n func: T,\n delay: number\n): ((...args: Parameters<T>) => void) => {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let lastExecTime = 0;\n \n return (...args: Parameters<T>) => {\n const currentTime = Date.now();\n \n if (currentTime - lastExecTime > delay) {\n func(...args);\n lastExecTime = currentTime;\n } else {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n \n timeoutId = setTimeout(() => {\n func(...args);\n lastExecTime = Date.now();\n }, delay - (currentTime - lastExecTime));\n }\n };\n};\n\n/**\n * Check if a keyboard event is a navigation shortcut\n */\nexport const isNavigationShortcut = (event: KeyboardEvent): NavigationDirection | null => {\n const { code, metaKey, ctrlKey, altKey } = event;\n const isModified = (metaKey || ctrlKey) && altKey;\n \n if (!isModified) return null;\n \n switch (code) {\n case 'BracketLeft':\n return 'prev';\n case 'BracketRight':\n return 'next';\n case 'Home':\n return 'first';\n case 'End':\n return 'last';\n default:\n return null;\n }\n};\n\n/**\n * Simple debounce function\n */\nexport const debounce = <T extends (...args: any[]) => any>(\n func: T,\n delay: number\n): ((...args: Parameters<T>) => void) => {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n \n return (...args: Parameters<T>) => {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n \n timeoutId = setTimeout(() => {\n func(...args);\n }, delay);\n };\n};\n\n","/**\n * Simplified Iteration Deck Store\n * \n * Basic state management for iteration decks and their active slides.\n */\n\nimport { isDevelopment } from '../utils/index.js';\n\n/**\n * Slide metadata for store management\n */\nexport interface SlideMetadata {\n /** Unique slide ID */\n id: string;\n /** Human-readable slide label */\n label: string;\n}\n\n/**\n * Deck metadata for store management\n */\nexport interface DeckMetadata {\n /** All slide IDs in this deck */\n slideIds: string[];\n /** Slide metadata with labels */\n slides: SlideMetadata[];\n /** Currently active slide ID */\n activeSlideId: string;\n /** Optional deck label */\n label?: string;\n /** Whether this deck should show navigation controls */\n isInteractive: boolean;\n}\n\n/**\n * Iteration Deck Store Interface\n */\nexport interface IterationStore {\n /** Record mapping deck IDs to their currently active slide IDs */\n activeDecks: Record<string, string>;\n \n /** Record mapping deck IDs to their metadata (slide IDs, labels, etc.) */\n deckMetadata: Record<string, DeckMetadata>;\n \n /** Sets the active slide for a specific deck */\n setActiveSlide: (deckId: string, slideId: string) => void;\n \n /** Registers a deck with all its slides */\n registerDeck: (deckId: string, slideIds: string[], label?: string, isInteractive?: boolean, slides?: SlideMetadata[]) => void;\n \n /** Removes a deck from the store */\n removeDeck: (deckId: string) => void;\n \n /** Gets the active slide ID for a specific deck */\n getActiveSlide: (deckId: string) => string | undefined;\n \n /** Gets all slide IDs for a specific deck */\n getDeckSlides: (deckId: string) => string[];\n \n /** Gets deck metadata (slides, active slide, label) */\n getDeckMetadata: (deckId: string) => DeckMetadata | undefined;\n \n /** Gets all registered deck IDs */\n getRegisteredDecks: () => string[];\n \n /** Gets only interactive deck IDs (for toolbar dropdown) */\n getInteractiveDecks: () => string[];\n \n /** Environment flag - true in production builds, false in development */\n isProduction: boolean;\n \n /** Currently selected deck for toolbar navigation */\n selectedDeckId?: string;\n \n /** Sets the selected deck for toolbar navigation */\n setSelectedDeck: (deckId: string) => void;\n \n /** For testing: Override production mode */\n _setProductionModeForTesting?: (isProduction: boolean) => void;\n \n /** Runtime environment detection update */\n updateEnvironmentDetection?: () => void;\n}\n\n// Simple store implementation\nclass SimpleStore implements IterationStore {\n activeDecks: Record<string, string> = {};\n deckMetadata: Record<string, DeckMetadata> = {};\n selectedDeckId?: string;\n \n // For testing: allow overriding production mode\n private _testProductionModeOverride?: boolean;\n \n // Environment detection state - lazy initialization\n private _environmentDetected: boolean = false;\n private _cachedIsProduction: boolean = true; // Conservative default\n \n /**\n * Lazy environment detection - happens on first access\n */\n private _ensureEnvironmentDetected(): void {\n if (!this._environmentDetected && typeof window !== 'undefined') {\n this._cachedIsProduction = !isDevelopment();\n this._environmentDetected = true;\n }\n }\n \n /**\n * Dynamic production state check with lazy detection\n */\n get isProduction(): boolean {\n if (this._testProductionModeOverride !== undefined) {\n return this._testProductionModeOverride;\n }\n \n // Lazy detection on first access\n this._ensureEnvironmentDetected();\n return this._cachedIsProduction;\n }\n \n /**\n * Update environment detection at runtime (for backward compatibility)\n * @deprecated Use lazy detection instead\n */\n updateEnvironmentDetection(): void {\n // Force re-detection\n this._environmentDetected = false;\n this._ensureEnvironmentDetected();\n }\n \n /**\n * For testing: Override production mode\n */\n _setProductionModeForTesting(isProduction: boolean): void {\n this._testProductionModeOverride = isProduction;\n // Notify listeners of the change\n this.notifyListeners();\n }\n \n constructor() {\n // Store initialized with lazy environment detection\n }\n \n private listeners: Set<(state: IterationStore) => void> = new Set();\n \n registerDeck(deckId: string, slideIds: string[], label?: string, isInteractive: boolean = true, slides?: SlideMetadata[]): void {\n // Create slide metadata if not provided\n const slideMetadata = slides || slideIds.map(id => ({ id, label: id }));\n \n // Store deck metadata\n this.deckMetadata[deckId] = {\n slideIds: [...slideIds],\n slides: slideMetadata,\n activeSlideId: slideIds[0] || '',\n label,\n isInteractive\n };\n \n // Set active slide (use existing if valid, otherwise first slide)\n const currentActive = this.activeDecks[deckId];\n const validActiveSlide = slideIds.includes(currentActive) ? currentActive : slideIds[0];\n this.activeDecks[deckId] = validActiveSlide;\n \n // Update metadata with actual active slide\n this.deckMetadata[deckId].activeSlideId = validActiveSlide;\n \n this.notifyListeners();\n }\n \n setActiveSlide(deckId: string, slideId: string): void {\n this.activeDecks[deckId] = slideId;\n \n // Update metadata if it exists\n if (this.deckMetadata[deckId]) {\n this.deckMetadata[deckId].activeSlideId = slideId;\n }\n \n this.notifyListeners();\n }\n \n removeDeck(deckId: string): void {\n delete this.activeDecks[deckId];\n delete this.deckMetadata[deckId];\n \n if (this.selectedDeckId === deckId) {\n const remainingDecks = this.getRegisteredDecks();\n this.selectedDeckId = remainingDecks.length > 0 ? remainingDecks[0] : undefined;\n }\n this.notifyListeners();\n }\n \n getActiveSlide(deckId: string): string | undefined {\n return this.activeDecks[deckId];\n }\n \n getDeckSlides(deckId: string): string[] {\n return this.deckMetadata[deckId]?.slideIds || [];\n }\n \n getDeckMetadata(deckId: string): DeckMetadata | undefined {\n return this.deckMetadata[deckId];\n }\n \n getRegisteredDecks(): string[] {\n return Object.keys(this.activeDecks);\n }\n \n getInteractiveDecks(): string[] {\n return Object.keys(this.deckMetadata).filter(deckId => \n this.deckMetadata[deckId]?.isInteractive === true\n );\n }\n \n setSelectedDeck(deckId: string): void {\n this.selectedDeckId = deckId;\n this.notifyListeners();\n }\n \n // Subscription management\n subscribe(listener: (state: IterationStore) => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n \n private notifyListeners(): void {\n this.listeners.forEach(listener => listener(this));\n }\n}\n\n// Global store instance\nconst store = new SimpleStore();\n\n/**\n * Subscribe to store changes\n */\nexport const subscribeToIterationStore = (\n listener: (state: IterationStore) => void\n): (() => void) => {\n return store.subscribe(listener);\n};\n\n/**\n * Get current store state\n */\nexport const getIterationStoreState = (): IterationStore => {\n return store;\n};\n\n/**\n * Check if we're in development mode\n */\nexport const isDevelopmentMode = (): boolean => {\n return !store.isProduction;\n};","/**\n * Shared Styling System\n * \n * This file exports Tailwind class strings as TypeScript constants for cross-framework consistency.\n * These styles work with both React components and Lit web components, ensuring visual consistency\n * across the entire iteration-deck ecosystem.\n * \n * Benefits:\n * - Cross-framework consistency (React, Lit, etc.)\n * - Type safety with TypeScript\n * - Tailwind's build optimization and purging\n * - Single source of truth for component styling\n */\n\n// =============================================================================\n// TOOLBAR STYLES\n// =============================================================================\n\nexport const toolbarStyles = {\n // Base toolbar container - fixed positioning with pill shape\n container: [\n // Positioning\n 'fixed bottom-2 left-1/2 -translate-x-1/2',\n 'z-[9999]',\n \n // Layout\n 'flex items-center',\n 'min-w-80', // 320px equivalent\n 'sm:min-w-96', // 384px equivalent\n 'sm:h-10', // Slightly taller on larger screens\n\n // Spacing - mobile first\n 'gap-1 px-2 py-1',\n 'sm:gap-2 sm:px-1 sm:py-1 sm:bottom-4',\n 'lg:p-3',\n \n // Visual design - signature pill shape\n 'rounded-[40px]', // Large border radius\n 'backdrop-blur-md',\n 'shadow-lg',\n \n // Typography\n 'font-medium text-sm leading-none',\n 'text-gray-700',\n \n // Background with glass effect\n 'bg-gray-200/80',\n 'dark:bg-gray-900/70 dark:text-gray-200'\n ].join(' '),\n\n // Inner container for toolbar content\n inner: [\n 'flex items-center gap-2 w-full'\n ].join(' '),\n \n // Deck selector dropdown\n selector: {\n container: [\n 'relative flex items-center'\n ].join(' '),\n\n select: [\n 'absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer z-[2]'\n ].join(' '),\n\n button: [\n // Layout\n 'flex items-center justify-between',\n 'h-7 min-w-30',\n 'gap-3 px-3',\n \n // Visual design with dark mode\n 'bg-white border-2 border-gray-300',\n 'dark:bg-gray-800 dark:border-gray-600',\n 'rounded-3xl', // 24px border radius\n \n // Typography with dark mode\n 'text-sm font-normal leading-3',\n 'text-gray-700 dark:text-gray-200',\n \n // Interactive with dark mode\n 'cursor-pointer select-none',\n 'hover:bg-gray-50 dark:hover:bg-gray-600',\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400'\n ].join(' '),\n\n text: [\n 'flex-1 overflow-hidden text-ellipsis whitespace-nowrap'\n ].join(' '),\n\n arrow: [\n 'text-[8px] text-gray-500 dark:text-gray-400 pointer-events-none'\n ].join(' ')\n },\n\n // Separator between elements\n separator: [\n 'w-px h-5 bg-gray-400/60 dark:bg-gray-500/70 sm:h-4'\n ].join(' '),\n\n // Slide navigation section\n slideNavigation: {\n container: [\n 'flex items-center gap-0 sm:gap-1'\n ].join(' '),\n\n prevButton: [\n // Shape - left side of segmented control\n 'rounded-l-3xl border-r-0',\n \n // Interactive states with dark mode - consistent with main button\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset',\n 'dark:focus:ring-blue-400',\n 'active:bg-gray-100 active:scale-95',\n 'dark:active:bg-gray-500',\n \n // Disabled state with dark mode - consistent\n 'disabled:opacity-40 disabled:cursor-not-allowed',\n 'disabled:hover:bg-white disabled:active:scale-100',\n 'dark:disabled:hover:bg-gray-700'\n ].join(' '),\n\n nextButton: [\n // Shape - right side of segmented control \n 'rounded-r-3xl border-l-0',\n \n // Interactive states with dark mode - consistent with main button\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset',\n 'dark:focus:ring-blue-400',\n 'active:bg-gray-100 active:scale-95',\n 'dark:active:bg-gray-500',\n \n // Disabled state with dark mode - consistent\n 'disabled:opacity-40 disabled:cursor-not-allowed',\n 'disabled:hover:bg-white disabled:active:scale-100',\n 'dark:disabled:hover:bg-gray-700'\n ].join(' ')\n },\n\n // Slide info section\n slideInfo: {\n container: [\n 'flex-1 flex flex-col gap-0 sm:px-2 sm:max-w-4xl lg:max-w-none lg:w-80'\n ].join(' '),\n\n label: [\n 'text-gray-700 dark:text-gray-200 text-xs font-medium leading-4 overflow-hidden text-ellipsis whitespace-nowrap sm:text-sm'\n ].join(' ')\n },\n\n // Slide indicators (dots)\n indicators: {\n container: [\n 'flex items-center justify-start gap-1 w-full'\n ].join(' '),\n\n dot: [\n 'w-1 h-1 rounded-full bg-gray-600 dark:bg-gray-400 transition-opacity duration-200'\n ].join(' '),\n\n dotActive: [\n 'opacity-80'\n ].join(' '),\n\n dotInactive: [\n 'opacity-40'\n ].join(' ')\n },\n \n // Navigation buttons (segmented group)\n navigation: {\n container: [\n // Layout\n 'flex items-center',\n 'rounded-3xl',\n 'border-2 border-gray-300 dark:border-gray-600',\n 'overflow-hidden',\n 'h-7',\n \n // Visual design with dark mode\n 'bg-gray-200 dark:bg-gray-600',\n ].join(' '),\n \n button: [\n // Layout\n 'flex items-center justify-center',\n 'w-8 h-full',\n \n // Background with dark mode - clean transitions\n 'bg-white dark:bg-gray-800',\n 'transition-all duration-150 ease-out',\n \n // Typography with dark mode\n 'text-gray-600 dark:text-gray-300 text-sm',\n \n // Interactive states with dark mode support\n 'cursor-pointer select-none',\n 'hover:bg-gray-50 hover:text-gray-800',\n 'dark:hover:bg-gray-600 dark:hover:text-gray-100',\n 'active:bg-gray-100 active:scale-95',\n 'dark:active:bg-gray-500',\n 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset',\n 'dark:focus:ring-blue-400',\n \n // Disabled state with dark mode\n 'disabled:opacity-40 disabled:cursor-not-allowed',\n 'disabled:hover:bg-white disabled:hover:text-gray-600',\n 'dark:disabled:hover:bg-gray-700 dark:disabled:hover:text-gray-300',\n 'disabled:active:scale-100'\n ].join(' ')\n }\n} as const;\n\n// =============================================================================\n// ITERATION DECK STYLES\n// =============================================================================\n\nexport const deckStyles = {\n // Basic deck container\n container: [\n 'block min-h-1'\n ].join(' ')\n} as const;\n\n// =============================================================================\n// CONFIDENCE BAR STYLES\n// =============================================================================\n\nexport const confidenceStyles = {\n // Confidence indicator base\n container: [\n 'inline-block relative'\n ].join(' '),\n \n bar: [\n 'bg-gray-200 rounded-full overflow-hidden relative'\n ].join(' '),\n \n fill: [\n 'h-full bg-gradient-to-r from-red-500 via-yellow-500 to-green-500 rounded-full transition-all duration-500'\n ].join(' ')\n} as const;\n\n// =============================================================================\n// TYPE EXPORTS\n// =============================================================================\n\nexport type ToolbarStyles = typeof toolbarStyles;\nexport type DeckStyles = typeof deckStyles;\nexport type ConfidenceStyles = typeof confidenceStyles;","/**\n * IterationDeckToolbar Lit Component\n * \n * Global singleton toolbar for iteration deck navigation and management.\n * Provides multi-deck support with dropdown selector, keyboard shortcuts,\n * and navigation controls for the currently active deck.\n * \n * Features:\n * - Singleton pattern - only one toolbar instance globally\n * - Development mode only (hidden in production)\n * - Multi-deck dropdown selector\n * - Previous/Next navigation for active deck\n * - Global keyboard shortcuts (Ctrl/Cmd + [ ] keys)\n * - Accessible UI with ARIA labels and focus management\n * - Dynamic deck registration/unregistration handling\n */\n\nimport { LitElement, html, nothing, css } from 'lit';\nimport { customElement, state } from 'lit/decorators.js';\nimport { \n subscribeToIterationStore, \n getIterationStoreState, \n isDevelopmentMode,\n type IterationStore \n} from '../../store/iteration-store.js';\nimport {\n isNavigationShortcut,\n warnLog,\n errorLog,\n throttle,\n type NavigationDirection\n} from '../../utils/index.js';\nimport type { DeckRegistration } from '../../types/index.js';\nimport type { IterationDeckSlide } from '../iteration-deck-slide/iteration-deck-slide.js';\n\n/**\n * Singleton instance tracking\n */\nlet toolbarInstance: IterationDeckToolbar | null = null;\n\n/**\n * Global IterationDeckToolbar singleton component\n * \n * This component automatically mounts when the first IterationDeck is registered\n * and handles all deck navigation for the entire application.\n */\n\n@customElement('iteration-deck-toolbar')\nexport class IterationDeckToolbar extends LitElement {\n /**\n * Current store state snapshot\n */\n @state()\n private storeState: IterationStore;\n\n // Note: Dropdown functionality will be implemented in future iterations\n\n\n /**\n * Store unsubscribe function\n */\n private unsubscribeFromStore?: () => void;\n\n /**\n * Keyboard event listener (throttled for performance)\n */\n private throttledKeyboardHandler = throttle(this.handleKeyboardNavigation.bind(this), 100);\n\n /**\n * Lit CSS styles - matches React version styling exactly\n */\n static styles = css`\n :host {\n position: fixed;\n bottom: 1rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 9999;\n }\n\n .toolbar-root {\n display: flex;\n flex-direction: row;\n align-items: center;\n min-width: 24rem;\n gap: 0.5rem;\n padding: 0.75rem;\n border-radius: 2.5rem;\n backdrop-filter: blur(12px);\n box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n font-weight: 500;\n font-size: 0.875rem;\n line-height: 1;\n color: rgb(55 65 81);\n background-color: rgb(229 231 235 / 0.8);\n }\n\n @media (prefers-color-scheme: dark) {\n .toolbar-root {\n background-color: rgb(17 24 39 / 0.7);\n color: rgb(229 231 235);\n }\n }\n\n .toolbar-separator {\n width: 1px;\n height: 1.5rem;\n background-color: rgb(156 163 175 / 0.6);\n }\n\n @media (prefers-color-scheme: dark) {\n .toolbar-separator {\n background-color: rgb(107 114 128 / 0.7);\n }\n }\n\n /* Deck Selector */\n .selector-container {\n position: relative;\n display: flex;\n align-items: center;\n }\n\n .selector-select {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n opacity: 0;\n cursor: pointer;\n z-index: 2;\n }\n\n .selector-button {\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 1.75rem;\n min-width: 7.5rem;\n gap: 0.75rem;\n padding: 0 0.75rem;\n background-color: white;\n border: 2px solid rgb(209 213 219);\n border-radius: 1.5rem;\n font-size: 0.875rem;\n font-weight: 400;\n line-height: 0.75rem;\n color: rgb(55 65 81);\n cursor: pointer;\n user-select: none;\n transition: all 150ms ease-out;\n }\n\n .selector-button:hover {\n background-color: rgb(249 250 251);\n color: rgb(31 41 55);\n }\n\n .selector-button:focus {\n outline: none;\n box-shadow: 0 0 0 2px rgb(59 130 246);\n }\n\n @media (prefers-color-scheme: dark) {\n .selector-button {\n background-color: rgb(31 41 55);\n border-color: rgb(75 85 99);\n color: rgb(229 231 235);\n }\n\n .selector-button:hover {\n background-color: rgb(75 85 99);\n color: rgb(243 244 246);\n }\n\n .selector-button:focus {\n box-shadow: 0 0 0 2px rgb(96 165 250);\n }\n }\n\n .selector-text {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .selector-arrow {\n font-size: 0.5rem;\n color: rgb(107 114 128);\n pointer-events: none;\n }\n\n @media (prefers-color-scheme: dark) {\n .selector-arrow {\n color: rgb(156 163 175);\n }\n }\n\n /* Slide Navigation */\n .slide-nav-container {\n display: flex;\n align-items: center;\n height: 1.75rem;\n }\n\n .nav-container {\n display: flex;\n align-items: center;\n height: 1.75rem;\n border-radius: 1.5rem;\n border: 2px solid rgb(209 213 219);\n overflow: hidden;\n background-color: rgb(229 231 235);\n }\n\n @media (prefers-color-scheme: dark) {\n .nav-container {\n background-color: rgb(75 85 99);\n border-color: rgb(75 85 99);\n }\n }\n\n .nav-button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 100%;\n background-color: white;\n transition: all 150ms ease-out;\n color: rgb(75 85 99);\n font-size: 0.875rem;\n cursor: pointer;\n user-select: none;\n border: none;\n outline: none;\n }\n\n .nav-button:hover:not(:disabled) {\n background-color: rgb(249 250 251);\n color: rgb(31 41 55);\n }\n\n .nav-button:active:not(:disabled) {\n background-color: rgb(243 244 246);\n transform: scale(0.95);\n }\n\n .nav-button:focus:not(:disabled) {\n box-shadow: inset 0 0 0 2px rgb(59 130 246);\n }\n\n .nav-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .nav-button:disabled:hover {\n background-color: white;\n color: rgb(75 85 99);\n transform: none;\n }\n\n @media (prefers-color-scheme: dark) {\n .nav-button {\n background-color: rgb(31 41 55);\n color: rgb(209 213 219);\n }\n\n .nav-button:hover:not(:disabled) {\n background-color: rgb(75 85 99);\n color: rgb(243 244 246);\n }\n\n .nav-button:active:not(:disabled) {\n background-color: rgb(107 114 128);\n }\n\n .nav-button:focus:not(:disabled) {\n box-shadow: inset 0 0 0 2px rgb(96 165 250);\n }\n\n .nav-button:disabled:hover {\n background-color: rgb(55 65 81);\n color: rgb(209 213 219);\n }\n }\n\n .prev-button {\n border-top-left-radius: 1.5rem;\n border-bottom-left-radius: 1.5rem;\n border-right: 0;\n }\n\n .next-button {\n border-top-right-radius: 1.5rem;\n border-bottom-right-radius: 1.5rem;\n border-left: 0;\n }\n\n /* Slide Info */\n .slide-info-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n padding: 0 0.5rem;\n width: 20rem;\n }\n\n .slide-info-label {\n color: rgb(55 65 81);\n font-size: 0.875rem;\n font-weight: 500;\n line-height: 1rem;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n @media (prefers-color-scheme: dark) {\n .slide-info-label {\n color: rgb(229 231 235);\n }\n }\n\n /* Slide Indicators */\n .indicators-container {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 0.25rem;\n width: 100%;\n }\n\n .indicator-dot {\n width: 0.25rem;\n height: 0.25rem;\n border-radius: 50%;\n background-color: rgb(156 163 175);\n transition: opacity 200ms;\n }\n\n .dot-active {\n opacity: 0.9;\n }\n\n .dot-inactive {\n opacity: 0.5;\n }\n `;\n\n constructor() {\n super();\n \n // Initialize store state\n this.storeState = getIterationStoreState();\n \n // Enforce singleton pattern\n if (toolbarInstance && toolbarInstance !== this) {\n warnLog('Multiple IterationDeckToolbar instances detected. Only one toolbar should exist globally.');\n // Remove the existing instance from DOM if it exists\n if (toolbarInstance.parentNode) {\n toolbarInstance.parentNode.removeChild(toolbarInstance);\n }\n }\n \n toolbarInstance = this;\n }\n\n connectedCallback() {\n super.connectedCallback();\n \n // Only mount in development mode (or if any deck has development features enabled in production)\n if (!this.shouldShowToolbar()) {\n return;\n }\n\n // Using Tailwind classes from shared styles for cross-framework consistency\n\n // Subscribe to store changes\n this.unsubscribeFromStore = subscribeToIterationStore((state) => {\n this.storeState = state;\n this.requestUpdate();\n \n // Auto-select first deck if none selected and we have interactive decks\n const deckIds = state.getInteractiveDecks();\n if (deckIds.length > 0 && !state.selectedDeckId) {\n const store = getIterationStoreState();\n store.setSelectedDeck(deckIds[0]);\n }\n });\n\n // Add global keyboard listeners\n document.addEventListener('keydown', this.throttledKeyboardHandler);\n \n // Set toolbar as visible\n this.setAttribute('visible', '');\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n \n // Clean up subscriptions\n this.unsubscribeFromStore?.();\n document.removeEventListener('keydown', this.throttledKeyboardHandler);\n \n // Clear singleton reference\n if (toolbarInstance === this) {\n toolbarInstance = null;\n }\n }\n\n /**\n * Check if any deck has development features enabled in production or if we're in natural development mode\n */\n private shouldShowToolbar(): boolean {\n if (isDevelopmentMode()) {\n return true;\n }\n \n // Check if any deck has development features enabled in production\n const deckElements = document.querySelectorAll('iteration-deck');\n for (const deckElement of deckElements) {\n if (deckElement.hasAttribute('enable-in-production')) {\n return true;\n }\n }\n \n return false;\n }\n\n /**\n * Handle global keyboard navigation shortcuts\n */\n private handleKeyboardNavigation(event: KeyboardEvent) {\n // Don't interfere with form inputs or other interactive elements\n const target = event.target as HTMLElement;\n if (target && (\n target.tagName === 'INPUT' || \n target.tagName === 'TEXTAREA' || \n target.tagName === 'SELECT' ||\n target.isContentEditable\n )) {\n return;\n }\n\n const direction = isNavigationShortcut(event);\n if (!direction) {\n return;\n }\n\n // Prevent default browser behavior\n event.preventDefault();\n event.stopPropagation();\n\n // Navigate the currently selected deck\n this.navigateSelectedDeck(direction);\n }\n\n /**\n * Navigate the currently selected deck in the specified direction\n */\n private navigateSelectedDeck(direction: NavigationDirection) {\n const selectedDeckId = this.getSelectedDeckId();\n if (!selectedDeckId) {\n return;\n }\n\n const deck = this.getCurrentDeck(selectedDeckId);\n if (!deck) {\n warnLog('Selected deck not found in store', selectedDeckId);\n return;\n }\n\n const currentSlideIndex = deck.slideIds.indexOf(deck.activeSlideId);\n let newSlideIndex: number;\n\n switch (direction) {\n case 'prev':\n newSlideIndex = currentSlideIndex > 0 ? currentSlideIndex - 1 : deck.slideIds.length - 1;\n break;\n case 'next':\n newSlideIndex = currentSlideIndex < deck.slideIds.length - 1 ? currentSlideIndex + 1 : 0;\n break;\n case 'first':\n newSlideIndex = 0;\n break;\n case 'last':\n newSlideIndex = deck.slideIds.length - 1;\n break;\n default:\n return;\n }\n\n const newSlideId = deck.slideIds[newSlideIndex];\n if (newSlideId) {\n const store = getIterationStoreState();\n store.setActiveSlide(selectedDeckId, newSlideId);\n }\n }\n\n /**\n * Get the currently selected deck ID\n */\n private getSelectedDeckId(): string | undefined {\n const deckIds = this.storeState.getInteractiveDecks();\n \n // If only one interactive deck, it's implicitly selected\n if (deckIds.length === 1) {\n return deckIds[0];\n }\n \n // Return explicitly selected deck\n return this.storeState.selectedDeckId;\n }\n\n /**\n * Get deck registration by ID\n */\n private getCurrentDeck(deckId?: string): DeckRegistration | null {\n if (!deckId) return null;\n \n const store = getIterationStoreState();\n const metadata = store.getDeckMetadata(deckId);\n if (!metadata) {\n return null;\n }\n\n return {\n id: deckId,\n label: metadata.label || deckId,\n slideIds: metadata.slideIds,\n activeSlideId: metadata.activeSlideId,\n registeredAt: Date.now()\n };\n }\n\n /**\n * Handle deck selection change from dropdown\n */\n private handleDeckSelection(event: Event) {\n const select = event.target as HTMLSelectElement;\n const deckId = select.value;\n \n if (deckId) {\n const store = getIterationStoreState();\n store.setSelectedDeck(deckId);\n \n // Auto-scroll to deck and highlight it\n this.scrollToDeckAndHighlight(deckId);\n }\n }\n\n /**\n * Scroll to deck and add glow effect\n */\n private scrollToDeckAndHighlight(deckId: string) {\n const deckElement = document.querySelector(`iteration-deck[id=\"${deckId}\"]`) as HTMLElement;\n \n if (!deckElement) {\n return;\n }\n \n const activeSlide = deckElement.querySelector('iteration-deck-slide[aria-hidden=\"false\"]') as IterationDeckSlide;\n const slottedContent = activeSlide?.getPrimarySlottedElement();\n \n const elementToHighlight = slottedContent || activeSlide || deckElement;\n\n const rect = deckElement.getBoundingClientRect();\n const currentScrollY = window.scrollY;\n const elementTop = rect.top + currentScrollY;\n const targetScrollY = elementTop - (window.innerHeight * 0.15);\n\n const finalScrollY = Math.max(0, targetScrollY);\n window.scrollTo({\n top: finalScrollY,\n behavior: 'smooth'\n });\n\n this.waitForScrollComplete(finalScrollY, () => {\n this.addGlowEffect(elementToHighlight);\n });\n }\n\n /**\n * Wait for scroll animation to complete and then execute callback\n */\n private waitForScrollComplete(targetY: number, callback: () => void) {\n let scrollTimeout: number;\n let maxWaitTimeout: number;\n \n const onScroll = () => {\n clearTimeout(scrollTimeout);\n \n // Check if we're close enough to the target (within 5px tolerance)\n scrollTimeout = window.setTimeout(() => {\n const currentY = window.scrollY;\n const tolerance = 5;\n \n if (Math.abs(currentY - targetY) <= tolerance) {\n // Scroll has stopped at the target position\n window.removeEventListener('scroll', onScroll);\n clearTimeout(maxWaitTimeout);\n callback();\n }\n }, 150); // Wait 150ms of no scroll movement to consider it \"stopped\"\n };\n \n // Safety timeout - trigger after 3 seconds maximum, even if scroll detection fails\n maxWaitTimeout = window.setTimeout(() => {\n window.removeEventListener('scroll', onScroll);\n clearTimeout(scrollTimeout);\n callback();\n }, 3000);\n \n // Start listening for scroll events\n window.addEventListener('scroll', onScroll);\n \n // Also trigger immediately if we're already at the target position\n const currentY = window.scrollY;\n if (Math.abs(currentY - targetY) <= 5) {\n window.removeEventListener('scroll', onScroll);\n clearTimeout(maxWaitTimeout);\n setTimeout(callback, 100); // Small delay to ensure DOM is settled\n }\n }\n\n /**\n * Inject global CSS for deck glow animations (called once)\n */\n private static ensureGlowStyles() {\n const styleId = 'iteration-deck-glow-styles';\n if (document.getElementById(styleId)) return; // Already injected\n \n const style = document.createElement('style');\n style.id = styleId;\n style.textContent = `\n /* Iteration Deck Glow Effect Global Styles */\n [data-iteration-glow] {\n border-radius: 8px;\n position: relative;\n animation: iteration-deck-glow 1s ease-in-out;\n }\n \n @keyframes iteration-deck-glow {\n 0% {\n box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.2), 0 0 25px rgba(236, 72, 153, 0.1);\n outline: 2px solid rgba(236, 72, 153, 0.2);\n }\n 50% {\n box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.4), 0 0 25px rgba(236, 72, 153, 0.3);\n outline: 2px solid rgba(236, 72, 153, 0.6);\n }\n 100% {\n box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.2), 0 0 25px rgba(236, 72, 153, 0.1);\n outline: 2px solid rgba(236, 72, 153, 0);\n }\n }\n \n /* Accessibility: respect reduced motion preference */\n @media (prefers-reduced-motion: reduce) {\n [data-iteration-glow] {\n animation: none;\n border: 2px solid rgba(236, 72, 153, 0.6);\n box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.3), 0 0 25px rgba(236, 72, 153, 0.2);\n }\n }\n `;\n document.head.appendChild(style);\n }\n\n /**\n * Add temporary glow effect to deck element using CSS classes\n * Much simpler and more performant than inline style manipulation\n */\n private addGlowEffect(deckElement: HTMLElement) {\n // Ensure global styles are injected\n IterationDeckToolbar.ensureGlowStyles();\n \n // Add glow data attribute to trigger CSS animation\n deckElement.setAttribute('data-iteration-glow', '');\n \n // Remove glow attribute after animation completes\n setTimeout(() => {\n deckElement.removeAttribute('data-iteration-glow');\n }, 800);\n }\n\n /**\n * Handle previous slide navigation\n */\n private handlePrevSlide() {\n try {\n this.navigateSelectedDeck('prev');\n } catch (error) {\n errorLog('Error navigating to previous slide:', error);\n }\n }\n\n /**\n * Handle next slide navigation\n */\n private handleNextSlide() {\n try {\n this.navigateSelectedDeck('next');\n } catch (error) {\n errorLog('Error navigating to next slide:', error);\n }\n }\n\n /**\n * Get display label for a deck\n */\n private getDeckLabel(deckId: string): string {\n const store = getIterationStoreState();\n const metadata = store.getDeckMetadata(deckId);\n return metadata?.label || deckId;\n }\n\n /**\n * Get current slide label for display\n */\n private getCurrentSlideLabel(): string {\n const selectedDeckId = this.getSelectedDeckId();\n if (!selectedDeckId) {\n return 'No deck selected';\n }\n\n const store = getIterationStoreState();\n const metadata = store.getDeckMetadata(selectedDeckId);\n if (!metadata || !metadata.activeSlideId) {\n return 'No slide active';\n }\n\n // Get current slide label from metadata\n const currentSlideLabel = metadata.slides?.find(slide => slide.id === metadata.activeSlideId)?.label;\n \n return currentSlideLabel || 'No slide selected';\n }\n\n /**\n * Check if we can navigate to previous slide\n */\n private canNavigatePrev(): boolean {\n const selectedDeckId = this.getSelectedDeckId();\n if (!selectedDeckId) {\n return false;\n }\n\n const deck = this.getCurrentDeck(selectedDeckId);\n if (!deck) {\n return false;\n }\n \n if (deck.slideIds.length <= 1) {\n return false;\n }\n\n return true; // Always allow navigation (wraps around)\n }\n\n /**\n * Render carousel-style slide indicators\n */\n private renderSlideIndicators() {\n const selectedDeckId = this.getSelectedDeckId();\n if (!selectedDeckId) {\n return '';\n }\n\n const deck = this.getCurrentDeck(selectedDeckId);\n if (!deck || deck.slideIds.length <= 1) {\n return '';\n }\n\n const currentSlideIndex = deck.slideIds.indexOf(deck.activeSlideId);\n\n return html`\n <div class=\"indicators-container\" aria-hidden=\"true\">\n ${deck.slideIds.map((_, index) => html`\n <div\n class=\"indicator-dot ${index === currentSlideIndex ? 'dot-active' : 'dot-inactive'}\"\n ></div>\n `)}\n </div>\n `;\n }\n\n\n /**\n * Check if we can navigate to next slide\n */\n private canNavigateNext(): boolean {\n const selectedDeckId = this.getSelectedDeckId();\n if (!selectedDeckId) {\n return false;\n }\n\n const deck = this.getCurrentDeck(selectedDeckId);\n if (!deck) {\n return false;\n }\n \n if (deck.slideIds.length <= 1) {\n return false;\n }\n\n return true; // Always allow navigation (wraps around)\n }\n\n render() {\n // Don't render in production mode (unless any deck is forcing dev mode)\n if (!this.shouldShowToolbar()) {\n return nothing;\n }\n\n const deckIds = this.storeState.getInteractiveDecks();\n const selectedDeckId = this.getSelectedDeckId();\n const hasMultipleDecks = deckIds.length > 1;\n const hasAnyDecks = deckIds.length > 0;\n \n // Don't render if no interactive decks are available\n if (!hasAnyDecks) {\n return nothing;\n }\n\n return html`\n <div class=\"toolbar-root\" role=\"toolbar\" aria-label=\"Iteration Deck Toolbar\">\n ${hasMultipleDecks ? html`\n <div class=\"selector-container\">\n <select \n class=\"selector-select\"\n @change=${this.handleDeckSelection}\n .value=${selectedDeckId || ''}\n aria-label=\"Select iteration deck\"\n >\n ${deckIds.map(deckId => html`\n <option \n value=${deckId} \n ?selected=${deckId === selectedDeckId}\n >\n ${this.getDeckLabel(deckId)}\n </option>\n `)}\n </select>\n <div class=\"selector-button\">\n <span class=\"selector-text\">${this.getDeckLabel(selectedDeckId || '')}</span>\n <span class=\"selector-arrow\">▼</span>\n </div>\n </div>\n <div class=\"toolbar-separator\"></div>\n ` : ''}\n \n <div class=\"slide-nav-container\">\n <nav class=\"nav-container\">\n <button \n class=\"nav-button prev-button\"\n @click=${this.handlePrevSlide}\n ?disabled=${!this.canNavigatePrev()}\n aria-label=\"Previous slide (Ctrl/Cmd+Alt+[)\"\n title=\"Previous slide (Ctrl/Cmd+Alt+[)\"\n >\n <svg width=\"18\" height=\"12\" viewBox=\"0 0 18 12\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M7 6l6-4v8l-6-4z\" />\n </svg>\n </button>\n \n <button \n class=\"nav-button next-button\"\n @click=${this.handleNextSlide}\n ?disabled=${!this.canNavigateNext()}\n aria-label=\"Next slide (Ctrl/Cmd+Alt+])\"\n title=\"Next slide (Ctrl/Cmd+Alt+])\"\n >\n <svg width=\"18\" height=\"12\" viewBox=\"0 0 18 12\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M11 6l-6 4V2l6 4z\" />\n </svg>\n </button>\n </nav>\n \n <div class=\"slide-info-container\">\n <span class=\"slide-info-label\" \n title=${this.getCurrentSlideLabel()}>\n ${this.getCurrentSlideLabel()}\n </span>\n ${this.renderSlideIndicators()}\n </div>\n </div>\n </div>\n `;\n }\n}\n\n/**\n * Check if any deck has development features enabled in production\n */\nfunction shouldShowToolbarGlobally(): boolean {\n if (isDevelopmentMode()) {\n return true;\n }\n \n // Check if any deck has development features enabled in production\n const deckElements = document.querySelectorAll('iteration-deck');\n for (const deckElement of deckElements) {\n if (deckElement.hasAttribute('enable-in-production')) {\n return true;\n }\n }\n \n return false;\n}\n\n/**\n * Utility function to ensure toolbar is mounted\n * Called by IterationDeck components when they connect\n */\nexport function ensureToolbarMounted(): void {\n // Only mount in development mode or if any deck has development features enabled in production\n if (!shouldShowToolbarGlobally()) {\n return;\n }\n\n // Check if toolbar already exists in DOM\n if (document.querySelector('iteration-deck-toolbar')) {\n return;\n }\n\n // Create and mount toolbar\n const toolbar = new IterationDeckToolbar();\n document.body.appendChild(toolbar);\n}\n\n/**\n * Utility function to clean up toolbar when no decks remain\n * Called by IterationDeck components when they disconnect\n */\nexport function cleanupToolbarIfEmpty(): void {\n // Small delay to allow for component cleanup\n setTimeout(() => {\n const store = getIterationStoreState();\n const deckIds = store.getInteractiveDecks();\n \n // If no interactive decks remain, remove the toolbar\n if (deckIds.length === 0) {\n const toolbar = document.querySelector('iteration-deck-toolbar');\n if (toolbar) {\n toolbar.remove();\n }\n }\n }, 100);\n}\n\n/**\n * Export the toolbar instance for direct access if needed\n */\nexport function getToolbarInstance(): IterationDeckToolbar | null {\n return toolbarInstance;\n}\n\n// Register the custom element\ndeclare global {\n interface HTMLElementTagNameMap {\n 'iteration-deck-toolbar': IterationDeckToolbar;\n }\n}","/**\n * IterationDeck Lit Web Component\n * \n * Main container component that wraps AI-generated UI variations with\n * intuitive controls for switching between them. Built for AI-first\n * prototyping workflows with slot-based architecture for universal compatibility.\n * \n * Features:\n * - Slot-based children rendering for framework-agnostic usage\n * - Zustand store integration for state management\n * - Environment detection (production vs development modes)\n * - Automatic slide detection and registration\n * - Keyboard shortcut support via global toolbar\n * - Multi-deck support with automatic cleanup\n */\n\nimport { LitElement, html, type PropertyValues } from 'lit';\nimport { customElement, property, state } from 'lit/decorators.js';\n\n// Import core types and utilities\nimport type { \n SlideChangeEvent, \n DeckRegistrationEvent \n} from '../../types/index.js';\nimport { \n isDevelopment, \n generateSlideId, \n validateDeckId, \n errorLog, \n warnLog \n} from '../../utils/index.js';\n\n// Import store integration\nimport { \n subscribeToIterationStore, \n getIterationStoreState, \n type IterationStore \n} from '../../store/iteration-store.js';\n\n// Import shared styles for Tailwind consistency\nimport { deckStyles } from '../../../../shared/styles.js';\n\n// Import toolbar integration\nimport { ensureToolbarMounted, cleanupToolbarIfEmpty } from '../iteration-deck-toolbar';\n\n\n/**\n * Internal interface for slide element data extracted from slots\n */\ninterface SlideElementData {\n id: string;\n label: string;\n element: Element;\n aiPrompt?: string;\n notes?: string;\n confidence?: number;\n}\n\n/**\n * IterationDeck Web Component\n * \n * @element iteration-deck\n * @slot - Default slot for IterationDeckSlide elements\n * \n * @fires slide-change - Fired when the active slide changes\n * @fires deck-registered - Fired when the deck is registered with the store\n * @fires deck-unregistered - Fired when the deck is unregistered from the store\n */\n@customElement('iteration-deck')\nexport class IterationDeck extends LitElement {\n /**\n * Unique identifier for this iteration deck\n * Used for state management across the application\n */\n @property({ type: String, reflect: true })\n id!: string;\n\n /**\n * Display label for this deck in the toolbar dropdown\n * Falls back to the deck ID if not provided\n */\n @property({ type: String, reflect: false })\n label?: string;\n\n /**\n * Origi