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 the newest result (the
11
- // scoped session's, or the fleet's), with ‹ › to step through the rest and
12
- // an index you can pull open beside or over the page. There is no separate
13
- // "list mode" to click through first — the list was a detour on the way to
14
- // the thing you came to read.
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 to read a report AND keep the index beside it; still leaves a
172
- // usable terminal on a 1440 display. The drag handle remembers a choice.
173
- const DEFAULT_DOCK_W = 620;
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(380, Math.min(Math.round(window.innerWidth * 0.7), w));
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) is a companion to the
203
- // reader, not a mode of its own: a sheet over the page in a narrow dock, a
204
- // rail beside it where there is room — either way only when you ask.
205
- // Hidden until asked for, everywhere: the page is what you opened Views
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) === "1"; } catch { return false; }
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 index OVER the page is the one layer to back out of; the
316
- // reader itself is the panel, so the next Escape closes it.
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, indexOpen, dockW]);
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
- // Where the index lives: beside the page when the panel is wide enough for
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" : "Everything published"}
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 ? (railOverlay ? " rail-over" : " rail-side") : "")}
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 && !railOverlay && list}
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: 240px; }
4515
- /* The index OVER the page (a narrow dock): a sheet under the nav bar. */
4516
- .views-index {
4517
- position: absolute; inset: 0; z-index: 2;
4518
- display: flex; flex-direction: column;
4519
- background: var(--panel);
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
- // The index rail is hidden until asked and the choice is remembered; most
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 not shown until asked — the page is the point. ☰ opens
78
- // it beside the page; a plain click on a row turns the page.
79
- expect(document.querySelector(".views-list")).toBeNull();
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, the index rides OVER it on demand, and keys are focus-scoped", async () => {
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
- // A 620px dock has no room for the index beside the page: ☰ lays it
122
- // OVER the page, and Esc from the panel backs that one layer out.
123
- fireEvent.click(screen.getByRole("button", { name: "Show the index" }));
124
- expect(document.querySelector(".views-index .views-list")).toBeTruthy();
125
- expect(document.querySelector("iframe.views-frame")).toBeTruthy(); // the page stays under it
126
- fireEvent.keyDown(panel, { key: "Escape" });
127
- expect(document.querySelector(".views-index")).toBeNull();
128
- // The next Esc from the panel closes it.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hop2",
3
- "version": "0.11.59",
3
+ "version": "0.11.60",
4
4
  "description": "🐰 Hop into your terminal from anywhere. Secure remote shell access with 2FA via Cloudflare Tunnel. Next-gen rewrite with native terminal sharing.",
5
5
  "main": "hop",
6
6
  "bin": {