@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/esm/core/EventBus.d.ts +16 -0
  4. package/dist/esm/core/FocusTrap.d.ts +9 -0
  5. package/dist/esm/core/Gallery.d.ts +221 -0
  6. package/dist/esm/core/GestureController.d.ts +57 -0
  7. package/dist/esm/core/LiveRegion.d.ts +6 -0
  8. package/dist/esm/core/SlideManager.d.ts +85 -0
  9. package/dist/esm/core/bodyScrollLock.d.ts +2 -0
  10. package/dist/esm/core/dom.d.ts +27 -0
  11. package/dist/esm/core/icons.d.ts +6 -0
  12. package/dist/esm/core/index.d.ts +5 -0
  13. package/dist/esm/core/index.js +2105 -0
  14. package/dist/esm/core/index.js.map +1 -0
  15. package/dist/esm/core/plugin.d.ts +54 -0
  16. package/dist/esm/core/rotateFlipNormalize.d.ts +19 -0
  17. package/dist/esm/core/scan.d.ts +19 -0
  18. package/dist/esm/core/types.d.ts +290 -0
  19. package/dist/esm/core/zoomTransition.d.ts +33 -0
  20. package/dist/esm/gestures/GestureEngine.d.ts +78 -0
  21. package/dist/esm/index.css +540 -0
  22. package/dist/esm/index.d.ts +32 -0
  23. package/dist/esm/index.js +24 -0
  24. package/dist/esm/index.js.map +1 -0
  25. package/dist/esm/index2.css +22 -0
  26. package/dist/esm/index3.css +170 -0
  27. package/dist/esm/index4.css +27 -0
  28. package/dist/esm/plugins/activeThumbnail/index.d.ts +26 -0
  29. package/dist/esm/plugins/activeThumbnail/index.js +63 -0
  30. package/dist/esm/plugins/activeThumbnail/index.js.map +1 -0
  31. package/dist/esm/plugins/autoplay/icons.d.ts +3 -0
  32. package/dist/esm/plugins/autoplay/index.d.ts +19 -0
  33. package/dist/esm/plugins/autoplay/index.js +207 -0
  34. package/dist/esm/plugins/autoplay/index.js.map +1 -0
  35. package/dist/esm/plugins/fullscreen/icons.d.ts +3 -0
  36. package/dist/esm/plugins/fullscreen/index.d.ts +17 -0
  37. package/dist/esm/plugins/fullscreen/index.js +74 -0
  38. package/dist/esm/plugins/fullscreen/index.js.map +1 -0
  39. package/dist/esm/plugins/layout/index.d.ts +191 -0
  40. package/dist/esm/plugins/layout/index.js +752 -0
  41. package/dist/esm/plugins/layout/index.js.map +1 -0
  42. package/dist/esm/plugins/layout/justified.d.ts +68 -0
  43. package/dist/esm/plugins/layout/masonry.d.ts +92 -0
  44. package/dist/esm/plugins/rotateFlip/icons.d.ts +5 -0
  45. package/dist/esm/plugins/rotateFlip/index.d.ts +17 -0
  46. package/dist/esm/plugins/rotateFlip/index.js +138 -0
  47. package/dist/esm/plugins/rotateFlip/index.js.map +1 -0
  48. package/dist/esm/plugins/video/index.d.ts +13 -0
  49. package/dist/esm/plugins/video/index.js +207 -0
  50. package/dist/esm/plugins/video/index.js.map +1 -0
  51. package/dist/esm/plugins/video/vimeo.d.ts +43 -0
  52. package/dist/esm/plugins/video/youtube.d.ts +61 -0
  53. package/dist/esm/plugins/zoom/icons.d.ts +4 -0
  54. package/dist/esm/plugins/zoom/index.d.ts +30 -0
  55. package/dist/esm/plugins/zoom/index.js +274 -0
  56. package/dist/esm/plugins/zoom/index.js.map +1 -0
  57. package/dist/esm/plugins/zoom/zoomMath.d.ts +24 -0
  58. package/dist/esm/transitions/SlideTransition.d.ts +25 -0
  59. package/dist/esm/transitions/presets.d.ts +21 -0
  60. package/dist/esm/zoomTransition-bbKHpVpA.js +110 -0
  61. package/dist/esm/zoomTransition-bbKHpVpA.js.map +1 -0
  62. package/dist/shoji.css +759 -0
  63. package/dist/shoji.js +3907 -0
  64. package/dist/shoji.js.map +1 -0
  65. package/dist/shoji.min.css +1 -0
  66. package/dist/shoji.min.js +2 -0
  67. package/dist/shoji.min.js.map +1 -0
  68. package/package.json +77 -0
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Core theme tokens (DESIGN.md §9). Dark is the default theme; `.shoji-light`
3
+ * overrides to light. No hardcoded colors/sizes elsewhere in JS or CSS —
4
+ * everything routes through these custom properties.
5
+ *
6
+ * `.shoji-outer` always mounts directly on `document.body` (Gallery.ts),
7
+ * never nested inside the host's own gallery container — so `.shoji-light`
8
+ * (or any `--shoji-*` override) must be applied to `<body>`/`<html>`/another
9
+ * real ancestor of the mount point, not the gallery container itself, or it
10
+ * silently does nothing (custom properties only inherit down real DOM
11
+ * ancestry).
12
+ */
13
+ :root {
14
+ /* .shoji-outer's own page-level z-index — relative to the rest of the
15
+ host page, not to anything inside the lightbox. Real pages routinely
16
+ put sticky navbars/sidebars/toast containers at 1000-9999, so this
17
+ needs real headroom, not the small internal scale below (which only
18
+ orders backdrop/slides/overlay/toolbar/toast *against each other*,
19
+ inside the stacking context .shoji-outer's own z-index establishes). */
20
+ --shoji-z-base: 9000;
21
+
22
+ --shoji-z-backdrop: 0;
23
+ --shoji-z-slides: 10;
24
+ --shoji-z-overlay: 20;
25
+ --shoji-z-toolbar: 30;
26
+ --shoji-z-toast: 40;
27
+
28
+ --shoji-color-bg: #0b0b0d;
29
+ --shoji-color-fg: #f5f5f5;
30
+ --shoji-color-accent: #4da3ff;
31
+ --shoji-color-backdrop: rgba(0, 0, 0, 0.92);
32
+ --shoji-color-toolbar-bg: rgba(20, 20, 22, 0.85);
33
+ --shoji-color-disabled: rgba(245, 245, 245, 0.35);
34
+
35
+ --shoji-focus-ring: 2px solid var(--shoji-color-accent);
36
+ --shoji-progress: var(--shoji-color-accent);
37
+
38
+ --shoji-spacing-sm: 0.5rem;
39
+ --shoji-spacing-md: 1rem;
40
+ --shoji-spacing-lg: 1.5rem;
41
+
42
+ --shoji-easing: cubic-bezier(0.4, 0, 0.2, 1);
43
+ --shoji-duration: 300ms;
44
+ /* Caps how tall the caption box can grow before it scrolls internally —
45
+ an unbounded caption on a long string would otherwise creep up over a
46
+ video slide's native control bar underneath it (bottom-left, same
47
+ corner), silently swallowing scrub/volume/fullscreen clicks with no
48
+ way to reach them. `min()` also bounds it on a short/wide dialog
49
+ (mobile landscape), where a `rem`-only cap could still eat most of
50
+ the available height. */
51
+ --shoji-caption-max-height: min(8rem, 30%);
52
+ /* Reserved space at the top of a provider embed (YouTube etc.) so its own
53
+ hover-revealed title bar never sits under Shoji's toolbar — matches the
54
+ toolbar's own rendered height (44px button + top/bottom padding).
55
+ Permanent, not tied to the toolbar's auto-hide state, so the video
56
+ never resizes as the toolbar fades in/out. HTML5 video has no top-of-
57
+ player UI at all, so it's untouched. */
58
+ --shoji-provider-video-top-inset: calc(44px + 2 * var(--shoji-spacing-sm));
59
+ /* DESIGN.md §2.4 — the gesture engine's own documented default easing for
60
+ drag-to-navigate/drag-to-close settle animations specifically (a snap
61
+ tuned for "released mid-motion," distinct from --shoji-easing's
62
+ open/close/zoom-transition curve). */
63
+ --shoji-momentum-easing: cubic-bezier(0.2, 0, 0, 1);
64
+
65
+ /* System-font stack: no webfont download, renders instantly, looks native
66
+ (San Francisco / Segoe UI / Roboto) on every platform. */
67
+ --shoji-font-family:
68
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
69
+ 'Apple Color Emoji', 'Segoe UI Emoji';
70
+ }
71
+
72
+ .shoji-light {
73
+ --shoji-color-bg: #fafafa;
74
+ --shoji-color-fg: #111111;
75
+ --shoji-color-backdrop: rgba(255, 255, 255, 0.96);
76
+ --shoji-color-toolbar-bg: rgba(255, 255, 255, 0.9);
77
+ --shoji-color-disabled: rgba(17, 17, 17, 0.35);
78
+ }
79
+
80
+ @media (prefers-reduced-motion: reduce) {
81
+ :root {
82
+ --shoji-duration: 0ms;
83
+ }
84
+ }
85
+
86
+ /* Lightbox structure (DESIGN.md §2.3 slides, §2.6 dialog). Built lazily on first open(). */
87
+ .shoji-outer {
88
+ display: none;
89
+ position: fixed;
90
+ inset: 0;
91
+ z-index: var(--shoji-z-base);
92
+ }
93
+
94
+ .shoji-outer.shoji-open {
95
+ display: block;
96
+ }
97
+
98
+ .shoji-backdrop {
99
+ position: absolute;
100
+ inset: 0;
101
+ z-index: var(--shoji-z-backdrop);
102
+ background: var(--shoji-color-backdrop);
103
+ }
104
+
105
+ .shoji-dialog {
106
+ position: relative;
107
+ z-index: var(--shoji-z-slides);
108
+ display: flex;
109
+ flex-direction: column;
110
+ height: 100%;
111
+ color: var(--shoji-color-fg);
112
+ font-family: var(--shoji-font-family);
113
+ outline: none;
114
+ /* DESIGN.md §2.4 — the gesture engine owns horizontal/vertical drag on
115
+ this element; without this, touch browsers intercept the same gesture
116
+ for native scroll/pull-to-refresh/back-navigation before JS ever sees
117
+ enough movement to lock a direction. Buttons/video inside remain
118
+ interactive regardless (this is about implicit browser gesture
119
+ handling, not pointer-events). */
120
+ touch-action: none;
121
+ }
122
+
123
+ .shoji-toolbar {
124
+ position: absolute;
125
+ inset: 0 0 auto 0;
126
+ z-index: var(--shoji-z-toolbar);
127
+ display: flex;
128
+ justify-content: space-between;
129
+ padding: var(--shoji-spacing-sm);
130
+ pointer-events: none;
131
+ /* Reported from real usage: the Zoom plugin's frequent scale() updates on
132
+ the photo (a sibling, not a descendant) could show as GPU tile-seam
133
+ artifacts (thin lines) through the toolbar's own semi-transparent,
134
+ border-radius buttons — the same class of rendering quirk already noted
135
+ for progressive image decode. Promoting the toolbar to its own stable
136
+ compositing layer, decoupled from the photo's repaints, avoids it. */
137
+ will-change: transform;
138
+ }
139
+
140
+ /* DESIGN.md §3 — ctx.ui.toolbar(slot, ...) inserts plugin buttons into these;
141
+ pointer-events re-enabled per-slot (not per-button) so gaps between slots
142
+ stay click-through even once a slot has content. `flex-wrap: wrap` (not
143
+ `overflow-x: auto`) is deliberate: several plugins loaded together on a
144
+ narrow/mobile viewport can add up to more 44px buttons than the width
145
+ allows (CLAUDE.md's own touch-target size, non-negotiable), and this
146
+ project treats horizontal scroll as something to design around, not
147
+ reach for (see the layout plugin's masonry `orientation: 'horizontal'`,
148
+ which was rebuilt specifically to avoid it) — wrapping to a second row
149
+ keeps every button reachable by a tap with no sideways scroll gesture
150
+ anywhere in the lightbox chrome either. */
151
+ .shoji-toolbar-slot {
152
+ display: flex;
153
+ flex-wrap: wrap;
154
+ align-items: center;
155
+ gap: var(--shoji-spacing-sm);
156
+ pointer-events: auto;
157
+ max-width: 45%;
158
+ }
159
+
160
+ .shoji-toolbar-left {
161
+ justify-content: flex-start;
162
+ }
163
+
164
+ .shoji-toolbar-right {
165
+ justify-content: flex-end;
166
+ }
167
+
168
+ .shoji-close,
169
+ .shoji-nav,
170
+ .shoji-toolbar-button {
171
+ appearance: none;
172
+ border: none;
173
+ padding: 0;
174
+ width: 44px;
175
+ height: 44px;
176
+ border-radius: 50%;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ background: rgba(0, 0, 0, 0.4);
181
+ color: var(--shoji-color-fg);
182
+ cursor: pointer;
183
+ }
184
+
185
+ .shoji-close:hover,
186
+ .shoji-nav:hover,
187
+ .shoji-toolbar-button:hover {
188
+ background: rgba(0, 0, 0, 0.6);
189
+ }
190
+
191
+ .shoji-close:focus-visible,
192
+ .shoji-nav:focus-visible,
193
+ .shoji-toolbar-button:focus-visible {
194
+ outline: var(--shoji-focus-ring);
195
+ outline-offset: 2px;
196
+ }
197
+
198
+ .shoji-nav {
199
+ position: absolute;
200
+ top: 50%;
201
+ transform: translateY(-50%);
202
+ z-index: var(--shoji-z-toolbar);
203
+ }
204
+
205
+ .shoji-nav-prev {
206
+ left: var(--shoji-spacing-md);
207
+ }
208
+
209
+ .shoji-nav-next {
210
+ right: var(--shoji-spacing-md);
211
+ }
212
+
213
+ .shoji-nav:disabled {
214
+ opacity: 0.3;
215
+ cursor: default;
216
+ pointer-events: none;
217
+ }
218
+
219
+ .shoji-close[hidden],
220
+ .shoji-nav[hidden],
221
+ .shoji-toolbar-button[hidden] {
222
+ display: none;
223
+ }
224
+
225
+ /* DESIGN.md §2.3 — while the active slide's own content is still loading,
226
+ feature/plugin toolbar buttons (zoom, rotate, fullscreen, autoplay, any
227
+ custom plugin's own button) dim and stop responding, since they'd
228
+ otherwise act on content about to be replaced. Deliberately scoped to
229
+ `.shoji-toolbar-button` only — `.shoji-close`/`.shoji-nav` are different
230
+ classes, so closing and navigating are never blocked by a slow image.
231
+ `pointer-events: none` here is the mouse half; Gallery.ts's matching
232
+ `tabIndex = -1` is the keyboard half (this alone wouldn't stop
233
+ Enter/Space activating an already-focused button). */
234
+ .shoji-outer.shoji-slide-loading .shoji-toolbar-button {
235
+ opacity: 0.3;
236
+ cursor: default;
237
+ pointer-events: none;
238
+ }
239
+
240
+ .shoji-slides {
241
+ position: relative;
242
+ flex: 1;
243
+ overflow: hidden;
244
+ }
245
+
246
+ .shoji-slide {
247
+ position: absolute;
248
+ inset: 0;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ /* transform (translateX per pool offset) is set inline by SlideManager */
253
+ }
254
+
255
+ .shoji-slide-media {
256
+ /* Definite size (not max-width/max-height alone) so the img's own
257
+ percentage-based max-width/max-height below has something concrete to
258
+ resolve against — otherwise it sizes to the image's natural dimensions
259
+ instead of being contained, and object-fit: contain never engages. */
260
+ width: 100%;
261
+ height: 100%;
262
+ display: flex;
263
+ align-items: center;
264
+ justify-content: center;
265
+ /* Anchors .shoji-video-play-overlay's absolute positioning to this box
266
+ specifically, not implicitly to whichever positioned ancestor happens
267
+ to be next up (currently .shoji-slide, coincidentally the same size —
268
+ explicit here so that stays true even if that ever changes). */
269
+ position: relative;
270
+ }
271
+
272
+ .shoji-slide-img,
273
+ .shoji-slide-video {
274
+ display: block;
275
+ max-width: 100%;
276
+ max-height: 100%;
277
+ object-fit: contain;
278
+ }
279
+
280
+ /* max-width/max-height above only ever *cap* size — a real photo is normally
281
+ bigger than the slide area, so that's correct. A low-res open() placeholder
282
+ (§2.3) is the opposite case: deliberately small, so it needs to be forced
283
+ up to fill the frame (blurry, on purpose) instead of rendering at its own
284
+ tiny natural size. object-fit: contain (inherited above) still letterboxes
285
+ it within that forced box, just per the thumbnail's own aspect ratio. */
286
+ .shoji-slide-open-placeholder {
287
+ width: 100%;
288
+ height: 100%;
289
+ }
290
+
291
+ /* DESIGN.md §4-video — a provider's embed (an <iframe>, typically) has no
292
+ natural size Shoji can measure the way a decoded image/video does. A real
293
+ bug, reported from real usage, found in two parts:
294
+ 1. Stretching the embed to fill this box regardless of the video's own
295
+ aspect ratio pushed the bottom of the video (and a provider's own
296
+ controls, always along that edge) below the visible area whenever the
297
+ box wasn't already the same shape as the video.
298
+ 2. Fixing that by fitting to the box's actual shape instead exposed a
299
+ second issue: neither YouTube's nor Vimeo's own player page renders
300
+ with a background matching Shoji's theme in the letterboxed gap
301
+ *inside* the iframe (both showed white) — a color this page has no
302
+ way to reach or override, since that content is cross-origin.
303
+ Assumes 16:9 (overwhelmingly the common case for both providers, and
304
+ exactly correct for the video this was diagnosed against) and *contains*
305
+ within whatever box is actually available — the same shape object-fit
306
+ already gives an <img>/<video> above, achieved differently here since
307
+ object-fit doesn't apply to an iframe (or the wrapper div Vimeo's own
308
+ SDK builds one inside) — so any remaining gap is outside the iframe
309
+ instead of inside it, showing Shoji's own dark backdrop through this
310
+ element's own transparent background rather than the provider's white. */
311
+ .shoji-slide-provider-video {
312
+ position: relative;
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ width: 100%;
317
+ height: 100%;
318
+ box-sizing: border-box;
319
+ padding-top: var(--shoji-provider-video-top-inset);
320
+ /* Establishes `cqw`/`cqh` below as this element's own *content* box
321
+ (padding already excluded) — safe to size independently of children
322
+ since its own width/height already come from its parent, not from
323
+ whatever ends up inside it. */
324
+ container-type: size;
325
+ }
326
+
327
+ .shoji-slide-provider-video[hidden] {
328
+ display: none;
329
+ }
330
+
331
+ /* `.shoji-video-mount` — src/plugins/video/vimeo.ts's own wrapper div,
332
+ which its SDK would otherwise size itself (a small fixed default
333
+ normally, or its own `responsive: true` option's aspect-ratio-from-width-
334
+ alone computation, ignoring how much height is actually available either
335
+ way) — `!important` discards whatever it tries to set inline. The direct
336
+ `> iframe` case is YouTube, which has no such wrapper at all.
337
+ `min(100cqw, 100cqh * 16/9)`/`min(100cqh, 100cqw * 9/16)` is the
338
+ contain-within-either-axis formula `object-fit: contain` gives an
339
+ <img>/<video> natively — plain `aspect-ratio` alone can't do this for a
340
+ non-replaced element like a div/iframe: it only ever derives *one*
341
+ dimension from the other, so whichever of width/height stays a fixed
342
+ `100%` never actually shrinks even once the derived dimension gets
343
+ capped by `max-width`/`max-height`, silently distorting the ratio in
344
+ whichever direction wasn't the binding constraint. Confirmed directly at
345
+ both a wide-short viewport (height the binding constraint) and a
346
+ narrow-tall one (width the binding constraint) — container query units
347
+ are what actually solve both at once, not `aspect-ratio` by itself. */
348
+ .shoji-slide-provider-video > iframe,
349
+ .shoji-video-mount {
350
+ width: min(100cqw, calc(100cqh * 16 / 9)) !important;
351
+ height: min(100cqh, calc(100cqw * 9 / 16)) !important;
352
+ margin: 0 !important;
353
+ padding: 0 !important;
354
+ }
355
+
356
+ .shoji-slide-provider-video iframe {
357
+ display: block;
358
+ border: 0;
359
+ }
360
+
361
+ /* Once `.shoji-video-mount` above is already correctly contain-fitted, its
362
+ own iframe just needs to fill it completely — no `aspect-ratio` of its
363
+ own to fight the wrapper's. Same `!important`/static-not-absolute
364
+ reasoning as `.shoji-video-mount` — Vimeo's SDK may set its own inline
365
+ sizing/position on the iframe too, not just on the wrapper. */
366
+ .shoji-video-mount iframe {
367
+ position: static !important;
368
+ width: 100% !important;
369
+ height: 100% !important;
370
+ }
371
+
372
+ /* A paused video's own play affordance — native browser styling for this
373
+ varies too much to rely on alone (some show a big centered play icon,
374
+ some don't). Faint/translucent, same visual language as the toolbar's
375
+ own round icon buttons, just bigger — a primary, hard-to-miss affordance,
376
+ not a small chrome control. Deliberately not gated by any GalleryOptions
377
+ flag (purely presentational — hiding it changes nothing functionally,
378
+ the video is still clickable either way); a host not wanting it just
379
+ overrides this selector in their own CSS. */
380
+ .shoji-video-play-overlay {
381
+ position: absolute;
382
+ top: 50%;
383
+ left: 50%;
384
+ transform: translate(-50%, -50%);
385
+ width: var(--shoji-video-overlay-size, 64px);
386
+ height: var(--shoji-video-overlay-size, 64px);
387
+ border-radius: 50%;
388
+ border: none;
389
+ padding: 0;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ background: var(--shoji-video-overlay-bg, rgba(0, 0, 0, 0.4));
394
+ color: var(--shoji-video-overlay-color, var(--shoji-color-fg));
395
+ cursor: pointer;
396
+ }
397
+
398
+ .shoji-video-play-overlay[hidden] {
399
+ display: none;
400
+ }
401
+
402
+ /* DESIGN.md §2.5 — the temporary clone SlideTransition animates the
403
+ outgoing slide's content out on. Sits above the normal (unpositioned
404
+ z-index) pool slots by DOM order alone, but z-index is set explicitly
405
+ here rather than relied on implicitly, matching how every other overlay
406
+ layer in this file states its stacking intent up front. Not interactive
407
+ (it's a decorative clone mid-removal) and never receives its own size —
408
+ it fills `.shoji-slides` exactly like the real `.shoji-slide` it's a
409
+ snapshot of, so the cloned `.shoji-slide-media` inside it (already
410
+ `width/height: 100%`) lines up pixel-for-pixel with no extra CSS needed. */
411
+ .shoji-slide-ghost {
412
+ position: absolute;
413
+ inset: 0;
414
+ z-index: 1;
415
+ pointer-events: none;
416
+ }
417
+
418
+ .shoji-slide-placeholder {
419
+ color: var(--shoji-color-disabled);
420
+ }
421
+
422
+ /* DESIGN.md §2.3 — shown in place of a slide's content while it's still
423
+ decoding/loading (outside the `preload` window, where nothing was ready
424
+ ahead of time). CSS-only rotation (no JS-driven layout, per CLAUDE.md);
425
+ disabled under prefers-reduced-motion the same as every other animation
426
+ here, rather than left spinning regardless. */
427
+ .shoji-slide-spinner {
428
+ width: var(--shoji-spinner-size, 40px);
429
+ height: var(--shoji-spinner-size, 40px);
430
+ border-radius: 50%;
431
+ border: var(--shoji-spinner-thickness, 3px) solid var(--shoji-color-disabled);
432
+ border-top-color: var(--shoji-color-accent);
433
+ animation: shoji-spin 0.8s linear infinite;
434
+ }
435
+
436
+ @keyframes shoji-spin {
437
+ to {
438
+ transform: rotate(360deg);
439
+ }
440
+ }
441
+
442
+ @media (prefers-reduced-motion: reduce) {
443
+ .shoji-slide-spinner {
444
+ animation: none;
445
+ }
446
+ }
447
+
448
+ /* A normal flex item within .shoji-toolbar-left (not independently
449
+ positioned) — .shoji-slides still gets the full dialog height either way,
450
+ no reserved gutter at top or bottom, since the toolbar itself overlays the
451
+ slides rather than pushing them down. Flowing alongside whatever else is
452
+ in that slot (e.g. a plugin's ctx.ui.toolbar('left', ...) button) keeps it
453
+ from overlapping and stealing clicks the way independent absolute
454
+ positioning did. */
455
+ .shoji-counter {
456
+ padding: 0.25rem 0.6rem;
457
+ border-radius: 999px;
458
+ background: rgba(0, 0, 0, 0.4);
459
+ font-size: 0.875rem;
460
+ white-space: nowrap;
461
+ }
462
+
463
+ .shoji-counter[hidden] {
464
+ display: none;
465
+ }
466
+
467
+ .shoji-caption {
468
+ position: absolute;
469
+ bottom: var(--shoji-spacing-md);
470
+ left: var(--shoji-spacing-md);
471
+ z-index: var(--shoji-z-toolbar);
472
+ width: fit-content;
473
+ max-width: calc(100% - 2 * var(--shoji-spacing-md));
474
+ max-height: var(--shoji-caption-max-height);
475
+ overflow-y: auto;
476
+ overscroll-behavior: contain;
477
+ padding: var(--shoji-spacing-sm) var(--shoji-spacing-md);
478
+ border-radius: 8px;
479
+ background: var(--shoji-color-toolbar-bg);
480
+ text-align: left;
481
+ }
482
+
483
+ /* A real bug: height-capping the caption (above) still isn't enough on its
484
+ own — a video that fills most of the dialog leaves little to no
485
+ letterboxing gap, so even a short caption's opaque background can land
486
+ directly on the native control bar underneath it (same corner). Letting
487
+ clicks pass straight through the caption on a video slide (Gallery.ts
488
+ toggles this class from `item.video`, HTML5 or a provider like YouTube
489
+ alike) is what actually guarantees the controls stay reachable, not just
490
+ "usually clear of them." Re-enabled on any real child element so a rich
491
+ HTML caption's own links/buttons (DESIGN.md §2.1) stay clickable — only
492
+ a plain-string caption (no element children at all) ever fully passes
493
+ through. Photo slides are untouched: nothing below them needs protecting,
494
+ so the caption keeps its normal scrollable/selectable behavior there. */
495
+ .shoji-caption--video {
496
+ pointer-events: none;
497
+ }
498
+
499
+ .shoji-caption--video > * {
500
+ pointer-events: auto;
501
+ }
502
+
503
+ .shoji-caption[hidden] {
504
+ display: none;
505
+ }
506
+
507
+ /* DESIGN.md §2.8 — idle auto-hide. Opacity only, never display:none, never
508
+ removed from the tab order: a hidden-but-focused control stays interactive,
509
+ just visually faded (see Gallery's isControlFocused() guard). */
510
+ .shoji-toolbar,
511
+ .shoji-nav,
512
+ .shoji-counter,
513
+ .shoji-caption {
514
+ transition: opacity var(--shoji-duration) var(--shoji-easing);
515
+ }
516
+
517
+ .shoji-controls-hidden .shoji-toolbar,
518
+ .shoji-controls-hidden .shoji-nav,
519
+ .shoji-controls-hidden .shoji-counter,
520
+ .shoji-controls-hidden .shoji-caption {
521
+ opacity: 0;
522
+ pointer-events: none;
523
+ }
524
+
525
+ /* !important: wins over any plugin's own cursor styling (e.g. zoom.css's
526
+ zoom-in/grab affordances) without core needing to know that plugin's
527
+ selectors — the hidden state means nothing should suggest interactivity. */
528
+ .shoji-controls-hidden,
529
+ .shoji-controls-hidden * {
530
+ cursor: none !important;
531
+ }
532
+
533
+ .shoji-live-region {
534
+ position: absolute;
535
+ width: 1px;
536
+ height: 1px;
537
+ overflow: hidden;
538
+ clip: rect(0 0 0 0);
539
+ white-space: nowrap;
540
+ }
@@ -0,0 +1,32 @@
1
+ import { Gallery } from './core';
2
+ export type { GalleryItem, GalleryOptions, GalleryEvents, MediaSource, VideoDescriptor, Unsubscribe, } from './core';
3
+ export type { ShojiPlugin, PluginContext, VideoProviderRenderer } from './core/plugin';
4
+ /**
5
+ * `new Shoji(el, options)` — the UMD global (CLAUDE.md: "UMD build exposes
6
+ * exactly one global"). The default export *is* the constructor, not a
7
+ * namespace object, so `<script src="shoji.js">` usage matches the default
8
+ * import used by bundler/ESM consumers. Headless/tree-shaking consumers who
9
+ * want the class as a named import can pull it from the `shoji/core`
10
+ * subpath instead.
11
+ *
12
+ * Official plugins are attached as static properties (`Shoji.Autoplay`) so
13
+ * the single-file build stays "one global" (CLAUDE.md) while still shipping
14
+ * every official plugin's code — self-registering, no extra <script> tags —
15
+ * per CLAUDE.md's single-file distribution requirement. A host opts a
16
+ * gallery into one with `new Shoji(el, { plugins: [Shoji.Autoplay] })`.
17
+ * Bundler users who only want to pull in specific plugins' code (tree-shaking
18
+ * this static attachment away) should import from the `shoji/plugins/*`
19
+ * subpath instead of this default export.
20
+ */
21
+ declare const Shoji: typeof Gallery & {
22
+ Autoplay: import('./core').ShojiPlugin;
23
+ Layout: import('./core').ShojiPlugin;
24
+ ActiveThumbnail: import('./core').ShojiPlugin;
25
+ Fullscreen: import('./core').ShojiPlugin;
26
+ RotateFlip: import('./core').ShojiPlugin;
27
+ Video: import('./core').ShojiPlugin;
28
+ Zoom: import('./core').ShojiPlugin;
29
+ /** `package.json`'s own version — the single source of truth, read via a named JSON import so a bundler can tree-shake away everything else in package.json (devDependencies, scripts, ...) rather than embedding the whole file. */
30
+ version: string;
31
+ };
32
+ export default Shoji;
@@ -0,0 +1,24 @@
1
+ import { Gallery } from "./core/index.js";
2
+ import { ActiveThumbnail } from "./plugins/activeThumbnail/index.js";
3
+ import { Autoplay } from "./plugins/autoplay/index.js";
4
+ import { Fullscreen } from "./plugins/fullscreen/index.js";
5
+ import { Layout } from "./plugins/layout/index.js";
6
+ import { RotateFlip } from "./plugins/rotateFlip/index.js";
7
+ import { Video } from "./plugins/video/index.js";
8
+ import { Zoom } from "./plugins/zoom/index.js";
9
+ const version = "0.1.0-alpha.10";
10
+ const Shoji = Object.assign(Gallery, {
11
+ Autoplay,
12
+ Layout,
13
+ ActiveThumbnail,
14
+ Fullscreen,
15
+ RotateFlip,
16
+ Video,
17
+ Zoom,
18
+ /** `package.json`'s own version — the single source of truth, read via a named JSON import so a bundler can tree-shake away everything else in package.json (devDependencies, scripts, ...) rather than embedding the whole file. */
19
+ version
20
+ });
21
+ export {
22
+ Shoji as default
23
+ };
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/index.ts"],"sourcesContent":["import { Gallery } from './core';\nimport { ActiveThumbnail } from './plugins/activeThumbnail';\nimport { Autoplay } from './plugins/autoplay';\nimport { Fullscreen } from './plugins/fullscreen';\nimport { Layout } from './plugins/layout';\nimport { RotateFlip } from './plugins/rotateFlip';\nimport { Video } from './plugins/video';\nimport { Zoom } from './plugins/zoom';\nimport { version } from '../package.json';\n\nimport './styles/shoji.css';\n\nexport type {\n GalleryItem,\n GalleryOptions,\n GalleryEvents,\n MediaSource,\n VideoDescriptor,\n Unsubscribe,\n} from './core';\nexport type { ShojiPlugin, PluginContext, VideoProviderRenderer } from './core/plugin';\n\n/**\n * `new Shoji(el, options)` — the UMD global (CLAUDE.md: \"UMD build exposes\n * exactly one global\"). The default export *is* the constructor, not a\n * namespace object, so `<script src=\"shoji.js\">` usage matches the default\n * import used by bundler/ESM consumers. Headless/tree-shaking consumers who\n * want the class as a named import can pull it from the `shoji/core`\n * subpath instead.\n *\n * Official plugins are attached as static properties (`Shoji.Autoplay`) so\n * the single-file build stays \"one global\" (CLAUDE.md) while still shipping\n * every official plugin's code — self-registering, no extra <script> tags —\n * per CLAUDE.md's single-file distribution requirement. A host opts a\n * gallery into one with `new Shoji(el, { plugins: [Shoji.Autoplay] })`.\n * Bundler users who only want to pull in specific plugins' code (tree-shaking\n * this static attachment away) should import from the `shoji/plugins/*`\n * subpath instead of this default export.\n */\nconst Shoji = Object.assign(Gallery, {\n Autoplay,\n Layout,\n ActiveThumbnail,\n Fullscreen,\n RotateFlip,\n Video,\n Zoom,\n /** `package.json`'s own version — the single source of truth, read via a named JSON import so a bundler can tree-shake away everything else in package.json (devDependencies, scripts, ...) rather than embedding the whole file. */\n version,\n});\n\nexport default Shoji;\n"],"names":[],"mappings":";;;;;;;;;AAuCA,MAAM,QAAQ,OAAO,OAAO,SAAS;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AACF,CAAC;"}
@@ -0,0 +1,22 @@
1
+ /* DESIGN.md §4-autoplay — thin progress bar along the bottom edge, tracking
2
+ time-to-next-advance for timed (image) slides. Hidden during video slides,
3
+ where the video's own runtime — not a fixed interval — drives advancement. */
4
+ .shoji-autoplay-progress {
5
+ position: absolute;
6
+ left: 0;
7
+ right: 0;
8
+ bottom: 0;
9
+ height: 3px;
10
+ background: rgba(255, 255, 255, 0.15);
11
+ z-index: var(--shoji-z-toolbar);
12
+ }
13
+
14
+ .shoji-autoplay-progress[hidden] {
15
+ display: none;
16
+ }
17
+
18
+ .shoji-autoplay-progress-bar {
19
+ height: 100%;
20
+ width: 0%;
21
+ background: var(--shoji-progress);
22
+ }