Migrate PDF player from Angular to Lit web component#134
Open
HarishGangula wants to merge 21 commits intoSunbird-Knowlg:masterfrom
Open
Migrate PDF player from Angular to Lit web component#134HarishGangula wants to merge 21 commits intoSunbird-Knowlg:masterfrom
HarishGangula wants to merge 21 commits intoSunbird-Knowlg:masterfrom
Conversation
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
projects/sunbird-pdf-player-lit/with a complete Lit-based rewritesunbird-pdf-player.ts(533 lines) with full player logicpdf-viewer,header,sidebar,navigation,start-page,end-page,error@project-sunbird/telemetry-sdkBuild & Tooling:
Removed:
projects/sunbird-pdf-player/)angular.json)projects/pdf-player-wc/)Updated:
web-component-demo/index.html- New demo page for Lit componentpackage.json- Simplified with Lit, Vite, and Playwright dependencies.gitignore- Updated to exclude new build artifactsType of Change
How Has This Been Tested?
web-component-demo/index.html) with sample PDFTest Configuration:
Checklist
Migration Notes
The new web component is used as:
Configuration interface remains compatible with the original API. See
projects/sunbird-pdf-player-lit/README.mdfor integration details.https://claude.ai/code/session_01Awuia5KaQMV65ZbUssJLHz