Compare commits

..

8 Commits

Author SHA1 Message Date
f9eda9f18b fix: Remove text visibility bounds check 2026-03-21 23:59:37 +01:00
e3999e6506 fix: Ensure horizontal/vertical line text is visible on screen
- Add bounds checking to only render text when line is on screen
- Fix text positioning to be consistent with line position
2026-03-21 23:56:41 +01:00
c8d9cf9645 fix: Show Text tab immediately for all line types 2026-03-21 23:50:14 +01:00
9fefd1847a fix: Show Text tab for horizontal and vertical line settings 2026-03-21 23:47:27 +01:00
09c7657070 feat: Add text support for horizontal and vertical lines
- Add text-related default settings (text, textColor, fontSize, bold, italic, alignVert, alignHorz)
- Support text rendering on horizontal/vertical lines with alignment options
- Add label position storage and hit detection for draggable text labels
2026-03-21 23:43:19 +01:00
30ac99479f feat: Implement settings panel for vertical and horizontal lines
- Add default settings (color, width, style, opacity) for horizontal_line and vertical_line
- Enable settings panel to open on double-click for horizontal/vertical lines
- Update rendering to support full styling (color, width, style, opacity) with visual selection handles
- Show/hide Text tab based on drawing type (trend_line/ray/rectangle only)
- Add proper drag handling to move entire horizontal/vertical lines
2026-03-21 23:41:05 +01:00
43fa8efda7 feat: Implement draggable Trendline Settings Panel with Shift+Click and TF layout fixes 2026-03-21 23:28:23 +01:00
fd816edc54 UI: Move drawing toolbar to top and remove Cursor, Ray, and Fibonacci buttons 2026-03-21 23:13:05 +01:00
6 changed files with 716 additions and 51 deletions

View File

@ -217,3 +217,51 @@ body {
} }
} }
/* Trendline Settings Panel */
#trendlineSettingsPanel input[type=range] {
-webkit-appearance: none;
background: transparent;
}
#trendlineSettingsPanel input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: #ffffff;
cursor: pointer;
border: 2px solid #1a2333;
margin-top: -4px;
}
#trendlineSettingsPanel input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: #2d3a4f;
border-radius: 2px;
}
.color-swatch {
width: 20px;
height: 20px;
border-radius: 3px;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.1s, border-color 0.1s;
}
.color-swatch:hover {
transform: scale(1.1);
border-color: #ffffff;
}
.color-swatch.active {
border-color: #ffffff;
box-shadow: 0 0 0 1px #ffffff;
}
.tl-thickness-btn[data-active="true"],
.tl-style-btn[data-active="true"] {
background-color: #2d3a4f;
}

BIN
ignore/line_settings.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
ignore/text.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -24,7 +24,7 @@
#rightSidebar.collapsed { #rightSidebar.collapsed {
transform: translateX(100%); transform: translateX(100%);
} }
/* Tab Styles in Sidebar */ /* Tab Styles in Sidebar */
.sidebar-tab { .sidebar-tab {
@apply px-4 py-2 text-sm font-medium text-gray-400 hover:text-white transition-colors border-b-2 border-transparent; @apply px-4 py-2 text-sm font-medium text-gray-400 hover:text-white transition-colors border-b-2 border-transparent;
@ -32,7 +32,7 @@
.sidebar-tab.active { .sidebar-tab.active {
@apply text-blue-500 border-blue-500; @apply text-blue-500 border-blue-500;
} }
/* Hide scrollbar for clean UI */ /* Hide scrollbar for clean UI */
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
@ -52,14 +52,14 @@
<button id="mobileMenuBtn" class="md:hidden w-10 h-10 flex items-center justify-center text-[#8fa2b3] hover:text-white hover:bg-[#2d3a4f] rounded-md transition-colors z-50"> <button id="mobileMenuBtn" class="md:hidden w-10 h-10 flex items-center justify-center text-[#8fa2b3] hover:text-white hover:bg-[#2d3a4f] rounded-md transition-colors z-50">
<span class="material-symbols-outlined text-lg">menu</span> <span class="material-symbols-outlined text-lg">menu</span>
</button> </button>
<!-- Search/Symbol Button --> <!-- Search/Symbol Button -->
<div class="hidden md:flex items-center space-x-3 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]"> <div class="hidden md:flex items-center space-x-3 bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span> <span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
<span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span> <span class="font-bold text-sm text-[#dfe2f2]">BTC/USD</span>
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span> <span class="material-symbols-outlined text-sm text-[#8fa2b3]">chevron_right</span>
</div> </div>
<!-- Mobile Search --> <!-- Mobile Search -->
<div class="md:hidden flex items-center bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]"> <div class="md:hidden flex items-center bg-[#1a2333] px-3 py-1.5 rounded-md cursor-pointer border border-[#2d3a4f]">
<span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span> <span class="material-symbols-outlined text-sm text-[#8fa2b3]">search</span>
@ -67,7 +67,7 @@
<span class="material-symbols-outlined text-sm text-[#8fa2b3] ml-2">chevron_right</span> <span class="material-symbols-outlined text-sm text-[#8fa2b3] ml-2">chevron_right</span>
</div> </div>
</div> </div>
<div class="hidden md:flex items-center gap-2 mr-4"> <div class="hidden md:flex items-center gap-2 mr-4">
<div class="w-2 h-2 rounded-full bg-green-500" id="statusDot"></div> <div class="w-2 h-2 rounded-full bg-green-500" id="statusDot"></div>
<span class="text-xs text-[#8fa2b3]" id="statusText">Live</span> <span class="text-xs text-[#8fa2b3]" id="statusText">Live</span>
@ -104,55 +104,149 @@
<!-- Chart Container --> <!-- Chart Container -->
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper"> <section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper">
<div id="chart" class="w-full h-full"></div> <div id="chart" class="w-full h-full"></div>
<!-- Vertical Drawing Toolbar (Left) --> <!-- Horizontal Drawing Toolbar (Top) -->
<div class="absolute left-2 top-1/2 -translate-y-1/2 flex flex-col gap-1 z-30 bg-[#1a2333]/80 backdrop-blur border border-[#2d3a4f] p-1 rounded-md shadow-xl" id="drawingToolbar"> <div class="absolute top-2 left-1/2 -translate-x-1/2 flex flex-row gap-1 z-30 bg-[#1a2333]/80 backdrop-blur border border-[#2d3a4f] p-1 rounded-md shadow-xl" id="drawingToolbar">
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('cursor')" title="Cursor"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('trend_line', event)" title="Trend Line">
<span class="material-symbols-outlined text-sm">near_me</span>
</button>
<div class="w-full h-[1px] bg-[#2d3a4f] my-0.5"></div>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('trend_line')" title="Trend Line">
<span class="material-symbols-outlined text-sm">call_split</span> <span class="material-symbols-outlined text-sm">call_split</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('ray')" title="Ray"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('horizontal_line', event)" title="Horizontal Line">
<span class="material-symbols-outlined text-sm">trending_flat</span>
</button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('horizontal_line')" title="Horizontal Line">
<span class="material-symbols-outlined text-sm">swap_horizontal_circle</span> <span class="material-symbols-outlined text-sm">swap_horizontal_circle</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('vertical_line')" title="Vertical Line"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('vertical_line', event)" title="Vertical Line">
<span class="material-symbols-outlined text-sm">swap_vert</span> <span class="material-symbols-outlined text-sm">swap_vert</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('fib_retracement')" title="Fibonacci Retracement"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('rectangle', event)" title="Rectangle">
<span class="material-symbols-outlined text-sm">reorder</span>
</button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('rectangle')" title="Rectangle">
<span class="material-symbols-outlined text-sm">crop_square</span> <span class="material-symbols-outlined text-sm">crop_square</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('text')" title="Text Label"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('text', event)" title="Text Label">
<span class="material-symbols-outlined text-sm">text_fields</span> <span class="material-symbols-outlined text-sm">text_fields</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_up')" title="Arrow Up"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_up', event)" title="Arrow Up">
<span class="material-symbols-outlined text-sm">arrow_upward</span> <span class="material-symbols-outlined text-sm">arrow_upward</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_down')" title="Arrow Down"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('arrow_down', event)" title="Arrow Down">
<span class="material-symbols-outlined text-sm">arrow_downward</span> <span class="material-symbols-outlined text-sm">arrow_downward</span>
</button> </button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('measure')" title="Measure"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('measure', event)" title="Measure">
<span class="material-symbols-outlined text-sm">straighten</span> <span class="material-symbols-outlined text-sm">straighten</span>
</button> </button>
<div class="w-full h-[1px] bg-[#2d3a4f] my-0.5"></div> <div class="w-[1px] h-full bg-[#2d3a4f] mx-0.5"></div>
<button class="w-8 h-8 text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('clear')" title="Clear All"> <button class="w-8 h-8 text-red-400 hover:text-red-300 hover:bg-red-900/20 rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('clear', event)" title="Clear All">
<span class="material-symbols-outlined text-sm">delete</span> <span class="material-symbols-outlined text-sm">delete</span>
</button> </button>
</div> </div>
<!-- Trendline Settings Panel -->
<div id="trendlineSettingsPanel" class="hidden absolute top-14 left-1/2 -translate-x-1/2 bg-[#1a2333] border border-[#2d3a4f] rounded-lg shadow-2xl z-50 w-[280px] p-0 text-sm font-['Inter']">
<!-- Drag Handle -->
<div id="tlPanelHeader" class="h-8 flex items-center justify-between px-3 border-b border-[#2d3a4f] cursor-move bg-[#1f2937] rounded-t-lg">
<span class="text-[10px] uppercase tracking-wider font-bold text-gray-400">Settings</span>
<button class="text-gray-500 hover:text-white" onclick="window.toggleTLSettings(false)">
<span class="material-symbols-outlined text-sm">close</span>
</button>
</div>
<div class="p-4">
<!-- Tabs -->
<div class="flex border-b border-[#2d3a4f] mb-4">
<button class="flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium" id="tlTabStyle" onclick="window.switchTLTab('style')">Style</button>
<button class="flex-1 py-2 text-center text-gray-400 hover:text-white" id="tlTabText" onclick="window.switchTLTab('text')">Text</button>
</div>
<!-- Style Tab Content -->
<div id="tlContentStyle">
<!-- Color Grid (Line) -->
<div class="grid grid-cols-8 gap-1 mb-4" id="tlColorGrid">
<!-- Colors injected by JS -->
</div>
<!-- Opacity -->
<div class="mb-4">
<div class="flex justify-between text-xs text-gray-400 mb-1">
<span>Opacity</span>
<span id="tlOpacityValue">100%</span>
</div>
<input type="range" min="0" max="100" value="100" class="w-full h-1 bg-[#2d3a4f] rounded-lg appearance-none cursor-pointer" id="tlOpacityInput">
</div>
<!-- Thickness -->
<div class="mb-4">
<label class="text-xs text-gray-400 mb-1 block">Thickness</label>
<div class="flex bg-[#0d1421] rounded border border-[#2d3a4f] p-1">
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(1)" data-thickness="1"><div class="w-4 h-[1px] bg-white"></div></button>
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(2)" data-thickness="2"><div class="w-4 h-[2px] bg-white"></div></button>
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(3)" data-thickness="3"><div class="w-4 h-[3px] bg-white"></div></button>
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-thickness-btn" onclick="window.setTLThickness(4)" data-thickness="4"><div class="w-4 h-[4px] bg-white"></div></button>
</div>
</div>
<!-- Line Style -->
<div class="mb-4">
<label class="text-xs text-gray-400 mb-1 block">Line style</label>
<div class="flex bg-[#0d1421] rounded border border-[#2d3a4f] p-1">
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-style-btn" onclick="window.setTLStyle(0)" data-style="0"><div class="w-6 border-b border-white"></div></button>
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-style-btn" onclick="window.setTLStyle(1)" data-style="1"><div class="w-6 border-b border-dashed border-white"></div></button>
<button class="flex-1 h-6 flex items-center justify-center hover:bg-[#2d3a4f] rounded data-[active=true]:bg-[#2d3a4f] tl-style-btn" onclick="window.setTLStyle(2)" data-style="2"><div class="w-6 border-b border-dotted border-white"></div></button>
</div>
</div>
</div>
<!-- Text Tab Content -->
<div id="tlContentText" class="hidden">
<div class="flex gap-2 mb-4 relative">
<!-- Text Color Button -->
<div class="w-8 h-8 rounded border border-[#2d3a4f] cursor-pointer" id="tlTextColorBtn" style="background-color: #2962ff"></div>
<!-- Text Color Picker Popup -->
<div id="tlTextColorPicker" class="hidden absolute top-10 left-0 bg-[#1a2333] border border-[#2d3a4f] rounded shadow-2xl p-2 z-[60] w-[210px]">
<div class="grid grid-cols-8 gap-1" id="tlTextColorGrid">
<!-- Colors injected by JS -->
</div>
</div>
<!-- Font Size -->
<select id="tlFontSize" class="bg-[#0d1421] border border-[#2d3a4f] rounded text-xs px-2 h-8 text-white focus:outline-none">
<option value="10">10</option>
<option value="12">12</option>
<option value="14" selected>14</option>
<option value="16">16</option>
<option value="20">20</option>
<option value="24">24</option>
</select>
<!-- Style Toggles -->
<button class="w-8 h-8 border border-[#2d3a4f] rounded flex items-center justify-center hover:bg-[#2d3a4f] data-[active=true]:bg-blue-600" id="tlBoldBtn" onclick="window.toggleTLBold()">B</button>
<button class="w-8 h-8 border border-[#2d3a4f] rounded flex items-center justify-center hover:bg-[#2d3a4f] italic data-[active=true]:bg-blue-600" id="tlItalicBtn" onclick="window.toggleTLItalic()">I</button>
</div>
<textarea id="tlTextInput" class="w-full h-24 bg-[#0d1421] border border-[#2d3a4f] rounded p-2 text-xs text-white focus:border-blue-500 focus:outline-none mb-4" placeholder="Add text"></textarea>
<div class="flex items-center justify-between mb-4">
<label class="text-xs text-gray-400">Alignment</label>
<div class="flex gap-2">
<select id="tlAlignVert" class="bg-[#0d1421] border border-[#2d3a4f] rounded text-xs px-2 h-6 text-white focus:outline-none">
<option value="top">Top</option>
<option value="middle">Middle</option>
<option value="bottom">Bottom</option>
</select>
<select id="tlAlignHorz" class="bg-[#0d1421] border border-[#2d3a4f] rounded text-xs px-2 h-6 text-white focus:outline-none">
<option value="left">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Overlay Controls --> <!-- Overlay Controls -->
<div class="absolute bottom-4 right-4 flex gap-2 z-10 opacity-0 hover:opacity-100 transition-opacity" id="priceScaleControls"> <div class="absolute bottom-4 right-4 flex gap-2 z-10 opacity-0 hover:opacity-100 transition-opacity" id="priceScaleControls">
<button class="w-8 h-8 bg-[#1e222d] border border-[#2d3a4f] text-gray-300 flex items-center justify-center rounded hover:bg-[#2d3a4f] transition-colors shadow-lg" id="btnSettings" title="Settings"> <button class="w-8 h-8 bg-[#1e222d] border border-[#2d3a4f] text-gray-300 flex items-center justify-center rounded hover:bg-[#2d3a4f] transition-colors shadow-lg" id="btnSettings" title="Settings">
<span class="material-symbols-outlined text-sm">settings</span> <span class="material-symbols-outlined text-sm">settings</span>
</button> </button>
<!-- Settings Popup --> <!-- Settings Popup -->
<div class="hidden absolute bottom-10 right-0 bg-[#1a2333] border border-[#2d3a4f] rounded-lg py-2 z-50 w-64 shadow-xl text-sm" id="settingsPopup"> <div class="hidden absolute bottom-10 right-0 bg-[#1a2333] border border-[#2d3a4f] rounded-lg py-2 z-50 w-64 shadow-xl text-sm" id="settingsPopup">
@ -197,7 +291,7 @@
<input type="color" id="candleDownColor" value="#ff9800" class="flex-1 h-6 cursor-pointer"> <input type="color" id="candleDownColor" value="#ff9800" class="flex-1 h-6 cursor-pointer">
</div> </div>
</div> </div>
<!-- Decimals --> <!-- Decimals -->
<div class="px-4 py-2 flex items-center justify-between"> <div class="px-4 py-2 flex items-center justify-between">
<label class="text-sm text-gray-300">Decimals</label> <label class="text-sm text-gray-300">Decimals</label>
@ -205,7 +299,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Navigation Controls --> <!-- Navigation Controls -->
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-10 opacity-0 hover:opacity-100 transition-opacity" id="navControls"> <div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 z-10 opacity-0 hover:opacity-100 transition-opacity" id="navControls">
<button class="w-8 h-8 bg-[#1e222d]/80 backdrop-blur border border-[#2d3a4f] text-white rounded-full flex items-center justify-center hover:bg-blue-600 transition-colors" id="navLeft"> <button class="w-8 h-8 bg-[#1e222d]/80 backdrop-blur border border-[#2d3a4f] text-white rounded-full flex items-center justify-center hover:bg-blue-600 transition-colors" id="navLeft">
@ -225,7 +319,7 @@
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-bold flex items-center gap-2"> <h2 class="text-lg font-bold flex items-center gap-2">
<span class="material-symbols-outlined text-[#b6c4ff]">analytics</span> <span class="material-symbols-outlined text-[#b6c4ff]">analytics</span>
Technical Analysis Technical Analysis
<span id="taInterval" class="text-[10px] bg-blue-600/20 text-blue-400 px-2 py-0.5 rounded ml-2 border border-blue-600/30">1D</span> <span id="taInterval" class="text-[10px] bg-blue-600/20 text-blue-400 px-2 py-0.5 rounded ml-2 border border-blue-600/30">1D</span>
</h2> </h2>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -238,7 +332,7 @@
</button> </button>
</div> </div>
</div> </div>
<div id="taContent" class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div id="taContent" class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-gray-500 text-sm p-4 text-center w-full col-span-full">Loading analysis data...</div> <div class="text-gray-500 text-sm p-4 text-center w-full col-span-full">Loading analysis data...</div>
</div> </div>
@ -314,7 +408,7 @@
<span class="material-symbols-outlined">close</span> <span class="material-symbols-outlined">close</span>
</button> </button>
</div> </div>
<!-- Desktop Sidebar Toggle --> <!-- Desktop Sidebar Toggle -->
<button id="sidebarToggleBtn" class="md:flex hidden w-8 h-8 bg-[#1a2333] border border-[#2d3a4f] text-gray-300 flex items-center justify-center rounded hover:bg-[#2d3a4f] transition-colors" title="Toggle Sidebar"> <button id="sidebarToggleBtn" class="md:flex hidden w-8 h-8 bg-[#1a2333] border border-[#2d3a4f] text-gray-300 flex items-center justify-center rounded hover:bg-[#2d3a4f] transition-colors" title="Toggle Sidebar">
<span class="material-symbols-outlined text-sm">menu</span> <span class="material-symbols-outlined text-sm">menu</span>
@ -353,7 +447,7 @@
window.toggleSidebarDisplay = function(tabName) { window.toggleSidebarDisplay = function(tabName) {
const sidebar = document.getElementById('rightSidebar'); const sidebar = document.getElementById('rightSidebar');
if (!sidebar) return; if (!sidebar) return;
const isOpen = !sidebar.classList.contains('collapsed'); const isOpen = !sidebar.classList.contains('collapsed');
const activeTab = document.querySelector('.sidebar-tab.active'); const activeTab = document.querySelector('.sidebar-tab.active');
const currentTabName = activeTab ? activeTab.dataset.tab : null; const currentTabName = activeTab ? activeTab.dataset.tab : null;
@ -366,7 +460,7 @@
// Otherwise, OPEN it (remove collapsed) and switch tab // Otherwise, OPEN it (remove collapsed) and switch tab
sidebar.classList.remove('collapsed'); sidebar.classList.remove('collapsed');
if (tabName) { if (tabName) {
// Find and click the hidden tab button to trigger existing logic // Find and click the hidden tab button to trigger existing logic
const tabBtn = document.querySelector(`.sidebar-tab[data-tab="${tabName}"]`); const tabBtn = document.querySelector(`.sidebar-tab[data-tab="${tabName}"]`);
@ -395,7 +489,7 @@
} }
}; };
</script> </script>
<script src="./config.js"></script> <script src="./config.js"></script>
<script type="module" src="./js/app.js"></script> <script type="module" src="./js/app.js"></script>
</body> </body>

View File

@ -458,7 +458,10 @@ export class TradingDashboard {
// Initialize Drawing Manager // Initialize Drawing Manager
this.drawingManager = new DrawingManager(this, chartContainer); this.drawingManager = new DrawingManager(this, chartContainer);
window.activateDrawingTool = (tool) => this.drawingManager.setTool(tool); window.activateDrawingTool = (tool, event) => {
const e = event || window.event;
this.drawingManager.setTool(tool, e);
};
// Setup price format selector change handler // Setup price format selector change handler
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View File

@ -16,6 +16,49 @@ export class DrawingManager {
this.dragMode = null; this.dragMode = null;
this.isMouseDown = false; this.isMouseDown = false;
// Default Settings
this.defaults = {
trend_line: {
color: '#2962ff',
width: 2,
style: 0, // 0: Solid, 1: Dashed, 2: Dotted
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
},
horizontal_line: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
},
vertical_line: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
}
};
// Dragging offsets // Dragging offsets
this.dragStartPos = null; this.dragStartPos = null;
this.startP1 = null; this.startP1 = null;
@ -24,6 +67,7 @@ export class DrawingManager {
this.startTime = null; this.startTime = null;
this.init(); this.init();
this.initSettingsPanel();
} }
init() { init() {
@ -31,6 +75,18 @@ export class DrawingManager {
// Mouse Events // Mouse Events
container.addEventListener('mousedown', (e) => this.handleMouseDown(e)); container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
container.addEventListener('dblclick', (e) => {
const pos = this.getMousePos(e);
const hit = this.findHit(pos.x, pos.y);
if (hit) {
this.selectedDrawing = hit.drawing;
// Only open panel for supported types
if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(hit.drawing.type)) {
this.toggleSettingsPanel(true);
}
this.update();
}
});
window.addEventListener('mousemove', (e) => this.handleMouseMove(e)); window.addEventListener('mousemove', (e) => this.handleMouseMove(e));
window.addEventListener('mouseup', (e) => this.handleMouseUp(e)); window.addEventListener('mouseup', (e) => this.handleMouseUp(e));
@ -164,7 +220,13 @@ export class DrawingManager {
}); });
} }
setTool(tool) { setTool(tool, event) {
// Shift + Click on trend_line button opens settings
if (tool === 'trend_line' && event && event.shiftKey) {
this.toggleSettingsPanel(true);
return;
}
this.activeTool = tool; this.activeTool = tool;
if (tool === 'cursor') { if (tool === 'cursor') {
@ -301,17 +363,47 @@ export class DrawingManager {
return; return;
} }
const color = '#2962ff'; const color = this.defaults.trend_line.color;
const p1 = { time: pos.time, price: pos.price }; const p1 = { time: pos.time, price: pos.price };
const p2 = { time: pos.time, price: pos.price }; const p2 = { time: pos.time, price: pos.price };
const defs = this.defaults.trend_line;
if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') { if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') {
this.currentDrawing = { type: this.activeTool, p1, p2, color }; this.currentDrawing = {
type: this.activeTool, p1, p2,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity,
text: defs.text,
textColor: defs.textColor,
fontSize: defs.fontSize,
bold: defs.bold,
italic: defs.italic,
alignVert: defs.alignVert,
alignHorz: defs.alignHorz
};
} else if (this.activeTool === 'horizontal_line') { } else if (this.activeTool === 'horizontal_line') {
this.drawings.push({ type: 'horizontal_line', price: pos.price, color }); const defs = this.defaults.horizontal_line;
this.drawings.push({
type: 'horizontal_line',
price: pos.price,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity
});
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'vertical_line') { } else if (this.activeTool === 'vertical_line') {
this.drawings.push({ type: 'vertical_line', time: pos.time, color }); const defs = this.defaults.vertical_line;
this.drawings.push({
type: 'vertical_line',
time: pos.time,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity
});
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'fib_retracement') { } else if (this.activeTool === 'fib_retracement') {
this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' }; this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
@ -376,6 +468,16 @@ export class DrawingManager {
return; // Critical: exit here so we don't move measurement points return; // Critical: exit here so we don't move measurement points
} }
if (this.dragMode === 'label') {
const dx = pos.x - this.dragStartPos.x;
const dy = pos.y - this.dragStartPos.y;
if (!d.labelOffset) d.labelOffset = { x: 0, y: 0 };
d.labelOffset.x += dx;
d.labelOffset.y += dy;
this.dragStartPos = pos;
this.update();
return;
}
if (this.dragMode === 'p1') { if (this.dragMode === 'p1') {
d.p1 = { time: pos.time, price: pos.price }; d.p1 = { time: pos.time, price: pos.price };
} else if (this.dragMode === 'p2') { } else if (this.dragMode === 'p2') {
@ -476,10 +578,26 @@ export class DrawingManager {
} }
} else if (d.price !== undefined && d.type === 'horizontal_line') { } else if (d.price !== undefined && d.type === 'horizontal_line') {
const dy = this.series.priceToCoordinate(d.price); const dy = this.series.priceToCoordinate(d.price);
if (dy !== null && Math.abs(y - dy) < threshold) return { drawing: d, part: 'price' }; if (dy !== null && Math.abs(y - dy) < threshold) {
if (d.text && d.labelPos) {
if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width &&
y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) {
return { drawing: d, part: 'label' };
}
}
return { drawing: d, part: 'all' };
}
} else if (d.time !== undefined && d.type === 'vertical_line') { } else if (d.time !== undefined && d.type === 'vertical_line') {
const dx = this.timeToX(d.time); const dx = this.timeToX(d.time);
if (dx !== null && Math.abs(x - dx) < threshold) return { drawing: d, part: 'time' }; if (dx !== null && Math.abs(x - dx) < threshold) {
if (d.text && d.labelPos) {
if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width &&
y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) {
return { drawing: d, part: 'label' };
}
}
return { drawing: d, part: 'all' };
}
} else if (d.time !== undefined && d.price !== undefined) { } else if (d.time !== undefined && d.price !== undefined) {
const dx = this.timeToX(d.time); const dx = this.timeToX(d.time);
const dy = this.series.priceToCoordinate(d.price); const dy = this.series.priceToCoordinate(d.price);
@ -604,6 +722,20 @@ export class DrawingManager {
return x1 + ratio * (x2 - x1); return x1 + ratio * (x2 - x1);
} }
hexToRgba(hex, opacity) {
let r = 0, g = 0, b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex.slice(1, 3), 16);
g = parseInt(hex.slice(3, 5), 16);
b = parseInt(hex.slice(5, 7), 16);
}
return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
}
render(target) { render(target) {
target.useMediaCoordinateSpace((scope) => { target.useMediaCoordinateSpace((scope) => {
const ctx = scope.context; const ctx = scope.context;
@ -615,8 +747,16 @@ export class DrawingManager {
allDrawings.forEach(d => { allDrawings.forEach(d => {
const isSelected = d === this.selectedDrawing; const isSelected = d === this.selectedDrawing;
ctx.save(); ctx.save();
ctx.strokeStyle = d.color;
ctx.lineWidth = isSelected ? 3 : 2; // Base Style
const color = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.strokeStyle = color;
ctx.lineWidth = d.width || (isSelected ? 3 : 2);
if (d.style === 1) ctx.setLineDash([10, 5]); // Dashed
else if (d.style === 2) ctx.setLineDash([2, 2]); // Dotted
else ctx.setLineDash([]); // Solid
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; } if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
if (d.type === 'trend_line' || d.type === 'ray') { if (d.type === 'trend_line' || d.type === 'ray') {
@ -634,7 +774,29 @@ export class DrawingManager {
ctx.lineTo(x2, y2); ctx.lineTo(x2, y2);
} }
ctx.stroke(); ctx.stroke();
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]); // Text should not be dashed
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
ctx.fillStyle = d.textColor || color;
ctx.textAlign = d.alignHorz || 'left';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
const tx = (x1 + x2) / 2;
const ty = (y1 + y2) / 2;
const offset = 10;
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty);
ctx.fillText(d.text, tx, finalTy);
ctx.restore();
}
if (isSelected) { if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
@ -657,19 +819,131 @@ export class DrawingManager {
} }
} else if (d.type === 'horizontal_line') { } else if (d.type === 'horizontal_line') {
const y = series.priceToCoordinate(d.price); const y = series.priceToCoordinate(d.price);
if (y !== null) { if (y !== null && y >= 0 && y <= scope.mediaSize.height) {
ctx.save();
ctx.strokeStyle = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.lineWidth = d.width || 2;
if (d.style === 1) ctx.setLineDash([10, 5]);
else if (d.style === 2) ctx.setLineDash([2, 2]);
else ctx.setLineDash([]);
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(-1000, y); ctx.moveTo(-1000, y);
ctx.lineTo(scope.mediaSize.width + 1000, y); ctx.lineTo(scope.mediaSize.width + 1000, y);
ctx.stroke(); ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
// Store label position for hit detection (before rendering)
let textX = scope.mediaSize.width / 2;
let textY = y;
let labelPos = null;
if (d.text) {
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
const font = `${fontStyle}${fontSize}px Inter`;
ctx.font = font;
const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16;
const labelHeight = 24;
const defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2;
const offset = 10;
const defaultLabelY = d.alignVert === 'top' ? y - 40 : (d.alignVert === 'bottom' ? y + 20 : y - 12);
labelPos = {
x: defaultLabelX + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth,
height: labelHeight
};
textX = scope.mediaSize.width / 2;
textY = d.alignVert === 'top' ? y - offset : (d.alignVert === 'bottom' ? y + offset : y);
}
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]);
ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
ctx.textAlign = d.alignHorz || 'center';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
ctx.fillText(d.text, textX, textY);
ctx.restore();
}
// Store label position for hit detection
if (labelPos) {
d.labelPos = labelPos;
}
ctx.restore();
} }
} else if (d.type === 'vertical_line') { } else if (d.type === 'vertical_line') {
const x = this.timeToX(d.time); const x = this.timeToX(d.time);
if (x !== null) { if (x !== null && x >= 0 && x <= scope.mediaSize.width) {
ctx.save();
ctx.strokeStyle = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.lineWidth = d.width || 2;
if (d.style === 1) ctx.setLineDash([10, 5]);
else if (d.style === 2) ctx.setLineDash([2, 2]);
else ctx.setLineDash([]);
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, -1000); ctx.moveTo(x, -1000);
ctx.lineTo(x, scope.mediaSize.height + 1000); ctx.lineTo(x, scope.mediaSize.height + 1000);
ctx.stroke(); ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
// Store label position for hit detection (before rendering)
let textX = x;
let textY = 0;
let labelPos = null;
if (d.text) {
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
const font = `${fontStyle}${fontSize}px Inter`;
ctx.font = font;
const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16;
const labelHeight = 24;
const offset = 10;
const defaultLabelY = d.alignVert === 'top' ? -50 : (d.alignVert === 'bottom' ? 20 : -12);
labelPos = {
x: x - labelWidth / 2 + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth,
height: labelHeight
};
textY = d.alignVert === 'top' ? -offset : (d.alignVert === 'bottom' ? offset : 0);
}
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]);
ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
ctx.textAlign = d.alignHorz || 'center';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
ctx.fillText(d.text, textX, textY);
ctx.restore();
}
// Store label position for hit detection
if (labelPos) {
d.labelPos = labelPos;
}
ctx.restore();
} }
} else if (d.type === 'fib_retracement') { } else if (d.type === 'fib_retracement') {
const x1 = this.timeToX(d.p1.time); const x1 = this.timeToX(d.p1.time);
@ -829,8 +1103,6 @@ export class DrawingManager {
const labelWidth = Math.max(...labelLines.map(l => ctx.measureText(l).width)) + 24; const labelWidth = Math.max(...labelLines.map(l => ctx.measureText(l).width)) + 24;
const labelHeight = 65; const labelHeight = 65;
// Natural position: Centered horizontally at midpoint,
// vertically offset from the top/bottom edge.
const midX = (x1 + x2) / 2; const midX = (x1 + x2) / 2;
const topY = Math.min(y1, y2); const topY = Math.min(y1, y2);
const bottomY = Math.max(y1, y2); const bottomY = Math.max(y1, y2);
@ -881,4 +1153,252 @@ export class DrawingManager {
ctx.quadraticCurveTo(x, y, x + radius, y); ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath(); ctx.closePath();
} }
initSettingsPanel() {
const panel = document.getElementById('trendlineSettingsPanel');
const header = document.getElementById('tlPanelHeader');
this.activeTLTab = 'style';
// Dragging Logic
let isDragging = false;
let startX, startY, initialX, initialY;
header.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
panel.classList.remove('left-1/2', '-translate-x-1/2', 'top-14');
panel.style.left = initialX + 'px';
panel.style.top = initialY + 'px';
panel.style.margin = '0';
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = (initialX + dx) + 'px';
panel.style.top = (initialY + dy) + 'px';
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
// Global functions for HTML event handlers
window.toggleTLSettings = (show) => this.toggleSettingsPanel(show);
window.switchTLTab = (tab) => {
this.activeTLTab = tab;
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type);
document.getElementById('tlTabStyle').className = tab === 'style' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white';
const textTab = document.getElementById('tlTabText');
if (isLineWithText) {
textTab.className = tab === 'text' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white';
textTab.style.display = 'block';
document.getElementById('tlContentText').className = tab === 'text' ? 'block' : 'hidden';
} else {
textTab.style.display = 'none';
document.getElementById('tlContentText').className = 'hidden';
}
document.getElementById('tlContentStyle').className = tab === 'style' ? 'block' : 'hidden';
const textPicker = document.getElementById('tlTextColorPicker');
if (textPicker) textPicker.classList.add('hidden');
this.updateSettingsPanelUI();
};
window.setTLThickness = (width) => this.applySettings('width', width);
window.setTLStyle = (style) => this.applySettings('style', style);
window.toggleTLBold = () => {
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold;
this.applySettings('bold', !current);
};
window.toggleTLItalic = () => {
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic;
this.applySettings('italic', !current);
};
// Initialize Color Grids
const colors = [
'#ffffff', '#e0e0e0', '#bdbdbd', '#9e9e9e', '#757575', '#616161', '#424242', '#212121', '#000000',
'#ef5350', '#ff9800', '#ffeb3b', '#66bb6a', '#009688', '#00bcd4', '#2962ff', '#673ab7', '#9c27b0', '#e91e63',
'#ffcdd2', '#ffe0b2', '#fff9c4', '#c8e6c9', '#b2dfdb', '#b2ebf2', '#bbdefb', '#d1c4e9', '#e1bee7', '#f8bbd0',
'#ef9a9a', '#ffcc80', '#fff59d', '#a5d6a7', '#80cbc4', '#80deea', '#90caf9', '#b39ddb', '#ce93d8', '#f48fb1',
'#e57373', '#ffb74d', '#fff176', '#81c784', '#4db6ac', '#4dd0e1', '#64b5f6', '#9575cd', '#ba68c8', '#f06292',
'#f44336', '#fb8c00', '#fdd835', '#4caf50', '#00897b', '#00acc1', '#1e88e5', '#5e35b1', '#8e24aa', '#d81b60',
'#c62828', '#ef6c00', '#f9a825', '#2e7d32', '#00695c', '#00838f', '#1565c0', '#4527a0', '#6a1b9a', '#ad1457',
'#b71c1c', '#e65100', '#f57f17', '#1b5e20', '#004d40', '#006064', '#0d47a1', '#311b92', '#4a148c', '#880e4f'
];
const styleGrid = document.getElementById('tlColorGrid');
const textGrid = document.getElementById('tlTextColorGrid');
const populateGrid = (gridEl, key, closePopupEl = null) => {
if (!gridEl) return;
gridEl.innerHTML = '';
colors.forEach(color => {
const btn = document.createElement('div');
btn.className = 'color-swatch';
btn.style.backgroundColor = color;
btn.onclick = (e) => {
e.stopPropagation();
this.applySettings(key, color);
if (closePopupEl) closePopupEl.classList.add('hidden');
};
gridEl.appendChild(btn);
});
};
populateGrid(styleGrid, 'color');
populateGrid(textGrid, 'textColor', document.getElementById('tlTextColorPicker'));
const textColorBtn = document.getElementById('tlTextColorBtn');
const textPicker = document.getElementById('tlTextColorPicker');
if (textColorBtn && textPicker) {
textColorBtn.onclick = (e) => {
e.stopPropagation();
textPicker.classList.toggle('hidden');
};
}
// Bind Inputs
document.getElementById('tlOpacityInput').addEventListener('input', (e) => {
document.getElementById('tlOpacityValue').textContent = e.target.value + '%';
this.applySettings('opacity', parseInt(e.target.value));
});
document.getElementById('tlFontSize').addEventListener('change', (e) => {
this.applySettings('fontSize', parseInt(e.target.value));
});
document.getElementById('tlTextInput').addEventListener('input', (e) => {
this.applySettings('text', e.target.value);
});
document.getElementById('tlAlignVert').addEventListener('change', (e) => {
this.applySettings('alignVert', e.target.value);
});
document.getElementById('tlAlignHorz').addEventListener('change', (e) => {
this.applySettings('alignHorz', e.target.value);
});
// Close on click outside
document.addEventListener('mousedown', (e) => {
const panel = document.getElementById('trendlineSettingsPanel');
const toolbar = document.getElementById('drawingToolbar');
const textPicker = document.getElementById('tlTextColorPicker');
if (textPicker && !textPicker.classList.contains('hidden') && !textPicker.contains(e.target) && e.target !== document.getElementById('tlTextColorBtn')) {
textPicker.classList.add('hidden');
}
if (panel && !panel.classList.contains('hidden')) {
if (!panel.contains(e.target) && !toolbar.contains(e.target)) {
panel.classList.add('hidden');
}
}
});
}
toggleSettingsPanel(show = true) {
const panel = document.getElementById('trendlineSettingsPanel');
if (!panel) return;
if (show) {
panel.classList.remove('hidden');
this.updateSettingsPanelUI();
} else {
panel.classList.add('hidden');
}
}
updateSettingsPanelUI() {
const settings = this.selectedDrawing || this.defaults[this.selectedDrawing ? this.selectedDrawing.type : 'trend_line'];
document.querySelectorAll('#tlColorGrid .color-swatch').forEach(el => {
if (el.style.backgroundColor === settings.color) el.classList.add('active');
else el.classList.remove('active');
});
document.getElementById('tlOpacityInput').value = settings.opacity !== undefined ? settings.opacity : 100;
document.getElementById('tlOpacityValue').textContent = (settings.opacity !== undefined ? settings.opacity : 100) + '%';
document.querySelectorAll('.tl-thickness-btn').forEach(btn => {
btn.setAttribute('data-active', parseInt(btn.dataset.thickness) === settings.width);
});
document.querySelectorAll('.tl-style-btn').forEach(btn => {
btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style);
});
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
const textColorBtn = document.getElementById('tlTextColorBtn');
const textPicker = document.getElementById('tlTextColorPicker');
const fontInput = document.getElementById('tlFontSize');
const textInput = document.getElementById('tlTextInput');
const alignVert = document.getElementById('tlAlignVert');
const alignHorz = document.getElementById('tlAlignHorz');
const boldBtn = document.getElementById('tlBoldBtn');
const italicBtn = document.getElementById('tlItalicBtn');
if (isLineWithText) {
if (textColorBtn && textPicker) {
textColorBtn.style.backgroundColor = settings.textColor || settings.color;
textPicker.classList.remove('hidden');
}
fontInput.value = settings.fontSize || 14;
textInput.value = settings.text || '';
boldBtn.setAttribute('data-active', !!settings.bold);
italicBtn.setAttribute('data-active', !!settings.italic);
alignVert.value = settings.alignVert || 'top';
alignHorz.value = settings.alignHorz || 'left';
} else {
if (textPicker) textPicker.classList.add('hidden');
}
}
applySettings(key, value) {
if (this.selectedDrawing) {
if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
const isLineWithText = ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
if (!isLineWithText) return;
this.selectedDrawing[key] = value;
} else {
this.selectedDrawing[key] = value;
}
this.update();
} else {
const drawingType = this.selectedDrawing ? this.selectedDrawing.type : 'trend_line';
if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
if (drawingType === 'trend_line' || drawingType === 'ray' || drawingType === 'rectangle') {
this.defaults.trend_line[key] = value;
}
} else {
if (drawingType === 'trend_line' || drawingType === 'ray' || drawingType === 'rectangle') {
this.defaults.trend_line[key] = value;
} else if (drawingType === 'horizontal_line') {
this.defaults.horizontal_line[key] = value;
} else if (drawingType === 'vertical_line') {
this.defaults.vertical_line[key] = value;
} else {
this.defaults.trend_line[key] = value;
}
}
}
this.updateSettingsPanelUI();
}
} }