blacktrigram 0.7.11 → 0.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/DATA_MODEL.md +347 -0
  2. package/lib/App.d.ts.map +1 -1
  3. package/lib/App2.js +0 -6
  4. package/lib/App2.js.map +1 -1
  5. package/lib/assets/index.css +102 -94
  6. package/lib/components/screens/combat/CombatScreen3D.d.ts +1 -1
  7. package/lib/components/screens/combat/CombatScreen3D.js +2 -2
  8. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  9. package/lib/components/screens/combat/components/controls/CombatButtons.d.ts.map +1 -1
  10. package/lib/components/screens/combat/components/controls/CombatButtons.js +22 -7
  11. package/lib/components/screens/combat/components/controls/CombatButtons.js.map +1 -1
  12. package/lib/components/screens/combat/components/controls/CombatControlsPanel.d.ts +2 -0
  13. package/lib/components/screens/combat/components/controls/CombatControlsPanel.d.ts.map +1 -1
  14. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js +7 -4
  15. package/lib/components/screens/combat/components/controls/CombatControlsPanel.js.map +1 -1
  16. package/lib/components/screens/combat/components/controls/PauseMenu.d.ts.map +1 -1
  17. package/lib/components/screens/combat/components/controls/PauseMenu.js +15 -5
  18. package/lib/components/screens/combat/components/controls/PauseMenu.js.map +1 -1
  19. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js +15 -16
  20. package/lib/components/screens/combat/components/feedback/RoundAnnouncementOverlayHtml.js.map +1 -1
  21. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js +1 -2
  22. package/lib/components/screens/combat/components/hud/DifficultyIndicator.js.map +1 -1
  23. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js +28 -24
  24. package/lib/components/screens/combat/components/hud/PlayerStateOverlayHtml.js.map +1 -1
  25. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js +2 -4
  26. package/lib/components/screens/combat/components/indicators/InputBufferDisplay.js.map +1 -1
  27. package/lib/components/screens/controls/ControlsScreen3D.d.ts.map +1 -1
  28. package/lib/components/screens/controls/ControlsScreen3D.js +3 -2
  29. package/lib/components/screens/controls/ControlsScreen3D.js.map +1 -1
  30. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js +28 -30
  31. package/lib/components/screens/controls/components/InteractiveControlDemoOverlayHtml.js.map +1 -1
  32. package/lib/components/screens/controls/components/VisualKeyboard3D.d.ts.map +1 -1
  33. package/lib/components/screens/controls/components/VisualKeyboard3D.js +4 -3
  34. package/lib/components/screens/controls/components/VisualKeyboard3D.js.map +1 -1
  35. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  36. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.d.ts.map +1 -1
  37. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js +5 -2
  38. package/lib/components/screens/intro/components/MenuButtonsOverlayHtml.js.map +1 -1
  39. package/lib/components/screens/philosophy/components/TrigramSymbol3D.d.ts.map +1 -1
  40. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js +4 -4
  41. package/lib/components/screens/training/components/FootworkDrillsOverlayHtml.js.map +1 -1
  42. package/lib/components/screens/training/components/hud/TrainingTopHUD.js +1 -1
  43. package/lib/components/screens/training/components/hud/TrainingTopHUD.js.map +1 -1
  44. package/lib/components/shared/base/ResponsiveContainer.d.ts +6 -0
  45. package/lib/components/shared/base/ResponsiveContainer.d.ts.map +1 -1
  46. package/lib/components/shared/three/effects/ActionFeedback.js +1 -2
  47. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  48. package/lib/components/shared/three/effects/DamageNumbers.js +1 -2
  49. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  50. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js +5 -5
  51. package/lib/components/shared/three/ui/BodyPartHealthDisplay.js.map +1 -1
  52. package/lib/components/shared/three/ui/BreathingIndicator2.js +3 -2
  53. package/lib/components/shared/three/ui/BreathingIndicator2.js.map +1 -1
  54. package/lib/components/shared/three/ui/TechniqueCard.d.ts.map +1 -1
  55. package/lib/components/shared/three/ui/TechniqueCard.js +27 -30
  56. package/lib/components/shared/three/ui/TechniqueCard.js.map +1 -1
  57. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.d.ts.map +1 -1
  58. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js +57 -59
  59. package/lib/components/shared/three/ui/VitalPointOverlayControlsHtml.js.map +1 -1
  60. package/lib/components/shared/ui/BaseHUDContainer.d.ts +40 -0
  61. package/lib/components/shared/ui/BaseHUDContainer.d.ts.map +1 -1
  62. package/lib/components/shared/ui/BaseHUDContainer.js +40 -0
  63. package/lib/components/shared/ui/BaseHUDContainer.js.map +1 -1
  64. package/lib/components/shared/ui/MobileHUDLayout.d.ts +13 -0
  65. package/lib/components/shared/ui/MobileHUDLayout.d.ts.map +1 -1
  66. package/lib/components/shared/ui/SplashScreen.js +10 -10
  67. package/lib/components/shared/ui/SplashScreen.js.map +1 -1
  68. package/lib/components/shared/ui/VitalPointOverlayControlsPure.d.ts.map +1 -1
  69. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js +57 -62
  70. package/lib/components/shared/ui/VitalPointOverlayControlsPure.js.map +1 -1
  71. package/lib/components/shared/ui/VolumeControl.js +7 -7
  72. package/lib/components/shared/ui/VolumeControl.js.map +1 -1
  73. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"VitalPointOverlayControlsPure.js","names":[],"sources":["../../../../src/components/shared/ui/VitalPointOverlayControlsPure.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsPure - Pure DOM controls for vital point visualization\n *\n * Pure DOM version (no Three.js Html wrapper) for use in HUD overlays.\n * Identical functionality to VitalPointOverlayControlsHtml but renders as pure React DOM.\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/ui/VitalPointOverlayControlsPure\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../types/common\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsPure component\n */\nexport interface VitalPointOverlayControlsPureProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n\n/**\n * Convert numeric color to CSS hex string\n */\nconst colorToHex = (color: number): string => {\n return `#${color.toString(16).padStart(6, \"0\")}`;\n};\n\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return \"#ff0000\"; // Red\n case VitalPointSeverity.CRITICAL:\n return \"#ff6600\"; // Orange\n case VitalPointSeverity.MAJOR:\n return \"#ffaa00\"; // Gold\n case VitalPointSeverity.MODERATE:\n return \"#ffd700\"; // Yellow\n case VitalPointSeverity.MINOR:\n return \"#00ff88\"; // Green\n default:\n return \"#00ffff\"; // Cyan\n }\n};\n\n/**\n * VitalPointOverlayControlsPure Component\n * Pure DOM version - renders directly without Three.js Html wrapper\n */\nexport const VitalPointOverlayControlsPure: React.FC<\n VitalPointOverlayControlsPureProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n // Use internal state if no external control provided\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n // Get system statistics\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n // Default screen position - left side, below player 1 status (accounting for stance indicator)\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile],\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n // Get filtered count\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n // Filter by severity\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n // Filter by region\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n // Match both left and right arm vital points\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\"),\n );\n } else if (regionFilter === \"legs\") {\n // Match both left and right leg vital points\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\"),\n );\n } else {\n // Simple prefix match for head_ or torso_\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n // Filter by search query\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query),\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n // Toggle severity filter\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange],\n );\n\n // Severity options\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n // Region options\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n // Panel styles\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${colorToHex(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: \"#ffffff\",\n boxShadow: `0 0 30px ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40, inset 0 0 20px ${colorToHex(KOREAN_COLORS.UI_BACKGROUND_DARK)}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}40`,\n background: `linear-gradient(90deg, ${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n )}00 0%, ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}10 50%, ${colorToHex(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: colorToHex(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${colorToHex(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: \"#ffffff\",\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${colorToHex(\n KOREAN_COLORS.ACCENT_GOLD,\n )} 0%, ${colorToHex(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${colorToHex(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? colorToHex(KOREAN_COLORS.ACCENT_GOLD)\n : colorToHex(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? \"#1a1a1a\" : \"#ffffff\",\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${colorToHex(\n KOREAN_COLORS.ACCENT_GOLD,\n )}60, inset 0 2px 4px rgba(255,255,255,0.2)`\n : `0 2px 8px ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${colorToHex(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${colorToHex(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${colorToHex(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: colorToHex(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${colorToHex(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: \"#ffffff\",\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: colorToHex(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )} 0%, ${colorToHex(KOREAN_COLORS.ACCENT_BLUE)} 100%)`\n : `${colorToHex(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: \"#ffffff\",\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: colorToHex(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: \"#ffffff\",\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: colorToHex(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = colorToHex(\n KOREAN_COLORS.ACCENT_RED,\n );\n e.currentTarget.style.background = `${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = colorToHex(\n KOREAN_COLORS.TEXT_SECONDARY,\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: colorToHex(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: colorToHex(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: colorToHex(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${colorToHex(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}`,\n borderRadius: \"6px\",\n color: colorToHex(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${colorToHex(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${colorToHex(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: colorToHex(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: colorToHex(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${colorToHex(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n fontSize: smallFontSize,\n color: colorToHex(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default VitalPointOverlayControlsPure;\n"],"mappings":"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"}
1
+ {"version":3,"file":"VitalPointOverlayControlsPure.js","names":[],"sources":["../../../../src/components/shared/ui/VitalPointOverlayControlsPure.tsx"],"sourcesContent":["/**\n * VitalPointOverlayControlsPure - Pure DOM controls for vital point visualization\n *\n * Pure DOM version (no Three.js Html wrapper) for use in HUD overlays.\n * Identical functionality to VitalPointOverlayControlsHtml but renders as pure React DOM.\n *\n * Provides comprehensive controls for the 70-point vital point overlay system:\n * - Toggle overlay visibility\n * - Filter by severity level\n * - Filter by body region\n * - Search vital points\n * - Adjust marker scale\n * - Toggle labels\n * - Toggle animations\n *\n * @module components/shared/ui/VitalPointOverlayControlsPure\n */\n\nimport React, { useCallback, useMemo, useState } from \"react\";\nimport {\n KOREAN_VITAL_POINTS,\n getVitalPointsStats,\n} from \"../../../systems/vitalpoint/KoreanVitalPoints\";\nimport { VitalPointSeverity } from \"../../../types/common\";\nimport { Z_INDEX } from \"../../../types/LayoutTypes\";\nimport { FONT_FAMILY, KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString } from \"@/utils/colorUtils\";\nimport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\nexport type { BodyRegionFilter } from \"../three/effects/VitalPointMarkers3D\";\n\n/**\n * Props for VitalPointOverlayControlsPure component\n */\nexport interface VitalPointOverlayControlsPureProps {\n /** Whether overlay is currently visible */\n readonly visible: boolean;\n /** Callback when visibility changes */\n readonly onVisibleChange: (visible: boolean) => void;\n /** Current severity filters */\n readonly severityFilters: VitalPointSeverity[];\n /** Callback when severity filters change */\n readonly onSeverityFiltersChange: (filters: VitalPointSeverity[]) => void;\n /** Current region filter */\n readonly regionFilter: BodyRegionFilter;\n /** Callback when region filter changes */\n readonly onRegionFilterChange: (filter: BodyRegionFilter) => void;\n /** Current search query */\n readonly searchQuery?: string;\n /** Callback when search query changes */\n readonly onSearchQueryChange?: (query: string) => void;\n /** Whether labels are shown */\n readonly showLabels: boolean;\n /** Callback when label visibility changes */\n readonly onShowLabelsChange: (show: boolean) => void;\n /** Whether animations are enabled */\n readonly animated: boolean;\n /** Callback when animation state changes */\n readonly onAnimatedChange: (animated: boolean) => void;\n /** Marker scale multiplier */\n readonly scale: number;\n /** Callback when scale changes */\n readonly onScaleChange: (scale: number) => void;\n /** Whether on mobile device */\n readonly isMobile?: boolean;\n /**\n * Screen position for the control panel.\n *\n * All values must be valid CSS position values, such as `\"20px\"`, `\"10%\"`, `\"1rem\"`, or `\"auto\"`.\n * These are applied directly to the `style` of the container.\n */\n readonly screenPosition?: {\n /** CSS `top` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n top?: string;\n /** CSS `left` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n left?: string;\n /** CSS `right` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n right?: string;\n /** CSS `bottom` position (e.g., \"20px\", \"10%\", \"1rem\", \"auto\") */\n bottom?: string;\n };\n}\n/**\n * Get color for severity level\n */\nconst getSeverityColor = (severity: VitalPointSeverity): string => {\n switch (severity) {\n case VitalPointSeverity.LETHAL:\n return hexColorToCSS(KOREAN_COLORS.NEGATIVE_RED);\n case VitalPointSeverity.CRITICAL:\n return hexColorToCSS(KOREAN_COLORS.HEALTH_LOW);\n case VitalPointSeverity.MAJOR:\n return hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD);\n case VitalPointSeverity.MODERATE:\n return hexColorToCSS(KOREAN_COLORS.TRIGRAM_GEON_PRIMARY);\n case VitalPointSeverity.MINOR:\n return hexColorToCSS(KOREAN_COLORS.POSITIVE_GREEN);\n default:\n return hexColorToCSS(KOREAN_COLORS.NEON_CYAN);\n }\n};\n\n/**\n * VitalPointOverlayControlsPure Component\n * Pure DOM version - renders directly without Three.js Html wrapper\n */\nexport const VitalPointOverlayControlsPure: React.FC<\n VitalPointOverlayControlsPureProps\n> = ({\n visible,\n onVisibleChange,\n severityFilters,\n onSeverityFiltersChange,\n regionFilter,\n onRegionFilterChange,\n searchQuery: externalSearchQuery,\n onSearchQueryChange,\n showLabels,\n onShowLabelsChange,\n animated,\n onAnimatedChange,\n scale,\n onScaleChange,\n isMobile = false,\n screenPosition,\n}) => {\n const [expanded, setExpanded] = useState(false);\n\n // Use internal state if no external control provided\n const [internalSearchQuery, setInternalSearchQuery] = useState(\"\");\n const searchQuery = externalSearchQuery ?? internalSearchQuery;\n const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery;\n\n // Get system statistics\n const stats = useMemo(() => getVitalPointsStats(), []);\n\n // Default screen position - left side, below player 1 status (accounting for stance indicator)\n const defaultPosition: {\n top?: string;\n left?: string;\n right?: string;\n bottom?: string;\n } = useMemo(\n () => ({\n top: isMobile ? \"180px\" : \"220px\",\n left: isMobile ? \"10px\" : \"20px\",\n }),\n [isMobile],\n );\n\n const finalPosition = screenPosition ?? defaultPosition;\n\n // Get filtered count\n const filteredCount = useMemo(() => {\n let points = [...KOREAN_VITAL_POINTS];\n\n // Filter by severity\n if (severityFilters.length > 0) {\n points = points.filter((vp) => severityFilters.includes(vp.severity));\n }\n\n // Filter by region\n if (regionFilter !== \"all\") {\n if (regionFilter === \"arms\") {\n // Match both left and right arm vital points\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"arm_left_\") || vp.id.startsWith(\"arm_right_\"),\n );\n } else if (regionFilter === \"legs\") {\n // Match both left and right leg vital points\n points = points.filter(\n (vp) =>\n vp.id.startsWith(\"leg_left_\") || vp.id.startsWith(\"leg_right_\"),\n );\n } else {\n // Simple prefix match for head_ or torso_\n const prefix = `${regionFilter}_`;\n points = points.filter((vp) => vp.id.startsWith(prefix));\n }\n }\n\n // Filter by search query\n if (searchQuery) {\n const query = searchQuery.toLowerCase();\n points = points.filter(\n (vp) =>\n vp.names.korean.toLowerCase().includes(query) ||\n vp.names.english.toLowerCase().includes(query) ||\n vp.names.romanized.toLowerCase().includes(query) ||\n vp.id.toLowerCase().includes(query),\n );\n }\n\n return points.length;\n }, [severityFilters, regionFilter, searchQuery]);\n\n // Toggle severity filter\n const toggleSeverityFilter = useCallback(\n (severity: VitalPointSeverity) => {\n const newFilters = severityFilters.includes(severity)\n ? severityFilters.filter((s) => s !== severity)\n : [...severityFilters, severity];\n onSeverityFiltersChange(newFilters);\n },\n [severityFilters, onSeverityFiltersChange],\n );\n\n // Severity options\n const severityOptions: VitalPointSeverity[] = [\n VitalPointSeverity.LETHAL,\n VitalPointSeverity.CRITICAL,\n VitalPointSeverity.MAJOR,\n VitalPointSeverity.MODERATE,\n VitalPointSeverity.MINOR,\n ];\n\n // Region options\n const regionOptions: {\n value: BodyRegionFilter;\n label: string;\n korean: string;\n }[] = [\n { value: \"all\", label: \"All Regions\", korean: \"전체\" },\n { value: \"head\", label: \"Head\", korean: \"머리\" },\n { value: \"torso\", label: \"Torso\", korean: \"몸통\" },\n { value: \"arms\", label: \"Arms\", korean: \"팔\" },\n { value: \"legs\", label: \"Legs\", korean: \"다리\" },\n ];\n\n // Panel styles\n const panelWidth = isMobile ? 280 : 350;\n const buttonHeight = isMobile ? 32 : 36;\n const fontSize = isMobile ? 11 : 13;\n const smallFontSize = isMobile ? 9 : 10;\n\n return (\n <div\n style={{\n position: \"absolute\",\n top: finalPosition.top,\n left: finalPosition.left,\n right: finalPosition.right,\n bottom: finalPosition.bottom,\n width: panelWidth,\n background: `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}f0`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"12px\",\n padding: isMobile ? \"12px\" : \"16px\",\n fontFamily: FONT_FAMILY.KOREAN,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n boxShadow: `0 0 30px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40, inset 0 0 20px ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}80`,\n transition: \"all 0.3s ease\",\n pointerEvents: \"all\",\n zIndex: Z_INDEX.MODAL,\n }}\n data-testid=\"vital-point-overlay-controls\"\n >\n {/* Header with toggle */}\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n marginBottom: \"12px\",\n paddingBottom: \"12px\",\n borderBottom: `1px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}40`,\n background: `linear-gradient(90deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_DARK,\n )}00 0%, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}10 50%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)}00 100%)`,\n }}\n >\n <div>\n <div style={{ fontSize: isMobile ? 14 : 16, fontWeight: \"bold\" }}>\n 급소 오버레이 | Vital Points\n </div>\n <div\n style={{\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n marginTop: \"2px\",\n }}\n >\n {filteredCount} / {stats.total} 표시 | Showing\n </div>\n </div>\n <button\n onClick={() => setExpanded(!expanded)}\n style={{\n background: `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}`,\n borderRadius: \"6px\",\n padding: \"8px 14px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n boxShadow: `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"scale(1.05)\";\n e.currentTarget.style.boxShadow = `0 4px 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"scale(1)\";\n e.currentTarget.style.boxShadow = `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}30`;\n }}\n data-testid=\"toggle-expand-button\"\n >\n {expanded ? \"▼\" : \"▶\"}\n </button>\n </div>\n\n {/* Main toggle */}\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => onVisibleChange(!visible)}\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: visible\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.SECONDARY_YELLOW)} 100%)`\n : `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_DARK)} 100%)`,\n border: `2px solid ${\n visible\n ? hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)\n : hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)\n }`,\n borderRadius: \"8px\",\n color: visible ? hexColorToCSS(KOREAN_COLORS.KOREAN_BLACK) : hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: isMobile ? 13 : 15,\n fontWeight: \"bold\",\n cursor: \"pointer\",\n transition: \"all 0.3s ease\",\n boxShadow: visible\n ? `0 4px 16px ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_GOLD,\n )}60, inset 0 2px 4px ${hexToRgbaString(KOREAN_COLORS.TEXT_PRIMARY, 0.2)}`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`,\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.transform = \"translateY(-2px)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 6px 20px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}80`\n : `0 4px 12px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}50`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.transform = \"translateY(0)\";\n e.currentTarget.style.boxShadow = visible\n ? `0 4px 16px ${hexColorToCSS(KOREAN_COLORS.ACCENT_GOLD)}60`\n : `0 2px 8px ${hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN)}30`;\n }}\n data-testid=\"toggle-visibility-button\"\n >\n {visible ? \"✓ 활성화 | Enabled\" : \"비활성화 | Disabled\"}\n </button>\n </div>\n\n {/* Expanded controls */}\n {expanded && visible && (\n <>\n {/* Severity filters */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 심각도 필터 | Severity Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {severityOptions.map((severity) => {\n const isActive = severityFilters.includes(severity);\n const severityColor = getSeverityColor(severity);\n return (\n <button\n key={severity}\n onClick={() => toggleSeverityFilter(severity)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${severityColor} 0%, ${severityColor}cc 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${severityColor}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${severityColor}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`severity-filter-${severity}`}\n >\n {severity}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Region filter */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 부위 필터 | Region Filter\n </div>\n <div\n style={{\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"8px\",\n }}\n >\n {regionOptions.map((option) => {\n const isActive = regionFilter === option.value;\n return (\n <button\n key={option.value}\n onClick={() => onRegionFilterChange(option.value)}\n style={{\n background: isActive\n ? `linear-gradient(135deg, ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )} 0%, ${hexColorToCSS(KOREAN_COLORS.ACCENT_BLUE)} 100%)`\n : `${hexColorToCSS(KOREAN_COLORS.UI_BACKGROUND_MEDIUM)}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}`,\n borderRadius: \"6px\",\n padding: \"6px 12px\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n opacity: isActive ? 1 : 0.6,\n transition: \"all 0.2s ease\",\n fontWeight: isActive ? \"bold\" : \"normal\",\n boxShadow: isActive\n ? `0 2px 8px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}50`\n : \"none\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.opacity = \"1\";\n e.currentTarget.style.transform = \"scale(1.05)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.opacity = isActive ? \"1\" : \"0.6\";\n e.currentTarget.style.transform = \"scale(1)\";\n }}\n data-testid={`region-filter-${option.value}`}\n >\n {option.korean} | {option.label}\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Search box with clear button */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 검색 | Search\n </div>\n <div style={{ position: \"relative\" }}>\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"급소 이름... | Point name...\"\n style={{\n width: \"100%\",\n height: buttonHeight,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n borderRadius: \"8px\",\n padding: \"0 40px 0 14px\", // Add right padding for clear button\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize,\n fontFamily: FONT_FAMILY.KOREAN,\n transition: \"all 0.2s ease\",\n outline: \"none\",\n }}\n onFocus={(e) => {\n e.currentTarget.style.borderColor = hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n );\n e.currentTarget.style.boxShadow = `0 0 12px ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n }}\n onBlur={(e) => {\n e.currentTarget.style.borderColor = `${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`;\n e.currentTarget.style.boxShadow = \"none\";\n }}\n data-testid=\"search-input\"\n />\n {/* Clear button */}\n {searchQuery && (\n <button\n onClick={() => setSearchQuery(\"\")}\n style={{\n position: \"absolute\",\n right: \"8px\",\n top: \"50%\",\n transform: \"translateY(-50%)\",\n background: \"transparent\",\n border: \"none\",\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n cursor: \"pointer\",\n fontSize: \"16px\",\n padding: \"4px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n borderRadius: \"4px\",\n transition: \"all 0.2s ease\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.ACCENT_RED,\n );\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.color = hexColorToCSS(\n KOREAN_COLORS.TEXT_SECONDARY,\n );\n e.currentTarget.style.background = \"transparent\";\n }}\n data-testid=\"search-clear-button\"\n title=\"Clear search\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n\n {/* Display options */}\n <div style={{ marginBottom: \"14px\" }}>\n <div\n style={{\n fontSize,\n marginBottom: \"8px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n fontWeight: \"600\",\n textTransform: \"uppercase\",\n letterSpacing: \"1px\",\n }}\n >\n 표시 옵션 | Display Options\n </div>\n <div\n style={{ display: \"flex\", flexDirection: \"column\", gap: \"8px\" }}\n >\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={showLabels}\n onChange={(e) => onShowLabelsChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"show-labels-checkbox\"\n />\n <span style={{ fontSize }}>라벨 표시 | Show Labels</span>\n </label>\n <label\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n cursor: \"pointer\",\n }}\n >\n <input\n type=\"checkbox\"\n checked={animated}\n onChange={(e) => onAnimatedChange(e.target.checked)}\n style={{\n width: \"16px\",\n height: \"16px\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"animated-checkbox\"\n />\n <span style={{ fontSize }}>애니메이션 | Animations</span>\n </label>\n </div>\n </div>\n\n {/* Reset filters button */}\n {(severityFilters.length > 0 ||\n regionFilter !== \"all\" ||\n searchQuery !== \"\") && (\n <div style={{ marginBottom: \"14px\" }}>\n <button\n onClick={() => {\n onSeverityFiltersChange([]);\n onRegionFilterChange(\"all\");\n setSearchQuery(\"\");\n }}\n style={{\n width: \"100%\",\n height: buttonHeight - 4,\n background: `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`,\n border: `2px solid ${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}`,\n borderRadius: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_ORANGE),\n fontSize: smallFontSize,\n cursor: \"pointer\",\n transition: \"all 0.2s ease\",\n fontWeight: \"bold\",\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.ACCENT_ORANGE,\n )}20`;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = `${hexColorToCSS(\n KOREAN_COLORS.UI_BACKGROUND_MEDIUM,\n )}`;\n e.currentTarget.style.transform = \"translateY(0)\";\n }}\n data-testid=\"reset-filters-button\"\n >\n 🔄 필터 초기화 | Reset Filters\n </button>\n </div>\n )}\n\n {/* Scale slider */}\n <div>\n <div\n style={{\n fontSize,\n marginBottom: \"6px\",\n color: hexColorToCSS(KOREAN_COLORS.ACCENT_CYAN),\n }}\n >\n 크기 | Scale: {scale.toFixed(1)}x\n </div>\n <input\n type=\"range\"\n min=\"0.5\"\n max=\"2.0\"\n step=\"0.1\"\n value={scale}\n onChange={(e) => onScaleChange(parseFloat(e.target.value))}\n style={{\n width: \"100%\",\n accentColor: hexColorToCSS(KOREAN_COLORS.PRIMARY_CYAN),\n }}\n data-testid=\"scale-slider\"\n />\n </div>\n\n {/* Statistics */}\n <div\n style={{\n marginTop: \"12px\",\n paddingTop: \"12px\",\n borderTop: `1px solid ${hexColorToCSS(\n KOREAN_COLORS.PRIMARY_CYAN,\n )}40`,\n fontSize: smallFontSize,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_SECONDARY),\n }}\n >\n <div>\n 머리: {stats.byRegion.head} | 몸통: {stats.byRegion.torso}\n </div>\n <div>\n 팔: {stats.byRegion.arms} | 다리: {stats.byRegion.legs}\n </div>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport default VitalPointOverlayControlsPure;\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { useAudio } from "../../../audio/AudioProvider.js";
2
2
  import { KOREAN_COLORS } from "../../../types/constants/colors.js";
3
- import { hexToRgbaString, toHex } from "../../../utils/colorUtils.js";
3
+ import { hexColorToCSS, hexToRgbaString, toHex } from "../../../utils/colorUtils.js";
4
4
  import React, { useCallback, useMemo, useState } from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/components/shared/ui/VolumeControl.tsx
@@ -108,7 +108,7 @@ var VolumeControl = ({ position = "top-right", style, showLabels = true, compact
108
108
  accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`
109
109
  }), [compact]);
110
110
  const labelStyle = useMemo(() => ({
111
- color: "#ffffff",
111
+ color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),
112
112
  fontSize: compact ? "11px" : "12px",
113
113
  fontWeight: "bold",
114
114
  minWidth: compact ? "40px" : "50px",
@@ -135,8 +135,8 @@ var VolumeControl = ({ position = "top-right", style, showLabels = true, compact
135
135
  "data-testid": "mute-toggle-button",
136
136
  "aria-label": isMuted ? "Unmute audio" : "Mute audio",
137
137
  style: {
138
- background: isMuted ? "#666666" : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,
139
- color: "white",
138
+ background: isMuted ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT) : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,
139
+ color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),
140
140
  border: "none",
141
141
  padding: "6px 12px",
142
142
  borderRadius: "6px",
@@ -259,8 +259,8 @@ var VolumeControl = ({ position = "top-right", style, showLabels = true, compact
259
259
  "data-testid": "mute-toggle-button",
260
260
  "aria-label": isMuted ? "Unmute audio" : "Mute audio",
261
261
  style: {
262
- background: isMuted ? "#666666" : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,
263
- color: "white",
262
+ background: isMuted ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT) : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,
263
+ color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),
264
264
  border: "none",
265
265
  padding: "8px 16px",
266
266
  borderRadius: "8px",
@@ -275,7 +275,7 @@ var VolumeControl = ({ position = "top-right", style, showLabels = true, compact
275
275
  }),
276
276
  /* @__PURE__ */ jsx("div", {
277
277
  style: {
278
- color: audio.isAudioReady ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}` : "#999",
278
+ color: audio.isAudioReady ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}` : hexColorToCSS(KOREAN_COLORS.UI_GRAY),
279
279
  fontSize: "10px",
280
280
  marginTop: "4px",
281
281
  textAlign: "center"
@@ -1 +1 @@
1
- {"version":3,"file":"VolumeControl.js","names":[],"sources":["../../../../src/components/shared/ui/VolumeControl.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexToRgbaString, toHex } from \"../../../utils/colorUtils\";\n\nexport interface VolumeControlProps {\n readonly position?:\n | \"top-right\"\n | \"bottom-right\"\n | \"top-left\"\n | \"bottom-left\"\n | \"custom\";\n readonly style?: React.CSSProperties;\n readonly showLabels?: boolean;\n readonly compact?: boolean;\n}\n\n/**\n * Volume Control Component\n *\n * Provides controls for:\n * - Master volume\n * - Music volume\n * - SFX volume\n * - Mute/unmute toggle\n *\n * Inspired by template game (https://github.com/Hack23/game)\n */\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\n position = \"top-right\",\n style,\n showLabels = true,\n compact = false,\n}) => {\n const audio = useAudio();\n\n // Local state to track values for UI (prevents issues if audio not ready)\n const [masterVolume, setMasterVolume] = useState(audio.masterVolume ?? 1.0);\n const [musicVolume, setMusicVolume] = useState(audio.musicVolume ?? 0.7);\n const [sfxVolume, setSfxVolume] = useState(audio.sfxVolume ?? 0.8);\n const [isMuted, setIsMuted] = useState(audio.muted ?? false);\n\n // Sync local state with audio manager state changes\n React.useEffect(() => {\n setMasterVolume(audio.masterVolume ?? 1.0);\n setMusicVolume(audio.musicVolume ?? 0.7);\n setSfxVolume(audio.sfxVolume ?? 0.8);\n setIsMuted(audio.muted ?? false);\n }, [audio.masterVolume, audio.musicVolume, audio.sfxVolume, audio.muted]);\n\n // Get position styles (memoized)\n const getPositionStyle = useMemo((): React.CSSProperties => {\n if (position === \"custom\") return {};\n\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n zIndex: 1000,\n padding: compact ? \"8px 12px\" : \"12px 16px\",\n };\n\n switch (position) {\n case \"top-right\":\n return { ...baseStyle, top: \"20px\", right: \"20px\" };\n case \"bottom-right\":\n return { ...baseStyle, bottom: \"20px\", right: \"20px\" };\n case \"top-left\":\n return { ...baseStyle, top: \"20px\", left: \"20px\" };\n case \"bottom-left\":\n return { ...baseStyle, bottom: \"20px\", left: \"20px\" };\n default:\n return baseStyle;\n }\n }, [position, compact]);\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n ...getPositionStyle,\n display: \"flex\",\n flexDirection: compact ? \"row\" : \"column\",\n alignItems: \"center\",\n gap: compact ? \"12px\" : \"8px\",\n background: \"rgba(33, 38, 45, 0.95)\",\n borderRadius: \"12px\",\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n pointerEvents: \"auto\", // Enable interaction even when parent has pointerEvents: none\n ...style,\n }),\n [getPositionStyle, compact, style],\n );\n\n const handleMasterVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMasterVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"master\", value);\n }\n },\n [audio],\n );\n\n const handleMusicVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMusicVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"music\", value);\n }\n },\n [audio],\n );\n\n const handleSfxVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setSfxVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"sfx\", value);\n }\n },\n [audio],\n );\n\n const handleMuteToggle = useCallback(() => {\n setIsMuted((prevMuted) => {\n const newMuted = !prevMuted;\n if (audio.isAudioReady) {\n if (newMuted) {\n audio.mute();\n } else {\n audio.unmute();\n }\n }\n return newMuted;\n });\n }, [audio]);\n\n const sliderStyle = useMemo(\n (): React.CSSProperties => ({\n width: compact ? \"60px\" : \"100px\",\n cursor: \"pointer\",\n accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }),\n [compact],\n );\n\n const labelStyle = useMemo(\n (): React.CSSProperties => ({\n color: \"#ffffff\",\n fontSize: compact ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n minWidth: compact ? \"40px\" : \"50px\",\n textAlign: \"left\",\n }),\n [compact],\n );\n\n const valueStyle = useMemo(\n (): React.CSSProperties => ({\n color: `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`,\n fontSize: compact ? \"10px\" : \"11px\",\n minWidth: \"35px\",\n textAlign: \"right\",\n }),\n [compact],\n );\n\n const controlRowStyle = useMemo(\n (): React.CSSProperties => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n width: \"100%\",\n }),\n [],\n );\n\n if (compact) {\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? \"#666666\"\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: \"white\",\n border: \"none\",\n padding: \"6px 12px\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇\" : \"🔊\"}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n aria-label=\"마스터 볼륨 | Master Volume\"\n style={sliderStyle}\n title=\"마스터 볼륨 | Master Volume\"\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n {showLabels && (\n <div\n style={{\n color: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n textAlign: \"center\",\n }}\n >\n 🎵 음량 | Volume\n </div>\n )}\n\n {/* Master Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"master-volume\" style={labelStyle}>\n 전체 | Master\n </label>\n <input\n id=\"master-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n\n {/* Music Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"music-volume\" style={labelStyle}>\n 음악 | Music\n </label>\n <input\n id=\"music-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={musicVolume}\n onChange={handleMusicVolumeChange}\n data-testid=\"music-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(musicVolume * 100)}%</span>\n </div>\n\n {/* SFX Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"sfx-volume\" style={labelStyle}>\n 효과음 | SFX\n </label>\n <input\n id=\"sfx-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={sfxVolume}\n onChange={handleSfxVolumeChange}\n data-testid=\"sfx-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(sfxVolume * 100)}%</span>\n </div>\n\n {/* Mute Toggle */}\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? \"#666666\"\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: \"white\",\n border: \"none\",\n padding: \"8px 16px\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n marginTop: \"4px\",\n width: \"100%\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇 음소거 해제 | Unmute\" : \"🔊 음소거 | Mute\"}\n </button>\n\n {/* Audio Status Indicator */}\n <div\n style={{\n color: audio.isAudioReady\n ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`\n : \"#999\",\n fontSize: \"10px\",\n marginTop: \"4px\",\n textAlign: \"center\",\n }}\n >\n {audio.isAudioReady\n ? \"✓ 오디오 준비됨 | Audio Ready\"\n : \"⏳ 초기화 중... | Initializing...\"}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"VolumeControl.js","names":[],"sources":["../../../../src/components/shared/ui/VolumeControl.tsx"],"sourcesContent":["import React, { useCallback, useMemo, useState } from \"react\";\nimport { useAudio } from \"../../../audio/AudioProvider\";\nimport { KOREAN_COLORS } from \"@/types/constants\";\nimport { hexColorToCSS, hexToRgbaString, toHex } from \"../../../utils/colorUtils\";\n\nexport interface VolumeControlProps {\n readonly position?:\n | \"top-right\"\n | \"bottom-right\"\n | \"top-left\"\n | \"bottom-left\"\n | \"custom\";\n readonly style?: React.CSSProperties;\n readonly showLabels?: boolean;\n readonly compact?: boolean;\n}\n\n/**\n * Volume Control Component\n *\n * Provides controls for:\n * - Master volume\n * - Music volume\n * - SFX volume\n * - Mute/unmute toggle\n *\n * Inspired by template game (https://github.com/Hack23/game)\n */\nexport const VolumeControl: React.FC<VolumeControlProps> = ({\n position = \"top-right\",\n style,\n showLabels = true,\n compact = false,\n}) => {\n const audio = useAudio();\n\n // Local state to track values for UI (prevents issues if audio not ready)\n const [masterVolume, setMasterVolume] = useState(audio.masterVolume ?? 1.0);\n const [musicVolume, setMusicVolume] = useState(audio.musicVolume ?? 0.7);\n const [sfxVolume, setSfxVolume] = useState(audio.sfxVolume ?? 0.8);\n const [isMuted, setIsMuted] = useState(audio.muted ?? false);\n\n // Sync local state with audio manager state changes\n React.useEffect(() => {\n setMasterVolume(audio.masterVolume ?? 1.0);\n setMusicVolume(audio.musicVolume ?? 0.7);\n setSfxVolume(audio.sfxVolume ?? 0.8);\n setIsMuted(audio.muted ?? false);\n }, [audio.masterVolume, audio.musicVolume, audio.sfxVolume, audio.muted]);\n\n // Get position styles (memoized)\n const getPositionStyle = useMemo((): React.CSSProperties => {\n if (position === \"custom\") return {};\n\n const baseStyle: React.CSSProperties = {\n position: \"absolute\",\n zIndex: 1000,\n padding: compact ? \"8px 12px\" : \"12px 16px\",\n };\n\n switch (position) {\n case \"top-right\":\n return { ...baseStyle, top: \"20px\", right: \"20px\" };\n case \"bottom-right\":\n return { ...baseStyle, bottom: \"20px\", right: \"20px\" };\n case \"top-left\":\n return { ...baseStyle, top: \"20px\", left: \"20px\" };\n case \"bottom-left\":\n return { ...baseStyle, bottom: \"20px\", left: \"20px\" };\n default:\n return baseStyle;\n }\n }, [position, compact]);\n\n const containerStyle = useMemo(\n (): React.CSSProperties => ({\n ...getPositionStyle,\n display: \"flex\",\n flexDirection: compact ? \"row\" : \"column\",\n alignItems: \"center\",\n gap: compact ? \"12px\" : \"8px\",\n background: \"rgba(33, 38, 45, 0.95)\",\n borderRadius: \"12px\",\n border: `1px solid ${hexToRgbaString(KOREAN_COLORS.PRIMARY_CYAN, 0.2)}`,\n pointerEvents: \"auto\", // Enable interaction even when parent has pointerEvents: none\n ...style,\n }),\n [getPositionStyle, compact, style],\n );\n\n const handleMasterVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMasterVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"master\", value);\n }\n },\n [audio],\n );\n\n const handleMusicVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setMusicVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"music\", value);\n }\n },\n [audio],\n );\n\n const handleSfxVolumeChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const value = parseFloat(event.target.value);\n setSfxVolume(value);\n if (audio.isAudioReady) {\n audio.setVolume(\"sfx\", value);\n }\n },\n [audio],\n );\n\n const handleMuteToggle = useCallback(() => {\n setIsMuted((prevMuted) => {\n const newMuted = !prevMuted;\n if (audio.isAudioReady) {\n if (newMuted) {\n audio.mute();\n } else {\n audio.unmute();\n }\n }\n return newMuted;\n });\n }, [audio]);\n\n const sliderStyle = useMemo(\n (): React.CSSProperties => ({\n width: compact ? \"60px\" : \"100px\",\n cursor: \"pointer\",\n accentColor: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n }),\n [compact],\n );\n\n const labelStyle = useMemo(\n (): React.CSSProperties => ({\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n fontSize: compact ? \"11px\" : \"12px\",\n fontWeight: \"bold\",\n minWidth: compact ? \"40px\" : \"50px\",\n textAlign: \"left\",\n }),\n [compact],\n );\n\n const valueStyle = useMemo(\n (): React.CSSProperties => ({\n color: `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`,\n fontSize: compact ? \"10px\" : \"11px\",\n minWidth: \"35px\",\n textAlign: \"right\",\n }),\n [compact],\n );\n\n const controlRowStyle = useMemo(\n (): React.CSSProperties => ({\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n width: \"100%\",\n }),\n [],\n );\n\n if (compact) {\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"6px 12px\",\n borderRadius: \"6px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇\" : \"🔊\"}\n </button>\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n aria-label=\"마스터 볼륨 | Master Volume\"\n style={sliderStyle}\n title=\"마스터 볼륨 | Master Volume\"\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n );\n }\n\n return (\n <div style={containerStyle} data-testid=\"volume-control\">\n {showLabels && (\n <div\n style={{\n color: `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n fontSize: \"14px\",\n fontWeight: \"bold\",\n marginBottom: \"4px\",\n textAlign: \"center\",\n }}\n >\n 🎵 음량 | Volume\n </div>\n )}\n\n {/* Master Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"master-volume\" style={labelStyle}>\n 전체 | Master\n </label>\n <input\n id=\"master-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={masterVolume}\n onChange={handleMasterVolumeChange}\n data-testid=\"master-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(masterVolume * 100)}%</span>\n </div>\n\n {/* Music Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"music-volume\" style={labelStyle}>\n 음악 | Music\n </label>\n <input\n id=\"music-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={musicVolume}\n onChange={handleMusicVolumeChange}\n data-testid=\"music-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(musicVolume * 100)}%</span>\n </div>\n\n {/* SFX Volume */}\n <div style={controlRowStyle}>\n <label htmlFor=\"sfx-volume\" style={labelStyle}>\n 효과음 | SFX\n </label>\n <input\n id=\"sfx-volume\"\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={sfxVolume}\n onChange={handleSfxVolumeChange}\n data-testid=\"sfx-volume-slider\"\n style={sliderStyle}\n />\n <span style={valueStyle}>{Math.round(sfxVolume * 100)}%</span>\n </div>\n\n {/* Mute Toggle */}\n <button\n onClick={handleMuteToggle}\n data-testid=\"mute-toggle-button\"\n aria-label={isMuted ? \"Unmute audio\" : \"Mute audio\"}\n style={{\n background: isMuted\n ? hexColorToCSS(KOREAN_COLORS.UI_DISABLED_TEXT)\n : `#${toHex(KOREAN_COLORS.PRIMARY_CYAN)}`,\n color: hexColorToCSS(KOREAN_COLORS.TEXT_PRIMARY),\n border: \"none\",\n padding: \"8px 16px\",\n borderRadius: \"8px\",\n cursor: \"pointer\",\n fontWeight: \"bold\",\n fontSize: \"14px\",\n marginTop: \"4px\",\n width: \"100%\",\n }}\n title={isMuted ? \"음소거 해제 | Unmute\" : \"음소거 | Mute\"}\n >\n {isMuted ? \"🔇 음소거 해제 | Unmute\" : \"🔊 음소거 | Mute\"}\n </button>\n\n {/* Audio Status Indicator */}\n <div\n style={{\n color: audio.isAudioReady\n ? `#${toHex(KOREAN_COLORS.ACCENT_GOLD)}`\n : hexColorToCSS(KOREAN_COLORS.UI_GRAY),\n fontSize: \"10px\",\n marginTop: \"4px\",\n textAlign: \"center\",\n }}\n >\n {audio.isAudioReady\n ? \"✓ 오디오 준비됨 | Audio Ready\"\n : \"⏳ 초기화 중... | Initializing...\"}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blacktrigram",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Black Trigram (흑괘) - Korean Martial Arts Combat Simulator. Reusable game systems, combat mechanics, animation framework, and Korean martial arts data built with React, Three.js, and TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -186,7 +186,7 @@
186
186
  "three": "^0.183.0"
187
187
  },
188
188
  "devDependencies": {
189
- "@aws-sdk/client-bedrock-runtime": "3.1024.0",
189
+ "@aws-sdk/client-bedrock-runtime": "3.1025.0",
190
190
  "@eslint/js": "10.0.1",
191
191
  "@react-three/drei": "10.7.7",
192
192
  "@react-three/fiber": "9.5.0",
@@ -200,20 +200,20 @@
200
200
  "@types/react-dom": "19.2.3",
201
201
  "@types/three": "0.183.1",
202
202
  "@vitejs/plugin-react": "6.0.1",
203
- "@vitest/coverage-v8": "4.1.2",
204
- "@vitest/ui": "4.1.2",
203
+ "@vitest/coverage-v8": "4.1.3",
204
+ "@vitest/ui": "4.1.3",
205
205
  "cypress": "15.13.0",
206
206
  "cypress-junit-reporter": "1.3.1",
207
207
  "cypress-multi-reporters": "2.0.5",
208
208
  "cypress-wait-until": "3.0.2",
209
209
  "dependency-cruiser": "17.3.10",
210
- "dotenv": "17.4.0",
210
+ "dotenv": "17.4.1",
211
211
  "eslint": "10.2.0",
212
212
  "eslint-plugin-react-hooks": "7.0.1",
213
213
  "eslint-plugin-react-refresh": "0.5.2",
214
214
  "globals": "17.4.0",
215
215
  "jest-axe": "10.0.0",
216
- "jsdom": "29.0.1",
216
+ "jsdom": "29.0.2",
217
217
  "knip": "6.3.0",
218
218
  "license-compliance": "3.0.1",
219
219
  "mocha-junit-reporter": "2.2.1",
@@ -237,10 +237,10 @@
237
237
  "typedoc-plugin-missing-exports": "4.1.3",
238
238
  "typescript": "6.0.2",
239
239
  "typescript-eslint": "8.58.0",
240
- "vite": "8.0.3",
241
- "vite-bundle-analyzer": "1.3.6",
240
+ "vite": "8.0.6",
241
+ "vite-bundle-analyzer": "1.3.7",
242
242
  "vite-tsconfig-paths": "6.1.1",
243
- "vitest": "4.1.2"
243
+ "vitest": "4.1.3"
244
244
  },
245
245
  "overrides": {
246
246
  "eslint-plugin-react-hooks": {