@michaelyagi/shoji 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/esm/core/EventBus.d.ts +16 -0
- package/dist/esm/core/FocusTrap.d.ts +9 -0
- package/dist/esm/core/Gallery.d.ts +221 -0
- package/dist/esm/core/GestureController.d.ts +57 -0
- package/dist/esm/core/LiveRegion.d.ts +6 -0
- package/dist/esm/core/SlideManager.d.ts +85 -0
- package/dist/esm/core/bodyScrollLock.d.ts +2 -0
- package/dist/esm/core/dom.d.ts +27 -0
- package/dist/esm/core/icons.d.ts +6 -0
- package/dist/esm/core/index.d.ts +5 -0
- package/dist/esm/core/index.js +2105 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/core/plugin.d.ts +54 -0
- package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
- package/dist/esm/core/scan.d.ts +19 -0
- package/dist/esm/core/types.d.ts +290 -0
- package/dist/esm/core/zoomTransition.d.ts +33 -0
- package/dist/esm/gestures/GestureEngine.d.ts +78 -0
- package/dist/esm/index.css +540 -0
- package/dist/esm/index.d.ts +32 -0
- package/dist/esm/index.js +24 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index2.css +22 -0
- package/dist/esm/index3.css +170 -0
- package/dist/esm/index4.css +27 -0
- package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
- package/dist/esm/plugins/activeThumbnail/index.js +63 -0
- package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
- package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
- package/dist/esm/plugins/autoplay/index.d.ts +19 -0
- package/dist/esm/plugins/autoplay/index.js +207 -0
- package/dist/esm/plugins/autoplay/index.js.map +1 -0
- package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
- package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
- package/dist/esm/plugins/fullscreen/index.js +74 -0
- package/dist/esm/plugins/fullscreen/index.js.map +1 -0
- package/dist/esm/plugins/layout/index.d.ts +191 -0
- package/dist/esm/plugins/layout/index.js +752 -0
- package/dist/esm/plugins/layout/index.js.map +1 -0
- package/dist/esm/plugins/layout/justified.d.ts +68 -0
- package/dist/esm/plugins/layout/masonry.d.ts +92 -0
- package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
- package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
- package/dist/esm/plugins/rotateFlip/index.js +138 -0
- package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
- package/dist/esm/plugins/video/index.d.ts +13 -0
- package/dist/esm/plugins/video/index.js +207 -0
- package/dist/esm/plugins/video/index.js.map +1 -0
- package/dist/esm/plugins/video/vimeo.d.ts +43 -0
- package/dist/esm/plugins/video/youtube.d.ts +61 -0
- package/dist/esm/plugins/zoom/icons.d.ts +4 -0
- package/dist/esm/plugins/zoom/index.d.ts +30 -0
- package/dist/esm/plugins/zoom/index.js +274 -0
- package/dist/esm/plugins/zoom/index.js.map +1 -0
- package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
- package/dist/esm/transitions/SlideTransition.d.ts +25 -0
- package/dist/esm/transitions/presets.d.ts +21 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
- package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
- package/dist/shoji.css +759 -0
- package/dist/shoji.js +3907 -0
- package/dist/shoji.js.map +1 -0
- package/dist/shoji.min.css +1 -0
- package/dist/shoji.min.js +2 -0
- package/dist/shoji.min.js.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Yagi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Shoji
|
|
2
|
+
|
|
3
|
+
[](https://github.com/MichaelYagi/shoji/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@michaelyagi/shoji)
|
|
5
|
+
|
|
6
|
+
A zero-dependency, plugin-first TypeScript lightbox/gallery. Drop in two `<script>`/`<link>` tags, or install from npm — everything beyond the minimal lightbox, including every official plugin, is opt-in.
|
|
7
|
+
|
|
8
|
+
> **Status: alpha.** The API is still settling — expect breaking changes between `0.x` releases.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Zero runtime dependencies**, TypeScript strict mode throughout.
|
|
13
|
+
- **Selector mode or dynamic mode** — scan existing DOM markup, or hand it an `items` array.
|
|
14
|
+
- **Plugin system** — zoom (pinch/pan/wheel), fullscreen, rotate/flip, autoplay/slideshow, active-thumbnail sync, YouTube video embeds, and a grid/masonry/justified layout engine, each independently opt-in.
|
|
15
|
+
- **Accessibility is not a plugin** — keyboard nav, focus trapping, ARIA roles, and screen-reader announcements live in core.
|
|
16
|
+
- **Themeable via CSS custom properties** — no hardcoded colors/sizes in JS.
|
|
17
|
+
- **Single-file distribution** — `dist/shoji.js` + `dist/shoji.css` (core + every official plugin, self-registering) for zero-setup `<script>` tag use, plus tree-shakable per-plugin ESM entries for bundler users.
|
|
18
|
+
- Size-budgeted and CI-enforced: core ≤ 21 kB, each plugin ≤ 8 kB, full bundle ≤ 90 kB (all min+gzip).
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<link rel="stylesheet" href="shoji.min.css" />
|
|
24
|
+
<script src="shoji.min.js"></script>
|
|
25
|
+
<script>
|
|
26
|
+
new Shoji('#gallery');
|
|
27
|
+
</script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @michaelyagi/shoji@alpha
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import Shoji from '@michaelyagi/shoji';
|
|
36
|
+
import '@michaelyagi/shoji/style.css';
|
|
37
|
+
|
|
38
|
+
new Shoji('#gallery', { plugins: [Shoji.Zoom] });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<div id="gallery">
|
|
45
|
+
<a href="photo-1-full.jpg"><img src="photo-1-thumb.jpg" alt="Sunset over the bay" /></a>
|
|
46
|
+
<a href="photo-2-full.jpg"><img src="photo-2-thumb.jpg" alt="Mountain trail" /></a>
|
|
47
|
+
</div>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
new Shoji('#gallery');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
No items array, no options object, no plugins — every default is chosen so this is a complete integration. See [`docs/guides/getting-started.html`](docs/guides/getting-started.html) for dynamic mode, the full `data-shoji-*` attribute mapping, and every `GalleryItem` field.
|
|
55
|
+
|
|
56
|
+
## Docs
|
|
57
|
+
|
|
58
|
+
- **[michaelyagi.github.io/shoji](https://michaelyagi.github.io/shoji)** — the published docs site, auto-deployed from `main` (see `.github/workflows/ci.yml`'s `publish-docs` job).
|
|
59
|
+
- **[npmjs.com/package/@michaelyagi/shoji](https://www.npmjs.com/package/@michaelyagi/shoji)** — the published package, auto-published on tagged releases (see `.github/workflows/ci.yml`'s `publish-npm` job).
|
|
60
|
+
- [`docs/index.html`](docs/index.html) — the same guides/examples/API reference, as local static HTML (open directly, or run `npm run docs` first to regenerate the API reference from source).
|
|
61
|
+
- [`docs/examples/`](docs/examples/) — real, runnable, self-contained example pages (copy one wholesale into your own project).
|
|
62
|
+
- [`DESIGN.md`](DESIGN.md) — the living architecture/spec document this project is built from.
|
|
63
|
+
|
|
64
|
+
## Development
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install
|
|
68
|
+
npm run dev # Vite dev server over demo/
|
|
69
|
+
npm run build # dist/shoji.(min.)js + .css, plus per-plugin ESM entries
|
|
70
|
+
npm test # Vitest unit tests
|
|
71
|
+
npm run test:e2e # Playwright e2e (chromium/firefox/webkit/mobile)
|
|
72
|
+
npm run lint # eslint + prettier check
|
|
73
|
+
npm run typecheck # tsc --noEmit
|
|
74
|
+
npm run size # build + size-limit budget check
|
|
75
|
+
npm run docs # regenerate docs/api/ (typedoc)
|
|
76
|
+
npm run verify # the full gate: typecheck + lint + unit + e2e + size
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`demo/assets/` (sample photos/video for the local demo pages) is gitignored — drop your own media there, or the demo pages just render with 0 items.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT © Michael Yagi
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type EventMap = Record<string, unknown>;
|
|
2
|
+
export type Unsubscribe = () => void;
|
|
3
|
+
type Listener<T> = (detail: T) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Typed pub/sub used by Gallery and every plugin (`ctx.on`). Plugin
|
|
6
|
+
* subscriptions are unsubscribed automatically on destroy by the caller
|
|
7
|
+
* that owns the PluginContext, not by this class.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EventBus<Events extends EventMap> {
|
|
10
|
+
private listeners;
|
|
11
|
+
on<K extends keyof Events>(event: K, fn: Listener<Events[K]>): Unsubscribe;
|
|
12
|
+
off<K extends keyof Events>(event: K, fn: Listener<Events[K]>): void;
|
|
13
|
+
emit<K extends keyof Events>(event: K, detail: Events[K]): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** DESIGN.md §2.6 — focus trap on open, focus restore on close, Tab cycles within the dialog. */
|
|
2
|
+
export declare class FocusTrap {
|
|
3
|
+
private container;
|
|
4
|
+
private previouslyFocused;
|
|
5
|
+
private readonly onKeydown;
|
|
6
|
+
private getFocusable;
|
|
7
|
+
activate(container: HTMLElement): void;
|
|
8
|
+
deactivate(): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Unsubscribe } from './EventBus';
|
|
2
|
+
import { GalleryEvents, GalleryItem, GalleryItemInput, GalleryOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Core lifecycle (DESIGN.md §2.2), item model / DOM scanning (§2.1), and the
|
|
5
|
+
* lightbox itself: pooled slides (§2.3), dialog semantics/focus trap/live
|
|
6
|
+
* region (§2.6, non-optional), gestures (§2.4, via `GestureController`) and
|
|
7
|
+
* slide-to-slide transitions (§2.5, via `SlideTransition`).
|
|
8
|
+
*/
|
|
9
|
+
export declare class Gallery {
|
|
10
|
+
/** DESIGN.md §2.7 — the live instance mounted on `el`, or `undefined` if none exists there (or it's since been destroyed). */
|
|
11
|
+
static getInstance(el: HTMLElement): Gallery | undefined;
|
|
12
|
+
/** DESIGN.md §2.7 — every live instance, e.g. for page-wide teardown or devtools inspection. */
|
|
13
|
+
static instances(): IterableIterator<Gallery>;
|
|
14
|
+
/** Not `readonly` — `reinit()` (§2.7) reassigns this + every option-derived field below; initializers here just satisfy strict-init. */
|
|
15
|
+
options: GalleryOptions;
|
|
16
|
+
readonly element: HTMLElement;
|
|
17
|
+
private readonly bus;
|
|
18
|
+
private selector;
|
|
19
|
+
private isDynamicMode;
|
|
20
|
+
private preload;
|
|
21
|
+
private locale;
|
|
22
|
+
private showCounter;
|
|
23
|
+
private captionVisibleOnVideo;
|
|
24
|
+
private loop;
|
|
25
|
+
private closable;
|
|
26
|
+
private autoHideDelay;
|
|
27
|
+
private readonly focusTrap;
|
|
28
|
+
private readonly liveRegion;
|
|
29
|
+
private autoHideTimer;
|
|
30
|
+
private autoHidden;
|
|
31
|
+
private hoveredControlCount;
|
|
32
|
+
private isClosing;
|
|
33
|
+
private itemList;
|
|
34
|
+
private scannedElements;
|
|
35
|
+
private activeIndex;
|
|
36
|
+
private opened;
|
|
37
|
+
private destroyed;
|
|
38
|
+
private slides;
|
|
39
|
+
private dom;
|
|
40
|
+
private gesture;
|
|
41
|
+
private transition;
|
|
42
|
+
private readonly shortcuts;
|
|
43
|
+
private readonly pluginStorage;
|
|
44
|
+
/** Backs `getActivePlugins()`. */
|
|
45
|
+
private readonly activePluginNames;
|
|
46
|
+
private readonly videoProviders;
|
|
47
|
+
private zoomGate;
|
|
48
|
+
private pluginCleanups;
|
|
49
|
+
private readonly onContainerClick;
|
|
50
|
+
private readonly onOuterClick;
|
|
51
|
+
/** DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle clock. `autoHideDelay: 0` = "never show controls" — a no-op here. */
|
|
52
|
+
private readonly onActivity;
|
|
53
|
+
private readonly onKeydown;
|
|
54
|
+
constructor(target: HTMLElement | string, options?: GalleryOptions);
|
|
55
|
+
/** Everything the constructor does after `this.element` is resolved — shared with `reinit()` (§2.7). */
|
|
56
|
+
private applyOptions;
|
|
57
|
+
get items(): readonly GalleryItem[];
|
|
58
|
+
get currentIndex(): number;
|
|
59
|
+
/** True from the first `open()` until `close()`/`destroy()`. */
|
|
60
|
+
get isOpen(): boolean;
|
|
61
|
+
/** True once `destroy()` has run. */
|
|
62
|
+
get isDestroyed(): boolean;
|
|
63
|
+
/** Whether auto-hide (§2.8) currently has controls faded. */
|
|
64
|
+
get controlsHidden(): boolean;
|
|
65
|
+
/** Names of plugins that actually initialized. */
|
|
66
|
+
getActivePlugins(): string[];
|
|
67
|
+
/** The active slide's `.shoji-slide-media` container. Empty before the first `open()`; `null` only after `destroy()`. */
|
|
68
|
+
getActiveMedia(): HTMLElement | null;
|
|
69
|
+
/** DESIGN.md §4-zoom — suspends drag-to-navigate/close while zoomed. Single slot, not a multi-subscriber event. */
|
|
70
|
+
registerZoomGate(isZoomed: () => boolean): () => void;
|
|
71
|
+
on<K extends keyof GalleryEvents>(event: K, fn: (detail: GalleryEvents[K]) => void): Unsubscribe;
|
|
72
|
+
private clampToRange;
|
|
73
|
+
private ensureLightbox;
|
|
74
|
+
/** DESIGN.md §3 — plugins init here, not the constructor. `requires` checks names loaded earlier; an unmet one is skipped (logged), not thrown. Cleared up front so a `reinit()` doesn't inherit names from before. */
|
|
75
|
+
private initPlugins;
|
|
76
|
+
/**
|
|
77
|
+
* DESIGN.md §3.1 — 'right' inserts immediately before the close button
|
|
78
|
+
* (never after), so close stays fixed rightmost and plugins cluster to
|
|
79
|
+
* its left in registration order: `plugins: [A, B, C]` reads A, B, C,
|
|
80
|
+
* close. 'left'/'center' are independent zones for a plugin that doesn't
|
|
81
|
+
* want to sit next to close.
|
|
82
|
+
*/
|
|
83
|
+
private pluginToolbar;
|
|
84
|
+
private buildToolbarButton;
|
|
85
|
+
private pluginOverlay;
|
|
86
|
+
/** DESIGN.md §2.8/§3 — pauses auto-hide while genuinely hovered: controls, caption, and any plugin overlay (`ctx.ui.overlay()`). Unsubscribe also corrects the count if removed mid-hover — a real risk for overlay content a plugin can toggle while the gallery stays open, unlike static buttons. */
|
|
87
|
+
private wireControlHover;
|
|
88
|
+
/**
|
|
89
|
+
* DESIGN.md §2.8 — paused only by a real *hover*. Used to also treat a
|
|
90
|
+
* *focused* control as active via `document.activeElement`, but a click
|
|
91
|
+
* leaves a `<button>` focused indefinitely, permanently blocking
|
|
92
|
+
* `hideControls()`. Focus still counts as activity (`focusin` →
|
|
93
|
+
* `onActivity()`), it just no longer blocks the eventual hide.
|
|
94
|
+
*/
|
|
95
|
+
private isControlActive;
|
|
96
|
+
/**
|
|
97
|
+
* The thumbnail for `index` — what the zoom transition animates to/from,
|
|
98
|
+
* and what `activeThumbnail` marks/scrolls-to. `data-shoji-id` is an
|
|
99
|
+
* explicit opt-in that works in any mode (needed for dynamic mode, which
|
|
100
|
+
* has no scanned DOM); checked first so it can override selector mode's
|
|
101
|
+
* default `scannedElements[index]` too, e.g. to target an inner element.
|
|
102
|
+
*/
|
|
103
|
+
getOriginElement(index: number): HTMLElement | null;
|
|
104
|
+
private scheduleAutoHide;
|
|
105
|
+
private hideControls;
|
|
106
|
+
private showControls;
|
|
107
|
+
/**
|
|
108
|
+
* DESIGN.md §2.1 — `caption` is `string | HTMLElement | DangerousHtmlCaption`:
|
|
109
|
+
* a plain string renders as text (`textContent`, auto-escaped, safe by
|
|
110
|
+
* default); an `HTMLElement` is appended as real DOM (the host built it);
|
|
111
|
+
* `{ dangerouslySetInnerHTML }` renders raw, unescaped HTML via `innerHTML`
|
|
112
|
+
* — Shoji does not sanitize it, the host must, same contract as React's
|
|
113
|
+
* identically-named escape hatch. Returns whether the caption element
|
|
114
|
+
* should be hidden.
|
|
115
|
+
*/
|
|
116
|
+
private renderCaption;
|
|
117
|
+
/**
|
|
118
|
+
* `.shoji-toolbar-button` only — never `.shoji-close`/`.shoji-nav`, which
|
|
119
|
+
* are different classes entirely, so this can't accidentally block
|
|
120
|
+
* closing or navigating away from a slow-loading slide. `tabIndex = -1`
|
|
121
|
+
* (not just the CSS below) so a keyboard user tabbing through the
|
|
122
|
+
* toolbar skips these entirely while disabled, not just visually dims
|
|
123
|
+
* them — `pointer-events: none` alone wouldn't stop Enter/Space
|
|
124
|
+
* activating an already-focused button.
|
|
125
|
+
*/
|
|
126
|
+
private setSlideLoading;
|
|
127
|
+
/**
|
|
128
|
+
* Content is always kept current (correct the instant loading finishes,
|
|
129
|
+
* no text flash) — only `hidden` also gates on `isActiveReady()`, so a
|
|
130
|
+
* caption for the *new* slide can't sit there readable while the
|
|
131
|
+
* image/video it describes is still a spinner.
|
|
132
|
+
*/
|
|
133
|
+
private updateCaptionVisibility;
|
|
134
|
+
private renderCurrentSlide;
|
|
135
|
+
open(index?: number): void;
|
|
136
|
+
/** DESIGN.md §2.3 — low-res open() placeholder source, checked in order: item.thumb, a live data-shoji-thumb on origin, else origin's own rendered <img>. */
|
|
137
|
+
private resolveOpenPlaceholderSrc;
|
|
138
|
+
/**
|
|
139
|
+
* DESIGN.md §2.2/§2.6 — navigates while open; always clamps an explicit
|
|
140
|
+
* out-of-range index (this is a directed jump, not a step — `loop` only
|
|
141
|
+
* affects `next()`/`prev()`, see below). No-op if not open. `animate`
|
|
142
|
+
* (default `true`) runs the configured §2.5 transition; hosts that want
|
|
143
|
+
* an instant jump — e.g. syncing to an external index without a visual
|
|
144
|
+
* flourish — can pass `{ animate: false }`.
|
|
145
|
+
*/
|
|
146
|
+
goTo(index: number, options?: {
|
|
147
|
+
animate?: boolean;
|
|
148
|
+
}): void;
|
|
149
|
+
next(): void;
|
|
150
|
+
prev(): void;
|
|
151
|
+
private nextIndex;
|
|
152
|
+
private prevIndex;
|
|
153
|
+
/**
|
|
154
|
+
* DESIGN.md §2.5 — the shared path behind `goTo()`/`next()`/`prev()`.
|
|
155
|
+
* `animate` is `false` only for `GestureController`'s own host callbacks
|
|
156
|
+
* (see `ensureLightbox()`): a gesture-completed swipe already played its
|
|
157
|
+
* own live-drag/settle animation, so running a *second*, `mode`-based
|
|
158
|
+
* transition on top of it would visibly double-animate. Every other
|
|
159
|
+
* caller — buttons, keyboard, autoplay, a plugin calling `goTo()` — gets
|
|
160
|
+
* the real, configured transition.
|
|
161
|
+
*/
|
|
162
|
+
private navigate;
|
|
163
|
+
/** DESIGN.md §2.5 — `mobileSettings.mode` overrides `mode` on a coarse-pointer device, evaluated fresh each navigation. */
|
|
164
|
+
private resolveTransitionMode;
|
|
165
|
+
private isMobileQuery;
|
|
166
|
+
/**
|
|
167
|
+
* DESIGN.md §2.5 — `mobileSettings.controls: false` starts controls
|
|
168
|
+
* hidden on a coarse-pointer device, reusing the existing §2.8 auto-hide
|
|
169
|
+
* mechanism (`hideControls()`) rather than a separate permanent-hide
|
|
170
|
+
* state: auto-hide is opacity-only, never removed from the tab order,
|
|
171
|
+
* and already reveals on any activity — a genuinely *permanent* hide
|
|
172
|
+
* would strand a touch user with no way to ever reach Close.
|
|
173
|
+
*/
|
|
174
|
+
private applyMobileControlsSetting;
|
|
175
|
+
close(): void;
|
|
176
|
+
/** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */
|
|
177
|
+
private resolveAspectRatio;
|
|
178
|
+
/**
|
|
179
|
+
* Idempotent on purpose: a pending zoom-out's `transitionend`/fallback
|
|
180
|
+
* timeout can still fire after `destroy()` has already force-finished the
|
|
181
|
+
* close (§ destroy() below) — the `!this.opened` guard makes that a no-op
|
|
182
|
+
* instead of a second `close`/`afterClose` emission on a torn-down gallery.
|
|
183
|
+
*/
|
|
184
|
+
private finishClose;
|
|
185
|
+
/** DESIGN.md §2.1 — diffs by id (fallback src), preserving the active slide. */
|
|
186
|
+
updateSlides(items: GalleryItemInput[], currentIndex?: number): void;
|
|
187
|
+
/**
|
|
188
|
+
* Sugar over `updateSlides()` for the common "insert some items" case —
|
|
189
|
+
* splices `items` into a copy of the current list at `atIndex` (default:
|
|
190
|
+
* append at the end) and hands the result to `updateSlides()`, which
|
|
191
|
+
* still does all the real work (active-item preservation, live re-render
|
|
192
|
+
* if open, `itemsUpdated`). `atIndex` follows `Array.prototype.splice`'s
|
|
193
|
+
* own semantics (negative counts from the end, out-of-range clamps) —
|
|
194
|
+
* no extra validation on top of that.
|
|
195
|
+
*/
|
|
196
|
+
addSlides(items: GalleryItemInput[], atIndex?: number): void;
|
|
197
|
+
/**
|
|
198
|
+
* Sugar over `updateSlides()` for the common "remove some items" case.
|
|
199
|
+
* Accepts a single id/index or an array mixing both: a `string` matches
|
|
200
|
+
* the same `id ?? src` key `updateSlides()` already uses for active-item
|
|
201
|
+
* preservation, a `number` matches that position in the *current*
|
|
202
|
+
* `items` list (resolved against the list as it is now, before any
|
|
203
|
+
* removal — passing `[0, 1]` removes the first two items, not "index 0,
|
|
204
|
+
* then whatever became index 1 after that").
|
|
205
|
+
*/
|
|
206
|
+
removeSlides(match: string | number | Array<string | number>): void;
|
|
207
|
+
/** DESIGN.md §2.7 — selector-mode only; sugar over updateSlides() sourced from a DOM rescan. */
|
|
208
|
+
refresh(): void;
|
|
209
|
+
/** Shared by `destroy()`/`reinit()` (§2.7): force-close, run plugin cleanups, drop gesture/transition/slide/dialog. */
|
|
210
|
+
private teardown;
|
|
211
|
+
destroy(): void;
|
|
212
|
+
/**
|
|
213
|
+
* DESIGN.md §2.7 — full teardown + reconstruction on `this`, not a new
|
|
214
|
+
* object: `Shoji.getInstance(el)`/any held reference keeps working across
|
|
215
|
+
* the call, unlike `destroy()` + `new Shoji(...)`. Omit `options` to
|
|
216
|
+
* rebuild unchanged (a hard reset); pass options to reconfigure
|
|
217
|
+
* structurally. `gallery.on(...)` listeners do NOT survive — the event
|
|
218
|
+
* bus is fully cleared, same as `destroy()` — re-`on()` anything needed.
|
|
219
|
+
*/
|
|
220
|
+
reinit(options?: GalleryOptions): void;
|
|
221
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { GestureEngineOptions } from '../gestures/GestureEngine';
|
|
2
|
+
import { SlideManager } from './SlideManager';
|
|
3
|
+
/**
|
|
4
|
+
* A real interactive control. Also used by `Gallery.ts`'s `isBackdropClick`,
|
|
5
|
+
* which used to have its own narrower list (missing select/input/textarea/
|
|
6
|
+
* a[href]) — a plugin-mounted non-button control got misread as a backdrop
|
|
7
|
+
* click and closed the gallery. One shared selector so that can't recur.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
|
|
10
|
+
/** What `GestureController` needs from `Gallery` — narrow on purpose, so this module never reaches into Gallery internals beyond this contract. */
|
|
11
|
+
export interface GestureControllerHost {
|
|
12
|
+
dialog: HTMLElement;
|
|
13
|
+
slides: SlideManager;
|
|
14
|
+
canGoNext(): boolean;
|
|
15
|
+
canGoPrev(): boolean;
|
|
16
|
+
next(): void;
|
|
17
|
+
prev(): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
/** `closable: false` (GalleryOptions) — false suspends vertical drag-to-close entirely: no live feedback, release never calls `close()`. */
|
|
20
|
+
canClose(): boolean;
|
|
21
|
+
onActivity(): void;
|
|
22
|
+
/** DESIGN.md §4-zoom — true while zoomed; suspends drag-to-navigate/close. */
|
|
23
|
+
isZoomed(): boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Relays for gestures core has no built-in behavior for — DESIGN.md §2.4; the zoom plugin (§4-zoom) consumes these via the gallery event bus. */
|
|
26
|
+
export interface GestureRelayCallbacks {
|
|
27
|
+
onTap(x: number, y: number): void;
|
|
28
|
+
onDoubleTap(x: number, y: number): void;
|
|
29
|
+
onPinchStart(centerX: number, centerY: number): void;
|
|
30
|
+
onPinchMove(scale: number, centerX: number, centerY: number): void;
|
|
31
|
+
onPinchEnd(): void;
|
|
32
|
+
onWheelZoom(deltaScale: number, x: number, y: number): void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* DESIGN.md §2.4 — owns the gesture-driven side of the lightbox: horizontal
|
|
36
|
+
* drag-to-navigate and vertical drag-to-close, built on `GestureEngine`
|
|
37
|
+
* (`src/gestures/`) plus the live-feedback/settle-animation mechanics
|
|
38
|
+
* specific to this lightbox's DOM. Split out of `Gallery.ts` (CLAUDE.md:
|
|
39
|
+
* files ≤ ~400 lines) — `Gallery` only constructs this, forwards the host
|
|
40
|
+
* callbacks it needs, and relays no-built-in-effect gestures onto its bus.
|
|
41
|
+
*/
|
|
42
|
+
export declare class GestureController {
|
|
43
|
+
private readonly host;
|
|
44
|
+
private readonly engine;
|
|
45
|
+
constructor(host: GestureControllerHost, relay: GestureRelayCallbacks, options?: Partial<GestureEngineOptions>);
|
|
46
|
+
destroy(): void;
|
|
47
|
+
/** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */
|
|
48
|
+
private readonly onDragMove;
|
|
49
|
+
private readonly onDragEnd;
|
|
50
|
+
/** `completed` decides intent; `canGoNext`/`canGoPrev` guard whether that's actually possible (`loop: false` at an end item). */
|
|
51
|
+
private finishHorizontalDrag;
|
|
52
|
+
private settleDragOffset;
|
|
53
|
+
/** Purely presentational drag feedback — scales/fades the dialog toward `close()`'s target state as the viewer drags, without closing until release decides the outcome. */
|
|
54
|
+
private applyVerticalDragFeedback;
|
|
55
|
+
private clearVerticalDragFeedback;
|
|
56
|
+
private finishVerticalDrag;
|
|
57
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { VideoProviderRenderer } from './plugin';
|
|
2
|
+
import { GalleryItem } from './types';
|
|
3
|
+
export interface SlideManagerOptions {
|
|
4
|
+
preload: number;
|
|
5
|
+
/** Play-overlay button's label. */
|
|
6
|
+
playVideoLabel: string;
|
|
7
|
+
videoProviders: Map<string, VideoProviderRenderer>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* DESIGN.md §2.3 — only `currentIndex ± preload` exist in the DOM. Content
|
|
11
|
+
* already resident and ready is never moved between slots — the slot that
|
|
12
|
+
* already holds it just gets its own `offset` relabeled to its new pool
|
|
13
|
+
* position; only content resident nowhere yet gets built fresh into
|
|
14
|
+
* whatever's left over. O(preload) re-render cost either way, but never a
|
|
15
|
+
* DOM reparent — which matters for a provider video's `<iframe>` (§4.3):
|
|
16
|
+
* most browsers reload an iframe moved to a new parent, so a live embed can
|
|
17
|
+
* only safely change position by relabeling, never by being moved.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SlideManager {
|
|
20
|
+
readonly element: HTMLElement;
|
|
21
|
+
private readonly slots;
|
|
22
|
+
private readonly preload;
|
|
23
|
+
private readonly playVideoLabel;
|
|
24
|
+
private readonly videoProviders;
|
|
25
|
+
private dragOffsetPx;
|
|
26
|
+
/** Ready nodes keyed by item index — trimmed to `centerIndex ± preload` each `render()`, same window the slots cover, so an evicted entry's video/iframe resources get released even if no slot ever reclaims it. */
|
|
27
|
+
private readonly cache;
|
|
28
|
+
/**
|
|
29
|
+
* Image decodes in flight, keyed by item index, not by whichever slot
|
|
30
|
+
* started them — a real bug this fixes: navigating before a preload
|
|
31
|
+
* decode resolved used to reassign that slot, abandoning the decode and
|
|
32
|
+
* starting a duplicate elsewhere, so a slow preload could never finish,
|
|
33
|
+
* only restart. `reveal()` looks up which slot currently wants this
|
|
34
|
+
* index at resolve time, so a reshuffled pool still lands it correctly.
|
|
35
|
+
*/
|
|
36
|
+
private readonly pending;
|
|
37
|
+
constructor(options: SlideManagerOptions);
|
|
38
|
+
/** The active slot's `.shoji-slide-media` — not `.shoji-slide`, whose transform is owned by pool-offset positioning. */
|
|
39
|
+
getActiveMedia(): HTMLElement | null;
|
|
40
|
+
/** False while the active slot's content is still decoding — Gallery.ts uses this right after `render()` to show the loading state immediately, without waiting for `onLoad`. */
|
|
41
|
+
isActiveReady(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* DESIGN.md §2.4 — live drag-to-navigate feedback: every slot's
|
|
44
|
+
* structural `offset * 100%` position gets this same `px` added on top,
|
|
45
|
+
* so dragging left/right visually slides the whole pool together (the
|
|
46
|
+
* adjacent preloaded slot already exists at `±100%`) — no new DOM, no
|
|
47
|
+
* content re-render mid-drag. `transition`: `null` during the live drag
|
|
48
|
+
* (1:1 with the pointer); a real value only for the post-release settle
|
|
49
|
+
* animation, reset back to `null` once that transition ends.
|
|
50
|
+
*/
|
|
51
|
+
setDragOffset(px: number, transition: string | null): void;
|
|
52
|
+
/** The pool slot's `.shoji-slide` root at a structural offset — what the drag settle animation (§2.4) waits for `transitionend` on. */
|
|
53
|
+
getSlotRoot(offset: number): HTMLElement | null;
|
|
54
|
+
private applyTransforms;
|
|
55
|
+
/** Re-renders whichever slots need a different item; `onLoad` fires per index once its media settles. `openPlaceholderSrc` (only from `Gallery.open()`) swaps the centerIndex slot's spinner for a low-res placeholder once it decodes, if not already ready. */
|
|
56
|
+
render(items: readonly GalleryItem[], centerIndex: number, onLoad: (index: number) => void, openPlaceholderSrc?: string): void;
|
|
57
|
+
private applyAspect;
|
|
58
|
+
/** Releases the slot's old content and swaps the new node in, caching it. `extra` rides as a second child. `ensureImageDecoding` goes through `moveIn` instead. */
|
|
59
|
+
private swapIn;
|
|
60
|
+
/**
|
|
61
|
+
* Inserts a just-decoded node into the slot waiting on it — skips
|
|
62
|
+
* `releaseVideo`, unlike `swapIn`: this is its first insertion anywhere,
|
|
63
|
+
* nothing outgoing to release. `render()`'s claim pass (Phase 1) never
|
|
64
|
+
* calls this — it reuses `ready` content in place, no DOM touch at all.
|
|
65
|
+
*/
|
|
66
|
+
private moveIn;
|
|
67
|
+
/** Starts decoding `item` for `index`, only if nothing already is (see `pending`). Resolves by looking up whichever slot currently wants this index, not the one active when the decode started. */
|
|
68
|
+
private ensureImageDecoding;
|
|
69
|
+
/** DESIGN.md §2.3 — swaps the spinner for the placeholder once *it* decodes, not immediately: `item.thumb` is often just `item.src` again, so an undecoded placeholder can leave as long a blank gap as the spinner it replaces. */
|
|
70
|
+
private revealOpenPlaceholder;
|
|
71
|
+
/** Native `<video controls>` — real playback, not just a poster. Not autoplayed/muted: a deliberate click-to-play, not a background loop. */
|
|
72
|
+
private renderVideo;
|
|
73
|
+
/** Tracks the video's own play/pause/ended state. Hide `.shoji-video-play-overlay` in CSS to opt out. */
|
|
74
|
+
private createVideoPlayOverlay;
|
|
75
|
+
/** DESIGN.md §4-video. Unregistered provider → same placeholder as no-source video. */
|
|
76
|
+
private renderProviderVideo;
|
|
77
|
+
destroy(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Pauses whatever's playing in `media` — native `<video>`, or a provider
|
|
81
|
+
* embed wired via `wirePlayableContract` (§4-video) — without
|
|
82
|
+
* releasing/destroying it, unlike `releaseVideo` above. `Gallery` calls
|
|
83
|
+
* this on `close()` and before every navigation, on the outgoing slide.
|
|
84
|
+
*/
|
|
85
|
+
export declare function pauseMedia(media: HTMLElement | null): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface LightboxLabels {
|
|
2
|
+
close: string;
|
|
3
|
+
previous: string;
|
|
4
|
+
next: string;
|
|
5
|
+
showCaption: string;
|
|
6
|
+
hideCaption: string;
|
|
7
|
+
}
|
|
8
|
+
export interface LightboxDom {
|
|
9
|
+
outer: HTMLElement;
|
|
10
|
+
dialog: HTMLElement;
|
|
11
|
+
counter: HTMLElement;
|
|
12
|
+
caption: HTMLElement;
|
|
13
|
+
closeButton: HTMLButtonElement;
|
|
14
|
+
prevButton: HTMLButtonElement;
|
|
15
|
+
nextButton: HTMLButtonElement;
|
|
16
|
+
captionToggleButton: HTMLButtonElement;
|
|
17
|
+
/** DESIGN.md §3 — `ctx.ui.toolbar(slot, ...)` inserts into these; close lives in `right`. */
|
|
18
|
+
toolbarLeft: HTMLElement;
|
|
19
|
+
toolbarCenter: HTMLElement;
|
|
20
|
+
toolbarRight: HTMLElement;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* DESIGN.md §2.6 — `.shoji-outer` is the exact element `ctx.ui.outer()` (§3)
|
|
24
|
+
* will return to plugins once the real plugin loader exists. Close/prev/next
|
|
25
|
+
* are core-owned baseline controls, not routed through any plugin API.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildLightboxDom(slides: HTMLElement, labels: LightboxLabels): LightboxDom;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** DESIGN.md §9 — inline SVG, stroke = currentColor, overridable per-button. */
|
|
2
|
+
export declare const CLOSE_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"/></svg>";
|
|
3
|
+
export declare const PREV_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 6l-6 6 6 6\"/></svg>";
|
|
4
|
+
export declare const NEXT_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 6l6 6-6 6\"/></svg>";
|
|
5
|
+
export declare const PLAY_ICON = "<svg viewBox=\"0 0 24 24\" width=\"28\" height=\"28\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>";
|
|
6
|
+
export declare const CAPTION_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"2\"/><path d=\"M7 10h10M7 14h6\"/></svg>";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Gallery } from './Gallery';
|
|
2
|
+
export { EventBus } from './EventBus';
|
|
3
|
+
export type { Unsubscribe } from './EventBus';
|
|
4
|
+
export type { GalleryItem, GalleryOptions, GalleryEvents, MediaSource, VideoDescriptor, } from './types';
|
|
5
|
+
export type { ShojiPlugin, PluginContext, ButtonSpec, KeySpec } from './plugin';
|