npm package: @sankaran-8600/component-viewer
Description: jQuery plugin for attachment and media preview in an overlay
Latest version: 1.0.7

# ComponentViewer

[![npm version](https://img.shields.io/npm/v/@sankaran-8600/component-viewer.svg)](https://www.npmjs.com/package/@sankaran-8600/component-viewer)

A jQuery plugin that opens attachments (images, video, audio, PDF, inline content, and more) in a shared overlay with a consistent toolbar, themes, and optional accessibility (WCAG) support. Built for feed- or post-style UIs where each container has its own set of items.

**Install from npm:** [`npm i @sankaran-8600/component-viewer`](https://www.npmjs.com/package/@sankaran-8600/component-viewer) · **License:** MIT

---

## Table of contents

- [Demo](#demo)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Options reference](#options-reference)
- [Item data](#item-data)
- [Content types](#content-types)
- [Toolbar](#toolbar)
- [Keyboard shortcuts](#keyboard-shortcuts)
- [Poll options](#poll-options)
- [Callbacks](#callbacks)
- [Public API](#public-api)
- [Accessibility (WCAG)](#accessibility-wcag)
- [Browser support](#browser-support)
- [Examples](#examples)

---

## Demo

Try the plugin in your browser with the full example page, which includes all content types, toolbar customisation, themes, poll options, loop/counter options, and image failure handling.

**[Open example.html](example.html)** — open this file in a browser to see multiple demo cases and use the programmatic API buttons to open specific items.

The example page uses jQuery 3.7, jPlayer, and PDF.js (loaded from CDN). Without jPlayer or PDF.js, the plugin uses native HTML5 media and an iframe for PDF — use a minimal page that loads only jQuery + the plugin to try that.

---

## Installation

### Dependencies

- **jQuery** (1.7+ or 2.x/3.x)
- **Optional:** [jPlayer](https://jplayer.org/) for video/audio playback
- **Optional:** [PDF.js](https://mozilla.github.io/pdf.js/) for PDF rendering
- **Optional:** [marked](https://github.com/markedjs/marked) for full Markdown (CommonMark) preview; otherwise a minimal built-in parser is used

Include the plugin and stylesheet after jQuery:

```html
<link rel="stylesheet" href="component-viewer.css" />
<script src="jquery.min.js"></script>
<script src="component-viewer.js"></script>
```

**When embedding in another project:** Load `component-viewer.css` *after* your app’s base/reset styles so the viewer’s look isn’t overridden. The stylesheet is plain CSS; **if you bundle with Less**, do not process this file as Less (Less interprets `min()`/`max()` and can change output). Either link to it as a separate stylesheet or import it unchanged: `@import (inline) "component-viewer.css";`

For video/audio with jPlayer, include jPlayer before the plugin. For PDF with pdf.js, include the library and set `pdf.workerSrc`. For full Markdown support, include marked (e.g. from a CDN) before the plugin.

**Minified build:** From this folder run `npm install` then `npm run build` to generate `component-viewer.min.js` and `component-viewer-japanese.min.js` (Terser). Use the `.min.js` files in production for smaller downloads.

To run the project’s code check (ESLint, aligned with [CodeCheck](https://cm-wiki.csez.zohocorpin.com/docs/Repository/CodeCheck)): `npm run lint` or `npm run codecheck`. To lint the CodeCheck + I18N variant: `npm run lint:codecheck`.

**CodeCheck + I18N variant:** `component-viewer.codecheck.js` is a copy of the plugin maintained for full CodeCheck and I18N compliance. It uses the same ESLint config; all user-facing strings go through `str(inst, key)` and `DEFAULT_STRINGS`; non-translatable blocks (e.g. SVG in `Icons`) are wrapped with `/* ignorei18n_start */` and `/* ignorei18n_end */` per I18N code check. Use this file when you need a build that satisfies strict CodeCheck and I18N rules.

---

## Quick start

1. **Mark up your items** with a common selector (default: `.cv-item`) and use `data-*` attributes for type, source, and title:

```html
<div id="my-gallery">
  <a class="att cv-item" data-type="image" data-title="Photo.jpg" href="https://example.com/photo.jpg">
    <img src="https://example.com/photo-thumb.jpg" alt="" />
  </a>
  <div class="att cv-item" data-type="pdf" data-title="Doc.pdf" data-src="https://example.com/doc.pdf" data-ext="PDF">
    PDF
  </div>
</div>
```

2. **Initialize the viewer** on the container:

```javascript
$('#my-gallery').componentViewer({
  toolbar: { download: true, zoom: true },
  pdf: { workerSrc: 'path/to/pdf.worker.min.js' }
});
```

3. **Click any item** to open it in the overlay. Use prev/next to move between items.

---

## Options reference

All options are optional. Defaults are in `$.fn.componentViewer.defaults`.

### General

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `selector` | string | `'.cv-item'` | CSS selector for items inside the container. |
| `loop` | boolean | `true` | If `true`, prev from first goes to last and next from last goes to first. If `false`, prev/next are hidden at first/last. |
| `overlayClose` | boolean | `true` | Close overlay when clicking the backdrop. |
| `keyboardNav` | boolean | `true` | Escape closes; Left/Right move prev/next; Space/M/D and custom shortcuts when applicable. |
| `shortcutsPopup` | boolean | `true` | If <code>true</code>, pressing <strong>?</strong> opens a popup listing keyboard shortcuts for the current view. Set to <code>false</code> to disable. |
| `showCounter` | boolean | `true` | Show the "1 / 6" counter in the header. Set to `false` to hide it. |
| `preloadAdjacentImages` | boolean | `true` | When `true`, the next and previous items are preloaded if they are images, so navigating to them is instant (Colorbox-style). Set to `false` to disable. |
| `carousel` | object | `{ enabled: false, navThreshold: 4 }` | Carousel options. Set <code>carousel.enabled: true</code> to show a header button that toggles a strip of thumbnails below the stage. <code>carousel.navThreshold</code> (default 4): when item count exceeds this, prev/next buttons appear on the strip. |
| `slideshow` | object \| `null` | `null` | When set to an object with `enabled: true`, the viewer auto-advances to the next item. Options: `interval` (seconds, default 4), `autoStart` (default `true`), `advanceMedia`: `'interval'` or `'onEnd'`. A "Play slideshow" / "Pause slideshow" toolbar button is shown; when `autoStart: true`, the button shows "Pause slideshow" initially. |
| `theme` | string | `'dark'` | Initial theme: `'dark'` or `'light'`. |
| `themeToggle` | boolean | `true` | Show the theme (dark/light) toggle in the header. |
| `fullscreen` | boolean | `true` | Show a header button to toggle overlay fullscreen (native Fullscreen API). Set to `false` to hide. Does not affect video/audio fullscreen. |
| `onThemeChange` | function | `null` | `function(theme, viewer)` called when theme changes. |

### PDF

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `pdf.workerSrc` | string | `null` | URL to pdf.js worker (required for PDF.js). |
| `pdf.cMapUrl` | string | `null` | Optional CMap URL for fonts. |
| `pdf.cMapPacked` | boolean | `true` | Use packed CMaps. |
| `pdf.annotations` | boolean | `true` | Render PDF annotations. |
| `pdf.autoFit` | boolean | `true` | Scale page to fit stage (width and height), within min/max scale for readability. |
| `pdf.autoFitMinScale` | number | `0.75` | When autoFit is true, scale never goes below this (75%) so the PDF stays readable. |
| `pdf.autoFitMaxScale` | number | `2.5` | Max scale when `autoFit` is true. |
| `pdf.twoPageView` | boolean | `false` | When true, a toolbar button is shown to toggle single/two-page view. PDF opens in single-page; user can switch to side-by-side (spread) view. Auto-fit fits both pages in width when in two-page mode. |

### Media (jPlayer)

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `supportedVideoFormats` | string | `null` | Comma-separated jPlayer formats for video (e.g. `'m4v, webmv'`). |
| `supportedAudioFormats` | string | `null` | Comma-separated jPlayer formats for audio (e.g. `'mp3, oga'`). |

### Inline (Highlight.js)

For **inline** (source/code) items you can enable syntax highlighting with [Highlight.js](https://highlightjs.org/). The plugin does not bundle it; the host page must include the script and a theme CSS.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `inline.syntaxHighlight` | boolean | `false` | When `true`, use `window.hljs` to highlight code (if present). |
| `inline.getLanguage` | function | `null` | `function(item)` returning a language string (e.g. `'javascript'`, `'java'`). If `null`, language is inferred from `item.fileExt` / `item.title`. |
| `onInlineHtml` | function | `null` | `function(content, item, viewer)` returning HTML for the inline body. When set, overrides built-in rendering (e.g. custom highlighter). |

**Loading Highlight.js for the highlight case:** Include the library and a theme in your page (before or with the viewer). The plugin only calls `window.hljs.highlight()`; all colors and styles come from the theme CSS you load. Example:

```html
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css">
```

Then enable highlighting: `inline: { syntaxHighlight: true }`. The viewer’s CSS only keeps Highlight.js token spans aligned (e.g. `.cv-inline-body .cv-inline-code [class*="hljs-"]`); it does not provide a theme.

### Data and callbacks

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `itemData` | function | `null` | `function($el, defaultItem)` — returns the item object for each element. Second argument is the item the plugin would build from `data-*` and DOM; you can add properties to `defaultItem` and return it, or return a new object. If `null`, item is built from `data-*` and DOM. |
| `onDownload` | function | `null` | `function(item, viewer)`. If provided, called when Download is clicked; otherwise default link download. |
| `onRender` | function | `null` | `function(item, $stage, viewer)`. If it appends to `$stage`, built-in renderer is skipped. May return `{ toolbar, destroy }`. |
| `onToolbar` | function | `null` | `function(item, defaultToolbar, viewer)`. Modify or replace the toolbar array. |
| `onLoading` | function | `null` | `function(item, viewer)` before an item is loaded. |
| `onOpen` | function | `null` | `function(item, $stage, viewer)` after the item is shown and toolbar is built. |
| `onComplete` | function | `null` | `function(item, viewer)` right after content is displayed (after transition if any). Like Colorbox `onComplete`. |
| `onCleanup` | function | `null` | `function(item, viewer)` at the start of the close process, before teardown. Like Colorbox `onCleanup`. |
| `onClose` | function | `null` | `function(item, viewer)` when the overlay has closed (item was the visible one). Like Colorbox `onClosed`. |

### Accessibility

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `wcag` | boolean | `false` | If `true`, enables focus trap, focus save/restore, dialog ARIA, and button labels. |

### Poll option

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `pollOption` | object | `null` | When set (e.g. `{ enabled: true, mode: 'radio', onSelect }`), items with `pollOptionLabel` show a row above the toolbar. |
| `pollOption.enabled` | boolean | — | Must be `true` to show poll UI. |
| `pollOption.mode` | string | `'radio'` | `'radio'` or `'checkbox'`. |
| `pollOption.onSelect` | function | — | `function(item, selected, viewer)`. `selected` is the new checked state. |
| `showAttachmentComment` | boolean | `false` | When `true`, items with a non-empty `item.comments` array show a compact dark overlay and a header button to toggle it (LC-Lightbox style). Only `item.comments` is supported: array of `{ title?, author?, text }`. For one comment use `[{ title?, author?, text }]`. Multiple comments show in-panel prev/next arrows and a counter. Use `itemData` or `data-comments` (JSON). |

---

## Item data

When `itemData` is not used, each item is built from the matched element:

| Field | Source |
|-------|--------|
| `type` | `data-type`, or `'markdown'` when file extension is `.md`, or `'image'` |
| `src` | `data-src` or `href` or first `img[src]` |
| `title` | `data-title` or `title` or `''` |
| `downloadUrl` | `data-download` |
| `fileExt` | `data-ext` |
| `fileSize` | `data-size` |
| `thumbnailUrl` | `data-thumbnail` or `data-poster` |
| `message` | `data-message` |
| `html` | `data-html` (for type `html`) |
| `content` | `data-content` (for type `markdown` or `inline`) |
| `pollOptionLabel` | `data-poll-option-label` |
| `pollOptionId` | `data-poll-option-id` |
| `supplied` | `data-supplied` (jPlayer format override) |

Use `itemData` to return a custom object per element. The callback receives `($el, defaultItem)`; you can add or override properties on `defaultItem` and return it, or return a new object (e.g. from your API).

---

## Content types

The plugin chooses a renderer by `item.type` (default `'image'`).

### Renderer order

1. **onRender(item, $stage, viewer)** — If it appends to `$stage`, the built-in renderer is skipped. Can return `{ toolbar: [...], destroy: function() }`.
2. **Built-in by type:** image, video, audio, pdf, inline, markdown, error, html.
3. **Unsupported** — If the stage is still empty, a "no preview" card is shown.

### Built-in types

| Type | Description |
|------|-------------|
| **image** | Image with zoom slider, wheel/pinch zoom, and drag pan. Invalid or failed load shows an error card without download; toolbar and footer are hidden. |
| **video** | jPlayer (or native `<video>` if jPlayer not loaded). |
| **audio** | jPlayer (or native `<audio>`). |
| **pdf** | PDF.js with page nav, thumbnails, zoom, rotate, print. Falls back to iframe if PDF.js not loaded. |
| **inline** | Source/code view with line numbers. Content from `item.content` or fetched from `item.src`. |
| **markdown** | Markdown rendered as HTML. Content from `item.content` or fetched from `item.src`. Include [marked](https://github.com/markedjs/marked) (e.g. from CDN) for full CommonMark support; otherwise a minimal built-in parser is used. Files with `.md` extension default to this type. |
| **error** | "Cannot preview" card with optional message and Download. |
| **html** | User-provided HTML in the stage. No toolbar, no download. |
| **Other** | Unsupported card with file icon, name, and optional Download. |

---

## Toolbar

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `toolbar.download` | boolean | `true` | Show the built-in Download button (when applicable). |
| `toolbar.zoom` | boolean | `true` | Show zoom widget for image items. |
| `toolbarItems` | array | `[]` | Custom toolbar items (objects, `'separator'`, or DOM nodes). |
| `zoom.min` | number | `1` | Minimum image zoom. |
| `zoom.max` | number | `5` | Maximum image zoom. |
| `zoom.step` | number | `0.01` | Slider step. |
| `zoom.wheelStep` | number | `0.15` | Zoom change per mouse wheel step. |
| `zoom.showPercentage` | boolean | `false` | Show zoom percentage (e.g. "150%") in the zoom widget. |
| `zoom.onZoom` | function | `null` | `function(zoomLevel, item, viewer)` when zoom changes. |

- **Resolution:** For built-in renderers, toolbar = renderer toolbar + `toolbarItems` + Download (when enabled). Zoom widget is shown for image items when `toolbar.zoom` is true.
- **onToolbar** can modify or replace the toolbar array before it is rendered.
- **onRender** can return a `toolbar` array for full control (no auto download/zoom).

### Ways to build the toolbar

| Source | Description |
|--------|-------------|
| **Renderer** | Built-in renderers (e.g. PDF) can return a `toolbar` array (buttons, separators, or DOM nodes). |
| **toolbarItems** | Option array merged after the renderer toolbar (with an optional separator before it). |
| **onToolbar** | Callback receives the merged array and can modify or replace it before rendering. |
| **onRender** | If you return `{ toolbar: [...] }`, that array is used as the full toolbar (no auto download/zoom). |
| **Download** | When `toolbar.download` is true, a Download button is appended after all items. |
| **Zoom widget** | Shown for image items when `toolbar.zoom` is true (separate from the toolbar array). |

### Toolbar entry types

Each element in a toolbar array can be one of:

| Type | Syntax | Description |
|------|--------|-------------|
| **Object** | `{ id, icon, label, ... }` | Rendered as a button. See object properties below. |
| **Separator** | `'separator'` or `'-'` | Rendered as a visual separator between buttons. |
| **DOM** | `HTMLElement` or jQuery object | Appended as-is (e.g. a span for "Page 1 / 10"). |

### Toolbar item object properties

When an entry is an object, the following properties are supported:

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `id` | string | — | Optional. Adds CSS class <code>cv-tb-<em>id</em></code> to the button. |
| `icon` | string | — | Icon: HTML string (e.g. <code>&lt;svg&gt;...&lt;/svg&gt;</code>) or CSS class (e.g. <code>fa fa-share</code>). HTML is sanitized. |
| `label` | string | — | Text label; used as tooltip if <code>tooltip</code> is not set. |
| `tooltip` | string | — | Button <code>title</code> and, when <code>wcag</code> is true, <code>aria-label</code>. Falls back to <code>label</code> or <code>id</code>. |
| `showLabel` | boolean | `false` | If <code>true</code>, the label is shown as text beside the icon. |
| `className` | string | — | Extra CSS class(es) on the button. |
| `shortcutKey` | string | — | Optional. Single-character keyboard shortcut (e.g. <code>'e'</code>) to trigger this button. Shown in the shortcuts popup (?) when the item is visible. Reserved keys: Escape, Arrow keys, Space, M, R, Q, D, P, ?, +, -, =. |
| `visible` | boolean or function | `true` | If <code>false</code> or a function that returns <code>false</code>, the button is not rendered. Function: <code>visible(item, viewer)</code>. |
| `onClick` | function | — | <code>function(item, viewer)</code> — called when the button is clicked. |

Example:

```javascript
toolbarItems: [
  { id: 'share', icon: 'fa fa-share', label: 'Share', onClick: function(item) { /* ... */ } },
  'separator',
  { id: 'delete', label: 'Delete', visible: function(item) { return item.canDelete; }, onClick: fn }
]
```

---

## Keyboard shortcuts

When `keyboardNav` is true, the following shortcuts are available. Press **?** (Shift+/) to open a popup that lists only the shortcuts **enabled for the current item** (by type and toolbar options).

| Key | Action |
|-----|--------|
| **Escape** | Close overlay (or close shortcuts popup if open). |
| **←** / **→** | Previous / next item. |
| **+** / **-** | Zoom in / out (image only, when zoom is enabled). |
| **Space** | Play / Pause (video or audio **only when rendered by the plugin**; not for custom-rendered or slideshow). |
| **M** | Mute / Unmute (video or audio only when plugin-rendered). |
| **R** | Cycle playback speed (video or audio only when plugin-rendered; cycles 0.5x → 0.75x → 1x → 1.25x → 1.5x → 2x). |
| **Q** | Toggle HD quality (video only when HD source is available). |
| **D** | Download (when the download button is visible). |
| **P** | Print (PDF view only; when the PDF print button is visible). |
| **F** | Toggle fullscreen (when the fullscreen button is visible). |
| **T** | Toggle theme dark/light (when the theme toggle is visible). |
| **C** | Toggle carousel / attachments strip (when carousel is enabled). |
| **S** | Play / Pause slideshow (when slideshow is enabled and the slideshow button is visible). |
| **?** | Show or hide the keyboard shortcuts popup. |

**Custom toolbar shortcuts:** Add `shortcutKey: 'e'` (or any single character) to a toolbar item to give it a keyboard shortcut. That shortcut appears in the popup only when the button is visible. Reserved keys (Escape, arrows, Space, M, R, Q, D, P, F, T, C, S, ?, +, -, =) are not available for custom items.

Set `shortcutsPopup: false` to disable the ? popup.

---

## Poll options

When `pollOption.enabled` is true and an item has `pollOptionLabel`, a row appears above the toolbar with the label and a radio or checkbox.

- **mode:** `'radio'` (single choice) or `'checkbox'` (multiple).
- **onSelect(item, selected, viewer)** is called when the user toggles; `selected` is the new checked state.

Useful for polls where each option is an image or attachment.

---

## Callbacks

| Callback | When |
|----------|------|
| `onLoading(item, viewer)` | Before the item is loaded. |
| `onOpen(item, $stage, viewer)` | After the item is shown and toolbar is built. |
| `onComplete(item, viewer)` | Right after content is displayed (after transition if any). |
| `onCleanup(item, viewer)` | At the start of the close process, before teardown. |
| `onClose(item, viewer)` | When the overlay has closed (item was the visible one). |
| `onThemeChange(theme, viewer)` | When the user toggles theme. |
| `onDownload(item, viewer)` | When the Download button is clicked (if provided). |
| `onRender(item, $stage, viewer)` | First chance to render; if you append to `$stage`, built-in is skipped. |
| `onToolbar(item, defaultToolbar, viewer)` | To modify the toolbar before it is rendered. |

---

## Public API

Call methods via the jQuery plugin or on the stored instance.

### Initialization

```javascript
$(container).componentViewer(options);
```

### Methods

```javascript
$(container).componentViewer('open', index);   // Open at index (default 0)
$(container).componentViewer('close');
$(container).componentViewer('next');
$(container).componentViewer('prev');
$(container).componentViewer('goTo', index);
$(container).componentViewer('currentItem');   // Returns current item object
$(container).componentViewer('setTheme', 'light' | 'dark');
$(container).componentViewer('refresh');      // Re-collect items, re-bind clicks
$(container).componentViewer('destroy');
```

### Globals

- **Defaults:** `$.fn.componentViewer.defaults`
- **Icons:** `$.fn.componentViewer.Icons` (SVG strings for close, prev, next, zoom, download, etc.)

---

## Accessibility (WCAG)

Set **`wcag: true`** to enable:

- **Focus trap** — Tab and Shift+Tab cycle only inside the overlay.
- **Focus save/restore** — Focus is saved on open and restored on close.
- **Initial focus** — Focus moves to the close button when the overlay opens.
- **ARIA** — Overlay and shell get appropriate roles and attributes; buttons get `aria-label` where needed.

When `wcag` is `false`, these behaviors are disabled and no dialog ARIA is applied.

---

## Browser support

| Browser | Minimum version |
|---------|------------------|
| Chrome (desktop) | 87 |
| Firefox (desktop) | 85 |
| Safari (desktop) | 14.1 |
| Edge (Chromium) | 87 |
| Safari (iOS) | 14.5 |
| Chrome (Android) | 87 |
| Internet Explorer | 11 (partial; not recommended) |

Requires jQuery 1.7+. Optional: jPlayer and PDF.js have their own browser requirements.

---

## Examples

### Basic gallery

```javascript
$('#gallery').componentViewer({
  toolbar: { download: true, zoom: true },
  pdf: { workerSrc: 'pdf.worker.min.js' }
});
```

### Loop disabled, no counter

```javascript
$('#gallery').componentViewer({
  loop: false,
  showCounter: false,
  toolbar: { download: true, zoom: true }
});
```

### Custom item data

```javascript
$('#gallery').componentViewer({
  itemData: function($el, defaultItem) {
    defaultItem.canDelete = $el.data('can-delete');
    defaultItem.attachmentId = $el.data('id');
    return defaultItem;
  },
  toolbar: { download: true }
});
```

### With WCAG and custom download

```javascript
$('#gallery').componentViewer({
  wcag: true,
  onDownload: function(item, viewer) {
    // Custom download logic
    window.location = item.downloadUrl || item.src;
  }
});
```

### Programmatic open

```javascript
var $gallery = $('#gallery').componentViewer({ toolbar: { download: true } });
$gallery.componentViewer('open', 2);  // Open third item
```

---

## File structure

```
component-viewer-v2/
├── component-viewer.js    # Plugin script
├── component-viewer.css   # Styles
├── README.md              # This documentation
├── documentation.html     # Full API docs (HTML, JavaDoc-style)
└── example.html           # Full examples (multiple cases)
```

**HTML documentation:** Open `documentation.html` in a browser for a detailed, navigable API reference (options, item data, content types, toolbar, callbacks, public API, browser support, and examples).

---

- **Toolbar toggle**  
  The two-page toggle appears in the PDF toolbar **only when** `pdf.twoPageView: true`. The button shows the “other” mode as tooltip (e.g. “Single-page view” when in two-page mode). It gets the `cv-active` class when two-page view is on. Toggling re-applies auto-fit and re-renders all pages.

- **Two-page rendering**  
  In two-page mode, pages are rendered in pairs (1–2, 3–4, …); the last page appears alone if the total is odd. A fixed gap between the two pages is used; auto-fit scale is computed so both pages fit in the stage width. New CSS class `.cv-pdf-spread` lays out the two pages side by side.

- **I18N and icon**  
  New default strings: `twoPageView`, `singlePageView`. New icon `Icons.twoPageView` (two side-by-side rectangles) used for the toggle button.

- **Example**  
  **Case 8a** in `example.html` demonstrates the two-page view: `pdf: { twoPageView: true }` with one PDF attachment. Open the PDF to see the toggle and switch between single- and two-page view.

- **Documentation**  
  README, `documentation.html`, and the Case 8a description in `example.html` updated to describe the option, default (single-page), and toggle behavior.

---

## License

MIT. See the plugin header in `component-viewer.js` for details.