SPB Git

Repository Public

Commit

web: charts — bibliothèque encyclopédique (cartes avec vignettes, panneau de détail, recherche name/aka/summary, tri, clavier, mode détail mobile), onglet About des réglages, tooltips riches des flyouts d'outils et des lignes de la légende (valeurs expliquées), bandeau première visite, lien profond ?tool=, 42 outils exposés avec les ids du moteur

Simon-Pierre Boucher committed · Sep 7, 2026 parent dc9c972
10 changed files +393 −77 Browse files
modified hfmarketdata/web/src/pages/charts/ChartsPage.jsx +27 −2
@@ -11,8 +11,9 @@ import { TF_LABEL } from '../../charts/data/bars.js'
11 import { LAYOUTS, initialState, parseSearch, readFavorites, readLastTools, readPrefs, toSearch, writeFavorites, writeLast, writeLastTools, writePrefs } from '../../charts/data/state.js' 11 import { LAYOUTS, initialState, parseSearch, readFavorites, readLastTools, readPrefs, toSearch, writeFavorites, writeLast, writeLastTools, writePrefs } from '../../charts/data/state.js'
12 import { pushRecent } from '../../charts/data/symbols.js' 12 import { pushRecent } from '../../charts/data/symbols.js'
13 import { defaultTemplate, deleteTemplate, parseTemplateJSON, readTemplates, saveTemplate, setDefaultTemplate, templateFrom, templatesJSON, writeTemplates } from '../../charts/data/templates.js' 13 import { defaultTemplate, deleteTemplate, parseTemplateJSON, readTemplates, saveTemplate, setDefaultTemplate, templateFrom, templatesJSON, writeTemplates } from '../../charts/data/templates.js'
14−import { defaultParams, indicatorDef, sanitizeParams } from './indicators.js' 14+import { catalog, defaultParams, indicatorDef, sanitizeParams } from './indicators.js'
15−import { toolGroup } from './drawtools.js' 15+import { ALL_TOOLS, toolGroup, toolSupported } from './drawtools.js'
16+import { Thumb } from '../../charts/catalog/ChartFigure.jsx'
16 import { buildTheme } from './theme.js' 17 import { buildTheme } from './theme.js'
17 import Toolbar from './Toolbar.jsx' 18 import Toolbar from './Toolbar.jsx'
18 import DrawingBar, { TOOL_KEYS } from './DrawingBar.jsx' 19 import DrawingBar, { TOOL_KEYS } from './DrawingBar.jsx'
@@ -37,6 +38,27 @@ const isDebug = () => import.meta.env.DEV || new URLSearchParams(window.location
37 const isTyping = e => { const t = e.target?.tagName; return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || e.target?.isContentEditable } 38 const isTyping = e => { const t = e.target?.tagName; return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || e.target?.isContentEditable }
38 const ADJ_ASSETS = new Set(['stock', 'etf', 'futures']) 39 const ADJ_ASSETS = new Set(['stock', 'etf', 'futures'])
39 const LAYOUT_KEYS = ['1', '2h', '2v', '4'] 40 const LAYOUT_KEYS = ['1', '2h', '2v', '4']
41+const INTRO_ITEMS = [{ kind: 'indicator', id: 'bollinger', label: 'Bollinger Bands' }, { kind: 'indicator', id: 'rsi', label: 'RSI' }, { kind: 'tool', id: 'fib', label: 'Fibonacci retracement' }]
42+
43+/** First-visit strip under the chart: "60 indicators · 42 drawing tools · no limits" with three thumbnails (our engine). */
44+function IntroBanner({ onAdd, onTool, onDismiss }) {
45+ const nInd = catalog().length
46+ const nTools = ALL_TOOLS.filter(t => toolSupported(t.id)).length
47+ return (
48+ <aside className="ch-intro" data-testid="ch-intro" aria-label="What you can do here">
49+ <div className="ch-intro-text"><strong>{nInd} indicators · {nTools} drawing tools · no limits</strong><span className="muted">Open the library (⌘I) or try one:</span></div>
50+ <div className="ch-intro-thumbs">
51+ {INTRO_ITEMS.map(it => (
52+ <button key={it.id} type="button" className="ch-intro-thumb" onClick={() => (it.kind === 'tool' ? onTool(it.id) : onAdd(it.id))} aria-label={`${it.kind === 'tool' ? 'Draw with' : 'Add'} ${it.label}`} data-testid={`ch-intro-${it.id}`}>
53+ <Thumb kind={it.kind} id={it.id} size="sm" /><span>{it.label}</span>
54+ </button>
55+ ))}
56+ </div>
57+ <a className="ch-intro-link" href="/docs/charts/indicators" target="_blank" rel="noopener">Encyclopedia ↗</a>
58+ <button type="button" className="icon-btn ch-intro-close" aria-label="Dismiss" onClick={onDismiss} data-testid="ch-intro-close">×</button>
59+ </aside>
60+ )
61+}
40 62
41 function nextColorIndex(cs) { 63 function nextColorIndex(cs) {
42 const used = new Set([...cs.indicators.map(i => i.colorIndex), ...cs.compares.map(c => c.colorIndex)].filter(x => x != null)) 64 const used = new Set([...cs.indicators.map(i => i.colorIndex), ...cs.compares.map(c => c.colorIndex)].filter(x => x != null))
@@ -170,6 +192,8 @@ export default function ChartsPage() {
170 }, [location.search]) // eslint-disable-line react-hooks/exhaustive-deps 192 }, [location.search]) // eslint-disable-line react-hooks/exhaustive-deps
171 193
172 useEffect(() => { writeLast(charts[0]) }, [charts[0].asset, charts[0].ticker, charts[0].tf, charts[0].type]) // eslint-disable-line react-hooks/exhaustive-deps 194 useEffect(() => { writeLast(charts[0]) }, [charts[0].asset, charts[0].ticker, charts[0].tf, charts[0].type]) // eslint-disable-line react-hooks/exhaustive-deps
195+ // deep link from the encyclopedia: /charts?tool=fib arms the drawing tool once
196+ useEffect(() => { const t = new URLSearchParams(window.location.search).get('tool'); if (t && toolSupported(t)) setToolAction(t) }, []) // eslint-disable-line react-hooks/exhaustive-deps
173 useEffect(() => { pushRecent({ asset: cs.asset, ticker: cs.ticker, label: cs.label }) }, [cs.asset, cs.ticker, cs.label]) 197 useEffect(() => { pushRecent({ asset: cs.asset, ticker: cs.ticker, label: cs.label }) }, [cs.asset, cs.ticker, cs.label])
174 useEffect(() => { writePrefs(prefs) }, [prefs]) 198 useEffect(() => { writePrefs(prefs) }, [prefs])
175 useEffect(() => { writeFavorites(favorites) }, [favorites]) 199 useEffect(() => { writeFavorites(favorites) }, [favorites])
@@ -386,6 +410,7 @@ export default function ChartsPage() {
386 replay={replay.on && replay.chartIndex === i ? replay : null} hiddenAll={hiddenAll} lockedAll={lockedAll} drawLock={!!prefs.drawLock} onFirstCatalog={onFirstCatalog} debug={debug} reloadKey={reloadKeys[i] || 0} /> 410 replay={replay.on && replay.chartIndex === i ? replay : null} hiddenAll={hiddenAll} lockedAll={lockedAll} drawLock={!!prefs.drawLock} onFirstCatalog={onFirstCatalog} debug={debug} reloadKey={reloadKeys[i] || 0} />
387 ))} 411 ))}
388 </div> 412 </div>
413+ {!prefs.introSeen && cs.indicators.length === 0 && status.phase !== 'error' && <IntroBanner onAdd={id => { a.addIndicator(id); a.setPref('introSeen', true) }} onTool={id => { setToolAction(id); a.setPref('introSeen', true) }} onDismiss={() => a.setPref('introSeen', true)} />}
389 {replay.on && !isMobile && <ReplayBar replay={replay} total={replayTotal} bar={replayBar} tf={charts[replay.chartIndex]?.tf} tz={charts[replay.chartIndex]?.asset === 'contract' ? 'UTC' : 'ET'} onToggle={replayApi.toggle} onStep={replayApi.step} onSpeed={replayApi.speed} onJumpMode={replayApi.jumpMode} onSeek={replayApi.seek} onExit={replayApi.exit} />} 414 {replay.on && !isMobile && <ReplayBar replay={replay} total={replayTotal} bar={replayBar} tf={charts[replay.chartIndex]?.tf} tz={charts[replay.chartIndex]?.asset === 'contract' ? 'UTC' : 'ET'} onToggle={replayApi.toggle} onStep={replayApi.step} onSpeed={replayApi.speed} onJumpMode={replayApi.jumpMode} onSeek={replayApi.seek} onExit={replayApi.exit} />}
390 </div> 415 </div>
391 {table && <BarsTable bars={chartData} range={activeRange} state={cs} decimals={activeView?.decimals() ?? 2} tz={cs.asset === 'contract' ? 'UTC' : 'ET'} onClose={() => setTable(false)} />} 416 {table && <BarsTable bars={chartData} range={activeRange} state={cs} decimals={activeView?.decimals() ?? 2} tz={cs.asset === 'contract' ? 'UTC' : 'ET'} onClose={() => setTable(false)} />}
modified hfmarketdata/web/src/pages/charts/DrawingBar.jsx +35 −7
@@ -1,17 +1,35 @@
1 // Drawing toolbar with flyouts (like a professional terminal): one button per group showing the group's last used 1 // Drawing toolbar with flyouts (like a professional terminal): one button per group showing the group's last used
2 // tool — click activates it, the chevron (or a right-click / long press) opens the flyout with every tool of the 2 // tool — click activates it, the chevron (or a right-click / long press) opens the flyout with every tool of the
3−// group. Below: magnet, "stay in drawing mode" lock, hide all, lock all, undo / redo, remove all. Under 640 px the 3+// group. Hovering / focusing a flyout item shows a rich tooltip (thumbnail rendered by our engine, one-liner, how to
4−// bar is horizontal and the flyouts open as bottom sheets (rendered by the parent through `onOpenGroup`). 4+// draw, shortcut) read from the lazy encyclopedia; under 640 px the flyouts are bottom sheets whose rows carry the
5+// thumbnail and the one-liner. Below the groups: magnet, "stay in drawing mode", hide all, lock all, undo / redo, remove all.
5 import React, { useEffect, useRef, useState } from 'react' 6 import React, { useEffect, useRef, useState } from 'react'
6 import { availableGroups, toolGroup } from './drawtools.js' 7 import { availableGroups, toolGroup } from './drawtools.js'
7 import BottomSheet from './BottomSheet.jsx' 8 import BottomSheet from './BottomSheet.jsx'
8 import { ChevronDownIcon } from '../../components/Icons.jsx' 9 import { ChevronDownIcon } from '../../components/Icons.jsx'
10+import { Thumb, useCatalog } from '../../charts/catalog/ChartFigure.jsx'
11+import { docsHref } from '../../charts/catalog/index.js'
9 import { CursorIcon, EyeIcon, EyeOffIcon, LockIcon, MagnetIcon, RedoIcon, TOOL_ICONS, TrashIcon, UndoIcon, UnlockIcon, TargetIcon } from './icons.jsx' 12 import { CursorIcon, EyeIcon, EyeOffIcon, LockIcon, MagnetIcon, RedoIcon, TOOL_ICONS, TrashIcon, UndoIcon, UnlockIcon, TargetIcon } from './icons.jsx'
10 13
11 export { TOOL_KEYS } from './drawtools.js' 14 export { TOOL_KEYS } from './drawtools.js'
12 15
13−function Flyout({ group, tool, onPick, onClose, horizontal }) { 16+/** Rich tooltip of one tool (desktop flyouts): image + one-liner + how to draw + shortcut. */
17+export function ToolTip({ entry, tool, className = '' }) {
18+ if (!entry) return null
19+ return (
20+ <div className={`ch-tooltip ${className}`} role="tooltip" data-testid="ch-tool-tip" data-tool={tool.id}>
21+ <Thumb kind="tool" id={tool.id} size="sm" alt="" className="ch-tooltip-thumb" eager />
22+ <div className="ch-tooltip-head"><strong>{entry.name}</strong>{tool.key && <kbd>{tool.key}</kbd>}</div>
23+ <p className="ch-tooltip-lead">{entry.oneLiner}</p>
24+ <ol className="ch-tooltip-steps">{entry.howToDraw.slice(0, 4).map((s, i) => <li key={i}>{s}</li>)}</ol>
25+ <a className="ch-tooltip-link" href={docsHref('tool', tool.id)} target="_blank" rel="noopener">Full guide ↗</a>
26+ </div>
27+ )
28+}
29+
30+function Flyout({ group, tool, onPick, onClose, horizontal, entries }) {
14 const ref = useRef(null) 31 const ref = useRef(null)
32+ const [tip, setTip] = useState(null)
15 useEffect(() => { 33 useEffect(() => {
16 const onDoc = e => { if (!ref.current?.contains(e.target)) onClose() } 34 const onDoc = e => { if (!ref.current?.contains(e.target)) onClose() }
17 const onKey = e => { if (e.key === 'Escape') { e.stopPropagation(); onClose() } } 35 const onKey = e => { if (e.key === 'Escape') { e.stopPropagation(); onClose() } }
@@ -20,14 +38,17 @@ function Flyout({ group, tool, onPick, onClose, horizontal }) {
20 setTimeout(() => ref.current?.querySelector('[aria-pressed="true"], button')?.focus(), 0) 38 setTimeout(() => ref.current?.querySelector('[aria-pressed="true"], button')?.focus(), 0)
21 return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey, true) } 39 return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey, true) }
22 }, [onClose]) 40 }, [onClose])
41+ const tipTool = tip ? group.tools.find(t => t.id === tip) : null
23 return ( 42 return (
24− <div ref={ref} className={`ch-flyout ${horizontal ? 'is-below' : ''}`} role="menu" aria-label={group.label} data-testid={`ch-flyout-${group.id}`}> 43+ <div ref={ref} className={`ch-flyout ${horizontal ? 'is-below' : ''}`} role="menu" aria-label={group.label} data-testid={`ch-flyout-${group.id}`} onMouseLeave={() => setTip(null)}>
25 <div className="ch-flyout-title">{group.label}</div> 44 <div className="ch-flyout-title">{group.label}</div>
26 {group.tools.map(t => { const Icon = TOOL_ICONS[t.id] || CursorIcon; return ( 45 {group.tools.map(t => { const Icon = TOOL_ICONS[t.id] || CursorIcon; return (
27− <button key={t.id} type="button" role="menuitemradio" aria-checked={tool === t.id} aria-pressed={tool === t.id} className="ch-flyout-item" data-tool={t.id} onClick={() => onPick(t.id)}> 46+ <button key={t.id} type="button" role="menuitemradio" aria-checked={tool === t.id} aria-pressed={tool === t.id} className="ch-flyout-item" data-tool={t.id} onClick={() => onPick(t.id)}
47+ onMouseEnter={() => setTip(t.id)} onFocus={() => setTip(t.id)} aria-describedby={tip === t.id && entries?.[t.id] ? 'ch-tool-tip' : undefined}>
28 <Icon /><span>{t.label}</span>{t.key && <kbd>{t.key}</kbd>} 48 <Icon /><span>{t.label}</span>{t.key && <kbd>{t.key}</kbd>}
29 </button> 49 </button>
30 ) })} 50 ) })}
51+ {tipTool && entries?.[tipTool.id] && <div id="ch-tool-tip" className="ch-flyout-tip"><ToolTip entry={entries[tipTool.id]} tool={tipTool} /></div>}
31 </div> 52 </div>
32 ) 53 )
33 } 54 }
@@ -35,6 +56,7 @@ function Flyout({ group, tool, onPick, onClose, horizontal }) {
35 export default function DrawingBar({ tool, onTool, onUndo, onRedo, onClear, magnet, onMagnet, lock, onLock, hiddenAll, onHideAll, lockedAll, onLockAll, hasDrawings, lastTools = {}, onLastTool, horizontal = false }) { 56 export default function DrawingBar({ tool, onTool, onUndo, onRedo, onClear, magnet, onMagnet, lock, onLock, hiddenAll, onHideAll, lockedAll, onLockAll, hasDrawings, lastTools = {}, onLastTool, horizontal = false }) {
36 const groups = availableGroups() 57 const groups = availableGroups()
37 const [open, setOpen] = useState(null) 58 const [open, setOpen] = useState(null)
59+ const entries = useCatalog(open ? 'tool' : null)
38 const current = toolGroup(tool) 60 const current = toolGroup(tool)
39 const pick = (g, id) => { onLastTool?.(g.id, id); onTool(tool === id ? null : id); setOpen(null) } 61 const pick = (g, id) => { onLastTool?.(g.id, id); onTool(tool === id ? null : id); setOpen(null) }
40 const lastOf = g => g.tools.find(t => t.id === lastTools[g.id]) || g.tools[0] 62 const lastOf = g => g.tools.find(t => t.id === lastTools[g.id]) || g.tools[0]
@@ -58,11 +80,17 @@ export default function DrawingBar({ tool, onTool, onUndo, onRedo, onClear, magn
58 <ActiveIcon /> 80 <ActiveIcon />
59 </button> 81 </button>
60 <button type="button" className="ch-tool-more" aria-label={`All ${g.label.toLowerCase()} tools`} aria-haspopup="menu" aria-expanded={open === g.id} onClick={() => setOpen(o => (o === g.id ? null : g.id))} data-testid={`ch-group-more-${g.id}`}><ChevronDownIcon /></button> 82 <button type="button" className="ch-tool-more" aria-label={`All ${g.label.toLowerCase()} tools`} aria-haspopup="menu" aria-expanded={open === g.id} onClick={() => setOpen(o => (o === g.id ? null : g.id))} data-testid={`ch-group-more-${g.id}`}><ChevronDownIcon /></button>
61− {open === g.id && !horizontal && <Flyout group={g} tool={tool} onPick={id => pick(g, id)} onClose={() => setOpen(null)} />} 83+ {open === g.id && !horizontal && <Flyout group={g} tool={tool} onPick={id => pick(g, id)} onClose={() => setOpen(null)} entries={entries} />}
62 {open === g.id && horizontal && ( 84 {open === g.id && horizontal && (
63 <BottomSheet open onClose={() => setOpen(null)} title={g.label} testId={`ch-flyout-${g.id}`}> 85 <BottomSheet open onClose={() => setOpen(null)} title={g.label} testId={`ch-flyout-${g.id}`}>
64 <div className="ch-sheet-grid"> 86 <div className="ch-sheet-grid">
65− {g.tools.map(t => { const TIcon = TOOL_ICONS[t.id] || CursorIcon; return <button key={t.id} type="button" className="ch-sheet-btn" aria-pressed={tool === t.id} data-tool={t.id} onClick={() => pick(g, t.id)}><span className="row" style={{ gap: 8 }}><TIcon />{t.label}</span>{t.key && <kbd className="muted">{t.key}</kbd>}</button> })} 87+ {g.tools.map(t => { const TIcon = TOOL_ICONS[t.id] || CursorIcon; const e = entries?.[t.id]; return (
88+ <button key={t.id} type="button" className="ch-sheet-btn ch-sheet-tool" aria-pressed={tool === t.id} data-tool={t.id} onClick={() => pick(g, t.id)}>
89+ <Thumb kind="tool" id={t.id} size="sm" className="ch-sheet-thumb" />
90+ <span className="ch-sheet-tool-body"><span className="row" style={{ gap: 8 }}><TIcon />{t.label}</span>{e && <span className="ch-sheet-tool-desc">{e.oneLiner}</span>}</span>
91+ {t.key && <kbd className="muted">{t.key}</kbd>}
92+ </button>
93+ ) })}
66 </div> 94 </div>
67 </BottomSheet> 95 </BottomSheet>
68 )} 96 )}
modified hfmarketdata/web/src/pages/charts/DrawingProps.jsx +2 −2
@@ -5,8 +5,8 @@ import { Select } from '../../components/Field.jsx'
5 import { DRAW_COLORS, LINE_STYLES, LINE_WIDTHS, toolDef } from './drawtools.js' 5 import { DRAW_COLORS, LINE_STYLES, LINE_WIDTHS, toolDef } from './drawtools.js'
6 import { CopyIcon, LockIcon, TrashIcon, UnlockIcon } from './icons.jsx' 6 import { CopyIcon, LockIcon, TrashIcon, UnlockIcon } from './icons.jsx'
7 7
8−const FILLABLE = new Set(['rect', 'ellipse', 'triangle', 'channel', 'channel-parallel', 'gann-box', 'long-position', 'short-position', 'date-price-range', 'price-range', 'date-range', 'vertical-range', 'callout', 'price-label', 'fib', 'fib-extension']) 8+const FILLABLE = new Set(['rect', 'ellipse', 'triangle', 'channel', 'gann-box', 'long', 'short', 'date-price-range', 'price-range', 'date-range', 'vrange', 'callout', 'price-label', 'fib', 'fib-extension'])
9−const TEXTUAL = new Set(['text', 'callout', 'price-label', 'flag', 'arrow-marker', 'hline', 'hray', 'trendline', 'ray', 'extended', 'vline']) 9+const TEXTUAL = new Set(['text', 'callout', 'price-label', 'flag', 'arrow-up', 'arrow-down', 'hline', 'hray', 'trendline', 'ray', 'extended', 'vline'])
10 10
11 export default function DrawingProps({ selection, onStyle, onText, onLock, onDuplicate, onDelete, sheet = false }) { 11 export default function DrawingProps({ selection, onStyle, onText, onLock, onDuplicate, onDelete, sheet = false }) {
12 if (!selection) return null 12 if (!selection) return null
added hfmarketdata/web/src/pages/charts/IndicatorDetail.jsx +72 −0
@@ -0,0 +1,72 @@
1+// Encyclopedia sheet of one indicator — the detail panel of the library and the "About" tab of the settings dialog.
2+// Reads the lazy catalogue entry (src/charts/catalog) + the page definition (inputs) and renders: thumbnail, summary,
3+// formula, how to read, defaults explained, best for, pairs with (clickable → add), pitfalls, history, docs link.
4+import React from 'react'
5+import { INDICATOR_CATEGORY_LABELS, docsHref } from '../../charts/catalog/index.js'
6+import { Thumb } from '../../charts/catalog/ChartFigure.jsx'
7+import { indicatorDef } from './indicators.js'
8+
9+export default function IndicatorDetail({ entry, def, onAdd, counts = {}, compact = false, testId = 'ch-ind-detail' }) {
10+ if (!entry) return null
11+ const d = def || indicatorDef(entry.id)
12+ return (
13+ <div className={`ch-detail ${compact ? 'is-compact' : ''}`} data-testid={testId} data-id={entry.id}>
14+ <Thumb kind="indicator" id={entry.id} size="sm" alt={`${entry.name} on a daily chart`} className="ch-detail-thumb" eager />
15+ <div className="ch-detail-head">
16+ <h3 className="ch-detail-title">{entry.name}</h3>
17+ <div className="ch-detail-tags">
18+ {entry.aka?.filter(a => a !== entry.name).slice(0, 3).map(a => <span key={a} className="ch-tag mono">{a}</span>)}
19+ <span className="ch-tag">{INDICATOR_CATEGORY_LABELS[entry.category] || entry.category}</span>
20+ <span className="ch-tag">{entry.pane === 'main' ? 'overlay' : 'own pane'}</span>
21+ {counts[entry.id] ? <span className="ch-tag is-on">×{counts[entry.id]} active</span> : null}
22+ </div>
23+ </div>
24+ <p className="ch-detail-lead">{entry.oneLiner}</p>
25+ <p className="ch-detail-text">{entry.summary}</p>
26+ {onAdd && <div className="ch-detail-actions"><button type="button" className="btn btn-sm btn-primary" onClick={() => onAdd(entry.id)} data-testid="ch-detail-add">Add to chart</button><a className="btn btn-sm btn-ghost" href={docsHref('indicator', entry.id)} target="_blank" rel="noopener" data-testid="ch-detail-docs">Read the full guide ↗</a></div>}
27+
28+ <h4>Formula</h4>
29+ <pre className="ch-detail-formula mono">{entry.formula.text}</pre>
30+ {entry.formula.vars?.length > 0 && <dl className="ch-detail-vars">{entry.formula.vars.map(([k, m]) => <React.Fragment key={k}><dt className="mono">{k}</dt><dd>{m}</dd></React.Fragment>)}</dl>}
31+
32+ <h4>How to read it</h4>
33+ <ul className="ch-detail-list">{entry.howToRead.map((s, i) => <li key={i}>{s}</li>)}</ul>
34+
35+ {entry.defaults?.length > 0 && (
36+ <>
37+ <h4>Default settings</h4>
38+ <dl className="ch-detail-defaults">
39+ {entry.defaults.map(([name, value, why]) => (
40+ <React.Fragment key={name}><dt><span className="mono">{name}</span> <b className="mono">{String(value)}</b></dt><dd>{why}</dd></React.Fragment>
41+ ))}
42+ </dl>
43+ </>
44+ )}
45+ {!entry.defaults?.length && d && !d.inputs?.length && <p className="ch-detail-text muted">No inputs — nothing to tune.</p>}
46+
47+ <h4>Best for</h4>
48+ <dl className="ch-detail-vars">
49+ <dt>Timeframes</dt><dd>{entry.bestFor.timeframes}</dd>
50+ <dt>Markets</dt><dd>{entry.bestFor.markets}</dd>
51+ {entry.bestFor.note && <><dt>Note</dt><dd>{entry.bestFor.note}</dd></>}
52+ </dl>
53+
54+ {entry.pairsWith?.length > 0 && (
55+ <>
56+ <h4>Pairs well with</h4>
57+ <div className="ch-detail-chips">
58+ {entry.pairsWith.map(id => { const pd = indicatorDef(id); return pd ? <button key={id} type="button" className="ch-chip" onClick={() => onAdd?.(id)} disabled={!onAdd} title={onAdd ? `Add ${pd.name}` : pd.name} data-testid={`ch-detail-pair-${id}`}>{pd.name}{onAdd && <small>+</small>}</button> : null })}
59+ </div>
60+ </>
61+ )}
62+
63+ <h4>Pitfalls</h4>
64+ <ul className="ch-detail-list">{entry.pitfalls.map((s, i) => <li key={i}>{s}</li>)}</ul>
65+
66+ <h4>History</h4>
67+ <p className="ch-detail-text">{entry.history}</p>
68+ {entry.related?.length > 0 && <p className="ch-detail-text small">{entry.related.map((r, i) => <React.Fragment key={r.href}>{i > 0 && ' · '}<a href={r.href} target="_blank" rel="noopener">{r.label}</a></React.Fragment>)}</p>}
69+ {!onAdd && <p className="ch-detail-text small"><a href={docsHref('indicator', entry.id)} target="_blank" rel="noopener" data-testid="ch-detail-docs">Read the full guide ↗</a></p>}
70+ </div>
71+ )
72+}
modified hfmarketdata/web/src/pages/charts/IndicatorLibrary.jsx +109 −50
@@ -1,79 +1,138 @@
1−// Indicator library (⌘I): searchable, category tabs (Favorites first), one-click add, favourites in localStorage, 1+// Indicator library (⌘I): encyclopedia cards (thumbnail rendered by our engine, name, one-liner, category, ×n
2−// short description per entry, and the list of active indicators with reorder / hide / settings / remove. 2+// active, favourite) with search over name / aka / summary, category tabs, sort (A–Z / category / popular), a detail
3+// panel on the right (the hovered or focused card; pinned with ⓘ — on phones ⓘ opens the sheet in detail mode) and the
4+// list of active indicators with reorder / hide / settings / remove. The encyclopedia chunk loads when the dialog opens.
3 import React, { useEffect, useMemo, useRef, useState } from 'react' 5 import React, { useEffect, useMemo, useRef, useState } from 'react'
4 import Dialog from './Dialog.jsx' 6 import Dialog from './Dialog.jsx'
5−import { Input } from '../../components/Field.jsx' 7+import { Input, Select } from '../../components/Field.jsx'
8+import useMediaQuery from '../../components/useMediaQuery.js'
6 import { CATEGORIES, catalog, indicatorShortLabel, searchCatalog } from './indicators.js' 9 import { CATEGORIES, catalog, indicatorShortLabel, searchCatalog } from './indicators.js'
10+import { searchEntries, sortEntries } from '../../charts/catalog/index.js'
11+import { Thumb, useCatalog } from '../../charts/catalog/ChartFigure.jsx'
12+import IndicatorDetail from './IndicatorDetail.jsx'
7 import { CloseIcon } from '../../components/Icons.jsx' 13 import { CloseIcon } from '../../components/Icons.jsx'
8 import { DownIcon, EyeIcon, EyeOffIcon, GearIcon, StarIcon, UpIcon } from './icons.jsx' 14 import { DownIcon, EyeIcon, EyeOffIcon, GearIcon, StarIcon, UpIcon } from './icons.jsx'
15+import { InfoIcon } from '../../components/Icons.jsx'
9 import { seriesColor } from './theme.js' 16 import { seriesColor } from './theme.js'
10 17
18+const SORTS = [['category', 'Category'], ['az', 'A–Z'], ['popular', 'Popular']]
19+
11 export default function IndicatorLibrary({ open, onClose, active = [], onAdd, onRemove, onToggleHidden, onMove, onOpenSettings, favorites = [], onToggleFavorite, theme }) { 20 export default function IndicatorLibrary({ open, onClose, active = [], onAdd, onRemove, onToggleHidden, onMove, onOpenSettings, favorites = [], onToggleFavorite, theme }) {
12 const [q, setQ] = useState('') 21 const [q, setQ] = useState('')
13 const [cat, setCat] = useState('All') 22 const [cat, setCat] = useState('All')
23+ const [sort, setSort] = useState('category')
24+ const [hover, setHover] = useState(null) // id under the pointer / focus
25+ const [pinned, setPinned] = useState(null) // id pinned with ⓘ (mobile: detail mode)
14 const inputRef = useRef(null) 26 const inputRef = useRef(null)
15− useEffect(() => { if (open) { setQ(''); setCat(favorites.length ? 'Favorites' : 'All') } }, [open]) // eslint-disable-line react-hooks/exhaustive-deps 27+ const isMobile = useMediaQuery('(max-width: 760px)')
28+ const entries = useCatalog(open ? 'indicator' : null)
29+ useEffect(() => { if (open) { setQ(''); setCat(favorites.length ? 'Favorites' : 'All'); setPinned(null); setHover(null) } }, [open]) // eslint-disable-line react-hooks/exhaustive-deps
16 const all = useMemo(() => catalog(), [open]) // eslint-disable-line react-hooks/exhaustive-deps 30 const all = useMemo(() => catalog(), [open]) // eslint-disable-line react-hooks/exhaustive-deps
17 const cats = ['All', ...CATEGORIES.filter(c => c === 'Favorites' ? favorites.length > 0 : all.some(d => d.category === c))] 31 const cats = ['All', ...CATEGORIES.filter(c => c === 'Favorites' ? favorites.length > 0 : all.some(d => d.category === c))]
18− const list = searchCatalog(q, q ? 'All' : cat, favorites)
19 const counts = useMemo(() => { const m = {}; for (const i of active) m[i.type] = (m[i.type] || 0) + 1; return m }, [active]) 32 const counts = useMemo(() => { const m = {}; for (const i of active) m[i.type] = (m[i.type] || 0) + 1; return m }, [active])
20 33
34+ // list = page catalog filtered by tab, then by the query (encyclopedia text when loaded), then sorted
35+ const list = useMemo(() => {
36+ let base = searchCatalog('', q ? 'All' : cat, favorites)
37+ if (q) {
38+ const byId = new Map(base.map(d => [d.id, d]))
39+ const hits = entries ? searchEntries(base.map(d => ({ ...d, ...(entries[d.id] || {}), id: d.id, name: d.name })), q) : searchCatalog(q, 'All', favorites)
40+ base = hits.map(h => byId.get(h.id)).filter(Boolean)
41+ return base
42+ }
43+ const withCat = base.map(d => ({ ...d, category: entries?.[d.id]?.category || d.category.toLowerCase() }))
44+ return sortEntries(withCat, sort, favorites).map(d => base.find(x => x.id === d.id))
45+ }, [q, cat, sort, favorites, entries, all]) // eslint-disable-line react-hooks/exhaustive-deps
46+
47+ const detailId = pinned || hover || list[0]?.id || null
48+ const detail = detailId ? entries?.[detailId] : null
49+ const detailDef = detailId ? all.find(d => d.id === detailId) : null
50+
21 const onKey = e => { 51 const onKey = e => {
22− if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return 52+ if (!['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return
23 const items = [...(e.currentTarget.querySelectorAll('[data-lib-item]') || [])] 53 const items = [...(e.currentTarget.querySelectorAll('[data-lib-item]') || [])]
24 if (!items.length) return 54 if (!items.length) return
25− e.preventDefault()
26 const i = items.indexOf(document.activeElement) 55 const i = items.indexOf(document.activeElement)
27− const next = e.key === 'ArrowDown' ? items[Math.min(items.length - 1, i + 1)] : items[Math.max(0, i - 1)] 56+ if (i < 0 && e.key !== 'ArrowDown') return
57+ e.preventDefault()
58+ const cols = Number(getComputedStyle(e.currentTarget.querySelector('.ch-lib-list') || e.currentTarget).getPropertyValue('--cols')) || 1
59+ const step = e.key === 'ArrowDown' ? cols : e.key === 'ArrowUp' ? -cols : e.key === 'ArrowRight' ? 1 : -1
60+ const next = items[Math.max(0, Math.min(items.length - 1, i < 0 ? 0 : i + step))]
28 next?.focus() 61 next?.focus()
29 } 62 }
30 63
64+ const add = id => { onAdd(id); if (isMobile) setPinned(null) }
65+ const oneLiner = d => entries?.[d.id]?.oneLiner || d.description || `${d.category} · ${d.pane === 'main' ? 'overlay' : 'own pane'}`
66+
67+ const mobileDetail = isMobile && pinned && detail
31 return ( 68 return (
32− <Dialog open={open} onClose={onClose} title="Indicators" testId="ch-library" size="lg" initialFocus={inputRef}> 69+ <Dialog open={open} onClose={onClose} title={mobileDetail ? detail.name : 'Indicators'} testId="ch-library" size="xl" initialFocus={inputRef}>
33− <div className="ch-lib"> 70+ {mobileDetail ? (
34− <div className="ch-lib-main" onKeyDown={onKey}> 71+ <div className="ch-lib-mobile-detail">
35− <div className="ch-lib-search"> 72+ <button type="button" className="btn btn-sm btn-ghost" onClick={() => setPinned(null)} data-testid="ch-lib-back">‹ Back to the library</button>
36− <Input ref={inputRef} type="search" placeholder="Search indicators… (name, id, description)" value={q} onChange={e => setQ(e.target.value)} aria-label="Search indicators" data-testid="ch-lib-search" /> 73+ <IndicatorDetail entry={detail} def={detailDef} onAdd={add} counts={counts} />
74+ </div>
75+ ) : (
76+ <div className="ch-lib">
77+ <div className="ch-lib-main" onKeyDown={onKey}>
78+ <div className="ch-lib-search">
79+ <Input ref={inputRef} type="search" placeholder="Search indicators… (name, abbreviation, what it does)" value={q} onChange={e => setQ(e.target.value)} aria-label="Search indicators" data-testid="ch-lib-search" />
80+ <Select value={sort} onChange={e => setSort(e.target.value)} aria-label="Sort indicators" data-testid="ch-lib-sort">{SORTS.map(([id, l]) => <option key={id} value={id}>{l}</option>)}</Select>
81+ </div>
82+ {!q && (
83+ <div className="ch-lib-tabs" role="tablist" aria-label="Categories">
84+ {cats.map(c => <button key={c} type="button" role="tab" aria-selected={cat === c} className="ch-lib-tab" onClick={() => setCat(c)} data-testid={`ch-lib-cat-${c.replace(/\W/g, '').toLowerCase()}`}>{c}{c === 'Favorites' ? ` (${favorites.length})` : ''}</button>)}
85+ </div>
86+ )}
87+ <div className="ch-lib-count muted small" aria-live="polite">{list.length} of {all.length} indicators{q ? ` match “${q}”` : ''}</div>
88+ <ul className="ch-lib-list is-cards" role="list" data-testid="ch-lib-list" onMouseLeave={() => setHover(null)}>
89+ {list.map(d => (
90+ <li key={d.id} className={`ch-card ${detailId === d.id ? 'is-current' : ''}`} data-testid={`ch-lib-card-${d.id}`} onMouseEnter={() => setHover(d.id)}>
91+ <button type="button" className="ch-lib-add ch-card-btn" data-lib-item onClick={() => add(d.id)} onFocus={() => setHover(d.id)} data-testid={`ch-lib-add-${d.id}`} aria-label={`Add ${d.name}`} aria-describedby={`ch-card-desc-${d.id}`}>
92+ <Thumb kind="indicator" id={d.id} size="sm" className="ch-card-thumb" />
93+ <span className="ch-card-body">
94+ <span className="ch-lib-name">{d.name}{counts[d.id] ? <span className="ch-lib-count-badge" aria-label={`${counts[d.id]} active`}>×{counts[d.id]}</span> : null}</span>
95+ <span className="ch-lib-desc" id={`ch-card-desc-${d.id}`}>{oneLiner(d)}</span>
96+ </span>
97+ </button>
98+ <span className="ch-card-side">
99+ <span className="ch-lib-meta">{d.category}</span>
100+ <button type="button" className={`ch-lib-star ${favorites.includes(d.id) ? 'is-on' : ''}`} aria-pressed={favorites.includes(d.id)} aria-label={favorites.includes(d.id) ? `Remove ${d.name} from favourites` : `Add ${d.name} to favourites`} onClick={() => onToggleFavorite(d.id)} data-testid={`ch-lib-fav-${d.id}`}><StarIcon filled={favorites.includes(d.id)} /></button>
101+ <button type="button" className={`ch-lib-star ch-lib-info ${pinned === d.id ? 'is-on' : ''}`} aria-pressed={pinned === d.id} aria-label={`About ${d.name}`} onClick={() => setPinned(p => (p === d.id && !isMobile ? null : d.id))} data-testid={`ch-lib-info-${d.id}`}><InfoIcon /></button>
102+ </span>
103+ </li>
104+ ))}
105+ {!list.length && <li className="ch-lib-empty muted small">No indicator matches “{q}”.</li>}
106+ </ul>
37 </div> 107 </div>
38− {!q && ( 108+ <aside className="ch-lib-side" aria-label="Indicator details and active indicators">
39− <div className="ch-lib-tabs" role="tablist" aria-label="Categories"> 109+ {!isMobile && (
40− {cats.map(c => <button key={c} type="button" role="tab" aria-selected={cat === c} className="ch-lib-tab" onClick={() => setCat(c)} data-testid={`ch-lib-cat-${c.replace(/\W/g, '').toLowerCase()}`}>{c}{c === 'Favorites' ? ` (${favorites.length})` : ''}</button>)} 110+ <div className="ch-lib-detail" data-testid="ch-lib-detail">
111+ {detail ? <IndicatorDetail entry={detail} def={detailDef} onAdd={add} counts={counts} compact /> : <p className="muted small" style={{ margin: 0 }} aria-busy={!entries}>{entries ? 'Hover or focus an indicator to read about it.' : 'Loading the encyclopedia…'}</p>}
112+ </div>
113+ )}
114+ <div className="ch-lib-active">
115+ <div className="ch-lib-active-title">Active ({active.length})</div>
116+ {active.length === 0 && <p className="muted small" style={{ margin: 0 }}>Nothing yet — click an indicator to add it.</p>}
117+ <ul role="list" className="ch-lib-active-list" data-testid="ch-lib-active">
118+ {active.map((ind, i) => (
119+ <li key={ind.id} className={`ch-lib-active-row ${ind.hidden ? 'is-hidden' : ''}`} data-testid="ch-lib-active-row">
120+ <span className="ch-swatch" style={{ background: theme ? seriesColor(theme, ind.colorIndex ?? 0) : undefined }} aria-hidden="true" />
121+ <span className="ch-lib-active-name">{indicatorShortLabel(ind)}</span>
122+ <span className="ch-lib-active-actions">
123+ <button type="button" className="ch-legend-x" aria-label={`Move ${indicatorShortLabel(ind)} up`} disabled={i === 0} onClick={() => onMove(ind.id, -1)}><UpIcon /></button>
124+ <button type="button" className="ch-legend-x" aria-label={`Move ${indicatorShortLabel(ind)} down`} disabled={i === active.length - 1} onClick={() => onMove(ind.id, 1)}><DownIcon /></button>
125+ <button type="button" className="ch-legend-x" aria-pressed={!!ind.hidden} aria-label={ind.hidden ? `Show ${indicatorShortLabel(ind)}` : `Hide ${indicatorShortLabel(ind)}`} onClick={() => onToggleHidden(ind.id)}>{ind.hidden ? <EyeOffIcon /> : <EyeIcon />}</button>
126+ <button type="button" className="ch-legend-x" aria-label={`Settings of ${indicatorShortLabel(ind)}`} onClick={() => onOpenSettings(ind.id)}><GearIcon /></button>
127+ <button type="button" className="ch-legend-x" aria-label={`Remove ${indicatorShortLabel(ind)}`} onClick={() => onRemove(ind.id)}><CloseIcon /></button>
128+ </span>
129+ </li>
130+ ))}
131+ </ul>
41 </div> 132 </div>
42− )} 133+ </aside>
43− <ul className="ch-lib-list" role="list" data-testid="ch-lib-list">
44− {list.map(d => (
45− <li key={d.id} className="ch-lib-item">
46− <button type="button" className="ch-lib-add" data-lib-item onClick={() => onAdd(d.id)} data-testid={`ch-lib-add-${d.id}`} aria-label={`Add ${d.name}`}>
47− <span className="ch-lib-name">{d.name}{counts[d.id] ? <span className="ch-lib-count">×{counts[d.id]}</span> : null}</span>
48− <span className="ch-lib-desc">{d.description || `${d.category} · ${d.pane === 'main' ? 'overlay' : 'own pane'}`}</span>
49− </button>
50− <span className="ch-lib-meta">{d.category}</span>
51− <button type="button" className={`ch-lib-star ${favorites.includes(d.id) ? 'is-on' : ''}`} aria-pressed={favorites.includes(d.id)} aria-label={favorites.includes(d.id) ? `Remove ${d.name} from favourites` : `Add ${d.name} to favourites`} onClick={() => onToggleFavorite(d.id)} data-testid={`ch-lib-fav-${d.id}`}><StarIcon filled={favorites.includes(d.id)} /></button>
52− </li>
53− ))}
54− {!list.length && <li className="ch-lib-empty muted small">No indicator matches “{q}”.</li>}
55− </ul>
56 </div> 134 </div>
57− <aside className="ch-lib-active" aria-label="Active indicators"> 135+ )}
58− <div className="ch-lib-active-title">Active ({active.length})</div>
59− {active.length === 0 && <p className="muted small" style={{ margin: 0 }}>Nothing yet — click an indicator to add it.</p>}
60− <ul role="list" className="ch-lib-active-list" data-testid="ch-lib-active">
61− {active.map((ind, i) => (
62− <li key={ind.id} className={`ch-lib-active-row ${ind.hidden ? 'is-hidden' : ''}`} data-testid="ch-lib-active-row">
63− <span className="ch-swatch" style={{ background: theme ? seriesColor(theme, ind.colorIndex ?? 0) : undefined }} aria-hidden="true" />
64− <span className="ch-lib-active-name">{indicatorShortLabel(ind)}</span>
65− <span className="ch-lib-active-actions">
66− <button type="button" className="ch-legend-x" aria-label={`Move ${indicatorShortLabel(ind)} up`} disabled={i === 0} onClick={() => onMove(ind.id, -1)}><UpIcon /></button>
67− <button type="button" className="ch-legend-x" aria-label={`Move ${indicatorShortLabel(ind)} down`} disabled={i === active.length - 1} onClick={() => onMove(ind.id, 1)}><DownIcon /></button>
68− <button type="button" className="ch-legend-x" aria-pressed={!!ind.hidden} aria-label={ind.hidden ? `Show ${indicatorShortLabel(ind)}` : `Hide ${indicatorShortLabel(ind)}`} onClick={() => onToggleHidden(ind.id)}>{ind.hidden ? <EyeOffIcon /> : <EyeIcon />}</button>
69− <button type="button" className="ch-legend-x" aria-label={`Settings of ${indicatorShortLabel(ind)}`} onClick={() => onOpenSettings(ind.id)}><GearIcon /></button>
70− <button type="button" className="ch-legend-x" aria-label={`Remove ${indicatorShortLabel(ind)}`} onClick={() => onRemove(ind.id)}><CloseIcon /></button>
71− </span>
72− </li>
73− ))}
74− </ul>
75− </aside>
76− </div>
77 </Dialog> 136 </Dialog>
78 ) 137 )
79 } 138 }
modified hfmarketdata/web/src/pages/charts/IndicatorSettings.jsx +13 −4
@@ -1,6 +1,7 @@
1 // Indicator settings dialog (double-click on the legend row or its ⚙): Inputs (form generated from the catalog 1 // Indicator settings dialog (double-click on the legend row or its ⚙): Inputs (form generated from the catalog
2−// `inputs`), Style (colour / width / line style / visibility per plot, levels), Visibility (per timeframe). 2+// `inputs`), Style (colour / width / line style / visibility per plot, levels), Visibility (per timeframe), About
3−// Every change is applied live to the chart through `onChange`; "Reset defaults" restores the catalog values. 3+// (the full encyclopedia sheet, lazy chunk). Every change is applied live to the chart through `onChange`;
4+// "Reset defaults" restores the catalog values.
4 import React, { useEffect, useState } from 'react' 5 import React, { useEffect, useState } from 'react'
5 import Dialog from './Dialog.jsx' 6 import Dialog from './Dialog.jsx'
6 import { Input, Select } from '../../components/Field.jsx' 7 import { Input, Select } from '../../components/Field.jsx'
@@ -8,8 +9,10 @@ import { TIMEFRAMES, TF_LABEL } from '../../charts/data/bars.js'
8 import { defaultParams, indicatorDef, indicatorShortLabel } from './indicators.js' 9 import { defaultParams, indicatorDef, indicatorShortLabel } from './indicators.js'
9 import { DRAW_COLORS, LINE_STYLES, LINE_WIDTHS } from './drawtools.js' 10 import { DRAW_COLORS, LINE_STYLES, LINE_WIDTHS } from './drawtools.js'
10 import { seriesColor } from './theme.js' 11 import { seriesColor } from './theme.js'
12+import { useCatalog } from '../../charts/catalog/ChartFigure.jsx'
13+import IndicatorDetail from './IndicatorDetail.jsx'
11 14
12−const TABS = [['inputs', 'Inputs'], ['style', 'Style'], ['visibility', 'Visibility']] 15+const TABS = [['inputs', 'Inputs'], ['style', 'Style'], ['visibility', 'Visibility'], ['about', 'About']]
13 16
14 function InputField({ inp, value, onChange }) { 17 function InputField({ inp, value, onChange }) {
15 const id = `ch-inp-${inp.name}` 18 const id = `ch-inp-${inp.name}`
@@ -32,6 +35,7 @@ function InputField({ inp, value, onChange }) {
32 export default function IndicatorSettings({ open, ind, plotColors, theme, onChange, onClose, onRemove }) { 35 export default function IndicatorSettings({ open, ind, plotColors, theme, onChange, onClose, onRemove }) {
33 const [tab, setTab] = useState('inputs') 36 const [tab, setTab] = useState('inputs')
34 useEffect(() => { if (open) setTab('inputs') }, [open, ind?.id]) 37 useEffect(() => { if (open) setTab('inputs') }, [open, ind?.id])
38+ const entries = useCatalog(open && tab === 'about' ? 'indicator' : null)
35 const def = ind ? indicatorDef(ind.type) : null 39 const def = ind ? indicatorDef(ind.type) : null
36 if (!open || !ind || !def) return null 40 if (!open || !ind || !def) return null
37 const set = patch => onChange(ind.id, patch) 41 const set = patch => onChange(ind.id, patch)
@@ -58,7 +62,12 @@ export default function IndicatorSettings({ open, ind, plotColors, theme, onChan
58 <div className="ch-set-form" role="tabpanel" data-testid="ch-ind-inputs"> 62 <div className="ch-set-form" role="tabpanel" data-testid="ch-ind-inputs">
59 {def.inputs.map(inp => <InputField key={inp.name} inp={inp} value={ind.params?.[inp.name]} onChange={v => setParam(inp.name, v)} />)} 63 {def.inputs.map(inp => <InputField key={inp.name} inp={inp} value={ind.params?.[inp.name]} onChange={v => setParam(inp.name, v)} />)}
60 {!def.inputs.length && <p className="muted small" style={{ margin: 0 }}>This indicator has no inputs.</p>} 64 {!def.inputs.length && <p className="muted small" style={{ margin: 0 }}>This indicator has no inputs.</p>}
61− {def.description && <p className="muted small ch-set-desc">{def.description}</p>} 65+ {def.description && <p className="muted small ch-set-desc">{def.description} <button type="button" className="ch-link-btn" onClick={() => setTab('about')} data-testid="ch-ind-about-link">Why these defaults?</button></p>}
66+ </div>
67+ )}
68+ {tab === 'about' && (
69+ <div className="ch-set-form" role="tabpanel" data-testid="ch-ind-about">
70+ {entries?.[ind.type] ? <IndicatorDetail entry={entries[ind.type]} def={def} /> : <p className="muted small" aria-busy="true" style={{ margin: 0 }}>{entries ? 'No encyclopedia entry for this indicator.' : 'Loading…'}</p>}
62 </div> 71 </div>
63 )} 72 )}
64 {tab === 'style' && ( 73 {tab === 'style' && (
modified hfmarketdata/web/src/pages/charts/Legend.jsx +23 −2
@@ -9,6 +9,21 @@ import { formatPrice, formatStampLabel, formatVolume } from '../../charts/data/s
9 import { indicatorDef, indicatorShortLabel } from './indicators.js' 9 import { indicatorDef, indicatorShortLabel } from './indicators.js'
10 import { EyeIcon, EyeOffIcon, GearIcon } from './icons.jsx' 10 import { EyeIcon, EyeOffIcon, GearIcon } from './icons.jsx'
11 import { seriesColor } from './theme.js' 11 import { seriesColor } from './theme.js'
12+import { loadIndicators } from '../../charts/catalog/index.js'
13+import { interpret } from '../../charts/catalog/interpret.js'
14+
15+/** Hover tooltip of a legend row: full name, one-liner from the encyclopedia and the current values explained. */
16+function IndicatorTip({ ind, def, entry, vals, bar }) {
17+ const lines = interpret(def, vals, bar)
18+ return (
19+ <div className="ch-legend-tip" role="tooltip" data-testid="ch-legend-tip">
20+ <strong>{entry?.name || def?.name || ind.type}</strong>
21+ {entry?.oneLiner && <p>{entry.oneLiner}</p>}
22+ {lines.length > 0 && <ul>{lines.map((l, i) => <li key={i}>{l}</li>)}</ul>}
23+ <span className="muted">Double-click for settings</span>
24+ </div>
25+ )
26+}
12 27
13 const fmtVal = (v, d) => (v == null || !Number.isFinite(v) ? '—' : formatPrice(v, typeof d === 'number' ? Math.min(d, 4) : 'auto')) 28 const fmtVal = (v, d) => (v == null || !Number.isFinite(v) ? '—' : formatPrice(v, typeof d === 'number' ? Math.min(d, 4) : 'auto'))
14 29
@@ -21,6 +36,11 @@ export default function Legend({ cs, name, hover, lastBar, prevBar, indValues, c
21 const dir = delta == null ? 'neutral' : delta >= 0 ? 'up' : 'down' 36 const dir = delta == null ? 'neutral' : delta >= 0 ? 'up' : 'down'
22 const stamp = bar ? formatStampLabel(bar.t, cs.tf, cs.tf === '1day' ? '' : tz) : '' 37 const stamp = bar ? formatStampLabel(bar.t, cs.tf, cs.tf === '1day' ? '' : tz) : ''
23 38
39+ // encyclopedia tooltip on the indicator name (chunk loaded on first hover only)
40+ const [tip, setTip] = useState(null) // indicator id
41+ const [entries, setEntries] = useState(null)
42+ const showTip = id => { setTip(id); if (!entries) loadIndicators().then(setEntries, () => setEntries({})) }
43+
24 // throttled live text (≤ 1 update / 600 ms) 44 // throttled live text (≤ 1 update / 600 ms)
25 const [live, setLive] = useState('') 45 const [live, setLive] = useState('')
26 const liveRef = useRef({ t: 0, timer: 0 }) 46 const liveRef = useRef({ t: 0, timer: 0 })
@@ -64,9 +84,10 @@ export default function Legend({ cs, name, hover, lastBar, prevBar, indValues, c
64 const keys = Object.keys(vals).length ? Object.keys(vals) : (def?.outputs || ['value']) 84 const keys = Object.keys(vals).length ? Object.keys(vals) : (def?.outputs || ['value'])
65 const label = indicatorShortLabel(ind) 85 const label = indicatorShortLabel(ind)
66 return ( 86 return (
67− <div key={ind.id} className={`ch-legend-row ${ind.hidden ? 'is-hidden' : ''}`} data-testid="ch-legend-ind" onDoubleClick={() => onOpenSettings(ind.id)}> 87+ <div key={ind.id} className={`ch-legend-row ${ind.hidden ? 'is-hidden' : ''} ${tip === ind.id ? 'is-tip' : ''}`} data-testid="ch-legend-ind" onDoubleClick={() => onOpenSettings(ind.id)} onMouseLeave={() => setTip(t => (t === ind.id ? null : t))}>
68 <span className="ch-swatch" style={{ background: color }} aria-hidden="true" /> 88 <span className="ch-swatch" style={{ background: color }} aria-hidden="true" />
69− <button type="button" className="ch-legend-btn" onClick={() => onOpenSettings(ind.id)} title="Settings (double-click)">{label}</button> 89+ <button type="button" className="ch-legend-btn" onClick={() => onOpenSettings(ind.id)} onMouseEnter={() => showTip(ind.id)} onFocus={() => showTip(ind.id)} onBlur={() => setTip(t => (t === ind.id ? null : t))} aria-describedby={tip === ind.id ? `ch-legend-tip-${ind.id}` : undefined}>{label}</button>
90+ {tip === ind.id && !compact && <div id={`ch-legend-tip-${ind.id}`} className="ch-legend-tip-wrap"><IndicatorTip ind={ind} def={def} entry={entries?.[ind.type]} vals={vals} bar={bar} /></div>}
70 {!ind.hidden && ( 91 {!ind.hidden && (
71 <span className="ch-legend-vals mono"> 92 <span className="ch-legend-vals mono">
72 {keys.map(k => <span key={k} style={{ color: ind.plots?.[k]?.color || colors[k] || undefined }}>{keys.length > 1 && <i>{k} </i>}{fmtVal(vals[k], decimals)}</span>)} 93 {keys.map(k => <span key={k} style={{ color: ind.plots?.[k]?.color || colors[k] || undefined }}>{keys.length > 1 && <i>{k} </i>}{fmtVal(vals[k], decimals)}</span>)}
modified hfmarketdata/web/src/pages/charts/charts.css +100 −0
@@ -323,3 +323,103 @@ button.ch-legend-btn:hover, .ch-legend-btn[aria-expanded="true"] { background: v
323 .ch-layout-2h .ch-grid, .ch-layout-4 .ch-grid { grid-template-columns: 1fr; grid-template-rows: none; grid-auto-rows: 1fr; } 323 .ch-layout-2h .ch-grid, .ch-layout-4 .ch-grid { grid-template-columns: 1fr; grid-template-rows: none; grid-auto-rows: 1fr; }
324 .ch-flyout { display: none; } 324 .ch-flyout { display: none; }
325 } 325 }
326+
327+/* ---- encyclopedia: library cards, detail panel, tooltips, intro strip (thumbnails rendered by our engine) -------- */
328+.ch-dialog.is-xl { width: min(1180px, calc(100vw - 32px)); max-height: min(86vh, 860px); }
329+.ch-thumb { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; border-radius: var(--r-sm); border: 1px solid var(--line); background: var(--bg-1); object-fit: cover; }
330+.ch-thumb-missing { display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: var(--r-sm); border: 1px dashed var(--line-2); background: var(--bg-1); }
331+.ch-lib-search { display: flex; gap: 8px; align-items: center; }
332+.ch-lib-search .select { width: 120px; flex: none; }
333+.ch-lib-count { margin: 6px 0 2px; }
334+.ch-lib-list.is-cards { --cols: 2; display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 8px; align-content: start; max-height: none; padding: 2px; }
335+.ch-card { position: relative; display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--bg-1); overflow: hidden; }
336+.ch-card:hover, .ch-card.is-current { border-color: var(--line-3); background: var(--bg-2); }
337+.ch-card.is-current { box-shadow: inset 0 0 0 1px var(--accent-soft); }
338+.ch-card-btn { flex-direction: column; align-items: stretch; gap: 8px; padding: 8px 8px 30px; width: 100%; }
339+.ch-card-thumb { border-radius: 4px; }
340+.ch-card-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
341+.ch-card .ch-lib-name { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; line-height: 1.25; }
342+.ch-card .ch-lib-desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.35; }
343+.ch-lib-count-badge { padding: 0 6px; border-radius: 999px; background: var(--accent-soft); color: var(--accent-strong); font-size: 10px; font-weight: 600; line-height: 16px; }
344+[data-theme="light"] .ch-lib-count-badge { color: var(--accent); }
345+.ch-card-side { position: absolute; left: 8px; right: 4px; bottom: 2px; display: flex; align-items: center; gap: 2px; pointer-events: none; }
346+.ch-card-side > * { pointer-events: auto; }
347+.ch-card-side .ch-lib-meta { flex: 1; }
348+.ch-card-side .ch-lib-star { width: 28px; height: 28px; }
349+.ch-card-side .ch-lib-star svg { width: 15px; height: 15px; }
350+.ch-lib-side { display: flex; flex-direction: column; gap: 12px; min-width: 0; border-left: 1px solid var(--line); padding-left: 16px; max-height: calc(min(86vh, 860px) - 110px); }
351+.ch-lib-side .ch-lib-active { border-left: 0; padding-left: 0; border-top: 1px solid var(--line); padding-top: 10px; flex: none; max-height: 34%; overflow: auto; }
352+.ch-lib-detail { flex: 1; min-height: 0; overflow: auto; padding-right: 4px; scrollbar-width: thin; }
353+.ch-lib { grid-template-columns: minmax(0, 1fr) 340px; }
354+.ch-lib-main .ch-lib-list.is-cards { max-height: calc(min(86vh, 860px) - 210px); overflow: auto; }
355+.ch-lib-mobile-detail { display: grid; gap: 10px; }
356+.ch-link-btn { border: 0; background: transparent; color: var(--accent); font: inherit; font-size: inherit; padding: 0; cursor: pointer; text-decoration: underline; }
357+
358+/* detail sheet */
359+.ch-detail { display: grid; gap: 8px; font-size: var(--fs-1); color: var(--fg-1); }
360+.ch-detail h4 { margin: 10px 0 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); }
361+.ch-detail-title { margin: 0; font-size: var(--fs-3); color: var(--fg); }
362+.ch-detail-head { display: grid; gap: 6px; }
363+.ch-detail-tags { display: flex; flex-wrap: wrap; gap: 4px; }
364+.ch-tag { padding: 0 6px; border-radius: 4px; background: var(--bg-2); color: var(--fg-2); font-size: 11px; line-height: 18px; }
365+.ch-tag.is-on { background: var(--accent-soft); color: var(--accent-strong); }
366+[data-theme="light"] .ch-tag.is-on { color: var(--accent); }
367+.ch-detail-lead { margin: 0; font-weight: 600; color: var(--fg); }
368+.ch-detail-text { margin: 0; line-height: 1.45; }
369+.ch-detail-text.small { font-size: var(--fs-0); color: var(--fg-2); }
370+.ch-detail-actions { display: flex; flex-wrap: wrap; gap: 6px; }
371+.ch-detail-formula { margin: 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--bg); font-size: 12px; line-height: 1.5; white-space: pre-wrap; overflow: auto; color: var(--fg); }
372+.ch-detail-vars, .ch-detail-defaults { margin: 0; display: grid; grid-template-columns: max-content 1fr; gap: 3px 10px; font-size: var(--fs-0); }
373+.ch-detail-vars dt { color: var(--fg); }
374+.ch-detail-vars dd, .ch-detail-defaults dd { margin: 0; color: var(--fg-2); line-height: 1.4; }
375+.ch-detail-defaults { grid-template-columns: 1fr; gap: 2px; }
376+.ch-detail-defaults dt { color: var(--fg); margin-top: 4px; }
377+.ch-detail-defaults dt b { margin-left: 6px; color: var(--accent-strong); }
378+[data-theme="light"] .ch-detail-defaults dt b { color: var(--accent); }
379+.ch-detail-list { margin: 0; padding-left: 16px; display: grid; gap: 4px; line-height: 1.4; }
380+.ch-detail-chips { display: flex; flex-wrap: wrap; gap: 6px; }
381+.ch-detail-chips .ch-chip { cursor: pointer; }
382+.ch-detail-chips .ch-chip:hover:not([disabled]) { border-color: var(--line-3); }
383+.ch-detail-chips .ch-chip[disabled] { cursor: default; }
384+.ch-detail.is-compact .ch-detail-thumb { max-height: 190px; }
385+
386+/* tool tooltips (flyouts) + sheet rows */
387+.ch-flyout-tip { position: absolute; left: calc(100% + 6px); top: 0; width: 300px; z-index: 1; }
388+.ch-tooltip { display: grid; gap: 6px; padding: 10px; background: var(--bg-1); border: 1px solid var(--line-2); border-radius: var(--r-md); box-shadow: var(--shadow-2); font-size: var(--fs-0); color: var(--fg-1); }
389+.ch-tooltip-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--fg); font-size: var(--fs-1); }
390+.ch-tooltip-head kbd { font-size: 10px; color: var(--fg-3); padding: 0 5px; border: 1px solid var(--line-2); border-radius: 4px; }
391+.ch-tooltip-lead { margin: 0; color: var(--fg-1); line-height: 1.4; }
392+.ch-tooltip-steps { margin: 0; padding-left: 18px; display: grid; gap: 2px; color: var(--fg-2); line-height: 1.35; }
393+.ch-tooltip-link { color: var(--accent); font-size: var(--fs-0); }
394+.ch-sheet-tool { min-height: 64px; padding: 6px 12px 6px 6px; }
395+.ch-sheet-thumb { width: 96px; flex: none; }
396+.ch-sheet-tool-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
397+.ch-sheet-tool-desc { font-size: var(--fs-0); font-weight: 400; color: var(--fg-2); line-height: 1.3; }
398+
399+/* legend tooltip */
400+.ch-legend-row.is-tip { z-index: 8; }
401+.ch-legend-tip-wrap { position: absolute; top: 100%; left: 18px; z-index: 9; padding-top: 4px; }
402+.ch-legend-tip { width: 300px; padding: 8px 10px; display: grid; gap: 4px; background: var(--bg-1); border: 1px solid var(--line-2); border-radius: var(--r-md); box-shadow: var(--shadow-2); font-size: var(--fs-0); color: var(--fg-1); font-family: var(--sans); }
403+.ch-legend-tip strong { color: var(--fg); }
404+.ch-legend-tip p { margin: 0; line-height: 1.4; }
405+.ch-legend-tip ul { margin: 0; padding-left: 16px; display: grid; gap: 2px; color: var(--fg); }
406+.ch-legend-tip .muted { font-size: 10px; }
407+
408+/* first-visit strip */
409+.ch-intro { position: absolute; left: 50%; bottom: 40px; transform: translateX(-50%); z-index: 3; display: flex; align-items: center; gap: 14px; max-width: calc(100% - 24px); padding: 8px 12px 8px 14px; background: color-mix(in srgb, var(--bg-1) 92%, transparent); border: 1px solid var(--line-2); border-radius: var(--r-lg, 12px); box-shadow: var(--shadow-2); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
410+.ch-intro-text { display: grid; gap: 1px; font-size: var(--fs-1); color: var(--fg); white-space: nowrap; }
411+.ch-intro-text .muted { font-size: var(--fs-0); }
412+.ch-intro-thumbs { display: flex; gap: 8px; }
413+.ch-intro-thumb { display: grid; gap: 3px; width: 112px; padding: 3px; border: 1px solid transparent; border-radius: var(--r-sm); background: transparent; color: var(--fg-2); font: inherit; font-size: 11px; cursor: pointer; text-align: left; }
414+.ch-intro-thumb:hover, .ch-intro-thumb:focus-visible { border-color: var(--line-3); color: var(--fg); background: var(--bg-2); }
415+.ch-intro-thumb span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
416+.ch-intro-link { font-size: var(--fs-0); color: var(--accent); white-space: nowrap; }
417+.ch-intro-close { flex: none; }
418+@media (max-width: 1100px) { .ch-lib { grid-template-columns: minmax(0, 1fr) 300px; } .ch-lib-list.is-cards { --cols: 1; } }
419+@media (max-width: 960px) { .ch-intro-text .muted, .ch-intro-link { display: none; } .ch-intro-thumb { width: 84px; } .ch-intro-thumb span { display: none; } }
420+@media (max-width: 760px) {
421+ .ch-lib-side { border-left: 0; padding-left: 0; max-height: none; }
422+ .ch-lib-main .ch-lib-list.is-cards { --cols: 1; max-height: none; overflow: visible; }
423+ .ch-flyout-tip { display: none; }
424+}
425+@media (max-width: 640px) { .ch-intro { bottom: 8px; gap: 8px; padding: 6px 8px; } .ch-intro-text { font-size: var(--fs-0); white-space: normal; } }
modified hfmarketdata/web/src/pages/charts/drawtools.js +8 −7
@@ -1,5 +1,5 @@
1 // Drawing tools of the /charts page, grouped like a professional terminal's flyout bar. Every tool id is the engine's 1 // Drawing tools of the /charts page, grouped like a professional terminal's flyout bar. Every tool id is the engine's
2−// `setDrawingTool(id)` value; tools the loaded engine does not list (`DRAWING_TOOLS` of engine v1) are hidden, so 2+// `setDrawingTool(id)` value (42 tools with the v2 engine; the encyclopedia in src/charts/catalog/tools.js describes each one); tools the loaded engine does not list (`DRAWING_TOOLS` of engine v1) are hidden, so
3 // the bar works with the v1 engine today and shows the full arsenal with the v2 engine (additive contract). 3 // the bar works with the v1 engine today and shows the full arsenal with the v2 engine (additive contract).
4 import { DRAWING_TOOLS } from '../../charts/engine/index.js' 4 import { DRAWING_TOOLS } from '../../charts/engine/index.js'
5 5
@@ -13,10 +13,10 @@ export const GROUPS = [
13 { id: 'vline', label: 'Vertical line', key: 'V' }, 13 { id: 'vline', label: 'Vertical line', key: 'V' },
14 { id: 'cross', label: 'Cross line', key: 'C' }, 14 { id: 'cross', label: 'Cross line', key: 'C' },
15 { id: 'channel', label: 'Parallel channel' }, 15 { id: 'channel', label: 'Parallel channel' },
16− { id: 'channel-parallel', label: 'Parallel channel (3 points)' },
17 { id: 'regression', label: 'Regression trend' }, 16 { id: 'regression', label: 'Regression trend' },
18 { id: 'pitchfork', label: 'Andrews pitchfork', key: 'P' }, 17 { id: 'pitchfork', label: 'Andrews pitchfork', key: 'P' },
19 { id: 'schiff', label: 'Schiff pitchfork' }, 18 { id: 'schiff', label: 'Schiff pitchfork' },
19+ { id: 'mschiff', label: 'Modified Schiff pitchfork' },
20 ] }, 20 ] },
21 { id: 'fib', label: 'Fibonacci & Gann', tools: [ 21 { id: 'fib', label: 'Fibonacci & Gann', tools: [
22 { id: 'fib', label: 'Fibonacci retracement', key: 'F' }, 22 { id: 'fib', label: 'Fibonacci retracement', key: 'F' },
@@ -31,10 +31,11 @@ export const GROUPS = [
31 { id: 'rect', label: 'Rectangle', key: 'R' }, 31 { id: 'rect', label: 'Rectangle', key: 'R' },
32 { id: 'ellipse', label: 'Ellipse', key: 'E' }, 32 { id: 'ellipse', label: 'Ellipse', key: 'E' },
33 { id: 'triangle', label: 'Triangle' }, 33 { id: 'triangle', label: 'Triangle' },
34− { id: 'polyline', label: 'Polyline' }, 34+ { id: 'path', label: 'Path (polyline)' },
35 { id: 'brush', label: 'Brush', key: 'B' }, 35 { id: 'brush', label: 'Brush', key: 'B' },
36 { id: 'arrow', label: 'Arrow', key: 'A' }, 36 { id: 'arrow', label: 'Arrow', key: 'A' },
37− { id: 'arrow-marker', label: 'Arrow marker' }, 37+ { id: 'arrow-up', label: 'Arrow up marker' },
38+ { id: 'arrow-down', label: 'Arrow down marker' },
38 ] }, 39 ] },
39 { id: 'text', label: 'Text & notes', tools: [ 40 { id: 'text', label: 'Text & notes', tools: [
40 { id: 'text', label: 'Text', key: 'X' }, 41 { id: 'text', label: 'Text', key: 'X' },
@@ -47,9 +48,9 @@ export const GROUPS = [
47 { id: 'price-range', label: 'Price range' }, 48 { id: 'price-range', label: 'Price range' },
48 { id: 'date-range', label: 'Date range' }, 49 { id: 'date-range', label: 'Date range' },
49 { id: 'date-price-range', label: 'Date & price range' }, 50 { id: 'date-price-range', label: 'Date & price range' },
50− { id: 'vertical-range', label: 'Vertical range' }, 51+ { id: 'vrange', label: 'Vertical range' },
51− { id: 'long-position', label: 'Long position', key: 'L' }, 52+ { id: 'long', label: 'Long position', key: 'L' },
52− { id: 'short-position', label: 'Short position', key: 'S' }, 53+ { id: 'short', label: 'Short position', key: 'S' },
53 ] }, 54 ] },
54 { id: 'patterns', label: 'Patterns', tools: [ 55 { id: 'patterns', label: 'Patterns', tools: [
55 { id: 'elliott-impulse', label: 'Elliott impulse wave (12345)' }, 56 { id: 'elliott-impulse', label: 'Elliott impulse wave (12345)' },
modified hfmarketdata/web/src/pages/charts/icons.jsx +4 −3
@@ -33,6 +33,7 @@ export const PolylineIcon = p => <I {...p}><path d="M3 15 7 6l4 7 3-9 3 8" /><Do
33 export const BrushIcon = p => <I {...p}><path d="M3 16c3 0 3-3 5-4s3 1 5-1 3-5 4-7" /></I> 33 export const BrushIcon = p => <I {...p}><path d="M3 16c3 0 3-3 5-4s3 1 5-1 3-5 4-7" /></I>
34 export const ArrowIcon = p => <I {...p}><path d="M4 16 16 4M9 4h7v7" /></I> 34 export const ArrowIcon = p => <I {...p}><path d="M4 16 16 4M9 4h7v7" /></I>
35 export const ArrowMarkerIcon = p => <I {...p}><path d="M10 3v12M5 10l5 5 5-5" /></I> 35 export const ArrowMarkerIcon = p => <I {...p}><path d="M10 3v12M5 10l5 5 5-5" /></I>
36+export const ArrowUpMarkerIcon = p => <I {...p}><path d="M10 17V5M5 10l5-5 5 5" /></I>
36 export const TextIcon = p => <I {...p}><path d="M4 5h12M10 5v11M7 16h6" /></I> 37 export const TextIcon = p => <I {...p}><path d="M4 5h12M10 5v11M7 16h6" /></I>
37 export const CalloutIcon = p => <I {...p}><path d="M3 4h14v8h-7l-3 4v-4H3z" /></I> 38 export const CalloutIcon = p => <I {...p}><path d="M3 4h14v8h-7l-3 4v-4H3z" /></I>
38 export const PriceLabelIcon = p => <I {...p}><path d="M3 7h9l4 3-4 3H3z" /><path d="M6 10h4" /></I> 39 export const PriceLabelIcon = p => <I {...p}><path d="M3 7h9l4 3-4 3H3z" /><path d="M6 10h4" /></I>
@@ -103,10 +104,10 @@ export const SERIES_ICONS = { candles: CandlesIcon, hollow: HollowIcon, ohlc: Oh
103 export const LAYOUT_ICONS = { '1': Layout1Icon, '2h': Layout2hIcon, '2v': Layout2vIcon, '4': Layout4Icon } 104 export const LAYOUT_ICONS = { '1': Layout1Icon, '2h': Layout2hIcon, '2v': Layout2vIcon, '4': Layout4Icon }
104 export const TOOL_ICONS = { 105 export const TOOL_ICONS = {
105 trendline: TrendlineIcon, ray: RayIcon, extended: ExtendedIcon, hray: HRayIcon, hline: HLineIcon, vline: VLineIcon, cross: CrossIcon, 106 trendline: TrendlineIcon, ray: RayIcon, extended: ExtendedIcon, hray: HRayIcon, hline: HLineIcon, vline: VLineIcon, cross: CrossIcon,
106− channel: ChannelIcon, 'channel-parallel': Channel3Icon, regression: RegressionIcon, pitchfork: PitchforkIcon, schiff: SchiffIcon, 107+ channel: Channel3Icon, regression: RegressionIcon, pitchfork: PitchforkIcon, schiff: SchiffIcon, mschiff: SchiffIcon,
107 fib: FibIcon, 'fib-extension': FibExtIcon, 'fib-timezones': FibTimeIcon, 'fib-fan': FibFanIcon, 'fib-arcs': FibArcsIcon, 'gann-fan': GannFanIcon, 'gann-box': GannBoxIcon, 108 fib: FibIcon, 'fib-extension': FibExtIcon, 'fib-timezones': FibTimeIcon, 'fib-fan': FibFanIcon, 'fib-arcs': FibArcsIcon, 'gann-fan': GannFanIcon, 'gann-box': GannBoxIcon,
108− rect: RectIcon, ellipse: EllipseIcon, triangle: TriangleIcon, polyline: PolylineIcon, brush: BrushIcon, arrow: ArrowIcon, 'arrow-marker': ArrowMarkerIcon, 109+ rect: RectIcon, ellipse: EllipseIcon, triangle: TriangleIcon, path: PolylineIcon, brush: BrushIcon, arrow: ArrowIcon, 'arrow-up': ArrowUpMarkerIcon, 'arrow-down': ArrowMarkerIcon,
109 text: TextIcon, callout: CalloutIcon, 'price-label': PriceLabelIcon, flag: FlagIcon, 110 text: TextIcon, callout: CalloutIcon, 'price-label': PriceLabelIcon, flag: FlagIcon,
110− measure: MeasureIcon, 'price-range': PriceRangeIcon, 'date-range': DateRangeIcon, 'date-price-range': DatePriceRangeIcon, 'vertical-range': VerticalRangeIcon, 'long-position': LongIcon, 'short-position': ShortIcon, 111+ measure: MeasureIcon, 'price-range': PriceRangeIcon, 'date-range': DateRangeIcon, 'date-price-range': DatePriceRangeIcon, vrange: VerticalRangeIcon, long: LongIcon, short: ShortIcon,
111 'elliott-impulse': ElliottIcon, 'elliott-correction': ElliottCorrIcon, xabcd: XabcdIcon, 'head-shoulders': HeadShouldersIcon, 112 'elliott-impulse': ElliottIcon, 'elliott-correction': ElliottCorrIcon, xabcd: XabcdIcon, 'head-shoulders': HeadShouldersIcon,
112 } 113 }
113 114
Menu

Theme