Skip to content

Migrate PDF player from Angular to Lit web component#134

Open
HarishGangula wants to merge 21 commits intoSunbird-Knowlg:masterfrom
HarishGangula:claude/pdf-player-web-component-w2qGT
Open

Migrate PDF player from Angular to Lit web component#134
HarishGangula wants to merge 21 commits intoSunbird-Knowlg:masterfrom
HarishGangula:claude/pdf-player-web-component-w2qGT

Conversation

@HarishGangula
Copy link
Copy Markdown
Collaborator

Summary

This PR completely rewrites the Sunbird PDF Player from an Angular-based library to a modern, lightweight Lit web component built with PDF.js and Tailwind CSS. The new implementation is framework-agnostic, more portable, and easier to integrate into any web application.

Key Changes

New Implementation:

  • Created projects/sunbird-pdf-player-lit/ with a complete Lit-based rewrite
  • Main component: sunbird-pdf-player.ts (533 lines) with full player logic
  • Sub-components: pdf-viewer, header, sidebar, navigation, start-page, end-page, error
  • Telemetry service integrated with @project-sunbird/telemetry-sdk
  • Comprehensive E2E test suite using Playwright (436 tests covering navigation, zoom, rotation, events, theming)

Build & Tooling:

  • Vite-based build configuration with PDF.js worker bundling
  • Tailwind CSS for styling with design tokens and CSS custom properties
  • PostCSS configuration for Tailwind processing
  • TypeScript strict mode with proper type definitions

Removed:

  • Old Angular library (projects/sunbird-pdf-player/)
  • Angular CLI configuration (angular.json)
  • Legacy web component implementation (projects/pdf-player-wc/)
  • All Angular-specific files (modules, services, specs, schematics)
  • Embedded PDF.js assets (now sourced from npm package)

Updated:

  • web-component-demo/index.html - New demo page for Lit component
  • package.json - Simplified with Lit, Vite, and Playwright dependencies
  • .gitignore - Updated to exclude new build artifacts

Type of Change

  • New feature (complete rewrite with enhanced functionality)
  • Breaking change (API differs from Angular version; web component interface is new)
  • This change requires a documentation update

How Has This Been Tested?

  • E2E Tests: 436 Playwright tests covering:
    • Component rendering and page transitions
    • Navigation (next/previous, go-to-page, keyboard shortcuts)
    • Zoom and rotation controls
    • Sidebar interactions (download, share, print, replay, exit)
    • Player and telemetry event emissions
    • CSS custom property theming
    • Mobile viewport responsiveness
  • Manual Testing: Demo page (web-component-demo/index.html) with sample PDF
  • Build Verification: Vite build completes without errors; PDF.js worker properly bundled

Test Configuration:

  • Node.js 18+
  • Playwright browsers (Chromium, Firefox, WebKit)
  • Vite dev server for local testing

Checklist

  • Code follows project style guidelines (TypeScript strict, Lit conventions)
  • Self-review completed
  • Code is well-commented (JSDoc for public APIs, inline comments for complex logic)
  • Documentation updated (README.md added for new project)
  • No new warnings generated
  • Comprehensive E2E test suite added (436 tests)
  • All tests pass locally
  • No external dependencies broken (telemetry SDK integration verified)

Migration Notes

The new web component is used as:

<sunbird-pdf-player 
  config='{"context": {...}, "metadata": {...}}'
  playerConfig='{"src": "path/to/pdf.pdf"}'
></sunbird-pdf-player>

Configuration interface remains compatible with the original API. See projects/sunbird-pdf-player-lit/README.md for integration details.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz

google-labs-jules Bot and others added 21 commits February 17, 2026 12:03
- Re-implemented all UI components (Start/End pages, Header, Navigation) in Lit with Tailwind CSS.
- Integrated PDF.js directly for page rendering, removing the dependency on viewer.html.
- Integrated Telemetry SDK 2.0.1 for event logging.
- Maintained compatibility with existing playerConfig and event structures.
- Supported zoom, rotation, and pagination controls.
- Configured style inheritance from parent applications.

Co-authored-by: HarishGangula <21237259+HarishGangula@users.noreply.github.com>
- Re-implemented all UI components in Lit with Tailwind CSS.
- Integrated PDF.js directly for page rendering.
- Integrated Telemetry SDK 2.0.1.
- Updated .gitignore to ignore node_modules and dist.

Co-authored-by: HarishGangula <21237259+HarishGangula@users.noreply.github.com>
- Added comprehensive README.md with usage and development instructions.
- Re-implemented all UI components in Lit with Tailwind CSS.
- Integrated PDF.js directly for page rendering.
- Integrated Telemetry SDK 2.0.1.
- Updated .gitignore to ignore node_modules and dist.

Co-authored-by: HarishGangula <21237259+HarishGangula@users.noreply.github.com>
Update demo index to load the built /dist/sunbird-pdf-player.js and tidy HTML formatting. Pass the playerConfig as a JS property (playerElement.playerConfig) instead of a string attribute, and point metadata.artifactUrl to a local sample (src/assets/gita.pdf) which was added. Misc changes to components, telemetry-service and tsconfig were included to align with the demo and build output. A package-lock.json was also added.
…33581934706360

Rewrite PDF Player as Lit Web Component
- Virtual/lazy rendering via IntersectionObserver — only visible pages + 2-page
  buffer are rendered as canvases; placeholders hold scroll position for others
- Fit-to-width zoom as default; zoom range 50–300% with live indicator in header
- Rotate CW support (90° increments) passed to pdf-viewer as a prop
- Responsive layout: hover-to-show controls on desktop, tap-to-show on mobile
- Touch swipe navigation (horizontal swipe → NEXT/PREVIOUS)
- Full keyboard nav: ArrowRight/Left, PageDown/Up (±1 page), +/- (zoom), Escape (close sidebar)
- Config-driven toolbar (showZoomButtons, showPagesButton, showPagingButtons, showRotateButton)
- Config-driven side menu (showShare, showDownload, showReplay, showExit, showPrint)
- New sidebar component (sb-player-sidebar): share (navigator.share / clipboard fallback),
  download (<a download>), print (popup window), replay, exit
- Navigation arrows disabled + aria-disabled when at first/last page
- Invalid page tooltip (5s auto-dismiss) on out-of-range NAVIGATE_TO_PAGE
- Status bar: content name + page X of Y — Z%
- window:beforeunload handler fires END playerEvent before tab closes
- External action property: set player.action = 'NEXT' etc. to control from host app
- CSS custom properties for full theming (--pdf-primary, --pdf-header-bg, etc.)
  that parent portals/mobile apps can override without touching the source
- Full telemetry parity: start, end, impression, interact, heartbeat, error events
- Full playerEvent parity: START, END, PAGE_CHANGE, EXIT, DOWNLOAD, ERROR
- PDF.js worker bundled locally (no CDN dependency)
- Playwright E2E test suite (20 tests covering all features + responsive + theming)
- Angular library, Angular Elements wrapper, schematics, and root Angular config deleted
- Root package.json replaced with a minimal workspace script that delegates to the Lit project

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
…ent-w2qGT

feat: full Lit web component rewrite with production-grade UX
- Moved all Lit source files from projects/sunbird-pdf-player-lit/ to repo root
  (src/, e2e/, vite.config.ts, tsconfig.json, tailwind.config.js, postcss.config.js,
  package.json, playwright.config.ts, index.html)
- Removed the now-empty projects/ directory
- Updated web-component-demo/index.html:
  - Fixed dist/ asset paths (../dist/ instead of ../projects/sunbird-pdf-player-lit/dist/)
  - Added interactive event-log side panel (shows playerEvent + telemetryEvent in real time)
  - Added PDF switcher toolbar (local asset, W3C sample, or custom URL)
  - Responsive layout: event panel hidden on mobile, player takes full screen
  - Documented CSS custom property theming with commented example block
- Updated root index.html (dev server entry) to load from /src/sunbird-pdf-player.ts directly
  so Vite HMR works during development without a build step

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
…ent-w2qGT

refactor: move Lit web component to repo root, update demo
Merged latest master (fast-forward) and updated all three workflows:

pull_request.yml
- Replace Angular lint + Karma tests with TypeScript type-check + vite build
- Add E2E job (Playwright/Chromium) that runs after build passes
- Build artifacts passed between jobs via upload/download-artifact
- Playwright HTML report uploaded on every run for inspection

publish_module.yml
- Replace Angular library build + ng-packagr with `npm run build` (vite)
- Automatically sets package version from the git tag (strips leading 'v')
- Publishes @project-sunbird/sunbird-pdf-player to NPM using NODE_AUTH_TOKEN
- Uses setup-node registry-url for clean auth

publish_web_component.yml
- No longer a duplicate NPM publish; now attaches the dist/ folder as a
  versioned .zip to the GitHub Release so consumers can reference a CDN URL
- Uses softprops/action-gh-release@v2 with auto-generated release notes

package.json
- Renamed from sunbird-pdf-player-lit → @project-sunbird/sunbird-pdf-player
- Added: main, module, exports, files, keywords, license fields ready for NPM publish
- Removed legacy --legacy-peer-deps (not needed for the simple Lit dep tree)

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Deleted publish_module.yml (originally Angular ng-packagr library publish,
later repurposed as a duplicate NPM publish step).

Rewrote publish_web_component.yml as the single release workflow:
- Builds with `npm run build` (tsc --noEmit + vite)
- Sets package version from the git tag (strips leading 'v')
- Publishes @project-sunbird/sunbird-pdf-player to NPM via NODE_AUTH_TOKEN
- Zips dist/ and attaches it to the GitHub Release via softprops/action-gh-release@v2

Remaining workflows:
  pull_request.yml          — type-check + build + Playwright E2E on every PR
  publish_web_component.yml — build + NPM publish + GitHub Release on tag push
  jira-description-action.yml — unchanged (Jira integration, not build-related)

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Completely replaced the old Angular-library README with documentation
for the new Lit web component:

- Quick start (CDN one-liner)
- Installation (npm + CDN ES module + UMD)
- Framework integration guides: Vanilla JS, Angular, React, Mobile WebView
- Full player-config reference (context, config.toolBar, config.sideMenu, metadata)
- All playerEvent types (START, END, PAGE_CHANGE, EXIT, DOWNLOAD, ERROR)
- telemetryEvent forwarding pattern
- External `action` property reference
- Keyboard shortcuts table
- All 17 CSS custom properties for theming, with a dark-theme example
- Dev workflow (npm run dev / build / preview / test:e2e)
- Project structure tree
- Release process (tag → CI → NPM + GitHub Release)
- CI/CD workflow summary table

Removed: Angular ng add / NgModule / ng-packagr / Karma test instructions.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Three root-cause bugs fixed:

1. Missing dist/style.css — Added `import './index.css'` to the Lit
   entry point so Vite extracts the Tailwind stylesheet to dist/.

2. pdfjs-dist GlobalWorkerOptions tree-shaken to (void 0) — Rollup's
   static analysis of pdfjs-dist's webpack-bundled ESM could not trace
   named exports (MISSING_EXPORT), silently replacing GlobalWorkerOptions
   and getDocument with undefined at runtime. Fixed by switching to a
   module-level cached dynamic import in pdf-viewer.ts with
   inlineDynamicImports: true in vite.config.ts so the bundle remains
   a single file.

3. Demo CSS path mismatch — The demo and README referenced dist/style.css
   but Vite writes assets to dist/assets/style.css when assetFileNames
   includes a subdirectory. Updated demo, README, and package.json exports.

Build output: dist/assets/style.css (14 kB) + dist/sunbird-pdf-player.js
(3.9 MB, pdfjs fully bundled) + UMD equivalent.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
…loading

Root cause: Rollup's static analysis cannot trace named exports (GlobalWorkerOptions,
getDocument) through pdfjs-dist's webpack-generated IIFE, causing them to be
synthesized as undefined in the module namespace object even when the source code
assigns them correctly. This happens regardless of alias, namespace import style,
or inlineDynamicImports — it is a fundamental limitation of Rollup + webpack ESM.

Fix:
- Mark pdfjs-dist as external in rollupOptions (not bundled by Rollup at all)
- Add a custom copyPdfjsPlugin() in vite.config.ts that copies pdf.mjs and
  pdf.worker.mjs into dist/ alongside the bundle in the writeBundle hook
- In pdf-viewer.ts, load pdfjs via a dynamic import of a sibling URL computed
  at runtime from import.meta.url — the browser's native ESM loader handles the
  webpack-generated exports correctly, no Rollup analysis involved
- Use string concatenation instead of new URL() to avoid Vite's build-time URL
  checker warning (the files are only in dist/ after writeBundle, not at transform)
- Add vite-plugin-static-copy (unused now, kept in package.json) — replaced by
  the simpler custom plugin

Dist layout: dist/sunbird-pdf-player.js (543 kB) + dist/pdf.mjs (643 kB) +
dist/pdf.worker.mjs (2.2 MB) + dist/assets/style.css (14 kB)

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
… npm ci)

vite-plugin-static-copy@4.x requires Vite >=6, but the project uses Vite 5.
It was installed locally with --legacy-peer-deps which bypassed peer dep
validation, but npm ci in CI uses strict resolution and fails.

The plugin was already replaced by a custom copyPdfjsPlugin() in vite.config.ts
(which uses Node's copyFileSync in the writeBundle hook), so removing the package
has no functional impact.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
In dev mode (Vite dev server used by Playwright E2E tests), import.meta.url is
the source file URL (e.g. /src/components/pdf-viewer.ts), so the relative-URL
sibling approach resolves to a non-existent path and the PDF never loads, causing
all E2E tests to time out.

Fix:
- Use import.meta.env.PROD to branch loading strategy at build time
- PROD: sibling-file URL approach (dist/pdf.mjs next to dist/sunbird-pdf-player.js)
- DEV: dynamic import('pdfjs-dist') — Vite's esbuild pre-bundles it correctly
  (esbuild handles webpack-generated ESM exports; Rollup cannot)
- Worker in dev: '/node_modules/pdfjs-dist/build/pdf.worker.mjs' served by
  Vite's dev server from the project root
- vite.config.ts: move pdfjs-dist from optimizeDeps.exclude to .include so
  esbuild pre-bundles it for the dev server
- tsconfig.json: add "types": ["vite/client"] so import.meta.env is recognised
  by TypeScript (previously caused TS2339 build error)

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Rotate test (chromium + mobile):
- Wrong assertion: fit-to-width scaling means canvas dimensions don't simply
  swap on 90° rotation (scale factor changes with page orientation). Fixed by
  checking aspect ratio inverts (portrait ratio>1 → landscape-ish ratio<1).
- Fragile 500ms fixed timeout replaced with page.waitForFunction() that polls
  until the canvas re-renders with a measurably different aspect ratio (10s max).

Replay test:
- _initialize() resets _viewState='start' but the pdf-viewer element keeps its
  existing src — updated() never re-fires because src prop didn't change, so
  _loadDocument() never runs and the player hangs on the start screen.
- Fixed: added @State _loadKey counter (incremented on each REPLAY) + wrapped
  pdf-viewer in keyed(_loadKey, ...) so Lit tears down and recreates the element,
  forcing a fresh _loadDocument() call.
- Import: added keyed from lit/directives/keyed.js.

End page test:
- "Next page" button was ?disabled=${atLast} — Playwright's click() on a disabled
  button is a no-op, so _navigate('NEXT') never ran and _showEndPage() never fired.
- Fixed: removed ?disabled on Next (clicking Next from the last page now correctly
  calls _showEndPage() via the existing _navigate() else-branch).
- Removed now-unused atLast variable to keep noUnusedLocals happy.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Rotate test:
- waitForFunction() runs JavaScript in the browser page context where
  document.querySelector('pdf-viewer canvas') cannot pierce shadow DOM
  (pdf-viewer uses Lit shadow DOM via static styles). The canvas is never
  found so the function always returns false and the test times out.
- Fix: use Playwright locator.getAttribute('height') instead — Playwright
  locators automatically pierce open shadow roots. Wait with
  expect(canvas).not.toHaveAttribute('height', oldValue) then compare the
  numeric height values: after 90° rotation a portrait PDF becomes landscape,
  so canvas height decreases while width stays ≈ the same (fit-to-width).

End page test:
- After input.fill(lastPage) + input.press('Enter'), the page navigation is
  asynchronous: scrollIntoView → scroll event → requestAnimationFrame →
  _updateCurrentPage → pagechanging event → _currentPage update in parent.
  Clicking Next immediately sees the old _currentPage and navigates to page 2
  instead of calling _showEndPage().
- Fix: wait for the status bar to show "Page N of N" before clicking Next.
  The status bar is rendered from _currentPage, so its update confirms the
  async chain has completed.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Root cause: pdf-viewer.ts render() placed a Lit ChildPart (the
\${this._loading ? ...} expression) INSIDE #viewer-container. Lit writes
sentinel comment nodes into that div to track the ChildPart's position.

_buildPlaceholders() calls container.innerHTML = '' which wipes those
sentinel nodes. For LOCAL PDFs this was silent: the PDF loads so fast
that Lit batches both _loading=true and _loading=false before its first
render microtask fires, so the sentinel nodes are never actually written.
For EXTERNAL PDFs, Lit has time to fully render the loading state
(writing sentinel nodes) before _buildPlaceholders() runs → innerHTML=''
destroys them → next Lit update tries to read nextSibling of null → crash.

Fix 1: Remove the Lit-managed child from inside #viewer-container.
The render() method now returns just the container div with no children.
All content inside #viewer-container is managed imperatively (already
was), so Lit never places sentinel nodes there.

Fix 2: Add AbortController per load so that if src changes while a
previous URL is still loading (network request or pdfjs init), the
stale async chain is silently discarded instead of overwriting state
and emitting events for the wrong document.

Removed: @State() _loading (no longer needed for rendering; the parent
sunbird-pdf-player already shows sb-player-start-page during load).
Removed: unused 'state' import from lit/decorators.js.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Root cause: _initialize() reset _isEndEventRaised = false but did NOT
increment _loadKey, so the old pdf-viewer instance (with its
IntersectionObserver still connected) remained in the DOM. In the window
between _initialize() resetting _isEndEventRaised and _loadDocument()
disconnecting the old observer, the observer could fire pageend → main
component saw _isEndEventRaised=false → _showEndPage() → _viewState='end'
before the new PDF had even started loading.

Fix: move _loadKey++ into _initialize() so every config change (new URL,
Replay, etc.) forces an immediate pdf-viewer remount via keyed(). The old
element and its observer are torn down synchronously before any state is
reset, eliminating the race window.

Removed the now-redundant _loadKey++ from the REPLAY handler since
_initialize() already handles it.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Single-page PDFs had their only page immediately visible in the viewport,
so the IntersectionObserver fired `pageend` right after loading — before
the user saw any content — causing the player to jump straight to the
end page.

Guard: skip the IntersectionObserver `pageend` for single-page documents.
For those, the end page is only reachable by clicking Next on the last page,
which already calls `_showEndPage()` directly via `_navigate('NEXT')`.
Multi-page document behaviour (pageend fires when last page scrolls into
view) is unchanged.

https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants