@michaelyagi/shoji 0.1.0-alpha.6
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 +81 -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 +50 -0
- package/dist/esm/core/LiveRegion.d.ts +6 -0
- package/dist/esm/core/SlideManager.d.ts +83 -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 +1966 -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 +8 -0
- package/dist/esm/core/types.d.ts +268 -0
- package/dist/esm/core/zoomTransition.d.ts +33 -0
- package/dist/esm/gestures/GestureEngine.d.ts +78 -0
- package/dist/esm/index.css +475 -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 +166 -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 +48 -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 +17 -0
- package/dist/esm/plugins/autoplay/index.js +177 -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 +746 -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 +95 -0
- package/dist/esm/plugins/video/index.js.map +1 -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 +690 -0
- package/dist/shoji.js +3605 -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/dist/shoji.css
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
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
|
+
}
|
|
23
|
+
/* DESIGN.md §5 — layout takes over the container entirely once enabled
|
|
24
|
+
(any host markup that was there at scan time is replaced by its own
|
|
25
|
+
generated tiles), so all of its structural styling lives here rather than
|
|
26
|
+
depending on host CSS. */
|
|
27
|
+
|
|
28
|
+
.shoji-layout {
|
|
29
|
+
position: relative;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* masonry & justified always give the container an explicit
|
|
33
|
+
`container.style.height` (index.ts) — never intrinsic/content-driven —
|
|
34
|
+
so CSS layout containment is safe here and doesn't collapse or misjudge
|
|
35
|
+
its size. A real, reported slowdown: opening/closing the lightbox reads
|
|
36
|
+
layout (getBoundingClientRect/offsetHeight — zoomTransition.ts's FLIP
|
|
37
|
+
technique) which forces the browser to flush *any* pending layout on the
|
|
38
|
+
page first, including this container's own (often large — hundreds of
|
|
39
|
+
individually absolutely-positioned tiles) subtree, even though none of
|
|
40
|
+
that subtree's layout ever changed. `contain: layout` tells the browser
|
|
41
|
+
this subtree's internal layout can't affect, and isn't affected by,
|
|
42
|
+
anything outside it, so that flush can skip descending into it. Not
|
|
43
|
+
applied to grid: its height is intrinsic (content-driven auto-sized
|
|
44
|
+
rows), which layout containment doesn't safely decouple the same way. */
|
|
45
|
+
.shoji-layout--masonry,
|
|
46
|
+
.shoji-layout--justified {
|
|
47
|
+
contain: layout style;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.shoji-layout-tile {
|
|
51
|
+
display: block;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.shoji-layout-tile img {
|
|
56
|
+
display: block;
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: 100%;
|
|
59
|
+
object-fit: cover;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* masonry & justified: both JS-positioned, absolutely placed, GPU-composited
|
|
63
|
+
moves only (transform), per CLAUDE.md — no top/left animation.
|
|
64
|
+
visibility: hidden until the JS layout pass actually sets a real
|
|
65
|
+
width/height/transform — masonry/justified skip that pass entirely
|
|
66
|
+
(index.ts's containerWidth <= 0 guard) if the container isn't sized yet
|
|
67
|
+
at construction, correcting only once the ResizeObserver's own
|
|
68
|
+
(asynchronous) initial notification fires. In that gap, an unstyled
|
|
69
|
+
tile falls back to CSS auto-sizing from its <img> child, which can be a
|
|
70
|
+
genuinely 0×0 or wildly wrong-shaped rect depending on image load
|
|
71
|
+
timing — clickable, but a click landing on it computes the zoom
|
|
72
|
+
transition's origin from that same bad rect, silently skipping the
|
|
73
|
+
animation (a 0×0 origin) or animating from the wrong place (a
|
|
74
|
+
wrong-shaped one). visibility: hidden (not display: none — keeps the
|
|
75
|
+
space-not-taking difference irrelevant here, but note it also removes
|
|
76
|
+
the tile from hit-testing, unlike opacity: 0) makes an unpositioned
|
|
77
|
+
tile un-clickable for that entire window instead — the JS layout
|
|
78
|
+
functions clear it back to visible the moment they actually set real
|
|
79
|
+
geometry. See DESIGN.md §2.3b. */
|
|
80
|
+
.shoji-layout--masonry .shoji-layout-tile,
|
|
81
|
+
.shoji-layout--justified .shoji-layout-tile {
|
|
82
|
+
position: absolute;
|
|
83
|
+
top: 0;
|
|
84
|
+
left: 0;
|
|
85
|
+
visibility: hidden;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.shoji-layout--masonry.shoji-layout--animate .shoji-layout-tile,
|
|
89
|
+
.shoji-layout--justified.shoji-layout--animate .shoji-layout-tile {
|
|
90
|
+
transition: transform var(--shoji-duration) var(--shoji-easing);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* grid: native CSS grid does the positioning, no JS math or transforms. */
|
|
94
|
+
.shoji-layout--grid {
|
|
95
|
+
display: grid;
|
|
96
|
+
grid-template-columns: repeat(auto-fill, minmax(var(--shoji-layout-column-width, 240px), 1fr));
|
|
97
|
+
gap: var(--shoji-layout-gutter, 8px);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.shoji-layout--grid .shoji-layout-tile {
|
|
101
|
+
aspect-ratio: var(--shoji-layout-tile-aspect, 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* headings (groupBy) — grid: a real grid row of its own, native flow, no JS
|
|
105
|
+
positioning needed (a full-span item naturally starts a new row after
|
|
106
|
+
it). masonry/justified: JS-positioned same as tiles, since those modes
|
|
107
|
+
don't have a document-flow "row" to insert into. */
|
|
108
|
+
.shoji-layout-heading {
|
|
109
|
+
margin: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.shoji-layout--grid .shoji-layout-heading {
|
|
113
|
+
grid-column: 1 / -1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.shoji-layout--masonry .shoji-layout-heading,
|
|
117
|
+
.shoji-layout--justified .shoji-layout-heading {
|
|
118
|
+
position: absolute;
|
|
119
|
+
top: 0;
|
|
120
|
+
left: 0;
|
|
121
|
+
width: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* justified headings are compact, inline labels, not full-width blocking
|
|
125
|
+
elements (see the groupBy option's doc comment in index.ts) — sized to
|
|
126
|
+
their own content by default, and to their own JS-computed max-width
|
|
127
|
+
only when the content-fitting pass decides a label needs to wrap. This
|
|
128
|
+
overrides the shared width: 100% rule above by source order (equal
|
|
129
|
+
specificity), not by a stronger selector. */
|
|
130
|
+
.shoji-layout--justified .shoji-layout-heading {
|
|
131
|
+
width: auto;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.shoji-layout--masonry.shoji-layout--animate .shoji-layout-heading,
|
|
136
|
+
.shoji-layout--justified.shoji-layout--animate .shoji-layout-heading {
|
|
137
|
+
transition: transform var(--shoji-duration) var(--shoji-easing);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Font customization points, per CLAUDE.md ("all styling through --shoji-*
|
|
141
|
+
custom properties with sane defaults"). .shoji-layout-heading itself (the
|
|
142
|
+
plain-string/default heading, and the shared parent of title+subtitle)
|
|
143
|
+
deliberately keeps its browser-default <h2> font-size untouched rather
|
|
144
|
+
than gaining its own --shoji-layout-heading-font-size — overriding that
|
|
145
|
+
here to "inherit" would shrink it from the UA stylesheet's own (larger)
|
|
146
|
+
h2 default, a visible regression for anyone not using the structured
|
|
147
|
+
form. Host CSS targeting .shoji-layout-heading directly already fully
|
|
148
|
+
controls it.
|
|
149
|
+
|
|
150
|
+
The title/subtitle spans below intentionally do NOT size off that same
|
|
151
|
+
(oversized, unreset) h2 default — title and subtitle both get an
|
|
152
|
+
explicit shared default size instead (matching .shoji-counter's own
|
|
153
|
+
0.875rem elsewhere in core, for visual consistency), decoupled from
|
|
154
|
+
whatever the plain-string heading's own size happens to be. The label
|
|
155
|
+
(title) reads as muted/normal-weight, the value (subtitle) as bold/
|
|
156
|
+
full-color — e.g. "Taken" vs a bolded date — the inverse of an earlier
|
|
157
|
+
version of this default that bolded the label instead. */
|
|
158
|
+
.shoji-layout-heading {
|
|
159
|
+
font-family: var(--shoji-layout-heading-font-family, inherit);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.shoji-layout-heading-title {
|
|
163
|
+
font-size: var(--shoji-layout-heading-title-font-size, 0.875rem);
|
|
164
|
+
font-weight: var(--shoji-layout-heading-title-font-weight, 400);
|
|
165
|
+
color: var(--shoji-layout-heading-title-color, #767676);
|
|
166
|
+
/* headingOverflow: 'fit''s ellipsis fallback (the one unavoidable case —
|
|
167
|
+
a single label wider than the container even alone) still needs
|
|
168
|
+
overflow-wrap: break-word so a single long unbroken token (a URL, a
|
|
169
|
+
hashtag) doesn't itself force the ellipsis box wider than intended. */
|
|
170
|
+
overflow-wrap: break-word;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.shoji-layout-heading-subtitle {
|
|
174
|
+
margin-left: 0.6em;
|
|
175
|
+
font-size: var(--shoji-layout-heading-subtitle-font-size, 0.875rem);
|
|
176
|
+
font-weight: var(--shoji-layout-heading-subtitle-font-weight, 700);
|
|
177
|
+
color: var(--shoji-layout-heading-subtitle-color, inherit);
|
|
178
|
+
overflow-wrap: break-word;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* stickyHeadings — grid mode only; masonry/justified headings are already
|
|
182
|
+
absolutely positioned by JS, which sticky can't meaningfully layer onto
|
|
183
|
+
(see the option's own doc comment in index.ts). */
|
|
184
|
+
.shoji-layout--grid .shoji-layout-heading--sticky {
|
|
185
|
+
position: sticky;
|
|
186
|
+
top: 0;
|
|
187
|
+
z-index: 1;
|
|
188
|
+
}
|
|
189
|
+
/* DESIGN.md §4-zoom — cursor affordances only; the transform itself is set
|
|
190
|
+
inline (per-slide, computed from gesture/button state, not a static
|
|
191
|
+
rule). Scoped to .shoji-zoom-enabled (added by the plugin itself, per
|
|
192
|
+
slide) rather than the bare .shoji-slide-img class — that class exists on
|
|
193
|
+
every slide image regardless of whether this plugin is even enabled for
|
|
194
|
+
a given gallery, and this CSS ships in the single-file bundle alongside
|
|
195
|
+
every other official plugin's, so an unscoped rule would leak a zoom-in
|
|
196
|
+
cursor onto galleries that never opted into this plugin at all. */
|
|
197
|
+
.shoji-zoom-enabled {
|
|
198
|
+
cursor: zoom-in;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.shoji-zoom-enabled.shoji-zoomed {
|
|
202
|
+
cursor: grab;
|
|
203
|
+
/* Reported from real usage: a scaled-up image could show GPU tile-seam
|
|
204
|
+
lines through its own content while zoomed — the same rasterization
|
|
205
|
+
quirk as the toolbar's own (shoji.css), just from the image's frequent
|
|
206
|
+
scale() updates instead of a neighbor's. Scoped to only while actually
|
|
207
|
+
zoomed (not the whole time this plugin is enabled) — at scale 1 there's
|
|
208
|
+
no scale-up to tile-seam in the first place, so no reason to spend the
|
|
209
|
+
extra memory a promoted layer costs. */
|
|
210
|
+
will-change: transform;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.shoji-zoom-enabled.shoji-zoomed:active {
|
|
214
|
+
cursor: grabbing;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Core theme tokens (DESIGN.md §9). Dark is the default theme; `.shoji-light`
|
|
218
|
+
* overrides to light. No hardcoded colors/sizes elsewhere in JS or CSS —
|
|
219
|
+
* everything routes through these custom properties.
|
|
220
|
+
*/
|
|
221
|
+
:root {
|
|
222
|
+
/* .shoji-outer's own page-level z-index — relative to the rest of the
|
|
223
|
+
host page, not to anything inside the lightbox. Real pages routinely
|
|
224
|
+
put sticky navbars/sidebars/toast containers at 1000-9999, so this
|
|
225
|
+
needs real headroom, not the small internal scale below (which only
|
|
226
|
+
orders backdrop/slides/overlay/toolbar/toast *against each other*,
|
|
227
|
+
inside the stacking context .shoji-outer's own z-index establishes). */
|
|
228
|
+
--shoji-z-base: 9000;
|
|
229
|
+
|
|
230
|
+
--shoji-z-backdrop: 0;
|
|
231
|
+
--shoji-z-slides: 10;
|
|
232
|
+
--shoji-z-overlay: 20;
|
|
233
|
+
--shoji-z-toolbar: 30;
|
|
234
|
+
--shoji-z-toast: 40;
|
|
235
|
+
|
|
236
|
+
--shoji-color-bg: #0b0b0d;
|
|
237
|
+
--shoji-color-fg: #f5f5f5;
|
|
238
|
+
--shoji-color-accent: #4da3ff;
|
|
239
|
+
--shoji-color-backdrop: rgba(0, 0, 0, 0.92);
|
|
240
|
+
--shoji-color-toolbar-bg: rgba(20, 20, 22, 0.85);
|
|
241
|
+
--shoji-color-disabled: rgba(245, 245, 245, 0.35);
|
|
242
|
+
|
|
243
|
+
--shoji-focus-ring: 2px solid var(--shoji-color-accent);
|
|
244
|
+
--shoji-progress: var(--shoji-color-accent);
|
|
245
|
+
|
|
246
|
+
--shoji-spacing-sm: 0.5rem;
|
|
247
|
+
--shoji-spacing-md: 1rem;
|
|
248
|
+
--shoji-spacing-lg: 1.5rem;
|
|
249
|
+
|
|
250
|
+
--shoji-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
251
|
+
--shoji-duration: 300ms;
|
|
252
|
+
/* Caps how tall the caption box can grow before it scrolls internally —
|
|
253
|
+
an unbounded caption on a long string would otherwise creep up over a
|
|
254
|
+
video slide's native control bar underneath it (bottom-left, same
|
|
255
|
+
corner), silently swallowing scrub/volume/fullscreen clicks with no
|
|
256
|
+
way to reach them. `min()` also bounds it on a short/wide dialog
|
|
257
|
+
(mobile landscape), where a `rem`-only cap could still eat most of
|
|
258
|
+
the available height. */
|
|
259
|
+
--shoji-caption-max-height: min(8rem, 30%);
|
|
260
|
+
/* Reserved space at the top of a provider embed (YouTube etc.) so its own
|
|
261
|
+
hover-revealed title bar never sits under Shoji's toolbar — matches the
|
|
262
|
+
toolbar's own rendered height (44px button + top/bottom padding).
|
|
263
|
+
Permanent, not tied to the toolbar's auto-hide state, so the video
|
|
264
|
+
never resizes as the toolbar fades in/out. HTML5 video has no top-of-
|
|
265
|
+
player UI at all, so it's untouched. */
|
|
266
|
+
--shoji-provider-video-top-inset: calc(44px + 2 * var(--shoji-spacing-sm));
|
|
267
|
+
/* DESIGN.md §2.4 — the gesture engine's own documented default easing for
|
|
268
|
+
drag-to-navigate/drag-to-close settle animations specifically (a snap
|
|
269
|
+
tuned for "released mid-motion," distinct from --shoji-easing's
|
|
270
|
+
open/close/zoom-transition curve). */
|
|
271
|
+
--shoji-momentum-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
272
|
+
|
|
273
|
+
/* System-font stack: no webfont download, renders instantly, looks native
|
|
274
|
+
(San Francisco / Segoe UI / Roboto) on every platform. */
|
|
275
|
+
--shoji-font-family:
|
|
276
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
|
277
|
+
'Apple Color Emoji', 'Segoe UI Emoji';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.shoji-light {
|
|
281
|
+
--shoji-color-bg: #fafafa;
|
|
282
|
+
--shoji-color-fg: #111111;
|
|
283
|
+
--shoji-color-backdrop: rgba(255, 255, 255, 0.96);
|
|
284
|
+
--shoji-color-toolbar-bg: rgba(255, 255, 255, 0.9);
|
|
285
|
+
--shoji-color-disabled: rgba(17, 17, 17, 0.35);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@media (prefers-reduced-motion: reduce) {
|
|
289
|
+
:root {
|
|
290
|
+
--shoji-duration: 0ms;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Lightbox structure (DESIGN.md §2.3 slides, §2.6 dialog). Built lazily on first open(). */
|
|
295
|
+
.shoji-outer {
|
|
296
|
+
display: none;
|
|
297
|
+
position: fixed;
|
|
298
|
+
inset: 0;
|
|
299
|
+
z-index: var(--shoji-z-base);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.shoji-outer.shoji-open {
|
|
303
|
+
display: block;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.shoji-backdrop {
|
|
307
|
+
position: absolute;
|
|
308
|
+
inset: 0;
|
|
309
|
+
z-index: var(--shoji-z-backdrop);
|
|
310
|
+
background: var(--shoji-color-backdrop);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.shoji-dialog {
|
|
314
|
+
position: relative;
|
|
315
|
+
z-index: var(--shoji-z-slides);
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-direction: column;
|
|
318
|
+
height: 100%;
|
|
319
|
+
color: var(--shoji-color-fg);
|
|
320
|
+
font-family: var(--shoji-font-family);
|
|
321
|
+
outline: none;
|
|
322
|
+
/* DESIGN.md §2.4 — the gesture engine owns horizontal/vertical drag on
|
|
323
|
+
this element; without this, touch browsers intercept the same gesture
|
|
324
|
+
for native scroll/pull-to-refresh/back-navigation before JS ever sees
|
|
325
|
+
enough movement to lock a direction. Buttons/video inside remain
|
|
326
|
+
interactive regardless (this is about implicit browser gesture
|
|
327
|
+
handling, not pointer-events). */
|
|
328
|
+
touch-action: none;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.shoji-toolbar {
|
|
332
|
+
position: absolute;
|
|
333
|
+
inset: 0 0 auto 0;
|
|
334
|
+
z-index: var(--shoji-z-toolbar);
|
|
335
|
+
display: flex;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
padding: var(--shoji-spacing-sm);
|
|
338
|
+
pointer-events: none;
|
|
339
|
+
/* Reported from real usage: the Zoom plugin's frequent scale() updates on
|
|
340
|
+
the photo (a sibling, not a descendant) could show as GPU tile-seam
|
|
341
|
+
artifacts (thin lines) through the toolbar's own semi-transparent,
|
|
342
|
+
border-radius buttons — the same class of rendering quirk already noted
|
|
343
|
+
for progressive image decode. Promoting the toolbar to its own stable
|
|
344
|
+
compositing layer, decoupled from the photo's repaints, avoids it. */
|
|
345
|
+
will-change: transform;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* DESIGN.md §3 — ctx.ui.toolbar(slot, ...) inserts plugin buttons into these;
|
|
349
|
+
pointer-events re-enabled per-slot (not per-button) so gaps between slots
|
|
350
|
+
stay click-through even once a slot has content. `flex-wrap: wrap` (not
|
|
351
|
+
`overflow-x: auto`) is deliberate: several plugins loaded together on a
|
|
352
|
+
narrow/mobile viewport can add up to more 44px buttons than the width
|
|
353
|
+
allows (CLAUDE.md's own touch-target size, non-negotiable), and this
|
|
354
|
+
project treats horizontal scroll as something to design around, not
|
|
355
|
+
reach for (see the layout plugin's masonry `orientation: 'horizontal'`,
|
|
356
|
+
which was rebuilt specifically to avoid it) — wrapping to a second row
|
|
357
|
+
keeps every button reachable by a tap with no sideways scroll gesture
|
|
358
|
+
anywhere in the lightbox chrome either. */
|
|
359
|
+
.shoji-toolbar-slot {
|
|
360
|
+
display: flex;
|
|
361
|
+
flex-wrap: wrap;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: var(--shoji-spacing-sm);
|
|
364
|
+
pointer-events: auto;
|
|
365
|
+
max-width: 45%;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.shoji-toolbar-left {
|
|
369
|
+
justify-content: flex-start;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.shoji-toolbar-right {
|
|
373
|
+
justify-content: flex-end;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.shoji-close,
|
|
377
|
+
.shoji-nav,
|
|
378
|
+
.shoji-toolbar-button {
|
|
379
|
+
appearance: none;
|
|
380
|
+
border: none;
|
|
381
|
+
padding: 0;
|
|
382
|
+
width: 44px;
|
|
383
|
+
height: 44px;
|
|
384
|
+
border-radius: 50%;
|
|
385
|
+
display: flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
justify-content: center;
|
|
388
|
+
background: rgba(0, 0, 0, 0.4);
|
|
389
|
+
color: var(--shoji-color-fg);
|
|
390
|
+
cursor: pointer;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.shoji-close:hover,
|
|
394
|
+
.shoji-nav:hover,
|
|
395
|
+
.shoji-toolbar-button:hover {
|
|
396
|
+
background: rgba(0, 0, 0, 0.6);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.shoji-close:focus-visible,
|
|
400
|
+
.shoji-nav:focus-visible,
|
|
401
|
+
.shoji-toolbar-button:focus-visible {
|
|
402
|
+
outline: var(--shoji-focus-ring);
|
|
403
|
+
outline-offset: 2px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.shoji-nav {
|
|
407
|
+
position: absolute;
|
|
408
|
+
top: 50%;
|
|
409
|
+
transform: translateY(-50%);
|
|
410
|
+
z-index: var(--shoji-z-toolbar);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.shoji-nav-prev {
|
|
414
|
+
left: var(--shoji-spacing-md);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.shoji-nav-next {
|
|
418
|
+
right: var(--shoji-spacing-md);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.shoji-nav:disabled {
|
|
422
|
+
opacity: 0.3;
|
|
423
|
+
cursor: default;
|
|
424
|
+
pointer-events: none;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.shoji-close[hidden],
|
|
428
|
+
.shoji-nav[hidden],
|
|
429
|
+
.shoji-toolbar-button[hidden] {
|
|
430
|
+
display: none;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* DESIGN.md §2.3 — while the active slide's own content is still loading,
|
|
434
|
+
feature/plugin toolbar buttons (zoom, rotate, fullscreen, autoplay, any
|
|
435
|
+
custom plugin's own button) dim and stop responding, since they'd
|
|
436
|
+
otherwise act on content about to be replaced. Deliberately scoped to
|
|
437
|
+
`.shoji-toolbar-button` only — `.shoji-close`/`.shoji-nav` are different
|
|
438
|
+
classes, so closing and navigating are never blocked by a slow image.
|
|
439
|
+
`pointer-events: none` here is the mouse half; Gallery.ts's matching
|
|
440
|
+
`tabIndex = -1` is the keyboard half (this alone wouldn't stop
|
|
441
|
+
Enter/Space activating an already-focused button). */
|
|
442
|
+
.shoji-outer.shoji-slide-loading .shoji-toolbar-button {
|
|
443
|
+
opacity: 0.3;
|
|
444
|
+
cursor: default;
|
|
445
|
+
pointer-events: none;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.shoji-slides {
|
|
449
|
+
position: relative;
|
|
450
|
+
flex: 1;
|
|
451
|
+
overflow: hidden;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.shoji-slide {
|
|
455
|
+
position: absolute;
|
|
456
|
+
inset: 0;
|
|
457
|
+
display: flex;
|
|
458
|
+
align-items: center;
|
|
459
|
+
justify-content: center;
|
|
460
|
+
/* transform (translateX per pool offset) is set inline by SlideManager */
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.shoji-slide-media {
|
|
464
|
+
/* Definite size (not max-width/max-height alone) so the img's own
|
|
465
|
+
percentage-based max-width/max-height below has something concrete to
|
|
466
|
+
resolve against — otherwise it sizes to the image's natural dimensions
|
|
467
|
+
instead of being contained, and object-fit: contain never engages. */
|
|
468
|
+
width: 100%;
|
|
469
|
+
height: 100%;
|
|
470
|
+
display: flex;
|
|
471
|
+
align-items: center;
|
|
472
|
+
justify-content: center;
|
|
473
|
+
/* Anchors .shoji-video-play-overlay's absolute positioning to this box
|
|
474
|
+
specifically, not implicitly to whichever positioned ancestor happens
|
|
475
|
+
to be next up (currently .shoji-slide, coincidentally the same size —
|
|
476
|
+
explicit here so that stays true even if that ever changes). */
|
|
477
|
+
position: relative;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.shoji-slide-img,
|
|
481
|
+
.shoji-slide-video {
|
|
482
|
+
display: block;
|
|
483
|
+
max-width: 100%;
|
|
484
|
+
max-height: 100%;
|
|
485
|
+
object-fit: contain;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* max-width/max-height above only ever *cap* size — a real photo is normally
|
|
489
|
+
bigger than the slide area, so that's correct. A low-res open() placeholder
|
|
490
|
+
(§2.3) is the opposite case: deliberately small, so it needs to be forced
|
|
491
|
+
up to fill the frame (blurry, on purpose) instead of rendering at its own
|
|
492
|
+
tiny natural size. object-fit: contain (inherited above) still letterboxes
|
|
493
|
+
it within that forced box, just per the thumbnail's own aspect ratio. */
|
|
494
|
+
.shoji-slide-open-placeholder {
|
|
495
|
+
width: 100%;
|
|
496
|
+
height: 100%;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* DESIGN.md §4-video — a provider's embed (an <iframe>, typically) has no
|
|
500
|
+
natural size Shoji can measure the way a decoded image/video does, so
|
|
501
|
+
this just fills whatever box it's given; supply item.width/height for a
|
|
502
|
+
correctly-shaped box (applyAspect() sets aspect-ratio on the slide-media
|
|
503
|
+
container itself from those), same as any other slide. */
|
|
504
|
+
.shoji-slide-provider-video {
|
|
505
|
+
width: 100%;
|
|
506
|
+
height: 100%;
|
|
507
|
+
box-sizing: border-box;
|
|
508
|
+
padding-top: var(--shoji-provider-video-top-inset);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.shoji-slide-provider-video[hidden] {
|
|
512
|
+
display: none;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.shoji-slide-provider-video iframe {
|
|
516
|
+
display: block;
|
|
517
|
+
width: 100%;
|
|
518
|
+
height: 100%;
|
|
519
|
+
border: 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* A paused video's own play affordance — native browser styling for this
|
|
523
|
+
varies too much to rely on alone (some show a big centered play icon,
|
|
524
|
+
some don't). Faint/translucent, same visual language as the toolbar's
|
|
525
|
+
own round icon buttons, just bigger — a primary, hard-to-miss affordance,
|
|
526
|
+
not a small chrome control. Deliberately not gated by any GalleryOptions
|
|
527
|
+
flag (purely presentational — hiding it changes nothing functionally,
|
|
528
|
+
the video is still clickable either way); a host not wanting it just
|
|
529
|
+
overrides this selector in their own CSS. */
|
|
530
|
+
.shoji-video-play-overlay {
|
|
531
|
+
position: absolute;
|
|
532
|
+
top: 50%;
|
|
533
|
+
left: 50%;
|
|
534
|
+
transform: translate(-50%, -50%);
|
|
535
|
+
width: var(--shoji-video-overlay-size, 64px);
|
|
536
|
+
height: var(--shoji-video-overlay-size, 64px);
|
|
537
|
+
border-radius: 50%;
|
|
538
|
+
border: none;
|
|
539
|
+
padding: 0;
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
justify-content: center;
|
|
543
|
+
background: var(--shoji-video-overlay-bg, rgba(0, 0, 0, 0.4));
|
|
544
|
+
color: var(--shoji-video-overlay-color, var(--shoji-color-fg));
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.shoji-video-play-overlay[hidden] {
|
|
549
|
+
display: none;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* DESIGN.md §2.5 — the temporary clone SlideTransition animates the
|
|
553
|
+
outgoing slide's content out on. Sits above the normal (unpositioned
|
|
554
|
+
z-index) pool slots by DOM order alone, but z-index is set explicitly
|
|
555
|
+
here rather than relied on implicitly, matching how every other overlay
|
|
556
|
+
layer in this file states its stacking intent up front. Not interactive
|
|
557
|
+
(it's a decorative clone mid-removal) and never receives its own size —
|
|
558
|
+
it fills `.shoji-slides` exactly like the real `.shoji-slide` it's a
|
|
559
|
+
snapshot of, so the cloned `.shoji-slide-media` inside it (already
|
|
560
|
+
`width/height: 100%`) lines up pixel-for-pixel with no extra CSS needed. */
|
|
561
|
+
.shoji-slide-ghost {
|
|
562
|
+
position: absolute;
|
|
563
|
+
inset: 0;
|
|
564
|
+
z-index: 1;
|
|
565
|
+
pointer-events: none;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.shoji-slide-placeholder {
|
|
569
|
+
color: var(--shoji-color-disabled);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* DESIGN.md §2.3 — shown in place of a slide's content while it's still
|
|
573
|
+
decoding/loading (outside the `preload` window, where nothing was ready
|
|
574
|
+
ahead of time). CSS-only rotation (no JS-driven layout, per CLAUDE.md);
|
|
575
|
+
disabled under prefers-reduced-motion the same as every other animation
|
|
576
|
+
here, rather than left spinning regardless. */
|
|
577
|
+
.shoji-slide-spinner {
|
|
578
|
+
width: var(--shoji-spinner-size, 40px);
|
|
579
|
+
height: var(--shoji-spinner-size, 40px);
|
|
580
|
+
border-radius: 50%;
|
|
581
|
+
border: var(--shoji-spinner-thickness, 3px) solid var(--shoji-color-disabled);
|
|
582
|
+
border-top-color: var(--shoji-color-accent);
|
|
583
|
+
animation: shoji-spin 0.8s linear infinite;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@keyframes shoji-spin {
|
|
587
|
+
to {
|
|
588
|
+
transform: rotate(360deg);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
@media (prefers-reduced-motion: reduce) {
|
|
593
|
+
.shoji-slide-spinner {
|
|
594
|
+
animation: none;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* A normal flex item within .shoji-toolbar-left (not independently
|
|
599
|
+
positioned) — .shoji-slides still gets the full dialog height either way,
|
|
600
|
+
no reserved gutter at top or bottom, since the toolbar itself overlays the
|
|
601
|
+
slides rather than pushing them down. Flowing alongside whatever else is
|
|
602
|
+
in that slot (e.g. a plugin's ctx.ui.toolbar('left', ...) button) keeps it
|
|
603
|
+
from overlapping and stealing clicks the way independent absolute
|
|
604
|
+
positioning did. */
|
|
605
|
+
.shoji-counter {
|
|
606
|
+
padding: 0.25rem 0.6rem;
|
|
607
|
+
border-radius: 999px;
|
|
608
|
+
background: rgba(0, 0, 0, 0.4);
|
|
609
|
+
font-size: 0.875rem;
|
|
610
|
+
white-space: nowrap;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.shoji-counter[hidden] {
|
|
614
|
+
display: none;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.shoji-caption {
|
|
618
|
+
position: absolute;
|
|
619
|
+
bottom: var(--shoji-spacing-md);
|
|
620
|
+
left: var(--shoji-spacing-md);
|
|
621
|
+
z-index: var(--shoji-z-toolbar);
|
|
622
|
+
width: fit-content;
|
|
623
|
+
max-width: calc(100% - 2 * var(--shoji-spacing-md));
|
|
624
|
+
max-height: var(--shoji-caption-max-height);
|
|
625
|
+
overflow-y: auto;
|
|
626
|
+
overscroll-behavior: contain;
|
|
627
|
+
padding: var(--shoji-spacing-sm) var(--shoji-spacing-md);
|
|
628
|
+
border-radius: 8px;
|
|
629
|
+
background: var(--shoji-color-toolbar-bg);
|
|
630
|
+
text-align: left;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* A real bug: height-capping the caption (above) still isn't enough on its
|
|
634
|
+
own — a video that fills most of the dialog leaves little to no
|
|
635
|
+
letterboxing gap, so even a short caption's opaque background can land
|
|
636
|
+
directly on the native control bar underneath it (same corner). Letting
|
|
637
|
+
clicks pass straight through the caption on a video slide (Gallery.ts
|
|
638
|
+
toggles this class from `item.video`, HTML5 or a provider like YouTube
|
|
639
|
+
alike) is what actually guarantees the controls stay reachable, not just
|
|
640
|
+
"usually clear of them." Re-enabled on any real child element so a rich
|
|
641
|
+
HTML caption's own links/buttons (DESIGN.md §2.1) stay clickable — only
|
|
642
|
+
a plain-string caption (no element children at all) ever fully passes
|
|
643
|
+
through. Photo slides are untouched: nothing below them needs protecting,
|
|
644
|
+
so the caption keeps its normal scrollable/selectable behavior there. */
|
|
645
|
+
.shoji-caption--video {
|
|
646
|
+
pointer-events: none;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.shoji-caption--video > * {
|
|
650
|
+
pointer-events: auto;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.shoji-caption[hidden] {
|
|
654
|
+
display: none;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* DESIGN.md §2.8 — idle auto-hide. Opacity only, never display:none, never
|
|
658
|
+
removed from the tab order: a hidden-but-focused control stays interactive,
|
|
659
|
+
just visually faded (see Gallery's isControlFocused() guard). */
|
|
660
|
+
.shoji-toolbar,
|
|
661
|
+
.shoji-nav,
|
|
662
|
+
.shoji-counter,
|
|
663
|
+
.shoji-caption {
|
|
664
|
+
transition: opacity var(--shoji-duration) var(--shoji-easing);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.shoji-controls-hidden .shoji-toolbar,
|
|
668
|
+
.shoji-controls-hidden .shoji-nav,
|
|
669
|
+
.shoji-controls-hidden .shoji-counter,
|
|
670
|
+
.shoji-controls-hidden .shoji-caption {
|
|
671
|
+
opacity: 0;
|
|
672
|
+
pointer-events: none;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/* !important: wins over any plugin's own cursor styling (e.g. zoom.css's
|
|
676
|
+
zoom-in/grab affordances) without core needing to know that plugin's
|
|
677
|
+
selectors — the hidden state means nothing should suggest interactivity. */
|
|
678
|
+
.shoji-controls-hidden,
|
|
679
|
+
.shoji-controls-hidden * {
|
|
680
|
+
cursor: none !important;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.shoji-live-region {
|
|
684
|
+
position: absolute;
|
|
685
|
+
width: 1px;
|
|
686
|
+
height: 1px;
|
|
687
|
+
overflow: hidden;
|
|
688
|
+
clip: rect(0 0 0 0);
|
|
689
|
+
white-space: nowrap;
|
|
690
|
+
}
|