423 lines
31 KiB
HTML
423 lines
31 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>/ESsent</title>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<!-- Font Awesome Free 7.3.1, from unpkg like the other two dependencies.
|
||
Pinned deliberately: an unpinned icon set is a silent redesign on someone
|
||
else's release. cdnjs 404s on this path — it does not carry 7.3.1. -->
|
||
<link rel="stylesheet" href="https://unpkg.com/@fortawesome/fontawesome-free@7.3.1/css/all.min.css">
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||
<script src="https://unpkg.com/lightweight-charts@5.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||
</head>
|
||
<body>
|
||
<div id="app">
|
||
<header>
|
||
<h1><strong>/ES</strong>sent</h1>
|
||
<div class="status" :class="status.stream"><i></i>{{ status.stream }}<span v-if="status.delay_minutes"> ({{ status.delay_minutes }}min delay)</span> · {{ status.source || 'source' }}<a v-if="status.needs_login" class="reconnect" href="/api/schwab/login">reconnect</a></div>
|
||
</header>
|
||
<main>
|
||
<section class="chart-shell">
|
||
<div class="chart-head">
|
||
<div class="quote"><span class="symbol">{{ status.symbol || 'ES=F' }}</span><span class="price">{{ price == null ? '—' : price.toFixed(2) }}</span><span class="quote-change" :class="quoteChange == null ? '' : quoteChange.points > 0 ? 'positive' : quoteChange.points < 0 ? 'negative' : 'flat'"><template v-if="quoteChange">{{ quoteChange.points >= 0 ? '+' : '' }}{{ quoteChange.points.toFixed(2) }} ({{ quoteChange.percent >= 0 ? '+' : '' }}{{ quoteChange.percent.toFixed(2) }}%)</template><template v-else>— (—%)</template></span></div>
|
||
<div class="chart-head-tools">
|
||
<button type="button" class="undo-btn" :disabled="!canUndo" :title="undoTitle" aria-label="Undo" @click="undo"><i class="fa-solid fa-rotate-left"></i></button>
|
||
<div class="timeframes"><button v-for="tf in timeframes" :key="tf" :class="{active: timeframe === tf}" @click="selectTimeframe(tf)">{{ tf }}</button></div>
|
||
</div>
|
||
</div>
|
||
<div id="chart" @dragover.prevent @drop.prevent="dropSymbol"></div>
|
||
<div class="statusbar">
|
||
<span v-if="extraDetail">FEED <b>{{ status.stream }}</b></span>
|
||
<span v-if="extraDetail">LAST BAR <b>{{ barAge }}</b></span>
|
||
<span v-if="extraDetail">HELD <b>{{ status.bars_held?.[timeframe] || 0 }} {{ timeframe }}</b></span>
|
||
<span v-if="armedTool === 'trendline'" class="arm-hint">Drag on the chart from one point to the other</span>
|
||
<span v-else-if="armedTool === 'level'" class="arm-hint">Click or drag on the chart to set the price</span>
|
||
<span v-else-if="armedTool === 'fibonacci'" class="arm-hint">Drag from one swing to the other</span>
|
||
<span v-else-if="selectedDrawing" class="arm-hint">Drawing selected — Delete removes it</span>
|
||
<button v-if="diagnosticMode" class="diag-capture" :disabled="captureBusy" @click="captureDiagnostic">
|
||
{{ captureBusy ? 'CAPTURING…' : 'CAPTURE DIAGNOSTIC' }}
|
||
</button>
|
||
<span class="data-freshness"><span>BUILD <b>{{ buildStamp }}</b></span><span>UPDATED <b>{{ dataUpdatedAt }}</b></span></span>
|
||
</div>
|
||
</section>
|
||
<aside>
|
||
<details class="sidebar-section">
|
||
<summary>Layers</summary>
|
||
<details class="layer-group ma-fold" open>
|
||
<summary>
|
||
<i class="fa-solid fa-chevron-down ma-fold-caret" aria-hidden="true"></i>
|
||
<label class="master" @click.stop><input type="checkbox" :checked="allEnabled('1d')" @change="toggleGroup('1d', $event.target.checked)"><span class="swatch tf-1d"></span>Daily MAs</label>
|
||
</summary>
|
||
<div class="ma-rows">
|
||
<div v-for="period in [10,20,50,100,200]" :key="period" class="ma-row">
|
||
<label><input type="checkbox" :value="period" v-model="prefs.enabled.ma['1d']">{{ period }}</label>
|
||
<span class="ma-value">{{ maValue(period) }}</span>
|
||
<button type="button" class="ma-alert" :class="{on: maAlertOn(period)}" :aria-pressed="maAlertOn(period)" :title="maAlertOn(period) ? 'Stop alerting on this average' : 'Alert when price reaches this average'" @click="toggleMaAlert(period)"><i :class="maAlertOn(period) ? 'fa-solid fa-bell' : 'fa-solid fa-bell-slash'"></i></button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<div class="layer-group">
|
||
<label><input type="checkbox" v-model="prefs.enabled.horizontal"><span class="swatch horizontal"></span>Prior day H/L/C</label>
|
||
<label><input type="checkbox" v-model="prefs.enabled.vwap"><span class="swatch vwap"></span>Session VWAP</label>
|
||
<label><input type="checkbox" v-model="prefs.enabled.rth"><span class="swatch rth"></span>SPY open/close</label>
|
||
</div>
|
||
<div class="layer-group">
|
||
<label><input type="checkbox" v-model="prefs.enabled.drawings"><span class="swatch drawings"></span>Drawings</label>
|
||
</div>
|
||
<div class="layer-group layer-inline">
|
||
<label><input type="checkbox" v-model="prefs.enabled.manual"><span class="swatch manual"></span>Manual lines</label>
|
||
<label class="disabled" title="Automatic trendline detection is not built yet">(<input type="checkbox" disabled>auto)</label>
|
||
</div>
|
||
<label class="score-hidden"><input type="checkbox" v-model="prefs.hidden_levels_score">Hidden levels still count toward confluence</label>
|
||
</details>
|
||
<details class="sidebar-section config-section">
|
||
<summary>Config</summary>
|
||
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
|
||
<label><input type="checkbox" v-model="autoScrollLivePrice"> Autoscroll to live price</label>
|
||
<label><input type="checkbox" v-model="extraDetail"> Extra detail</label>
|
||
</details>
|
||
<details class="sidebar-section tools-section" open>
|
||
<summary>Tools</summary>
|
||
<div class="tool tool-mark" :class="{armed: armedTool === 'symbol'}">
|
||
<div class="tool-head symbol-head" title="Choose a mark and click the chart, or drag a mark button onto it. Marks pin to that bar and price and can be dragged later with their grip.">
|
||
<button class="symbol-arm" @click="toggleSymbolPanel" :aria-expanded="symbolPanelOpen" :aria-pressed="armedTool === 'symbol'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-icons"></i></span>Mark
|
||
</button>
|
||
<details class="color-picker symbol-head-color" @click.stop>
|
||
<summary :title="drawingColorName(symbolColor)"
|
||
:aria-label="`Choose symbol color; current color ${drawingColorName(symbolColor)}`"
|
||
:style="{backgroundColor: symbolColor}"></summary>
|
||
<div class="color-popover">
|
||
<div v-for="row in drawingColorRows" :key="row.id" class="color-family-row">
|
||
<input v-model="colorRowLabels[row.id]" maxlength="24" class="color-family-input"
|
||
:aria-label="`${row.id} row annotation`" @click.stop
|
||
@blur="colorRowLabels[row.id] = colorRowLabels[row.id].trim() || row.id">
|
||
<div class="color-family-swatches">
|
||
<button v-for="color in row.colors" :key="color" type="button"
|
||
:class="{selected: color === symbolColor}" :style="{backgroundColor: color}"
|
||
:title="drawingColorName(color)" :aria-label="`Use ${drawingColorName(color)}`"
|
||
@click.stop="symbolColor = color; $event.currentTarget.closest('details').open = false"></button>
|
||
</div>
|
||
</div>
|
||
<div class="color-popover-footer">
|
||
<label class="custom-color" title="Custom color"><i class="fa-solid fa-palette"></i><input type="color" v-model="symbolColor" aria-label="Custom symbol color"></label>
|
||
<button type="button" class="palette-close" aria-label="Cancel color selection" title="Cancel"
|
||
@click.stop="$event.currentTarget.closest('details').open = false"><i class="fa-solid fa-xmark"></i></button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<span class="tool-state">{{ armedTool === 'symbol' ? 'click chart' : '' }}</span>
|
||
</div>
|
||
<div class="tool-body" v-show="symbolPanelOpen">
|
||
<div class="symbol-palette" aria-label="Mark choices">
|
||
<button v-for="symbol in symbolChoices" :key="symbol.icon" type="button" draggable="true"
|
||
:class="{active: selectedSymbol === symbol.icon}" :title="symbol.name" :aria-label="symbol.name"
|
||
@click="chooseSymbol(symbol)" @dragstart="startSymbolDrag($event, symbol)">
|
||
<i :class="`fa-solid fa-${symbol.icon}`"></i>
|
||
</button>
|
||
</div>
|
||
<label>Size<select v-model.number="symbolScale" aria-label="Mark size">
|
||
<option v-for="scale in symbolScales" :value="scale">{{ scale }}×</option>
|
||
</select></label>
|
||
</div>
|
||
</div>
|
||
<div class="tool tool-level" :class="{armed: armedTool === 'level'}">
|
||
<button class="tool-head" @click="armTool('level')" :aria-pressed="armedTool === 'level'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-minus"></i></span>Price level
|
||
<span class="tool-state">{{ armedTool === 'level' ? 'click on chart' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'level'">
|
||
<label>Label<input v-model.trim="alertNote" placeholder="optional" aria-label="Level label"></label>
|
||
<div class="row">
|
||
<label>Colour<input type="color" v-model="levelColor" aria-label="Level colour"></label>
|
||
<label>Width<select v-model.number="levelWidth" aria-label="Level width"><option v-for="width in 9" :value="width">{{ width }}px</option></select></label>
|
||
</div>
|
||
<label>Alert early (pts)<input type="number" min="0" step="0.25" v-model.number="alertEarlyPoints" placeholder="ATR default" aria-label="Alert early points"></label>
|
||
<form class="row price-row" @submit.prevent="addPriceAlert">
|
||
<label>Price<input type="number" step="0.25" v-model.number="alertPrice" :placeholder="price == null ? '0.00' : price.toFixed(2)" aria-label="Level price"></label>
|
||
<button type="submit" :disabled="!alertPrice">Add</button>
|
||
</form>
|
||
<p class="hint">Drag on the chart, or type an exact price. Alerts whenever price reaches it, whatever the confluence score.</p>
|
||
</div>
|
||
</div>
|
||
<div class="tool" :class="{armed: armedTool === 'fibonacci'}">
|
||
<button class="tool-head" @click="armTool('fibonacci')" :aria-pressed="armedTool === 'fibonacci'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-chart-line"></i></span>Fibonacci
|
||
<span class="tool-state">{{ armedTool === 'fibonacci' ? 'drag swings' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'fibonacci'">
|
||
<label>Label<input v-model.trim="drawName" placeholder="optional" aria-label="Fibonacci label"></label>
|
||
<div class="row">
|
||
<label>Colour<input type="color" v-model="drawColor" aria-label="Fibonacci colour"></label>
|
||
<label>Width<select v-model.number="drawWidth" aria-label="Fibonacci width"><option v-for="width in 9" :value="width">{{ width }}px</option></select></label>
|
||
</div>
|
||
<label class="check"><input type="checkbox" v-model="snap">Snap to highs/lows</label>
|
||
<p class="hint">Drag from one swing to the other. Levels at 0, 23.6, 38.2, 50, 61.8, 78.6 and 100.</p>
|
||
</div>
|
||
</div>
|
||
<div class="tool tool-trendline" :class="{armed: armedTool === 'trendline'}">
|
||
<button class="tool-head" @click="armTool('trendline')" :aria-pressed="armedTool === 'trendline'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-arrow-trend-up"></i></span>Trendline
|
||
<span class="tool-state">{{ armedTool === 'trendline' ? 'drag on chart' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'trendline'">
|
||
<label>Label<input v-model.trim="drawName" placeholder="optional" aria-label="Trendline label"></label>
|
||
<div class="row">
|
||
<div class="tool-color"><span>Colour</span>
|
||
<details class="color-picker" @click.stop>
|
||
<summary :title="drawingColorName(drawColor)" :aria-label="`Choose trendline color; current color ${drawingColorName(drawColor)}`" :style="{backgroundColor: drawColor}"></summary>
|
||
<div class="color-popover">
|
||
<div v-for="row in drawingColorRows" :key="row.id" class="color-family-row">
|
||
<input v-model="colorRowLabels[row.id]" maxlength="24" class="color-family-input" :aria-label="`${row.id} row annotation`" @click.stop @blur="colorRowLabels[row.id] = colorRowLabels[row.id].trim() || row.id">
|
||
<div class="color-family-swatches"><button v-for="color in row.colors" :key="color" type="button"
|
||
:class="{selected: color === drawColor}" :style="{backgroundColor: color}"
|
||
:title="drawingColorName(color)" :aria-label="`Use ${drawingColorName(color)}`"
|
||
@click.stop="drawColor = color; $event.currentTarget.closest('details').open = false"></button></div>
|
||
</div>
|
||
<div class="color-popover-footer">
|
||
<label class="custom-color" title="Custom color"><i class="fa-solid fa-palette"></i><input type="color" v-model="drawColor" aria-label="Custom trendline color"></label>
|
||
<button type="button" class="palette-close" aria-label="Cancel color selection" title="Cancel" @click.stop="$event.currentTarget.closest('details').open = false"><i class="fa-solid fa-xmark"></i></button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
<label>Width<select v-model.number="drawWidth" aria-label="Trendline width"><option v-for="width in 9" :value="width">{{ width }}px</option></select></label>
|
||
</div>
|
||
<div class="row">
|
||
<!-- Side is inferred from the extreme you snap to, so it only has
|
||
anything to say when snapping is off. -->
|
||
<label v-if="!snap">Side<select v-model="drawSide" aria-label="Trendline side"><option value="support">Support</option><option value="resistance">Resistance</option></select></label>
|
||
<span v-else class="side-auto" title="A high is resistance, a low is support">Side <b>auto</b></span>
|
||
<label class="check"><input type="checkbox" v-model="snap">Snap to highs/lows</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="tool tool-comment" :class="{armed: armedTool === 'comment'}">
|
||
<button class="tool-head" @click="armTool('comment')" :aria-pressed="armedTool === 'comment'">
|
||
<span class="tool-glyph"><i class="fa-solid fa-note-sticky"></i></span>Comment
|
||
<span class="tool-state">{{ armedTool === 'comment' ? 'click on chart' : '' }}</span>
|
||
</button>
|
||
<div class="tool-body" v-show="armedTool === 'comment'">
|
||
<label>Text<textarea v-model.trim="commentText" rows="2" placeholder="What happened here?" aria-label="Comment text"></textarea></label>
|
||
<label class="check"><input type="checkbox" v-model="commentFloat">Float (stay on screen)</label>
|
||
<p class="hint">Type the text, then click the chart to place it. Pinned comments sit on that bar; floating ones stay put as you scroll and can be dragged. Click one to collapse it.</p>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<details class="sidebar-section options-section" @toggle="onOptionsToggle">
|
||
<summary>Options</summary>
|
||
<div class="options-body">
|
||
<label>Expiration
|
||
<select v-model="optionExpiryId" :disabled="!optionExpirations.length" aria-label="Option expiration">
|
||
<option v-if="!optionExpirations.length" value="">Open to load</option>
|
||
<option v-for="row in optionExpirations" :key="row.id" :value="row.id">{{ row.label }}</option>
|
||
</select>
|
||
</label>
|
||
<div class="row">
|
||
<label>Side
|
||
<select v-model="optionSide" aria-label="Calls or puts">
|
||
<option value="P">Puts</option>
|
||
<option value="C">Calls</option>
|
||
</select>
|
||
</label>
|
||
<label>Filter
|
||
<select v-model="optionMode" aria-label="Filter by delta or price">
|
||
<option value="delta">Delta</option>
|
||
<option value="price">Price</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
<div class="row">
|
||
<label>From<input type="number" step="any" v-model.number="optionMin" :aria-label="optionMode === 'delta' ? 'Minimum delta' : 'Minimum mark'"></label>
|
||
<label>To<input type="number" step="any" v-model.number="optionMax" :aria-label="optionMode === 'delta' ? 'Maximum delta' : 'Maximum mark'"></label>
|
||
</div>
|
||
<div class="row price-row">
|
||
<button type="button" :disabled="optionBusy || !optionExpiryId" @click="searchOptions">{{ optionBusy ? 'Searching…' : optionContracts.length ? 'Refresh' : 'Search' }}</button>
|
||
<span class="hint" v-if="optionUnderlying">/ES {{ optionUnderlying.toFixed(2) }}</span>
|
||
</div>
|
||
<p class="hint" v-if="optionError">{{ optionError }}</p>
|
||
<p class="hint" v-else-if="!optionSearched">No quotes until you search.</p>
|
||
<p class="hint" v-else-if="!optionContracts.length">No contracts in that range.</p>
|
||
<div v-else class="option-results">
|
||
<div class="option-row option-head">
|
||
<span>Strike</span><span>Mark</span><span>Δ ≈</span><span></span>
|
||
</div>
|
||
<div v-for="row in optionContracts" :key="row.symbol" class="option-row">
|
||
<span>{{ row.strike.toFixed(0) }}</span>
|
||
<span>{{ row.mark.toFixed(2) }}</span>
|
||
<span>{{ row.abs_delta == null ? '—' : row.abs_delta.toFixed(2) }}</span>
|
||
<button type="button" :title="`${row.tos} · ${row.bid ?? '—'} × ${row.ask ?? '—'} · vol ${row.volume} · oi ${row.open_interest}`" @click="copyOption(row)">{{ optionCopied === row.symbol ? 'Copied' : 'Copy' }}</button>
|
||
</div>
|
||
<p class="hint" v-if="optionContracts.length">Bid/ask on copy target. Δ is approximate.</p>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
<details class="sidebar-section drawings-section" open>
|
||
<summary>Drawings ({{ filteredDrawings.length }}<span v-if="filteredDrawings.length !== drawings.length"> of {{ drawings.length }}</span>)</summary>
|
||
<div class="drawing-filters">
|
||
<select v-model="drawingKind" aria-label="Filter drawings by type">
|
||
<option value="all">All types</option>
|
||
<option value="trendline">Trendlines</option>
|
||
<option value="level">Levels</option>
|
||
<option value="comment">Comments</option>
|
||
<option value="symbol">Symbols</option>
|
||
<option value="fibonacci">Fibonacci</option>
|
||
</select>
|
||
<select v-model="drawingTf" aria-label="Filter drawings by timeframe">
|
||
<option value="all">All TFs</option>
|
||
<option v-for="tf in timeframes" :key="tf" :value="tf">{{ tf }}</option>
|
||
</select>
|
||
<input v-model="drawingFilter" placeholder="Filter text" aria-label="Filter drawings by text">
|
||
</div>
|
||
<div class="trendline-actions" v-if="drawings.length">
|
||
<button @click="toggleSelectAll" :disabled="!filteredDrawings.length">{{ allShownSelected ? 'Clear shown' : 'Select shown' }}</button>
|
||
<span class="selection-count">{{ selectedDrawings.length }} selected</span>
|
||
<button @click="toggleSelectedVisibility" :disabled="!selectedDrawings.length"
|
||
:aria-label="selectedAreHidden ? 'Show selected drawings' : 'Hide selected drawings'">
|
||
<i :class="selectedAreHidden ? 'fa-solid fa-eye' : 'fa-solid fa-eye-slash'"></i>
|
||
{{ selectedAreHidden ? 'Show' : 'Hide' }}
|
||
</button>
|
||
<button @click="deleteSelected" :disabled="!selectedDrawings.length" aria-label="Delete selected drawings">Delete</button>
|
||
</div>
|
||
<div class="drawing-list-shell">
|
||
<div class="drawing-list" ref="drawingList">
|
||
<div v-if="!drawings.length" class="empty">Nothing drawn yet.</div>
|
||
<div v-else-if="!filteredDrawings.length" class="empty">No drawings match this filter.</div>
|
||
|
||
<div v-for="item in filteredDrawings" :key="item.id" class="trendline-row"
|
||
:data-drawing-id="item.id"
|
||
:class="{selected: selectedDrawings.includes(item.id), 'hidden-drawing': item.hidden}">
|
||
<input class="line-select" type="checkbox"
|
||
:checked="selectedDrawings.includes(item.id)" :aria-label="`Select drawing ${item.number}`"
|
||
@click.stop @change="toggleDrawingSelection(item.id)">
|
||
|
||
<div class="drawing-content">
|
||
<div class="drawing-primary">
|
||
<input v-if="item.line" :value="item.line.label" aria-label="Drawing name"
|
||
@click.stop @change="renameLine(item.line, $event.target.value)">
|
||
<input v-else :value="item.comment.note" aria-label="Comment text"
|
||
@click.stop @change="renameLine(item.comment, $event.target.value)">
|
||
<label v-if="item.comment" class="drawing-state" @click.stop
|
||
:title="item.comment.pinned ? 'Pinned to its bar' : 'Floating on screen'">
|
||
<input type="checkbox" :checked="!item.comment.pinned" @change="togglePinned(item.comment)">
|
||
<i :class="item.comment.pinned ? 'fa-solid fa-thumbtack' : 'fa-solid fa-up-down-left-right'"></i>
|
||
</label>
|
||
<label v-else class="drawing-state" :class="{off: !item.line.armed}" @click.stop
|
||
:title="item.line.armed ? 'Armed — click to disarm' : 'Tripped — click to re-arm'">
|
||
<input type="checkbox" :checked="item.line.armed" @change="setArmed(item.line, $event.target.checked)">
|
||
<i :class="item.line.armed ? 'fa-solid fa-bell' : 'fa-solid fa-bell-slash'"></i>
|
||
</label>
|
||
<button class="drawing-delete" @click.stop="deleteDrawing(item)" aria-label="Delete drawing" title="Delete">
|
||
<i class="fa-solid fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="drawing-secondary">
|
||
<span v-if="item.kind === 'comment'">#{{ item.number }} · NOTE · {{ item.comment.pinned ? 'PIN' : 'FLOAT' }}</span>
|
||
<span v-else-if="item.kind === 'symbol'">#{{ item.number }} · SYMBOL · {{ item.comment.note }}</span>
|
||
<span v-else-if="item.kind === 'fibonacci'">#{{ item.number }} · FIB · {{ item.label }}</span>
|
||
<span v-else-if="item.kind === 'level'" class="level-editors">
|
||
<label>Price<input type="number" min="0.25" step="0.25" :value="item.line.anchor_p"
|
||
aria-label="Level price in drawing list"
|
||
@keydown.enter="$event.target.blur()"
|
||
@change="updateLevelNumber(item.line, 'anchor_p', $event.target.value)"></label>
|
||
<label>Early<input type="number" min="0" step="0.25" :value="item.line.alert_early_points ?? ''"
|
||
placeholder="ATR" aria-label="Level alert early points"
|
||
@keydown.enter="$event.target.blur()"
|
||
@change="updateLevelNumber(item.line, 'alert_early_points', $event.target.value)"></label>
|
||
</span>
|
||
<span v-else>#{{ item.number }} · {{ item.line.tf }} · {{ item.line.side === 'support' ? '↑' : '↓' }}</span>
|
||
|
||
<div class="drawing-controls" @click.stop v-if="item.line">
|
||
<select :value="item.line.line_width || 2" aria-label="Drawing width"
|
||
@change="updateLineStyle(item.line, {line_width: Number($event.target.value)})">
|
||
<option v-for="width in 9" :value="width">{{ width }}</option>
|
||
</select>
|
||
<details class="color-picker">
|
||
<summary :aria-label="`Choose drawing color; current color ${drawingColorName(item.line.color || '#65b7cf')}`"
|
||
:title="drawingColorName(item.line.color || '#65b7cf')"
|
||
:style="{ backgroundColor: item.line.color || '#65b7cf' }"></summary>
|
||
<div class="color-popover">
|
||
<div v-for="row in drawingColorRows" :key="row.id" class="color-family-row">
|
||
<input v-model="colorRowLabels[row.id]" maxlength="24" class="color-family-input" :aria-label="`${row.id} row annotation`" @click.stop @blur="colorRowLabels[row.id] = colorRowLabels[row.id].trim() || row.id">
|
||
<div class="color-family-swatches"><button v-for="color in row.colors" :key="color" type="button"
|
||
:class="{selected: color === item.line.color}" :style="{backgroundColor: color}"
|
||
:title="drawingColorName(color)" :aria-label="`Use ${drawingColorName(color)}`"
|
||
@click.stop="updateLineStyle(item.line, {color}); $event.currentTarget.closest('details').open = false"></button></div>
|
||
</div>
|
||
<div class="color-popover-footer">
|
||
<label class="custom-color" title="Custom color"><i class="fa-solid fa-palette"></i><input type="color" :value="item.line.color || '#65b7cf'" aria-label="Custom drawing color" @change="updateLineStyle(item.line, {color: $event.target.value})"></label>
|
||
<button type="button" class="palette-close" aria-label="Cancel color selection" title="Cancel" @click.stop="$event.currentTarget.closest('details').open = false"><i class="fa-solid fa-xmark"></i></button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
<div class="drawing-controls" @click.stop v-else>
|
||
<select v-if="item.kind === 'symbol'" class="mark-scale" :value="item.comment.scale || 1"
|
||
aria-label="Mark size"
|
||
@change="updateLineStyle(item.comment, {scale: Number($event.target.value)})">
|
||
<option v-for="scale in symbolScales" :value="scale">{{ scale }}×</option>
|
||
</select>
|
||
<button v-if="item.kind === 'comment'" class="collapse-toggle" @click.stop="toggleComment(item.comment)"
|
||
:title="item.comment.collapsed ? 'Expand comment' : 'Collapse comment'">
|
||
<i :class="item.comment.collapsed ? 'fa-solid fa-expand' : 'fa-solid fa-compress'"></i>
|
||
</button>
|
||
<details class="color-picker">
|
||
<summary :aria-label="`Choose comment color; current color ${drawingColorName(item.comment.color || '#c8992f')}`"
|
||
:title="drawingColorName(item.comment.color || '#c8992f')"
|
||
:style="{ backgroundColor: item.comment.color || '#c8992f' }"></summary>
|
||
<div class="color-popover">
|
||
<div v-for="row in drawingColorRows" :key="row.id" class="color-family-row">
|
||
<input v-model="colorRowLabels[row.id]" maxlength="24" class="color-family-input" :aria-label="`${row.id} row annotation`" @click.stop @blur="colorRowLabels[row.id] = colorRowLabels[row.id].trim() || row.id">
|
||
<div class="color-family-swatches"><button v-for="color in row.colors" :key="color" type="button"
|
||
:class="{selected: color === item.comment.color}" :style="{backgroundColor: color}"
|
||
:title="drawingColorName(color)" :aria-label="`Use ${drawingColorName(color)}`"
|
||
@click.stop="updateLineStyle(item.comment, {color}); $event.currentTarget.closest('details').open = false"></button></div>
|
||
</div>
|
||
<div class="color-popover-footer">
|
||
<label class="custom-color" title="Custom color"><i class="fa-solid fa-palette"></i><input type="color" :value="item.comment.color || '#c8992f'" aria-label="Custom comment color" @change="updateLineStyle(item.comment, {color: $event.target.value})"></label>
|
||
<button type="button" class="palette-close" aria-label="Cancel color selection" title="Cancel" @click.stop="$event.currentTarget.closest('details').open = false"><i class="fa-solid fa-xmark"></i></button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="drawing-list-resize" role="separator" aria-orientation="horizontal"
|
||
aria-label="Resize drawings list" @pointerdown="startDrawingListResize"></div>
|
||
</div>
|
||
</details>
|
||
<details class="sidebar-section">
|
||
<summary>Confluence zones</summary>
|
||
<div v-if="!clusters.length" class="empty">No active zones near current structure.</div>
|
||
<!-- One line each, ordered by price: the list then reads like the
|
||
chart does, highest zone at the top, and they all fit on screen. -->
|
||
<div v-for="cluster in clustersByPrice" :key="cluster.id" class="cluster-row" :class="cluster.side"
|
||
:title="cluster.members.map(member => member.label).join(' · ')">
|
||
<span class="cluster-band">{{ cluster.low.toFixed(2) }}–{{ cluster.high.toFixed(2) }}</span>
|
||
<span class="cluster-score">{{ cluster.score.toFixed(1) }}</span>
|
||
<span class="cluster-dist">{{ cluster.distance > 0 ? '+' : '' }}{{ cluster.distance.toFixed(1) }}</span>
|
||
<span class="cluster-members">{{ cluster.members.map(member => member.label).join(' · ') }}</span>
|
||
</div>
|
||
</details>
|
||
<details class="sidebar-section">
|
||
<summary>Events ({{ events.length }})</summary>
|
||
<div v-if="!events.length" class="empty">No events yet.</div>
|
||
<div v-for="event in events" :key="event.key" class="alert-entry" :class="event.kind">
|
||
<time><b v-if="event.number" class="event-number">#{{ event.number }}</b>{{ event.at }}</time>
|
||
<a v-if="event.url" :href="event.url" target="_blank" rel="noopener">{{ event.message }}</a>
|
||
<template v-else>{{ event.message }}</template>
|
||
</div>
|
||
<button v-if="eventsMore" class="events-more" type="button" @click="loadOlderEvents">More</button>
|
||
</details>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
<script src="/static/chart.js"></script>
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|