PageSourceSearch

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

js pressthink.org collected 2026-10-02 04:26:49 UTC 7,566 bytes, 220 lines download raw bytes

1import { useEffect, useRef, useState } from 'react';
2import { html } from '../html.js?v=3.8.36';
3import {
4  AlertCircle,
5  Filter,
6  Folder,
7  LayoutGrid,
8  RotateCw,
9} from 'lucide-react';
10import ArchiveResults from '../components/ArchiveResults.js?v=3.8.36';
11import Sidebar from '../components/Sidebar.js?v=3.8.36';
12import { RELEVANCE_SORT } from '../utils/searchRanking.js?v=3.8.36';
13
14const DesktopArchivePanel = ({
15  viewMode,
16  loading,
17  error,
18  filters,
19  setFilters,
20  facets,
21  autocompleteIndex,
22  sortBy,
23  setSortBy,
24  filteredRecords,
25  paginatedRecords,
26  folderGroups,
27  currentPage,
28  totalPages,
29  activeFilterCount,
30  onSetViewMode,
31  onSelectRecord,
32  onOpenFolder,
33  onPageChange,
34  onClearFilters,
35  onOpenStandard,
36}) => {
37  const [filterOpen, setFilterOpen] = useState(false);
38  const resultsRef = useRef(null);
39  const filterPanelRef = useRef(null);
40  const filterButtonRef = useRef(null);
41
42  useEffect(() => {
43    setFilterOpen(false);
44  }, [viewMode]);
45
46  useEffect(() => {
47    if (!filterOpen) return undefined;
48    let focusFrame = null;
49    let frame = null;
50    if (!filterPanelRef.current?.contains(document.activeElement)) {
51      frame = requestAnimationFrame(() => {
52        // The narrow drawer changes from visibility:hidden in the same commit.
53        // Give that style one painted frame before moving focus into it; a
54        // single requestAnimationFrame can run while the input is still treated
55        // as hidden and silently leave focus on the toggle.
56        focusFrame = requestAnimationFrame(() => {
57          document.getElementById('desktop-archive-search')?.focus();
58        });
59      });
60    }
61    const handleKeyDown = (event) => {
62      const drawerActive = !window.matchMedia
63        || window.matchMedia('(max-width: 1100px)').matches;
64      if (event.key !== 'Escape' || !drawerActive) return;
65      event.preventDefault();
66      setFilterOpen(false);
67      filterButtonRef.current?.focus();
68    };
69    document.addEventListener('keydown', handleKeyDown);
70    return () => {
71      if (frame !== null) cancelAnimationFrame(frame);
72      if (focusFrame !== null) cancelAnimationFrame(focusFrame);
73      document.removeEventListener('keydown', handleKeyDown);
74    };
75  }, [filterOpen]);
76
77  useEffect(() => {
78    if (!window.matchMedia) return undefined;
79    const drawerQuery = window.matchMedia('(max-width: 1100px)');
80    const preserveFocusedFilter = (event) => {
81      if (event.matches && filterPanelRef.current?.contains(document.activeElement)) {
82        setFilterOpen(true);
83      }
84    };
85    drawerQuery.addEventListener('change', preserveFocusedFilter);
86    return () => drawerQuery.removeEventListener('change', preserveFocusedFilter);
87  }, []);
88
89  const closeFilters = () => {
90    setFilterOpen(false);
91    filterButtonRef.current?.focus();
92  };
93
94  const changePage = (page) => {
95    onPageChange(page);
96    const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
97    resultsRef.current?.scrollIntoView({
98      behavior: reduceMotion ? 'auto' : 'smooth',
99      block: 'start',
100    });
101  };
102
103  const errorPanel = error && html`
104    <div className="desktop-archive-error archive-notice archive-notice--danger" role="alert">
105      <${AlertCircle} aria-hidden="true" />
106      <div>
107        <h3>Error loading archive</h3>
108        <p>${error}</p>
109        <button type="button" className="archive-action archive-action--danger" onClick=${() => window.location.reload()}>
110          <${RotateCw} aria-hidden="true" />
111          Reload page
112        </button>
113      </div>
114    </div>
115  `;
116
117  return html`
118    <div className="desktop-archive-panel archive-density--compact">
119      <header className="desktop-archive-heading">
120        <div>
121          <p className="desktop-eyebrow">Canonical collection</p>
122          <h3>${viewMode === 'folder' ? 'Browse archive folders' : 'Search and read the archive'}</h3>
123          <p>
124            This window uses the archive's live records, filters, details, citations, and share links.
125          </p>
126        </div>
127        <button type="button" className="desktop-standard-link archive-action archive-action--secondary" onClick=${onOpenStandard}>
128          Open standard view
129        </button>
130      </header>
131
132      <div className="desktop-archive-toolbar" aria-label="Archive view controls">
133        <div className="desktop-view-switch" role="group" aria-label="Archive layout">
134          <button
135            type="button"
136            className=${viewMode === 'grid' ? 'is-active' : ''}
137            aria-pressed=${viewMode === 'grid'}
138            onClick=${() => onSetViewMode('grid')}
138
139          >
140            <${LayoutGrid} aria-hidden="true" />
141            Cards
142          </button>
143          <button
144            type="button"
145            className=${viewMode === 'folder' ? 'is-active' : ''}
146            aria-pressed=${viewMode === 'folder'}
147            onClick=${() => onSetViewMode('folder')}
148          >
149            <${Folder} aria-hidden="true" />
150            Folders
151          </button>
152        </div>
153
154        <button
155          ref=${filterButtonRef}
156          type="button"
157          className="desktop-filter-toggle"
158          aria-expanded=${filterOpen}
159          aria-controls="desktop-archive-filter-panel"
160          onClick=${() => setFilterOpen((open) => !open)}
161        >
162          <${Filter} aria-hidden="true" />
163          Filters
164          ${activeFilterCount > 0 && html`<span>${activeFilterCount}</span>`}
165        </button>
166
167        <label className="desktop-sort-control">
168          <span>Sort</span>
169          <select value=${sortBy} onChange=${(event) => setSortBy(event.target.value)}>
170            ${Boolean(filters.search.trim()) && html`<option value=${RELEVANCE_SORT}>Relevance</option>`}
171            <option value="date-desc">Newest first</option>
172            <option value="date-asc">Oldest first</option>
173            <option value="title-asc">Title (A–Z)</option>
174          </select>
175        </label>
176      </div>
177
178      <div className="desktop-archive-layout">
179        <div id="desktop-archive-filter-panel" ref=${filterPanelRef}>
180          <${Sidebar}
181            facets=${facets}
182            filters=${filters}
183            setFilters=${setFilters}
184            isOpen=${filterOpen}
185            onClose=${closeFilters}
186            resetFilters=${onClearFilters}
187            autocompleteIndex=${autocompleteIndex}
188            variant="desktop"
189          />
190        </div>
191
192        <section className="desktop-archive-result-column" aria-labelledby="desktop-result-summary" ref=${resultsRef}>
193          <p id="desktop-result-summary" className="desktop-result-summary" aria-live="polite">
194            ${loading
195              ? 'Loading archive…'
196              : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'} found${filters.recordIds !== null ? ' in query results' : ''}`}
197          </p>
198          <${ArchiveResults}
199            compact=${true}
200            errorPanel=${errorPanel}
201            loading=${loading}
202            filteredRecords=${filteredRecords}
203            paginatedRecords=${paginatedRecords}
204            folderGroups=${folderGroups}
205            viewMode=${viewMode}
206            searchTerm=${filters.search}
207            currentPage=${currentPage}
208            totalPages=${totalPages}
209            onSelectRecord=${onSelectRecord}
210            onOpenFolder=${onOpenFolder}
211            onPageChange=${changePage}
212            onClearFilters=${onClearFilters}
213          />
214        </section>
215      </div>
216    </div>
217  `;
218};
219
220export default DesktopArchivePanel;

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.