1/* Hero demo only â auto-scroll â lock alert â unlock â feed inject â detail peek. 2 Not a fork of app-demo.js. Pairs with AppDemoHero.astro. */ 3(function () { 4 'use strict'; 5 6 var root = document.getElementById('tgDemoHero'); 7 if (!root) return; 8 9 var phone = document.getElementById('tgPhoneHero'); 10 var phoneWrap = document.getElementById('tgPhoneWrapHero'); 11 var feed = document.getElementById('tgFeedHero'); 12 var detailEl = document.getElementById('tgDetailHero'); 13 var lockBanners = document.getElementById('tgLockBannersHero'); 14 var heroCursor = document.getElementById('tgHeroCursor'); 15 var heroTrigger = document.getElementById('tgHeroTrigger'); 16 var dataEl = document.getElementById('tg-demo-hero-data'); 17 18 var LIVE_TEST = null; 19 try { LIVE_TEST = JSON.parse((dataEl && dataEl.textContent) || 'null'); } catch (e) { LIVE_TEST = null; } 20 if (!LIVE_TEST) return; 21 22 var LEVEL_LABEL = { info: 'INFO', success: 'SUCCESS', warning: 'WARNING', error: 'ERROR', meta: 'TRIGV' }; 23 var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 24 var systemThemeMq = window.matchMedia('(prefers-color-scheme: dark)'); 25 26 var sequenceActive = false; 27 var feedInner = null; 28 var liveRowEl = null; 29 var triggerDefaultLabel = heroTrigger ? heroTrigger.textContent.trim() : ''; 30 31 var TIMING = { 32 lockHold: reducedMotion ? 400 : 1500, 33 afterInject: reducedMotion ? 200 : 900, 34 detailHold: reducedMotion ? 2200 : 4000, 35 afterDetail: reducedMotion ? 100 : 350, 36 unlockFallback: reducedMotion ? 0 : 520, 37 cursorMove: reducedMotion ? 0 : 680, 38 cursorClick: 220, 39 }; 40 41 function esc(s) { 42 return String(s).replace(/[&<>"']/g, function (c) { 43 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 44 }); 45 } 46 47 function wait(ms) { 48 return new Promise(function (resolve) { setTimeout(resolve, ms); }); 49 } 50 51 function applySystemTheme() { 52 phone.setAttribute('data-theme', systemThemeMq.matches ? 'dark' : 'light'); 53 } 54 55 function setScreen(name) { 56 phone.setAttribute('data-screen', name); 57 phone.querySelectorAll('.tg-screen').forEach(function (s) { 58 s.hidden = s.getAttribute('data-screen-name') !== name; 59 }); 60 } 61 62 function updateBellBadge() { 63 var badge = phone.querySelector('[data-bell-badge]'); 64 if (!badge) return; 65 var count = feed.querySelectorAll('.tg-row[data-state="unread"]').length; 66 badge.textContent = count; 67 badge.style.display = count > 0 ? '' : 'none'; 68 } 69 70 function setClock() { 71 var now = new Date(); 72 var h = now.getHours() % 12 || 12; 73 var m = now.getMinutes(); 74 var time = h + ':' + (m < 10 ? '0' : '') + m; 75 root.querySelectorAll('[data-clock]').forEach(function (el) { el.textContent = time; }); 76 var ld = root.querySelector('[data-lockdate]'); 77 if (!ld) return; 78 var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 79 var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 80 ld.textContent = days[now.getDay()] + ', ' + now.getDate() + ' ' + months[now.getMonth()]; 81 } 82 83 function ensureFeedInner() { 84 if (feedInner) return feedInner; 85 feedInner = document.createElement('div'); 86 feedInner.className = 'tg-feed-inner'; 87 while (feed.firstChild) feedInner.appendChild(feed.firstChild); 88 feed.appendChild(feedInner); 89 return feedInner; 90 } 91 92 function resetFeedScroll() { 93 ensureFeedInner(); 94 feedInner.style.transform = 'translate3d(0, 0, 0)'; 95 feed.scrollTop = 0; 96 } 97 98 function measureFeedScrollMax() { 99 ensureFeedInner(); 100 var wasAuto = root.getAttribute('data-autoscroll') === 'true'; 101 if (wasAuto) root.removeAttribute('data-autoscroll'); 102 feedInner.style.transform = 'translate3d(0, 0, 0)'; 103 var max = feedInner.offsetHeight - feed.clientHeight; 104 if (wasAuto) root.setAttribute('data-autoscroll', 'true'); 105 return max > 4 ? max : 0; 106 } 107 108 function startAutoScroll() { 109 stopAutoScroll(); 110 if (reducedMotion || sequenceActive) return; 111 var max = measureFeedScrollMax(); 112 if (max <= 4) return; 113 feed.style.setProperty('--tg-feed-scroll-max', max + 'px'); 114 feed.style.setProperty('--tg-feed-scroll-duration', Math.max(18, max / 22) + 's'); 115 if (feedInner) { 116 feedInner.style.animation = ''; 117 feedInner.style.transform = ''; 118 } 119 root.setAttribute('data-autoscroll', 'true'); 120 } 121 122 function stopAutoScroll() { 123 root.removeAttribute('data-autoscroll'); 124 if (feedInner) feedInner.style.transform = 'translate3d(0, 0, 0)'; 125 if (feed) { 126 feed.style.removeProperty('--tg-feed-scroll-max'); 127 feed.style.removeProperty('--tg-feed-scroll-duration'); 128 } 129 } 130 131 function buildLockCard(n) { 132 var card = document.createElement('div'); 133 card.className = 'tg-lockcard tg-lockcard--hero-rise'; 134 card.setAttribute('role', 'presentation'); 135 card.innerHTML = 136 '<span class="tg-lockcard-ic">' + 137 '<img src="/assets/trigv-logomark.svg" alt="" class="tg-lockcard-mark tg-lockcard-mark--light" />' + 138 '<img src="/assets/trigv-logomark-dark.svg" alt="" class="tg-lockcard-mark tg-lockcard-mark--dark" />' + 139 '</span><span class="tg-lockcard-body">' + 140 '<span class="tg-lockcard-title">' + esc(n.title) + '</span>' + 141 '<span class="tg-lockcard-msg">' + esc(n.message) + '</span></span>'; 142 return card; 143 } 144 145 function buildFeedRow(n) { 146 var badge = n.level === 'meta' 147 ? '<span class="tg-badge tg-badge--trigv">Trigv</span>' 148 : (n.channel ? '<span class="tg-badge tg-badge--channel">' + esc(n.channel) + '</span>' : ''); 149 150 var wrap = document.createElement('div'); 151 wrap.className = 'tg-rowwrap tg-rowwrap--hero-inject'; 152 wrap.setAttribute('data-id', n.id); 153 wrap.innerHTML = 154 '<div class="tg-row" data-id="' + esc(n.id) + '" data-level="' + esc(n.level) + '" data-state="unread">' + 155 '<span class="tg-row-icon" aria-hidden="true"></span>' + 156 '<span class="tg-row-main"><span class="tg-row-top">' + 157 '<span class="tg-row-title">' + esc(n.title) + '</span>' + 158 '<span class="tg-row-time"><span class="tg-row-dot" aria-hidden="true"></span>' + esc(n.ago) + '</span>' + 159 '</span><span class="tg-row-msg">' + esc(n.message) + '</span>' + badge + '</span></div>'; 160 return wrap; 161 } 162 163 function injectLiveAlert() { 164 ensureFeedInner(); 165 var today = feedInner.querySelector('.tg-group[data-group="today"]'); 166 if (!today) { 167 today = document.createElement('div'); 168 today.className = 'tg-group'; 169 today.setAttribute('data-group', 'today'); 170 today.innerHTML = '<p class="tg-datehead">Today</p>'; 171 feedInner.insertBefore(today, feedInner.firstChild); 172 } 173 liveRowEl = buildFeedRow(LIVE_TEST); 174 var head = today.querySelector('.tg-datehead'); 175 today.insertBefore(liveRowEl, head ? head.nextSibling : today.firstChild); 176 updateBellBadge(); 177 resetFeedScroll(); 178 return liveRowEl; 179 } 180 181 function removeLiveAlert() { 182 if (liveRowEl && liveRowEl.parentNode) liveRowEl.parentNode.removeChild(liveRowEl); 183 liveRowEl = null; 184 updateBellBadge(); 185 } 186 187 function showDetail(n) { 188 var html = 189 '<div class="tg-detail-head">' +
190 '<span class="tg-pill" data-level="' + n.level + '">' + 191 '<span class="tg-row-icon" aria-hidden="true"></span>' + 192 (LEVEL_LABEL[n.level] || 'INFO') + '</span>' + 193 '<span class="tg-detail-when">' + esc(n.when) + '</span></div>' + 194 '<h2 class="tg-detail-title" data-level="' + n.level + '">' + esc(n.title) + '</h2>' + 195 '<p class="tg-detail-msg">' + esc(n.message) + '</p>'; 196 197 if (n.image) { 198 html += '<img class="tg-detail-img" src="' + esc(n.image) + '" alt="" loading="lazy" decoding="async" />' + 199 '<p class="tg-detail-imgnote">Image links may expire â content lives on your device.</p>'; 200 } 201 202 html += '<div class="tg-meta-card">' + 203 (n.channel ? '<div class="tg-meta-row"><span class="tg-meta-key">Channel</span><span class="tg-meta-val">#' + esc(n.channel) + '</span></div>' : '') + 204 '<div class="tg-meta-row"><span class="tg-meta-key">Event type</span><span class="tg-meta-val tg-meta-val--mono">' + esc(n.event) + '</span></div>' + 205 '</div>'; 206 207 detailEl.innerHTML = html; 208 detailEl.scrollTop = 0; 209 setScreen('detail'); 210 } 211 212 function unlockLockScreen() { 213 return new Promise(function (resolve) { 214 phone.setAttribute('data-unlocking', 'true'); 215 var lockScreen = phone.querySelector('.tg-screen--lock'); 216 var done = false; 217 var finish = function () { 218 if (done) return; 219 done = true; 220 phone.removeAttribute('data-unlocking'); 221 phone.removeAttribute('data-lock-mode'); 222 lockBanners.innerHTML = ''; 223 setScreen('feed'); 224 resolve(); 225 }; 226 if (reducedMotion || !lockScreen) { finish(); return; } 227 lockScreen.addEventListener('transitionend', finish, { once: true }); 228 setTimeout(finish, TIMING.unlockFallback); 229 }); 230 } 231 232 function hideCursor() { 233 if (!heroCursor) return; 234 heroCursor.hidden = true; 235 heroCursor.classList.remove('tg-hero-cursor--visible', 'tg-hero-cursor--click'); 236 } 237 238 function simulateCursorClick(targetEl) { 239 if (!heroCursor || !phoneWrap || !targetEl || reducedMotion) return Promise.resolve(); 240 241 return new Promise(function (resolve) { 242 var wrapRect = phoneWrap.getBoundingClientRect(); 243 var rowRect = targetEl.getBoundingClientRect(); 244 var endX = rowRect.left + rowRect.width * 0.55 - wrapRect.left; 245 var endY = rowRect.top + rowRect.height * 0.45 - wrapRect.top; 246 247 heroCursor.hidden = false; 248 heroCursor.style.transform = 'translate(' + (wrapRect.width * 0.72) + 'px, ' + (wrapRect.height * 0.18) + 'px)'; 249 heroCursor.classList.add('tg-hero-cursor--visible'); 250 251 requestAnimationFrame(function () { 252 heroCursor.style.transform = 'translate(' + endX + 'px, ' + endY + 'px)'; 253 }); 254 255 setTimeout(function () { 256 heroCursor.classList.add('tg-hero-cursor--click'); 257 setTimeout(function () { hideCursor(); resolve(); }, TIMING.cursorClick); 258 }, TIMING.cursorMove); 259 }); 260 } 261 262 function setTriggerBusy(busy) { 263 if (!heroTrigger) return; 264 heroTrigger.setAttribute('aria-disabled', busy ? 'true' : 'false'); 265 heroTrigger.classList.toggle('tg-hero-trigger--busy', busy); 266 heroTrigger.style.pointerEvents = busy ? 'none' : ''; 267 heroTrigger.style.opacity = busy ? '0.55' : ''; 268 heroTrigger.textContent = busy ? 'Look at the phone..' : triggerDefaultLabel; 269 } 270 271 function shouldScrollToPhone() { 272 var visual = document.querySelector('.hero-sandbox-visual'); 273 var target = visual || phoneWrap || root; 274 if (!target) return false; 275 276 if (window.matchMedia('(max-width: 960px)').matches) return true; 277 278 var rect = target.getBoundingClientRect(); 279 var header = document.getElementById('site-header'); 280 var headerH = header ? header.getBoundingClientRect().height : 0; 281 var visibleTop = headerH + 16; 282 var visibleBottom = window.innerHeight - 24; 283 return rect.top < visibleTop || rect.bottom > visibleBottom; 284 } 285 286 function scrollHeroPhoneIntoView() { 287 return new Promise(function (resolve) { 288 if (!shouldScrollToPhone()) { 289 resolve(); 290 return; 291 } 292 293 var visual = document.querySelector('.hero-sandbox-visual'); 294 var target = visual || phoneWrap || root; 295 var header = document.getElementById('site-header'); 296 var headerH = header ? header.getBoundingClientRect().height : 0; 297 var top = target.getBoundingClientRect().top + window.scrollY - headerH - 20; 298 299 if (reducedMotion) { 300 window.scrollTo(0, Math.max(0, top)); 301 resolve(); 302 return; 303 } 304 305 window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); 306 setTimeout(resolve, 500); 307 }); 308 } 309 310 async function runHeroSequence() { 311 if (sequenceActive) return; 312 sequenceActive = true; 313 setTriggerBusy(true); 314 stopAutoScroll(); 315 root.setAttribute('data-sequence', 'active'); 316 hideCursor(); 317 318 await scrollHeroPhoneIntoView(); 319 phone.setAttribute('data-lock-mode', 'hero'); 320 lockBanners.innerHTML = ''; 321 setScreen('lock'); 322 323 var card = buildLockCard(LIVE_TEST); 324 lockBanners.appendChild(card); 325 requestAnimationFrame(function () { card.classList.add('tg-lockcard--hero-rise-active'); }); 326 327 await wait(TIMING.lockHold); 328 await unlockLockScreen(); 329 330 var injected = injectLiveAlert(); 331 await wait(TIMING.afterInject); 332 333 if (injected) { 334 await simulateCursorClick(injected.querySelector('.tg-row') || injected); 335 showDetail(LIVE_TEST); 336 await wait(TIMING.detailHold); 337 setScreen('feed'); 338 await wait(TIMING.afterDetail); 339 } 340 341 removeLiveAlert(); 342 resetFeedScroll(); 343 sequenceActive = false;
344 root.removeAttribute('data-sequence'); 345 setTriggerBusy(false); 346 startAutoScroll(); 347 } 348 349 function trackTriggerAlert() { 350 try { 351 if (window.at && typeof window.at.track === 'function') { 352 window.at.track('Trigger Alert'); 353 } 354 } catch (err) { 355 /* analytics optional */ 356 } 357 } 358 359 function onTrigger(e) { 360 if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; 361 if (e.type === 'keydown') e.preventDefault(); 362 if (!sequenceActive) { 363 trackTriggerAlert(); 364 runHeroSequence(); 365 } 366 } 367 368 applySystemTheme(); 369 if (typeof systemThemeMq.addEventListener === 'function') { 370 systemThemeMq.addEventListener('change', applySystemTheme); 371 } else if (typeof systemThemeMq.addListener === 'function') { 372 systemThemeMq.addListener(applySystemTheme); 373 } 374 375 setClock(); 376 updateBellBadge(); 377 ensureFeedInner(); 378 requestAnimationFrame(function () { 379 requestAnimationFrame(startAutoScroll); 380 }); 381 382 if (heroTrigger) { 383 heroTrigger.addEventListener('click', onTrigger); 384 heroTrigger.addEventListener('keydown', onTrigger); 385 } 386 387 feed.addEventListener('wheel', function () { 388 if (root.getAttribute('data-autoscroll') === 'true' && !sequenceActive) stopAutoScroll(); 389 }, { passive: true }); 390 391 feed.addEventListener('touchstart', function () { 392 if (root.getAttribute('data-autoscroll') === 'true' && !sequenceActive) stopAutoScroll(); 393 }, { passive: true }); 394})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.