PageSourceSearch

https://trigv.com/js/app-demo-hero.js

js trigv.com collected 2026-10-02 05:07:54 UTC 14,365 bytes, 394 lines download raw bytes

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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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.