my-library-topiq
v1.5.3
Published
Vue 3 component library: 3D book, SCORM and Flowpaper players, and a framework-agnostic video modal
Maintainers
Readme
my-library-topiq
Vue 3 component library: 3D book, SCORM and Flowpaper players, and a framework-agnostic video modal.
All components are written with <script setup lang="ts"> and the Composition API. Vue is kept
external in every build — the library never bundles its own copy.
Installation
npm install my-library-topiqVue 3.5 or newer is a peer dependency — the library uses the copy already installed in your project instead of bundling its own.
Usage
Import from the package root:
import {
Button,
Book3D,
ScormPlayer,
createVideoModal,
createFetchVideoSourceResolver,
} from 'my-library-topiq';Or import a single module by its subpath, so nothing else ends up in your bundle:
import Button from 'my-library-topiq/button';
import ScormPlayer from 'my-library-topiq/scormPlayer';
import createVideoModal from 'my-library-topiq/videoModal';| Subpath | Export |
| -------------------- | -------------------------- |
| ./button | Button |
| ./book3D | Book3D |
| ./pageBook3D | PageBook3D |
| ./skewButton | SkewButton |
| ./scormPlayer | ScormPlayer |
| ./flowpaperPlayer | FlowpaperPlayer |
| ./videoModal | createVideoModal (default export only) |
Styles are bundled into the JavaScript — importing a component is enough, there is no separate CSS file to include.
Components
Button
| Prop | Type | Default | Description |
| ---------- | --------------------------------------------------------------- | ------- | ----------------- |
| type | 'primary' \| 'secondary' \| 'success' \| 'danger' \| 'warning' | — | Visual variant |
| disabled | boolean | false | Disables the button |
Emits click with the MouseEvent. Content goes into the default slot.
<Button type="primary" @click="onClick">Save</Button>SkewButton
| Prop | Type | Default | Description |
| -------------- | -------------------- | --------- | ------------------------------ |
| label | string | '' | Button text |
| reversIcon | boolean | false | Puts the arrows before the label |
| disabled | boolean | false | Disables the button |
| theme | 'light' \| 'dark' | 'light' | Color theme |
| customStyles | CustomStyles | {} | Per-instance color overrides |
CustomStyles accepts backgroundColor, textColor, hoverBackgroundColor, shadowColor,
hoverShadowColor and arrowColor; each one maps to a CSS custom property on the root element.
Emits click with the MouseEvent.
<SkewButton label="Read" theme="dark" @click="onClick" />Book3D
Renders an animated 3D book. Pages are provided through the default slot.
| Prop | Type | Default | Description |
| -------------- | ---------- | ------- | ------------------------------- |
| item | BookType | — | Book data (required) |
| outsideOpen | boolean | — | Opened state, supports v-model:outsideOpen |
| outsideFlip | boolean | — | Flipped state, supports v-model:outsideFlip |
BookType is { name, description?, img?, color_bg?, color_fg?, color_sh? }.
Slots: default (pages), bookCover, insideContent, imgContent.
Emits update:outsideOpen, update:outsideFlip and readAll (fired when the last page is reached).
<Book3D :item="book" v-model:outsideOpen="isOpen" @readAll="onReadAll">
<PageBook3D v-for="(page, i) in pages" :key="i" :index="i" />
</Book3D>PageBook3D
A single page of Book3D.
| Prop | Type | Default | Description |
| ------------- | ------------------- | ------------ | -------------------------- |
| index | number | 0 | Page order, drives z-index and rotation |
| isOpened | boolean | — | Whether the page is turned |
| visibleMark | boolean | true | Shows the bookmark |
| mark | 'top' \| 'right' | 'right' | Bookmark position |
| icon | string | 'pi-check' | Bookmark icon class |
| iconColor | string | 'white' | Icon color |
| markBkColor | string | 'red' | Bookmark background |
| pageBkColor | string | 'white' | Page background |
Emits openPage with the page index, and imLastPage with a boolean.
ScormPlayer
Embeds SCORM content in an iframe, scaling it to the container and driving the internal player API.
| Prop | Type | Default | Description |
| ----------- | -------- | ------- | ------------------------------ |
| src | string | — | SCORM entry URL (required) |
| zoom | number | 50 | Zoom level, in percent |
| pageLimit | string | — | Page limit passed to the player |
Emits loaded once the content is measured and scaled, and error with an Error.
<ScormPlayer :src="scormUrl" :zoom="80" @loaded="onLoaded" @error="onError" />FlowpaperPlayer
Embeds a Flowpaper PDF viewer and relays postMessage events coming from the iframe.
| Prop | Type | Default | Description |
| ----- | -------- | ------- | ------------------------- |
| src | string | — | Viewer URL (required) |
Supports v-model for the loading state (boolean, true until the iframe fires load).
Emits message with { type: 'CLICKED' | 'ALL', value: string } — messages prefixed with
CLICKED: are reported as CLICKED with the prefix stripped, everything else as ALL.
<FlowpaperPlayer v-model="loading" :src="pdfUrl" @message="onMessage" />createVideoModal
A framework-agnostic video modal, usable outside Vue as well. It appends its own markup to
document.body, resolves the video source and renders the matching player.
import createVideoModal from 'my-library-topiq/videoModal';
const videoModal = createVideoModal({ bookName, sourceStrategy: 'local' });
videoModal.pickElement(iframeSpot); // with a play overlay
videoModal.pickElement(iframeSpot, { overlay: false }); // without oneOptions:
| Option | Type | Default | Description |
| -------------------- | ----------------------------------------------- | --------- | ------------------------------ |
| bookName | string | — | Used in the default local video path; only required by the default resolver |
| sourceStrategy | 'youtube' \| 'local' \| (() => 'youtube' \| 'local') | 'local' | How the source is resolved; a function is re-evaluated on every open |
| resolveVideoSource | VideoSourceResolver | createFetchVideoSourceResolver() | Locates the local video file |
| youtubeProxyUrl | string | — | Base URL of a proxy to embed YouTube through; without it the embed points at youtube-nocookie.com |
| debug | boolean | false | Logs source resolution and the final embed URL to the console |
Returned API:
| Method | Description |
| ------------------------------- | -------------------------------------------------------- |
| pickElement(iframe, options?) | Binds click handlers to video elements inside the iframe |
| openVideoModal(url) | Opens the modal for a URL |
| closeVideoModal() | Closes the modal and disposes the active player |
| destroy() | Removes listeners and the modal markup from the document |
Locating the local video
By default the local strategy fetches /video-resource/:bookName/:resourceId and falls back to
YouTube when that resource is unavailable — your backend needs to serve that endpoint, returning
{ status: 'ok', path, type }. The base path is configurable:
import { createFetchVideoSourceResolver } from 'my-library-topiq';
createVideoModal({
bookName,
resolveVideoSource: createFetchVideoSourceResolver({ baseUrl: '/api/media' }),
});Lookup is not always an HTTP request, though — in a Capacitor app there is no server to fetch from,
and the file lives on the device. Pass your own resolveVideoSource to replace the transport, the
URL scheme and the response format all at once:
type VideoSource = {
src: string;
type?: string;
dispose?: () => void; // optional cleanup, e.g. URL.revokeObjectURL
};
type VideoSourceResolver = (
url: string,
bookName: string | undefined,
signal: AbortSignal
) => Promise<VideoSource | null>;Return null when the video is simply not there — that triggers the YouTube fallback, same as a
thrown error. Aborting through signal does not: it means the modal was closed or switched to
another video, so nothing is rendered.
import { Capacitor } from '@capacitor/core';
import { Directory, Filesystem } from '@capacitor/filesystem';
import createVideoModal from 'my-library-topiq/videoModal';
const videoModal = createVideoModal({
bookName,
resolveVideoSource: async (url, book) => {
const { pathname, searchParams } = new URL(url, location.origin);
const id = pathname === '/watch' ? searchParams.get('v') : pathname.split('/')[1];
try {
const { uri } = await Filesystem.getUri({
directory: Directory.Data,
path: `books/${book}/videos/${id}.mp4`,
});
return { src: Capacitor.convertFileSrc(uri), type: 'video/mp4' };
} catch {
return null; // not downloaded — fall back to YouTube
}
},
});bookName is only read by the default resolver, so a custom one can ignore it and omit the option
entirely — identify the book however your app does.
createFetchVideoSourceResolver is exported from the package root only; the ./videoModal subpath
keeps its single default export.
The YouTube strategy renders an iframe directly, with no player library involved.
youtubeProxyUrl
YouTube validates the origin of the embedding page and answers with error 153 when it does not
like it. Inside a Capacitor WebView window.location.origin is capacitor://your.host (iOS),
which YouTube rejects, and the embed cannot override it. Routing the embed through your own proxy
sidesteps the check — the proxy is the page YouTube sees:
const videoModal = createVideoModal({
bookName,
youtubeProxyUrl: 'https://proxy.pages.dev/',
});The video id is appended as ?v=, together with autoplay=1:
https://proxy.pages.dev/?v=dQw4w9WgXcQ&autoplay=1Query parameters already present in youtubeProxyUrl are preserved, so a proxy that needs its own
options (https://proxy.pages.dev/?lang=kk) works as is.
When youtubeProxyUrl is omitted, the embed goes straight to youtube-nocookie.com/embed/:id —
fine on a regular https page, subject to error 153 inside a WebView.
debug
Set debug: true to log the extracted video id and the final embed URL under a [VideoModal]
prefix.
Development
npm run build # full Rollup build into dist/
npm run dev # build in watch mode
npm run deploy # build + npm publishThere are no tests, linter or dev server in this project. Formatting is handled by Prettier
(single quotes, 2 spaces, printWidth 100).
Adding a component
Rollup scans src/components/ and builds every directory it finds, so a new component needs its
directory plus three registrations:
src/components/<ComponentName>/— the component,index.js(orindex.ts),index.d.tsandstyles/src/index.js— import, named export, and a field in the default objectsrc/index.d.ts— the same, plus a field in_defaultpackage.json→exports["./componentName"]withimport,requireandtypespaths
Keep the public types inside the component's own index.d.ts rather than importing them through the
@/* alias — rollup-plugin-dts does not resolve that alias and would emit a broken declaration.
License
ISC
