diff --git a/src/components/main-ui.tsx b/src/components/main-ui.tsx
index 004011d..ddd8e8b 100644
--- a/src/components/main-ui.tsx
+++ b/src/components/main-ui.tsx
@@ -13,6 +13,7 @@ import { OrganiseUI } from "./organise";
import { HeaderLayout } from "./header-layout";
import { PDFPreviewPane } from "./pdf-preview";
import { useFileListContext } from "../provider/fileListProvider";
+import { KeyboardNavProvider } from "../provider/keyboardNavProvider";
import { Button } from "./ui/button";
interface MainUIProps {
@@ -50,33 +51,35 @@ export function MainUI(props: MainUIProps) {
return (
-
-
-
-
-
- 0 ? "30%" : 6}>
- 0}
- fallback={
-
-
- }
- >
- setIsPreviewOpen(true)}
- onClose={() => setIsPreviewOpen(false)}
- />
-
+
+
+
+
-
-
+
+ 0 ? "30%" : 6}>
+ 0}
+ fallback={
+
+
+ }
+ >
+ setIsPreviewOpen(true)}
+ onClose={() => setIsPreviewOpen(false)}
+ />
+
+
+
+
+
);
}
diff --git a/src/components/pdf-preview.tsx b/src/components/pdf-preview.tsx
index 78ae82e..a432f7c 100644
--- a/src/components/pdf-preview.tsx
+++ b/src/components/pdf-preview.tsx
@@ -1,5 +1,6 @@
import { TextAttributes, type BoxRenderable, type CliRenderer } from "@opentui/core";
import { onResize, useRenderer } from "@opentui/solid";
+import { useBindings } from "@opentui/keymap/solid";
import { Show, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js";
import {
clearPDFPreview,
@@ -12,6 +13,7 @@ import { useFileListContext } from "../provider/fileListProvider";
import { Button } from "./ui/button";
import { PreviewButton } from "./ui/preview-button";
import { PDFPreviewFrame } from "./ui/index";
+import { useKeyboardNav } from "../hooks/useKeyboardNav";
type RendererCapabilities = CliRenderer["capabilities"];
@@ -21,6 +23,8 @@ const hasKittyGraphics = (capabilities: RendererCapabilities | null) =>
export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void }) {
const renderer = useRenderer();
const fl = useFileListContext();
+ const nav = useKeyboardNav();
+
const selectedFile = fl.selectedFile;
const initialKittySupport = hasKittyGraphics(renderer.capabilities ?? null);
@@ -46,12 +50,15 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
let layoutRefreshTimer: ReturnType | null = null;
const totalPages = () => Math.max(pageCount() ?? 0, 1);
+
const canGoPrev = () => page() > 1;
const canGoNext = () => page() < totalPages();
+ const goToPrevPage = () => setPage((value) => Math.max(1, value - 1));
+ const goToNextPage = () => setPage((value) => Math.min(totalPages(), value + 1));
+
const showSupportProbe = () =>
Boolean(selectedFile()) && !supported() && !capabilityProbeExpired();
const showUnsupported = () => Boolean(selectedFile()) && !supported() && capabilityProbeExpired();
- // const showLoading = () => Boolean(selectedFile()) && supported() && isLoading();
const showError = () =>
Boolean(selectedFile()) && supported() && !isLoading() && Boolean(error());
@@ -222,12 +229,63 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
}
});
+ createEffect(() => {
+ nav.registerElement({
+ id: "prev-page",
+ type: "button",
+ persistent: true,
+ onEnter: () => setPage((value) => Math.max(1, value - 1)),
+ });
+
+ nav.registerElement({
+ id: "next-page",
+ type: "button",
+ persistent: true,
+ onEnter: () => setPage((value) => Math.min(totalPages(), value + 1)),
+ });
+ });
+
+ useBindings(() => ({
+ priority: 100,
+ bindings:
+ nav.isInputMode() || !selectedFile()
+ ? []
+ : [
+ {
+ key: "left",
+ cmd: () => {
+ if (canGoPrev()) goToPrevPage();
+ },
+ },
+ {
+ key: "h",
+ cmd: () => {
+ if (canGoPrev()) goToPrevPage();
+ },
+ },
+ {
+ key: "right",
+ cmd: () => {
+ if (canGoNext()) goToNextPage();
+ },
+ },
+ {
+ key: "l",
+ cmd: () => {
+ if (canGoNext()) goToNextPage();
+ },
+ },
+ ],
+ }));
+
onCleanup(() => {
renderer.off("capabilities", handleCapabilitiesChange);
cancelPendingPreviewRender();
clearCapabilityProbeTimer();
clearLayoutRefreshTimer();
clearActivePreview();
+ nav.unregisterElement("prev-page");
+ nav.unregisterElement("next-page");
});
return (
@@ -262,6 +320,7 @@ export function PDFPreviewPane(props: { onOpen: () => void; onClose: () => void
label="◀"
disabled={!canGoPrev()}
onClick={() => setPage((value) => Math.max(1, value - 1))}
+ focused={nav.isFocused("prev-page")}
/>
void; onClose: () => void
label="▶"
disabled={!canGoNext()}
onClick={() => setPage((value) => Math.min(totalPages(), value + 1))}
+ focused={nav.isFocused("next-page")}
/>
diff --git a/src/hooks/useKeyboardNav.ts b/src/hooks/useKeyboardNav.ts
index 9555354..0103ba5 100644
--- a/src/hooks/useKeyboardNav.ts
+++ b/src/hooks/useKeyboardNav.ts
@@ -1,10 +1,10 @@
-import { createSignal, createMemo } from "solid-js";
+import { createSignal, createMemo, createContext, useContext } from "solid-js";
import { useBindings } from "@opentui/keymap/solid";
import type { FocusableElement } from "../model/models";
export type { FocusableElement };
-export function useKeyboardNav() {
+export function createKeyboardNav() {
const [focusIndex, setFocusIndex] = createSignal(0);
const [elements, setElements] = createSignal([]);
const [isInputMode, setIsInputMode] = createSignal(false);
@@ -62,7 +62,7 @@ export function useKeyboardNav() {
const getFocusedId = () => focusedElement()?.id ?? null;
const clearElements = () => {
- setElements([]);
+ setElements((prev) => prev.filter((e) => e.persistent));
setFocusIndex(0);
};
@@ -70,11 +70,10 @@ export function useKeyboardNav() {
priority: 100,
bindings: isInputMode()
? [
- // Let escape bubble to the app-level double-esc handler in src/index.tsx
{
key: "escape",
cmd: () => setIsInputMode(false),
- preventDefault: false,
+ preventDefault: false, // Let escape bubble to the app-level double-esc handler in src/index.tsx
},
{
key: "tab",
@@ -114,3 +113,9 @@ export function useKeyboardNav() {
setIsInputMode,
};
}
+
+export const KeyboardNavContext = createContext>();
+
+export function useKeyboardNav() {
+ return useContext(KeyboardNavContext) ?? createKeyboardNav();
+}
diff --git a/src/model/models.ts b/src/model/models.ts
index 97174d9..4918dd3 100644
--- a/src/model/models.ts
+++ b/src/model/models.ts
@@ -9,6 +9,7 @@ export interface FocusableElement {
onEnter?: () => void;
canFocus?: () => boolean;
onMouseDown?: () => void;
+ persistent?: boolean; // keeps element registered across clearElements calls
}
export interface Status {
diff --git a/src/provider/keyboardNavProvider.tsx b/src/provider/keyboardNavProvider.tsx
new file mode 100644
index 0000000..24f2453
--- /dev/null
+++ b/src/provider/keyboardNavProvider.tsx
@@ -0,0 +1,9 @@
+import { type ParentComponent } from "solid-js";
+import { KeyboardNavContext, createKeyboardNav } from "../hooks/useKeyboardNav";
+
+// Provides a single keyboard-nav instance so descendant panels (the active tool
+// and the PDF preview) share one focus ring instead of competing rings.
+export const KeyboardNavProvider: ParentComponent = (props) => {
+ const nav = createKeyboardNav();
+ return {props.children};
+};