diff --git a/CHANGELOG.md b/CHANGELOG.md
index 821aa62..b5727f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,12 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
-- Table of contents display.
+- Improved navigation UI with a modern sidebar layout for Table of Contents.
+- Navigation controls (Previous/Next) and TOC interaction logic implemented in Python.
+- Automatic navigation button state management (disabling at spine ends).
+- Active Table of Contents entry highlighting.
+- End-to-end tests using Playwright.
+- Representative screenshot in README.md.
- Project documentation (`CONTRIBUTING.md`, `CHANGELOG.md`, `AGENTS.md`).
- Custom exception hierarchy for error handling.
- `LICENSE` file (Apache 2.0).
### Changed
+- Refactored `index.html` to support a more structured layout.
+- Extended `DOMElement` protocol to include `disabled` and `className` properties.
- Updated documentation for accuracy and completeness.
- Updated Pyodide version in demo to v0.29.1.
diff --git a/README.md b/README.md
index 19c7f06..6eb3db2 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
This is the initial implementation of the `Imposition` library, a Python library intended to run under Pyodide for parsing and rendering EPUB files.
+
+
## Installation
This package is not yet available on PyPI. To install it, you must build the wheel from the source and then install it using `pip`:
diff --git a/assets/screenshot.png b/assets/screenshot.png
new file mode 100644
index 0000000..a4b2ac3
Binary files /dev/null and b/assets/screenshot.png differ
diff --git a/index.html b/index.html
index b40a143..7953473 100644
--- a/index.html
+++ b/index.html
@@ -1,25 +1,141 @@
header if it exists, otherwise clear
+ # Actually, in index.html I have
Contents
.
+ # rendition previously did: toc_container.innerHTML = ''
+ # Let's see if there's a specific container for the list.
+ # In my new index.html I didn't add a specific div for the list,
+ # but I can just append the ul to the toc div.
+ # To avoid clearing the
, I could find the ul or just append.
+ # Let's just clear everything for now as it was before, or better,
+ # find where to inject.
+
+ # If I want to be safe, I'll look for a
and replace it, or just clear.
+ # The previous implementation was:
toc_container.innerHTML = ''
+ # I'll add the
back from Python if I clear it.
+ header: DOMElement = self.dom_adapter.create_element('h3')
+ header.textContent = "Contents"
+ toc_container.appendChild(header)
+
+ self.toc_links = []
ul: DOMElement = self.dom_adapter.create_element('ul')
for item in self.book.toc:
@@ -66,10 +104,12 @@ def handler(event: DOMElement) -> None:
# Create a proxy for the onclick event handler
a.onclick = self.dom_adapter.create_proxy(create_handler(item['url']))
+ self.toc_links.append((a, item['url']))
li.appendChild(a)
ul.appendChild(li)
toc_container.appendChild(ul)
+ self.update_controls()
def display(self, chapter_url: Optional[str] = None) -> None:
@@ -95,6 +135,9 @@ def display(self, chapter_url: Optional[str] = None) -> None:
else:
chapter_href = self.book.spine[0]
+ if chapter_href in self.book.spine:
+ self.current_chapter_index = self.book.spine.index(chapter_href)
+
chapter_content: bytes = self.book.zip_file.read(chapter_href)
try:
@@ -134,6 +177,7 @@ def display(self, chapter_url: Optional[str] = None) -> None:
self.target_element.innerHTML = ''
self.target_element.appendChild(self.iframe)
+ self.update_controls()
def _embed_asset(self, element: ET.Element, attribute: str, chapter_path: str) -> None:
asset_path: Optional[str] = element.get(attribute)
@@ -157,6 +201,23 @@ def _embed_asset(self, element: ET.Element, attribute: str, chapter_path: str) -
print(f"Asset not found: {full_asset_path}")
pass
+ def update_controls(self) -> None:
+ """
+ Updates the state of navigation buttons and TOC highlighting.
+ """
+ if self.prev_button:
+ self.prev_button.disabled = (self.current_chapter_index == 0)
+ if self.next_button:
+ self.next_button.disabled = (self.current_chapter_index >= len(self.book.spine) - 1)
+
+ current_href = self.book.spine[self.current_chapter_index]
+ for a, url in self.toc_links:
+ chapter_url = url.split('#')[0]
+ if chapter_url == current_href:
+ a.className = 'active'
+ else:
+ a.className = ''
+
def next_chapter(self, event: Optional[Any] = None) -> None:
"""
Displays the next chapter in the book's spine.
diff --git a/tests/mocks.py b/tests/mocks.py
index 7fd4e45..714d630 100644
--- a/tests/mocks.py
+++ b/tests/mocks.py
@@ -14,6 +14,8 @@ def __init__(self, tag_name: str) -> None:
self.onclick: Optional[Callable[[Any], None]] = None
self.onload: str = ""
self.src: str = ""
+ self.disabled: bool = False
+ self.className: str = ""
self.preventDefault: Mock = Mock()
def appendChild(self, child: "MockDOMElement") -> None:
diff --git a/tests/screenshots/test_renders_first_chapter_passed.png b/tests/screenshots/test_renders_first_chapter_passed.png
index 565ea32..3f89542 100644
Binary files a/tests/screenshots/test_renders_first_chapter_passed.png and b/tests/screenshots/test_renders_first_chapter_passed.png differ
diff --git a/tests/test_e2e.py b/tests/test_e2e.py
index aa52231..e9f80df 100644
--- a/tests/test_e2e.py
+++ b/tests/test_e2e.py
@@ -21,12 +21,9 @@ def test_renders_first_chapter(page: Page, http_server):
iframe = viewer.locator("iframe")
expect(iframe).to_be_visible(timeout=15000)
- # The iframe should contain the cover image
- iframe_element = iframe.element_handle()
- assert iframe_element is not None
- frame = iframe_element.content_frame()
- assert frame is not None
- expect(frame.locator("img.x-ebookmaker-cover")).to_be_visible()
+ # Use frame_locator for better stability
+ frame_locator = page.frame_locator("#viewer iframe")
+ expect(frame_locator.locator("img.x-ebookmaker-cover")).to_be_visible()
# Take a screenshot on success
page.screenshot(path=f"{SCREENSHOT_DIR}/test_renders_first_chapter_passed.png")
@@ -36,3 +33,63 @@ def test_renders_first_chapter(page: Page, http_server):
# On failure, take a screenshot
page.screenshot(path=f"{SCREENSHOT_DIR}/test_renders_first_chapter_failed.png")
pytest.fail(f"Uncaught exception in browser console: {error_logs[0]}")
+
+def test_navigation_buttons(page: Page, http_server):
+ page.goto(http_server)
+
+ # Wait for app to load
+ iframe = page.locator("#viewer iframe")
+ expect(iframe).to_be_visible(timeout=15000)
+
+ prev_button = page.locator("#prev")
+ next_button = page.locator("#next")
+
+ # Initially "Previous" should be disabled
+ expect(prev_button).to_be_disabled()
+ expect(next_button).to_be_enabled()
+
+ frame_locator = page.frame_locator("#viewer iframe")
+ initial_text = frame_locator.locator("body").text_content() or ""
+
+ # Click "Next"
+ next_button.click()
+
+ # Wait for "Previous" to become enabled
+ expect(prev_button).to_be_enabled()
+
+ # Verify content changed
+ expect(frame_locator.locator("body")).not_to_have_text(initial_text, timeout=10000)
+
+ # Click "Previous"
+ prev_button.click()
+ expect(prev_button).to_be_disabled()
+ expect(frame_locator.locator("body")).to_have_text(initial_text)
+
+def test_toc_navigation(page: Page, http_server):
+ page.goto(http_server)
+
+ # Wait for app to load
+ expect(page.locator("#viewer iframe")).to_be_visible(timeout=15000)
+
+ toc_links = page.locator("#toc a")
+ # test_book.epub has 24 TOC links
+ expect(toc_links).to_have_count(24)
+
+ frame_locator = page.frame_locator("#viewer iframe")
+ initial_text = frame_locator.locator("body").text_content() or ""
+
+ # Click a link that is definitely in another file (e.g. STAVE TWO)
+ # Stave Two is link index 12 (playOrder 13)
+ stave_two_link = toc_links.nth(12)
+ expect(stave_two_link).to_have_text("STAVE TWO.")
+ stave_two_link.click()
+
+ # Verify it is now active
+ expect(stave_two_link).to_have_class("active")
+
+ # Verify content changed
+ expect(frame_locator.locator("body")).not_to_have_text(initial_text, timeout=10000)
+
+ # Verify buttons state
+ expect(page.locator("#prev")).to_be_enabled()
+ expect(page.locator("#next")).to_be_enabled()
diff --git a/tests/test_rendition.py b/tests/test_rendition.py
index 5c6ee0a..bccd699 100644
--- a/tests/test_rendition.py
+++ b/tests/test_rendition.py
@@ -47,9 +47,14 @@ def test_display_toc(mock_book, mock_dom_adapter):
toc_container = mock_dom_adapter.get_element_by_id("toc")
assert toc_container.innerHTML == ''
- assert len(toc_container.children) == 1
+ # Expect 2 children: