PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/components/Timeline.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:38 UTC 5,902 bytes, 146 lines download raw bytes

1
2import { useEffect, useMemo, useRef, useState } from 'react';
3import { html } from '../html.js?v=3.8.36';
4import { buildTimelineModel } from '../utils/timelineData.js?v=3.8.36';
5import { ChevronDown, ChevronUp } from 'lucide-react';
6
7const Timeline = ({ records, selectedYear, onSelectYear }) => {
8  const [isExpanded, setIsExpanded] = useState(() => (
9    typeof window === 'undefined' || window.innerWidth >= 640
10  ));
11  const [hoveredYear, setHoveredYear] = useState(null);
12  const [rovingYear, setRovingYear] = useState(null);
13  const yearButtonRefs = useRef(new Map());
14
15  const { timelineData, dataMinYear, dataMaxYear } = useMemo(
16    () => buildTimelineModel(records),
17    [records],
18  );
19
20  const enabledYears = useMemo(
21    () => timelineData.filter(data => data.count > 0).map(data => data.year),
22    [timelineData],
23  );
24
25  useEffect(() => {
26    setRovingYear(current => {
27      if (selectedYear && enabledYears.includes(selectedYear)) return selectedYear;
28      if (current && enabledYears.includes(current)) return current;
29      return enabledYears[0] || null;
30    });
31  }, [enabledYears, selectedYear]);
32
33  if (timelineData.length === 0) return null;
34
35  const maxCount = Math.max(...timelineData.map(d => d.count));
36  const logMax = maxCount > 0 ? Math.log(maxCount + 1) : 1;
37
38  const handleYearKeyDown = (event, year) => {
39    if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
40    event.preventDefault();
41    const currentIndex = enabledYears.indexOf(year);
42    let nextYear;
43    if (event.key === 'Home') nextYear = enabledYears[0];
44    else if (event.key === 'End') nextYear = enabledYears[enabledYears.length - 1];
45    else {
46      const direction = event.key === 'ArrowRight' ? 1 : -1;
47      nextYear = enabledYears[(currentIndex + direction + enabledYears.length) % enabledYears.length];
48    }
49    setRovingYear(nextYear);
50    yearButtonRefs.current.get(nextYear)?.focus();
51  };
52
53  return html`
54    <section className="archive-timeline" aria-labelledby="archive-timeline-title">
55      <div className="archive-timeline__header">
56        <button
57          type="button"
58          onClick=${() => setIsExpanded(!isExpanded)}
59          className="archive-timeline__toggle"
60          aria-expanded=${isExpanded}
61          aria-controls="archive-timeline-panel"
62        >
63          <h3 id="archive-timeline-title">Timeline</h3>
64          <span className="archive-timeline__range">
65            ${dataMinYear}–${dataMaxYear}
66          </span>
67          ${isExpanded
68            ? html`<${ChevronUp} className="w-4 h-4" aria-hidden="true" />`
69            : html`<${ChevronDown} className="w-4 h-4" aria-hidden="true" />`}
70        </button>
71        ${selectedYear && html`
72          <button
73            type="button"
74            onClick=${() => onSelectYear(null)}
75            className="archive-action archive-action--quiet archive-timeline__clear"
76          >
77            Clear ${selectedYear}
78          </button>
79        `}
80      </div>
81
82      ${isExpanded && html`
83        <div id="archive-timeline-panel" className="archive-panel archive-density--compact archive-timeline__panel">
84          <div
85            className="archive-timeline__scroll"
86            role="region"
87            aria-label="Archive timeline by year"
88          >
89            ${timelineData.map((data) => {
90              const heightPercent = maxCount > 0
91                ? (data.count > 0 ? Math.max((Math.log(data.count + 1) / logMax) * 100, 8) : 0)
92                : 0;
93              const isSelected = selectedYear === data.year;
94              const isHovered = hoveredYear === data.year;
95              const isYearLabel = parseInt(data.year) % 5 === 0
96                || data.year === timelineData[0].year
97                || data.year === timelineData[timelineData.length - 1].year;
98
99              return html`
100                <button
101                  type="button"
102                  key=${data.year}
103                  ref=${element => {
104                    if (element) yearButtonRefs.current.set(data.year, element);
105                    else yearButtonRefs.current.delete(data.year);
106                  }}
107                  disabled=${data.count === 0}
108                  tabIndex=${data.year === rovingYear ? 0 : -1}
109                  onClick=${() => onSelectYear(isSelected ? null : data.year)}
110                  onKeyDown=${event => handleYearKeyDown(event, data.year)}
111                  onMouseEnter=${() => setHoveredYear(data.year)}
112                  onMouseLeave=${() => setHoveredYear(null)}
113                  onFocus=${() => {
114                    setHoveredYear(data.year);
115                    setRovingYear(data.year);
116                  }}
117                  onBlur=${() => setHoveredYear(null)}
118                  className="archive-timeline__year"
119                  data-selected=${isSelected ? 'true' : 'false'}
120                  aria-label=${`${data.year}: ${data.count} record${data.count === 1 ? '' : 's'}`}
121                  aria-pressed=${isSelected}
122                >
123                  <span className="archive-timeline__tooltip" aria-hidden="true">
124                    <strong>${data.year}</strong>
125                    <span>${data.count} records</span>
126                  </span>
127                  <span
128                    className=${`archive-timeline__bar ${isHovered ? 'is-hovered' : ''}`}
129                    style=${{ height: data.count > 0 ? `${heightPercent}%` : '0px' }}
130                    aria-hidden="true"
131                  />
132                  ${isYearLabel && html`<span className="archive-timeline__tick" aria-hidden="true" />`}
133                  ${isYearLabel && html`
134                    <span className="archive-timeline__year-label" aria-hidden="true">${data.year}</span>
135                  `}
136                </button>
137              `;
138            })}
139          </div>
140        </div>
141      `}
142    </section>
143  `;
144};
145
146export default Timeline;

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.