/* ═══════════════════════════════════════════════════════════════════
MARKET CLOCK · Shadow Terminal premium world clock widget
Features:
- 7 financial hubs with live session danger coloring
- Analog/Digital toggle (persisted to localStorage)
- Auto day/night face per city: white face = daytime, dark = night
- Session color glow ring on each clock bezel
- Dubai (owner's local) always accented in blue
Usage:
═══════════════════════════════════════════════════════════════════ */
/* ── Inject keyframes once ─────────────────────────────────────────── */
(function injectClockCSS() {
if (document.getElementById('st-clock-css')) return;
const s = document.createElement('style');
s.id = 'st-clock-css';
s.textContent = `
@keyframes clock-digit-tick {
0% { opacity: 0.5; transform: translateY(-2px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes session-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.68; }
}
@keyframes clock-fadein {
0% { opacity: 0; transform: translateY(6px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes glow-pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.clock-digit-tick { animation: clock-digit-tick 0.15s ease; }
.session-pulse { animation: session-pulse 2s ease-in-out infinite; }
.clock-card-enter { animation: clock-fadein 0.45s cubic-bezier(.2,.7,.3,1) both; }
.st-clock-toggle {
display: flex; gap: 2px; padding: 2px;
background: #0B0D14;
border: 1px solid #1E2130; border-radius: 5px;
cursor: pointer; flex-shrink: 0;
}
.st-clock-toggle-btn {
padding: 3px 7px;
font-family: 'JetBrains Mono', monospace;
font-size: 9px; font-weight: 600; letter-spacing: 0.1em;
border: none; border-radius: 3px; cursor: pointer;
transition: background 0.18s, color 0.18s, border-color 0.18s;
}
.st-clocks-row::-webkit-scrollbar { height: 3px; }
.st-clocks-row::-webkit-scrollbar-track { background: transparent; }
.st-clocks-row::-webkit-scrollbar-thumb { background: #1E2130; border-radius: 2px; }
`;
document.head.appendChild(s);
})();
/* ── Design tokens ────────────────────────────────────────────────── */
const CK = {
bg: '#0B0D14',
panel: '#13161F',
panel2: '#181C28',
border: '#1E2130',
ink: '#ECEFF6',
muted: '#8B93A6',
muted2: '#5A6276',
amber: '#F7A600',
green: '#2EE49B',
red: '#FF5C77',
blue: '#6EA8FF',
gray: '#6B7280',
};
/* ── Session logic ────────────────────────────────────────────────── */
function getSessionForHour(utcHour) {
const h = ((utcHour % 24) + 24) % 24;
if (h >= 13 && h < 17) return {
key: 'OVERLAP', name: 'EU/US OVERLAP — DANGER ZONE', color: CK.red,
threshold: '75%', cycleProb: '0.20',
rec: 'Shadow threshold raised to 75% · Bot 2 conservative',
dangerLevel: 'EXTREME',
};
if (h >= 17 && h < 22) return {
key: 'US_SESSION', name: 'US SESSION', color: CK.amber,
threshold: '60%', cycleProb: '0.30',
rec: 'Shadow threshold 60% · Bot 2 moderate',
dangerLevel: 'HIGH',
};
if (h >= 7 && h < 13) return {
key: 'EU_SESSION', name: 'EU SESSION', color: CK.amber,
threshold: '60%', cycleProb: '0.30',
rec: 'Shadow threshold 60% · Bot 2 moderate',
dangerLevel: 'MEDIUM',
};
if (h >= 0 && h < 7) return {
key: 'ASIAN_SESSION', name: 'ASIAN SESSION — CALM', color: CK.green,
threshold: '55%', cycleProb: '0.45',
rec: 'Shadow threshold 55% · Bot 2 active — grids optimal',
dangerLevel: 'LOW',
};
return {
key: 'DEAD_ZONE', name: 'DEAD ZONE — LOW VOLUME', color: CK.gray,
threshold: '65%', cycleProb: '0.10',
rec: 'Hold positions · no new signals',
dangerLevel: 'LOW',
};
}
/* ── DST helpers ─────────────────────────────────────────────────── */
function lastSunday(year, month) {
const last = new Date(Date.UTC(year, month + 1, 0));
return new Date(Date.UTC(year, month + 1, 0 - last.getUTCDay()));
}
function nthSunday(year, month, n) {
const d = new Date(Date.UTC(year, month, 1));
const off = (7 - d.getUTCDay()) % 7;
return new Date(Date.UTC(year, month, 1 + off + (n - 1) * 7));
}
function ukOffset(now) {
const y = now.getUTCFullYear();
return (now >= lastSunday(y, 2) && now < lastSunday(y, 9)) ? 1 : 0;
}
function euOffset(now) {
const y = now.getUTCFullYear();
return (now >= lastSunday(y, 2) && now < lastSunday(y, 9)) ? 2 : 1;
}
function usOffset(now) {
const y = now.getUTCFullYear();
return (now >= nthSunday(y, 2, 2) && now < nthSunday(y, 10, 1)) ? -4 : -5;
}
/* ── Cities definition ───────────────────────────────────────────── */
function getCities(now) {
return [
{ id: 'tokyo', flag: '🇯🇵', city: 'TOKYO', offset: 9, sessionOpen: [0,6], sessionPeak: [1,4] },
{ id: 'seoul', flag: '🇰🇷', city: 'SEOUL', offset: 9, sessionOpen: [0,6], sessionPeak: [1,5] },
{ id: 'shanghai', flag: '🇨🇳', city: 'SHANGHAI', offset: 8, sessionOpen: [1,7], sessionPeak: [2,5] },
{ id: 'dubai', flag: '🇦🇪', city: 'DUBAI', offset: 4, sessionOpen: [5,14], sessionPeak: [8,12], isLocal: true },
{ id: 'frankfurt', flag: '🇩🇪', city: 'FRANKFURT', offset: euOffset(now), sessionOpen: [7,16], sessionPeak: [8,12] },
{ id: 'london', flag: '🇬🇧', city: 'LONDON', offset: ukOffset(now), sessionOpen: [7,17], sessionPeak: [8,12] },
{ id: 'newyork', flag: '🇺🇸', city: 'NEW YORK', offset: usOffset(now), sessionOpen: [13,22], sessionPeak: [14,20] },
];
}
/* ── Per-city status ─────────────────────────────────────────────── */
function getCityStatus(utcH, city) {
if (city.isLocal) return 'LOCAL';
const [os, oe] = city.sessionOpen;
const [ps, pe] = city.sessionPeak;
if (utcH >= ps && utcH < pe) return 'PEAK';
if (utcH >= os && utcH < oe) return 'OPEN';
return 'CLOSED';
}
/* ── Helpers ─────────────────────────────────────────────────────── */
function pad2(n) { return String(n).padStart(2, '0'); }
function localTime(now, offsetHours) {
const ms = now.getTime() + offsetHours * 3_600_000;
const local = new Date(ms);
return {
hh: pad2(local.getUTCHours()),
mm: pad2(local.getUTCMinutes()),
ss: pad2(local.getUTCSeconds()),
date: local,
localHour: local.getUTCHours(),
};
}
function dayDiff(nowUtc, localDate) {
const um = Date.UTC(nowUtc.getUTCFullYear(), nowUtc.getUTCMonth(), nowUtc.getUTCDate());
const lm = Date.UTC(localDate.getUTCFullYear(), localDate.getUTCMonth(), localDate.getUTCDate());
const d = (lm - um) / 86_400_000;
if (d > 0) return 'TOMORROW';
if (d < 0) return 'YESTERDAY';
return null;
}
function isDay(localHour) { return localHour >= 6 && localHour < 20; }
/* ══════════════════════════════════════════════════════════════════
ANALOG CLOCK — premium SVG bezel
White face = daytime · Dark face = nighttime
Session color ring around the bezel
══════════════════════════════════════════════════════════════════ */
function AnalogClock({ hh, mm, ss, localHour, ringColor, size = 80 }) {
const day = isDay(localHour);
const W = size;
const CX = W / 2;
const CY = W / 2;
const R = W / 2 - 6;
const secAngle = parseInt(ss) * 6;
const minAngle = (parseInt(mm) + parseInt(ss) / 60) * 6;
const hourAngle = ((parseInt(hh) % 12) + parseInt(mm) / 60 + parseInt(ss) / 3600) * 30;
function pt(angleDeg, len) {
const rad = (angleDeg - 90) * Math.PI / 180;
return [CX + len * Math.cos(rad), CY + len * Math.sin(rad)];
}
const [hx, hy] = pt(hourAngle, R * 0.50);
const [mx, my] = pt(minAngle, R * 0.70);
const [sx, sy] = pt(secAngle, R * 0.83);
const [stx, sty] = pt(secAngle + 180, R * 0.20);
const faceFill = day ? '#EEF1FA' : '#0C0E18';
const faceGrad1 = day ? '#F9FAFF' : '#13161F';
const tickMajor = day ? '#1E2130' : '#4A5068';
const tickMinor = day ? '#AAB0C0' : '#252840';
const hourHand = day ? '#0D0F18' : '#ECEFF6';
const minHand = day ? '#2A3050' : '#9AAABE';
const numeralC = day ? '#2A3050' : '#4A5068';
const bezelC = day ? '#C8D0E0' : '#1E2130';
const gradId = `fg-${day?'d':'n'}-${Math.round(R)}`;
const shadowId = `hs-${Math.round(R)}`;
const shadowC = day ? 'rgba(0,0,0,0.15)' : 'rgba(0,0,0,0.55)';
const ticks = Array.from({ length: 60 }, (_, i) => {
const a = i * 6;
const rad = (a - 90) * Math.PI / 180;
const isHour = i % 5 === 0;
const isQtr = i % 15 === 0;
const outer = R - 1;
const inner = isQtr ? R - 8 : isHour ? R - 5 : R - 3;
return { ox: CX + outer*Math.cos(rad), oy: CY + outer*Math.sin(rad),
ix: CX + inner*Math.cos(rad), iy: CY + inner*Math.sin(rad),
isHour, isQtr };
});
const qtrLabels = [
{ a: 0, label: '12' },
{ a: 90, label: '3' },
{ a: 180, label: '6' },
{ a: 270, label: '9' },
];
return (
);
}
/* ══════════════════════════════════════════════════════════════════
DAY/NIGHT BADGE — small sun/moon icon
══════════════════════════════════════════════════════════════════ */
function DayNightBadge({ localHour }) {
const day = isDay(localHour);
return (
{day
?
:
}
{day ? 'DAY' : 'NIGHT'}
);
}
/* ══════════════════════════════════════════════════════════════════
CLOCK CARD
══════════════════════════════════════════════════════════════════ */
function ClockCard({ city, now, utcHour, index, analogMode }) {
const { hh, mm, ss, date, localHour } = localTime(now, city.offset);
const status = getCityStatus(utcHour, city);
const diff = dayDiff(now, date);
const session = getSessionForHour(utcHour);
let ringColor, pillColor, pillLabel;
if (city.isLocal) {
ringColor = CK.blue; pillColor = CK.blue; pillLabel = 'YOUR TIME';
} else {
switch (status) {
case 'PEAK':
pillLabel = 'PEAK';
ringColor = session.key === 'OVERLAP' ? CK.red : CK.amber;
pillColor = ringColor;
break;
case 'OPEN':
pillLabel = 'OPEN';
ringColor = CK.green; pillColor = CK.green;
break;
default:
pillLabel = 'CLOSED';
ringColor = CK.gray; pillColor = CK.gray;
}
}
/* Compact sizes */
const minW = city.isLocal
? (analogMode ? 106 : 96)
: (analogMode ? 96 : 86);
return (
{/* Day/night badge — top-right */}
{/* Flag */}
{city.flag}
{/* Clock face */}
{analogMode ? (
) : (
{hh}:{mm}
:{ss}
)}
{/* Day diff */}
{diff && (
{diff}
)}
{/* City name */}
{city.city}
{/* Status pill */}
{pillLabel}
);
}
/* ══════════════════════════════════════════════════════════════════
SESSION BANNER + MODE TOGGLE
══════════════════════════════════════════════════════════════════ */
function SessionBanner({ session, analogMode, onToggle }) {
const isRed = session.key === 'OVERLAP';
const c = session.color;
const bgMap = {
OVERLAP: 'rgba(255,92,119,0.09)',
US_SESSION: 'rgba(247,166,0,0.05)',
EU_SESSION: 'rgba(247,166,0,0.05)',
ASIAN_SESSION: 'rgba(46,228,155,0.05)',
DEAD_ZONE: 'rgba(107,114,128,0.04)',
};
const borderMap = {
OVERLAP: 'rgba(255,92,119,0.25)',
US_SESSION: 'rgba(247,166,0,0.16)',
EU_SESSION: 'rgba(247,166,0,0.16)',
ASIAN_SESSION: 'rgba(46,228,155,0.16)',
DEAD_ZONE: 'rgba(107,114,128,0.10)',
};
return (
{/* Left: dot + name */}
{/* Right: rec + toggle */}
{session.rec}
{['ANA', 'DIG'].map(mode => {
const active = (mode === 'ANA') === analogMode;
return (
);
})}
);
}
/* ══════════════════════════════════════════════════════════════════
MARKET CLOCK — root component
Props:
overrideUtcHour {number|null} — force UTC hour (test only)
══════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════
NEXT FUNDING — countdown to Bybit's 8h funding (00 / 08 / 16 UTC).
Funding flips move the whole perp market, so it's a real signal.
══════════════════════════════════════════════════════════════════ */
function FundingCell({ now }) {
const secsNow = now.getUTCHours() * 3600 + now.getUTCMinutes() * 60 + now.getUTCSeconds();
const period = 8 * 3600;
const diff = period - (secsNow % period); // seconds to next 8h boundary
const hh = Math.floor(diff / 3600), mm = Math.floor((diff % 3600) / 60), ss = diff % 60;
const soon = diff < 1800; // within 30 min
const c = soon ? CK.red : CK.green;
return (
NEXT FUNDING
{pad2(hh)}:{pad2(mm)}:{pad2(ss)}
8H CYCLE · 00/08/16 UTC
);
}
function MarketClock({ overrideUtcHour = null }) {
const [now, setNow] = React.useState(() => new Date());
const [analogMode, setAnalogMode] = React.useState(() => {
try { return localStorage.getItem('st_clock_mode') === 'analog'; }
catch (_) { return false; }
});
React.useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
const toggleMode = React.useCallback(() => {
setAnalogMode(prev => {
const next = !prev;
try { localStorage.setItem('st_clock_mode', next ? 'analog' : 'digital'); }
catch (_) {}
return next;
});
}, []);
const displayNow = React.useMemo(() => {
if (overrideUtcHour == null) return now;
const d = new Date(now);
d.setUTCHours(overrideUtcHour, d.getUTCMinutes(), d.getUTCSeconds(), 0);
return d;
}, [now, overrideUtcHour]);
const utcHour = displayNow.getUTCHours();
const session = getSessionForHour(utcHour);
const cities = getCities(displayNow);
const utcStr = `${pad2(displayNow.getUTCHours())}:${pad2(displayNow.getUTCMinutes())}:${pad2(displayNow.getUTCSeconds())}`;
return (
{/* Session banner */}
{/* Clocks row */}
{cities.map((city, i) => (
))}
{/* NEXT FUNDING countdown — fills the gap before UTC */}
{/* UTC reference — far right */}
UTC · LIVE
{utcStr}
threshold {session.threshold}
);
}
window.MarketClock = MarketClock;
window.getSessionForHour = getSessionForHour;