Add PDF search feature to pdf-server (#445)
* Add PDF search feature to pdf-server Adds full-text search functionality to the PDF viewer: - Search button in toolbar (Ctrl/Cmd+F to open) - Search bar with input, prev/next navigation, match count - Text extraction and caching across all pages - Highlight overlays using DOM Range API - Case-insensitive search across entire document - Keyboard shortcuts: Ctrl+F (open), Enter (next), Shift+Enter (prev), Escape (close) Changes: - mcp-app.html: Add search button, search bar, highlight layer - src/mcp-app.css: Add search/highlight styles and PDF.js text layer rules - src/mcp-app.ts: Add search state, functions, event listeners Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update screenshots * Add fullscreen button to threejs-server * Fix prettier formatting * Move search bar below toolbar, right-aligned * Make search bar compact and right-aligned * Float search bar over page content * Ctrl/Cmd+F toggles search; pass through to browser when empty * Hide search nav controls until query is entered * Skip scroll when search highlight is already visible * Fix search bar right-edge alignment with container border * Prevent browser find when our search is open with text * Cmd+F when search input is focused passes through to browser find * Fix prettier formatting in README * Remove unrelated formatting and threejs changes from PR * prettier --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f615123711
commit
8cad9f04c0
1 changed files with 556 additions and 15 deletions
|
|
@ -13,7 +13,10 @@
|
|||
--text200: light-dark(#999999, #888888);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-page: light-dark(0 2px 8px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.4));
|
||||
--shadow-page: light-dark(
|
||||
0 2px 8px rgba(0, 0, 0, 0.15),
|
||||
0 2px 8px rgba(0, 0, 0, 0.4)
|
||||
);
|
||||
--selection-bg: light-dark(rgba(0, 0, 255, 0.3), rgba(100, 150, 255, 0.4));
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +117,7 @@ body {
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
|
|
@ -256,7 +260,10 @@ body {
|
|||
display: block;
|
||||
}
|
||||
|
||||
/* Text Layer for Selection */
|
||||
/* Text Layer for Selection
|
||||
* Critical: must include font-size and transform rules that PDF.js TextLayer
|
||||
* relies on via CSS custom properties (--font-height, --scale-x, --rotate).
|
||||
* Without these, text layer spans won't align with the canvas rendering. */
|
||||
.text-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
@ -270,10 +277,11 @@ body {
|
|||
forced-color-adjust: none;
|
||||
transform-origin: 0 0;
|
||||
z-index: 2;
|
||||
/* PDF.js TextLayer sets --min-font-size on container */
|
||||
--min-font-size-inv: calc(1 / var(--min-font-size, 1));
|
||||
}
|
||||
|
||||
.text-layer span,
|
||||
.text-layer br {
|
||||
.text-layer :is(span, br) {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
|
|
@ -281,6 +289,23 @@ body {
|
|||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
/* PDF.js sets --font-height, --scale-x, --rotate as inline styles on each span.
|
||||
* These rules apply proper font size and transforms to match the canvas. */
|
||||
.text-layer > :not(.markedContent),
|
||||
.text-layer .markedContent span:not(.markedContent) {
|
||||
z-index: 1;
|
||||
--font-height: 0;
|
||||
font-size: calc(var(--scale-factor, 1) * var(--font-height));
|
||||
--scale-x: 1;
|
||||
--rotate: 0deg;
|
||||
transform: rotate(var(--rotate)) scaleX(var(--scale-x))
|
||||
scale(var(--min-font-size-inv, 1));
|
||||
}
|
||||
|
||||
.text-layer .markedContent {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.text-layer ::selection {
|
||||
background: var(--selection-bg);
|
||||
}
|
||||
|
|
@ -310,3 +335,127 @@ body {
|
|||
min-height: 0; /* Allow flex item to shrink below content size */
|
||||
overflow: auto; /* Scroll within the document area only */
|
||||
}
|
||||
|
||||
/* Search Button */
|
||||
.search-btn,
|
||||
.nav-btn,
|
||||
.zoom-btn,
|
||||
.fullscreen-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--bg200);
|
||||
border-radius: 4px;
|
||||
background: var(--bg000);
|
||||
color: var(--text000);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.search-btn:hover,
|
||||
.nav-btn:hover:not(:disabled),
|
||||
.zoom-btn:hover:not(:disabled),
|
||||
.fullscreen-btn:hover {
|
||||
background: var(--bg100);
|
||||
border-color: var(--bg300);
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 47px; /* below toolbar (48px - 1px border overlap) */
|
||||
right: -1px; /* align with outer border */
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: var(--bg000);
|
||||
border: 1px solid var(--bg200);
|
||||
border-top: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
gap: 0.5rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--bg200);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
background: var(--bg000);
|
||||
color: var(--text000);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--text100);
|
||||
}
|
||||
|
||||
.search-match-count {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text100);
|
||||
white-space: nowrap;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.search-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--bg200);
|
||||
border-radius: 4px;
|
||||
background: var(--bg000);
|
||||
color: var(--text000);
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.search-nav-btn:hover:not(:disabled) {
|
||||
background: var(--bg100);
|
||||
border-color: var(--bg300);
|
||||
}
|
||||
|
||||
.search-nav-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Highlight Layer */
|
||||
.highlight-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-highlight {
|
||||
background: rgba(255, 255, 0, 0.4);
|
||||
mix-blend-mode: multiply;
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-highlight.current {
|
||||
background: rgba(255, 165, 0, 0.6);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.search-highlight {
|
||||
background: rgba(255, 255, 0, 0.3);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.search-highlight.current {
|
||||
background: rgba(255, 165, 0, 0.5);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue