hop2 0.11.59 → 0.11.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -7,11 +7,11 @@ import { relativeTime } from "../utils/switcherModel";
|
|
|
7
7
|
// this client was to catch the printed URL before it scrolled out of the
|
|
8
8
|
// viewport.
|
|
9
9
|
//
|
|
10
|
-
// ONE SHAPE: a reader. Opening Views lands you IN
|
|
11
|
-
// scoped session's, or the fleet's),
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// ONE SHAPE: a reader with its index beside it. Opening Views lands you IN
|
|
11
|
+
// the newest result (the scoped session's, or the fleet's), the list of
|
|
12
|
+
// everything published on the left, ‹ › to step through it. Both in one
|
|
13
|
+
// window, like a mail client — there is no separate "list mode" to click
|
|
14
|
+
// through first.
|
|
15
15
|
//
|
|
16
16
|
// Two placements of that one shape:
|
|
17
17
|
//
|
|
@@ -168,11 +168,11 @@ const previewSrc = (item: ViewItem) =>
|
|
|
168
168
|
: hrefFor(item);
|
|
169
169
|
|
|
170
170
|
const RAIL_KEY = "hop_views_rail";
|
|
171
|
-
// Wide enough
|
|
172
|
-
//
|
|
173
|
-
const DEFAULT_DOCK_W =
|
|
171
|
+
// Wide enough for the index beside a readable page; on a 1440 display that
|
|
172
|
+
// still leaves 680px of terminal. The drag handle remembers a choice.
|
|
173
|
+
const DEFAULT_DOCK_W = 760;
|
|
174
174
|
const clampDockW = (w: number) =>
|
|
175
|
-
Math.max(
|
|
175
|
+
Math.max(560, Math.min(Math.round(window.innerWidth * 0.75), w));
|
|
176
176
|
|
|
177
177
|
export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Props) => {
|
|
178
178
|
const [items, setItems] = useState<ViewItem[] | null>(null);
|
|
@@ -199,15 +199,13 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
199
199
|
return clampDockW(Number.isFinite(stored) && stored > 0 ? stored : DEFAULT_DOCK_W);
|
|
200
200
|
});
|
|
201
201
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
202
|
-
// The index (the list of everything published)
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// to see. Beside the page it stays as you left it (remembered).
|
|
202
|
+
// The index (the list of everything published) sits BESIDE the page, in
|
|
203
|
+
// the same window, like a mail client: the list on the left, the result on
|
|
204
|
+
// the right, both visible at once. ☰ hides it for a wider page, and that
|
|
205
|
+
// choice is remembered.
|
|
207
206
|
const [railOpen, setRailOpen] = useState<boolean>(() => {
|
|
208
|
-
try { return localStorage.getItem(RAIL_KEY)
|
|
207
|
+
try { return localStorage.getItem(RAIL_KEY) !== "0"; } catch { return true; }
|
|
209
208
|
});
|
|
210
|
-
const [indexOpen, setIndexOpen] = useState(false);
|
|
211
209
|
const now = Date.now();
|
|
212
210
|
|
|
213
211
|
// The scope arrives as whatever identifier the opener HAD — the URL's
|
|
@@ -312,10 +310,8 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
312
310
|
if (event.key === "Escape") {
|
|
313
311
|
event.preventDefault();
|
|
314
312
|
event.stopPropagation();
|
|
315
|
-
// The
|
|
316
|
-
|
|
317
|
-
if (railOverlay && indexOpen) setIndexOpen(false);
|
|
318
|
-
else onClose();
|
|
313
|
+
// The reader IS the panel: Escape closes it.
|
|
314
|
+
onClose();
|
|
319
315
|
return;
|
|
320
316
|
}
|
|
321
317
|
const dir = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1
|
|
@@ -329,7 +325,7 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
329
325
|
window.addEventListener("keydown", onKey, true);
|
|
330
326
|
return () => window.removeEventListener("keydown", onKey, true);
|
|
331
327
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
332
|
-
}, [onClose, preview, flatRows, docked
|
|
328
|
+
}, [onClose, preview, flatRows, docked]);
|
|
333
329
|
|
|
334
330
|
// Opening the panel IS the act of seeing what it shows.
|
|
335
331
|
useEffect(() => {
|
|
@@ -366,7 +362,6 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
366
362
|
if (!canPreview()) return; // narrow: the anchor's new tab is right
|
|
367
363
|
event.preventDefault();
|
|
368
364
|
setPreview(item);
|
|
369
|
-
setIndexOpen(false);
|
|
370
365
|
};
|
|
371
366
|
|
|
372
367
|
const copyLink = (item: ViewItem) => {
|
|
@@ -434,13 +429,8 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
434
429
|
el.addEventListener("pointerup", onUp);
|
|
435
430
|
};
|
|
436
431
|
|
|
437
|
-
|
|
438
|
-
// both, over it otherwise (a 620px dock with a 240px rail leaves 380px of
|
|
439
|
-
// page — enough for a plot, not for a report).
|
|
440
|
-
const railOverlay = docked ? dockW < 900 : false;
|
|
441
|
-
const railVisible = (railOverlay ? indexOpen : railOpen) && flatRows.length > 0;
|
|
432
|
+
const railVisible = railOpen && flatRows.length > 0;
|
|
442
433
|
const toggleRail = () => {
|
|
443
|
-
if (railOverlay) { setIndexOpen((v) => !v); return; }
|
|
444
434
|
setRailOpen((v) => {
|
|
445
435
|
try { localStorage.setItem(RAIL_KEY, v ? "0" : "1"); } catch { /* private mode */ }
|
|
446
436
|
return !v;
|
|
@@ -586,7 +576,7 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
586
576
|
<div className="views-nav">
|
|
587
577
|
<button type="button" className={"views-act views-rail-toggle" + (railVisible ? " on" : "")}
|
|
588
578
|
aria-label={railVisible ? "Hide the index" : "Show the index"} aria-pressed={railVisible}
|
|
589
|
-
title={railVisible ? "Hide the index" : "
|
|
579
|
+
title={railVisible ? "Hide the index (a wider page)" : "Show the index beside the page"}
|
|
590
580
|
onClick={toggleRail}>☰</button>
|
|
591
581
|
<button type="button" className="views-act" aria-label="Previous view" title="Previous (←)"
|
|
592
582
|
disabled={at <= 0} onClick={() => step(-1)}>‹</button>
|
|
@@ -638,7 +628,7 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
638
628
|
{!docked && <div className="views-backdrop" onClick={onClose} />}
|
|
639
629
|
<div className={"views-panel" + (docked ? " docked" : "")
|
|
640
630
|
+ (flatRows.length > 0 ? " reading" : "")
|
|
641
|
-
+ (railVisible ?
|
|
631
|
+
+ (railVisible ? " rail-side" : "")}
|
|
642
632
|
style={docked ? { width: dockW } : undefined}
|
|
643
633
|
role="dialog" aria-label="Published views"
|
|
644
634
|
ref={panelRef} tabIndex={-1}>
|
|
@@ -673,15 +663,10 @@ export const ViewsPanel = ({ session, sessions = [], dock = false, onClose }: Pr
|
|
|
673
663
|
</div>
|
|
674
664
|
{nav}
|
|
675
665
|
<div className="views-body">
|
|
676
|
-
{railVisible &&
|
|
666
|
+
{railVisible && list}
|
|
677
667
|
<div className="views-preview">
|
|
678
668
|
{empty || (canPreview() ? page : list)}
|
|
679
669
|
</div>
|
|
680
|
-
{railVisible && railOverlay && (
|
|
681
|
-
<div className="views-index" role="region" aria-label="Everything published">
|
|
682
|
-
{list}
|
|
683
|
-
</div>
|
|
684
|
-
)}
|
|
685
670
|
</div>
|
|
686
671
|
</div>
|
|
687
672
|
</>
|
|
@@ -4511,17 +4511,12 @@ body.pane-dragging { user-select: none; }
|
|
|
4511
4511
|
padding-right: 10px;
|
|
4512
4512
|
margin-right: 12px;
|
|
4513
4513
|
}
|
|
4514
|
-
.views-panel.docked.rail-side .views-list { flex-basis:
|
|
4515
|
-
/*
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
border: 1px solid var(--line); border-radius: 10px;
|
|
4521
|
-
padding: 8px 10px;
|
|
4522
|
-
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
|
|
4523
|
-
}
|
|
4524
|
-
.views-index .views-list { flex: 1; }
|
|
4514
|
+
.views-panel.docked.rail-side .views-list { flex-basis: 232px; }
|
|
4515
|
+
/* In the rail the title owns the row: the per-row actions would crowd it
|
|
4516
|
+
to three letters, and the reader's bar carries the same actions for the
|
|
4517
|
+
page that is open. (Still in the DOM — a screen reader finds them.) */
|
|
4518
|
+
.views-panel.rail-side .views-row .views-acts { display: none; }
|
|
4519
|
+
.views-panel.rail-side .views-row.selected { background: color-mix(in srgb, var(--accent, #7c5cff) 16%, transparent); }
|
|
4525
4520
|
.views-preview { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
4526
4521
|
/* The reader's bar: index toggle · ‹ n/N › · title · actions. */
|
|
4527
4522
|
.views-nav {
|
|
@@ -16,10 +16,7 @@ let store: Record<string, string> = {};
|
|
|
16
16
|
beforeEach(() => {
|
|
17
17
|
// jsdom's own localStorage is only partially implemented here, and the
|
|
18
18
|
// seen-markers are the whole point of these cases.
|
|
19
|
-
|
|
20
|
-
// cases below read rows from it, so they start with it open, the way a
|
|
21
|
-
// reader who opened it once would. The reader-first case clears this.
|
|
22
|
-
store = { hop_views_rail: "1" };
|
|
19
|
+
store = {};
|
|
23
20
|
vi.stubGlobal("localStorage", {
|
|
24
21
|
getItem: (key: string) => (key in store ? store[key] : null),
|
|
25
22
|
setItem: (key: string, value: string) => { store[key] = value; },
|
|
@@ -60,7 +57,6 @@ describe("ViewsPanel", () => {
|
|
|
60
57
|
|
|
61
58
|
it("opens STRAIGHT INTO the newest result; a row click switches the page; the row stays a real link", async () => {
|
|
62
59
|
const onClose = vi.fn();
|
|
63
|
-
delete store.hop_views_rail; // a first-time reader: nothing remembered
|
|
64
60
|
render(<ViewsPanel onClose={onClose} />);
|
|
65
61
|
await waitFor(() => expect(document.querySelector("iframe.views-frame")).toBeTruthy());
|
|
66
62
|
// jsdom's window is 1024 wide — the desk case, where the page exists.
|
|
@@ -74,10 +70,9 @@ describe("ViewsPanel", () => {
|
|
|
74
70
|
// browser's PDF viewer, which is half of what the pane is for.
|
|
75
71
|
expect(frame.hasAttribute("sandbox")).toBe(false);
|
|
76
72
|
expect(screen.getByText("1 / 3")).toBeTruthy();
|
|
77
|
-
// The index is
|
|
78
|
-
//
|
|
79
|
-
expect(document.querySelector(".views-list")).
|
|
80
|
-
fireEvent.click(screen.getByRole("button", { name: "Show the index" }));
|
|
73
|
+
// The index is beside the page from the start — both in one window; a
|
|
74
|
+
// plain click on a row turns the page.
|
|
75
|
+
expect(document.querySelector(".views-panel.rail-side .views-list")).toBeTruthy();
|
|
81
76
|
const row = screen.getByText("views-test.html").closest("a") as HTMLAnchorElement;
|
|
82
77
|
expect(row.getAttribute("target")).toBe("_blank");
|
|
83
78
|
fireEvent.click(row);
|
|
@@ -95,7 +90,7 @@ describe("ViewsPanel", () => {
|
|
|
95
90
|
expect(onClose).toHaveBeenCalled();
|
|
96
91
|
});
|
|
97
92
|
|
|
98
|
-
it("docked: the page is open at once
|
|
93
|
+
it("docked: the page is open at once with the index beside it, and keys are focus-scoped", async () => {
|
|
99
94
|
// The dock only exists on wide windows; jsdom defaults to 1024.
|
|
100
95
|
vi.stubGlobal("innerWidth", 1280);
|
|
101
96
|
const onClose = vi.fn();
|
|
@@ -118,14 +113,14 @@ describe("ViewsPanel", () => {
|
|
|
118
113
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
119
114
|
expect(onClose).not.toHaveBeenCalled();
|
|
120
115
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
expect(document.querySelector("
|
|
126
|
-
|
|
127
|
-
expect(document.querySelector(".views-
|
|
128
|
-
//
|
|
116
|
+
// The index sits beside the page in the dock too; ☰ hides it for a
|
|
117
|
+
// wider page and the choice is remembered.
|
|
118
|
+
expect(document.querySelector(".views-panel.rail-side .views-list")).toBeTruthy();
|
|
119
|
+
fireEvent.click(screen.getByRole("button", { name: "Hide the index" }));
|
|
120
|
+
expect(document.querySelector(".views-list")).toBeNull();
|
|
121
|
+
expect(store.hop_views_rail).toBe("0");
|
|
122
|
+
expect(document.querySelector("iframe.views-frame")).toBeTruthy();
|
|
123
|
+
// Esc from the panel closes it.
|
|
129
124
|
fireEvent.keyDown(panel, { key: "Escape" });
|
|
130
125
|
expect(onClose).toHaveBeenCalled();
|
|
131
126
|
});
|
package/package.json
CHANGED