// Theme helper — dark/light colors, buy/sell, utility formatters function makeTheme(dark, buyColor) { const buy = buyColor === 'blue' ? '#2F80ED' : '#16C784'; const sell = '#EA3943'; return { dark, bg: dark ? '#0B0D10' : '#F2F2F7', bgElevated: dark ? '#14171C' : '#FFFFFF', bgCard: dark ? '#1A1D22' : '#FFFFFF', bgSubtle: dark ? '#0F1114' : '#EDEDF0', text: dark ? '#F5F6F8' : '#0B0D10', textSec: dark ? 'rgba(235,235,245,0.58)' : 'rgba(60,60,67,0.62)', textTer: dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)', border: dark ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.08)', borderStrong: dark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)', buy, sell, buyBg: dark ? 'rgba(22,199,132,0.14)' : 'rgba(22,199,132,0.12)', sellBg: dark ? 'rgba(234,57,67,0.14)' : 'rgba(234,57,67,0.10)', accent: dark ? '#5AC8FA' : '#007AFF', tabBarBg: dark ? 'rgba(20,23,28,0.85)' : 'rgba(255,255,255,0.85)', }; } // ——— numeric runs are bidi-isolated (docs/i18n.md ID5) ——— // // Each formatter below produces a run that must read left-to-right and keep // its own sign, separators and symbol attached, whatever direction the text // around it flows. In an RTL paragraph a leading "+"/"-" is a NEUTRAL at a run // boundary and resolves to the PARAGRAPH direction — "-12.50" renders as // "12.50-", which is a different number to the reader, not a cosmetic // difference. "$" and "%" are European Terminators and drift the same way. // // bidiIsolate() (i18n.jsx) wraps the run in U+2068…U+2069 under RTL and is the // identity function otherwise, so the four LTR locales render byte-identical // output to F-i18n-1. It is reached through `window` because theme.jsx loads // BEFORE i18n.jsx in both shells; by the time any of these runs at render time // the export is in place, and the fallback keeps a formatter from throwing if // that load order ever changes. // // splitPrice/splitPip return the PARTS of one price for the fractional-pip // visual, which the caller renders as adjacent elements. Neither isolates its // parts, and neither may: isolating each part separately would let the pieces // reorder against each other. The isolate belongs on the element wrapping all // of them, and the call sites carry `unicodeBidi: 'isolate'`. // // The corollary is a trap worth naming, because splitPrice fell into it: a // splitter must compute from the RAW formatted number, never from fmtPrice(). // fmtPrice returns an ISOLATED run under RTL, so slicing its output counts the // invisible U+2069 as a digit (shifting the big-pip emphasis one place) and // tears the pair apart — leaving U+2068 in `big` and U+2069 in `small`, an // unmatched initiator whose scope then runs to the end of the paragraph. // splitPip was always right about this; splitPrice now matches it. function iso(s) { return window.bidiIsolate ? window.bidiIsolate(s) : s; } function fmtPrice(n, digits) { return iso(n.toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits })); } function fmtPct(n) { const s = n >= 0 ? '+' : ''; return iso(s + n.toFixed(2) + '%'); } function fmtMoney(n, dp = 2) { const s = n >= 0 ? '+' : '-'; return iso(s + '$' + Math.abs(n).toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp })); } // Split a price into big/small parts for the "fractional pip" visual function splitPrice(n, digits) { // Raw, NOT fmtPrice() — see the trap in the header comment above. const str = n.toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits }); // big part = integer + first (digits-1) decimals, small = last decimal if (digits === 0) return { big: str, small: '' }; const [intPart, decPart] = str.split('.'); const smallN = Math.max(1, Math.min(2, digits)); // last 1-2 digits are "big pip" emphasized const emphasize = decPart.slice(-smallN); const rest = decPart.slice(0, decPart.length - smallN); return { big: intPart + (rest ? '.' + rest : '.'), small: emphasize }; } // 3-part price split driven by pipDigits (ui_pip_size) vs digits (tick_size): // base price · significant pips (2 digits) · fractional pip (sub-pip ticks) // e.g. EURUSD 1.07833 → { base: "1.07", pips: "83", frac: "3" } function splitPip(value, digits, pipDigits) { if (pipDigits == null) pipDigits = digits; const fixed = value.toFixed(digits); const dot = fixed.indexOf('.'); const intRaw = dot === -1 ? fixed : fixed.slice(0, dot); const dec = dot === -1 ? '' : fixed.slice(dot + 1); const intp = Number(intRaw).toLocaleString('en-US'); // thousands separators const fracCount = Math.max(0, digits - pipDigits); // sub-pip (tick) digits const pipBlockEnd = dec.length - fracCount; const frac = fracCount > 0 ? dec.slice(pipBlockEnd) : ''; const sigCount = Math.min(2, pipBlockEnd); // up to 2 significant pip digits const pips = dec.slice(pipBlockEnd - sigCount, pipBlockEnd); const baseDec = dec.slice(0, pipBlockEnd - sigCount); const base = intp + (dec.length ? '.' : '') + baseDec; return { base, pips, frac }; } Object.assign(window, { makeTheme, fmtPrice, fmtPct, fmtMoney, splitPrice, splitPip });