פס מסע בגלילה

מראה למבקר איפה הוא נמצא בסיפור של הדף, בלי סרגל התקדמות יבש. התחנות נדלקות כשהסמן מגיע אליהן, והיעד נפתח בסוף. מתאים לדף שמספר תהליך: הרשמה, הסעה, שלבי עבודה. פס מסע בגלילה הוא רכיב מתוך רכיבים של כליקס. מה שמורידים הוא רכיב שלם, גם כ-HTML עצמאי וגם כרכיב React. אין ספרייה חיצונית, אין שלב בנייה, ואם כליקס ייעלם מחר הקוד ימשיך לעבוד.

רכיב · גלילה · בלי ספריות · מחשב בלבד

<!-- פס מסע בצד המסך — להדביק לפני </body> -->
<div id="klix-rail" aria-hidden="true"></div>

<style>
  #klix-rail{position:fixed;top:0;bottom:0;inset-inline-end:.75rem;width:4rem;z-index:40;
             pointer-events:none;color:#e2e8f0}
  /* בטלפון הפס גוזל רוחב ולא מוסיף מידע */
  @media (max-width:1023px){ #klix-rail{display:none} }
  #klix-rail .rail-line{position:absolute;inset-inline-start:50%;transform:translateX(-50%);
             width:1px;display:flex;flex-direction:column;align-items:center;
             justify-content:space-around;opacity:.3}
  #klix-rail .rail-line span{width:1px;height:.5rem;border-radius:999px;background:currentColor}
  #klix-rail .rail-node{position:absolute;inset-inline-start:50%;
             display:flex;align-items:center;justify-content:center;border-radius:999px;
             transition:opacity .5s ease,transform .5s ease}
  #klix-rail .rail-stop{width:1.75rem;height:1.75rem;font-size:.75rem;background:#f8fafc;
             box-shadow:0 1px 4px rgba(0,0,0,.25);opacity:.25;transform:translate(-50%,-50%) scale(.6)}
  #klix-rail .rail-stop.on{opacity:1;transform:translate(-50%,-50%) scale(1)}
  #klix-rail .rail-end{width:2.5rem;height:2.5rem;font-size:1.15rem;background:#fbbf24;
             box-shadow:0 6px 18px rgba(0,0,0,.3);opacity:.3;transform:translate(-50%,-50%) scale(.7)}
  #klix-rail .rail-end.on{opacity:1;transform:translate(-50%,-50%) scale(1)}
  #klix-rail .rail-marker{position:absolute;inset-inline-start:50%;font-size:1.5rem;
             transform:translate(-50%,-50%);filter:drop-shadow(0 2px 3px rgba(0,0,0,.35))}
</style>

<script>
(function () {
  // ═══ ערכו כאן ═══
  var MARKER = '🚗';
  var DESTINATION = '🎊';
  var STOPS = [
    { at: 0.18, icon: '👤', label: 'נרשמתם' },
    { at: 0.40, icon: '👥', label: 'הצטרפו אליכם' },
    { at: 0.60, icon: '👨‍👩‍👦', label: 'המשפחה בדרך' }
  ];
  var FROM = 14;      // תחילת המסלול, באחוזי גובה מסך
  var TO = 74;        // סופו
  var TRAVEL = 0.7;   // באיזו התקדמות גלילה מגיעים ליעד
  // ═════════════════

  var rail = document.getElementById('klix-rail');
  if (!rail) return;

  // מיקום על הפס עבור סף גלילה נתון. כך התחנה יושבת בדיוק במקום שבו
  // הסמן נמצא כשהסף נחצה, ולא במספר קבוע שנבחר בעין.
  function posAt(at) { return FROM + (TO - FROM) * Math.min(1, at / TRAVEL); }

  var line = document.createElement('div');
  line.className = 'rail-line';
  line.style.top = FROM + 'vh';
  line.style.bottom = (100 - TO) + 'vh';
  for (var i = 0; i < 12; i++) line.appendChild(document.createElement('span'));
  rail.appendChild(line);

  var nodes = STOPS.map(function (stop) {
    var el = document.createElement('div');
    el.className = 'rail-node rail-stop';
    el.style.top = posAt(stop.at) + 'vh';
    el.textContent = stop.icon;
    if (stop.label) el.title = stop.label;
    rail.appendChild(el);
    return el;
  });

  var end = document.createElement('div');
  end.className = 'rail-node rail-end';
  end.style.top = TO + 'vh';
  end.textContent = DESTINATION;
  rail.appendChild(end);

  var marker = document.createElement('div');
  marker.className = 'rail-marker';
  marker.textContent = MARKER;
  rail.appendChild(marker);

  // מיקום הסמן נגזר ישירות מהגלילה, כלומר מתנועה של המשתמש עצמו.
  // לכן אין כאן מה לכבות במצב תנועה מופחתת — אין תנועה אוטומטית.
  var pending = false;

  function read() {
    pending = false;
    var max = document.documentElement.scrollHeight - window.innerHeight;
    var p = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
    marker.style.top = posAt(p) + 'vh';
    for (var i = 0; i < nodes.length; i++) {
      nodes[i].classList.toggle('on', p >= STOPS[i].at);
    }
    end.classList.toggle('on', p >= TRAVEL);
  }

  function onScroll() {
    if (pending) return;
    pending = true;
    requestAnimationFrame(read);
  }

  read();
  window.addEventListener('scroll', onScroll, { passive: true });
  window.addEventListener('resize', onScroll);
})();
</script>