@michaelyagi/shoji 0.1.0-beta.2 → 0.1.0-beta.5
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/dist/core/shoji-core.css +833 -0
- package/dist/core/shoji-core.js +3296 -0
- package/dist/core/shoji-core.js.map +1 -0
- package/dist/core/shoji-core.min.css +1 -0
- package/dist/core/shoji-core.min.js +2 -0
- package/dist/core/shoji-core.min.js.map +1 -0
- package/dist/esm/core/Gallery.d.ts +2 -0
- package/dist/esm/core/index.js +94 -1
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/index.css +86 -820
- package/dist/esm/index.js +1 -1
- package/dist/esm/index2.css +819 -44
- package/dist/esm/index3.css +47 -159
- package/dist/esm/index4.css +169 -26
- package/dist/esm/index5.css +27 -0
- package/dist/esm/plugins/activeThumbnail/index.d.ts +7 -1
- package/dist/esm/plugins/activeThumbnail/index.js +55 -16
- package/dist/esm/plugins/activeThumbnail/index.js.map +1 -1
- package/dist/plugins/activeThumbnail.css +99 -0
- package/dist/plugins/activeThumbnail.js +116 -0
- package/dist/plugins/activeThumbnail.js.map +1 -0
- package/dist/plugins/activeThumbnail.min.css +1 -0
- package/dist/plugins/activeThumbnail.min.js +2 -0
- package/dist/plugins/activeThumbnail.min.js.map +1 -0
- package/dist/plugins/autoplay.css +58 -0
- package/dist/plugins/autoplay.js +288 -0
- package/dist/plugins/autoplay.js.map +1 -0
- package/dist/plugins/autoplay.min.css +1 -0
- package/dist/plugins/autoplay.min.js +2 -0
- package/dist/plugins/autoplay.min.js.map +1 -0
- package/dist/plugins/fullscreen.js +80 -0
- package/dist/plugins/fullscreen.js.map +1 -0
- package/dist/plugins/fullscreen.min.js +2 -0
- package/dist/plugins/fullscreen.min.js.map +1 -0
- package/dist/plugins/layout.css +170 -0
- package/dist/plugins/layout.js +767 -0
- package/dist/plugins/layout.js.map +1 -0
- package/dist/plugins/layout.min.css +1 -0
- package/dist/plugins/layout.min.js +2 -0
- package/dist/plugins/layout.min.js.map +1 -0
- package/dist/plugins/rotateFlip.js +196 -0
- package/dist/plugins/rotateFlip.js.map +1 -0
- package/dist/plugins/rotateFlip.min.js +2 -0
- package/dist/plugins/rotateFlip.min.js.map +1 -0
- package/dist/plugins/video.js +227 -0
- package/dist/plugins/video.js.map +1 -0
- package/dist/plugins/video.min.js +2 -0
- package/dist/plugins/video.min.js.map +1 -0
- package/dist/plugins/zoom.css +27 -0
- package/dist/plugins/zoom.js +450 -0
- package/dist/plugins/zoom.js.map +1 -0
- package/dist/plugins/zoom.min.css +1 -0
- package/dist/plugins/zoom.min.js +2 -0
- package/dist/plugins/zoom.min.js.map +1 -0
- package/dist/shoji.css +99 -0
- package/dist/shoji.js +60 -16
- package/dist/shoji.js.map +1 -1
- package/dist/shoji.min.css +1 -1
- package/dist/shoji.min.js +1 -1
- package/dist/shoji.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/esm/bodyScrollLock-CC5PJxJp.js +0 -96
- package/dist/esm/bodyScrollLock-CC5PJxJp.js.map +0 -1
|
@@ -0,0 +1,833 @@
|
|
|
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
|
+
--shoji-z-caption-modal: 50; /* DESIGN.md §2.3a — above everything else in the dialog, a nested dialog of its own */
|
|
28
|
+
|
|
29
|
+
--shoji-color-bg: #0b0b0d;
|
|
30
|
+
--shoji-color-fg: #f5f5f5;
|
|
31
|
+
--shoji-color-accent: #4da3ff;
|
|
32
|
+
/* Fully opaque hue — the backdrop's actual on-screen transparency is
|
|
33
|
+
--shoji-backdrop-opacity below, a real 0-1 alpha, not baked in here.
|
|
34
|
+
A custom theme is still free to use an rgba()/gradient value instead;
|
|
35
|
+
--shoji-backdrop-opacity then composites on top of that value's own
|
|
36
|
+
alpha (CSS opacity multiplies), same as it always has. */
|
|
37
|
+
--shoji-color-backdrop: #000000;
|
|
38
|
+
--shoji-color-toolbar-bg: rgba(20, 20, 22, 0.85);
|
|
39
|
+
--shoji-color-disabled: rgba(245, 245, 245, 0.35);
|
|
40
|
+
/* Real alpha: 1 = fully opaque, 0 = fully transparent. Settable directly
|
|
41
|
+
via GalleryOptions.backdropOpacity (Gallery.ts) without needing to know
|
|
42
|
+
or override the full color value. */
|
|
43
|
+
--shoji-backdrop-opacity: 1;
|
|
44
|
+
|
|
45
|
+
--shoji-focus-ring: 2px solid var(--shoji-color-accent);
|
|
46
|
+
--shoji-progress: var(--shoji-color-accent);
|
|
47
|
+
|
|
48
|
+
--shoji-spacing-sm: 0.5rem;
|
|
49
|
+
--shoji-spacing-md: 1rem;
|
|
50
|
+
--shoji-spacing-lg: 1.5rem;
|
|
51
|
+
|
|
52
|
+
--shoji-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
53
|
+
--shoji-duration: 300ms;
|
|
54
|
+
/* The toolbar's own rendered height (44px button + top/bottom padding) —
|
|
55
|
+
defined before the caption sizing below since it now derives from
|
|
56
|
+
this. Also reserves space at the top of a provider embed (YouTube
|
|
57
|
+
etc.) so its own hover-revealed title bar never sits under Shoji's
|
|
58
|
+
toolbar; permanent there, not tied to the toolbar's auto-hide state,
|
|
59
|
+
so the video never resizes as the toolbar fades in/out. HTML5 video
|
|
60
|
+
has no top-of-player UI at all, so it's untouched by that use. */
|
|
61
|
+
--shoji-provider-video-top-inset: calc(44px + 2 * var(--shoji-spacing-sm));
|
|
62
|
+
/* DESIGN.md §2.4/§4.3 — a native <video controls>'s own scrub-bar/tap-
|
|
63
|
+
region is browser-rendered chrome, not real DOM Shoji can measure or
|
|
64
|
+
exclude by selector; approximated as a fixed bottom margin instead,
|
|
65
|
+
read by GestureController.ts's isInVideoControlsMargin(). 56px covers
|
|
66
|
+
the tallest common native controls bar (mobile Safari) with a little
|
|
67
|
+
headroom; a host that knows its target browsers render a shorter one
|
|
68
|
+
can safely set this lower. Swipe-to-navigate/drag-to-close only ever
|
|
69
|
+
reach the video body above this margin — never guaranteed to line up
|
|
70
|
+
with the real control bar pixel-for-pixel, just a reasonable default. */
|
|
71
|
+
--shoji-video-gesture-margin: 56px;
|
|
72
|
+
/* Caps how wide the caption box can grow before wrapping — `width:
|
|
73
|
+
fit-content` alone let a long caption wrap out to the dialog's full
|
|
74
|
+
width (only actually bounded by the padding subtracted below),
|
|
75
|
+
reading as a bar spanning the screen rather than a caption sized to
|
|
76
|
+
its own content. Desktop-only (see the media query below) — on a
|
|
77
|
+
narrow/mobile viewport the caption still spans (almost) the full
|
|
78
|
+
width, unchanged from before. */
|
|
79
|
+
--shoji-caption-max-width: calc(100% - 2 * var(--shoji-spacing-md));
|
|
80
|
+
/* DESIGN.md §2.3a — the collapsed default height, applied on top of
|
|
81
|
+
--shoji-caption-max-height (which only ever guards against reaching
|
|
82
|
+
the toolbar above, not the vertically-centered nav arrows sharing the
|
|
83
|
+
caption's own left edge). Requested directly, after a first version
|
|
84
|
+
that always collapsed to one line regardless of how much real room
|
|
85
|
+
existed: grow as tall as it can while staying clear of the nav
|
|
86
|
+
arrow's own footprint — `50%` (the dialog's own height, the
|
|
87
|
+
containing block `.shoji-caption`'s percentages resolve against) is
|
|
88
|
+
exactly where `.shoji-nav`'s own `top: 50%` sits, so `50%` minus half
|
|
89
|
+
its 44px height minus the caption's own bottom inset minus a small
|
|
90
|
+
breathing gap is the real ceiling, not an arbitrary line count. Pure
|
|
91
|
+
geometry — the arrow is always dead-center regardless of viewport, no
|
|
92
|
+
measurement needed the way the toolbar's own wrapping needs
|
|
93
|
+
`--shoji-toolbar-height` (Gallery.ts's ResizeObserver) to answer.
|
|
94
|
+
Floored (`max()`) at roughly one line + padding so a pathologically
|
|
95
|
+
short dialog can't collapse this to zero — the arrow may end up
|
|
96
|
+
slightly covered in that extreme case, but showing no caption at all
|
|
97
|
+
would be worse. A caption tall enough to still clip past all of this
|
|
98
|
+
gets `.shoji-caption--truncated` (Gallery.ts) and its own modal to
|
|
99
|
+
read the rest. */
|
|
100
|
+
--shoji-caption-collapsed-max-height: max(
|
|
101
|
+
calc(50% - 22px - var(--shoji-spacing-md) - var(--shoji-spacing-sm)),
|
|
102
|
+
calc(1.6em + 2 * var(--shoji-spacing-sm))
|
|
103
|
+
);
|
|
104
|
+
/* DESIGN.md §2.4 — the gesture engine's own documented default easing for
|
|
105
|
+
drag-to-navigate/drag-to-close settle animations specifically (a snap
|
|
106
|
+
tuned for "released mid-motion," distinct from --shoji-easing's
|
|
107
|
+
open/close/zoom-transition curve). */
|
|
108
|
+
--shoji-momentum-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
109
|
+
|
|
110
|
+
/* System-font stack: no webfont download, renders instantly, looks native
|
|
111
|
+
(San Francisco / Segoe UI / Roboto) on every platform. */
|
|
112
|
+
--shoji-font-family:
|
|
113
|
+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
|
114
|
+
'Apple Color Emoji', 'Segoe UI Emoji';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.shoji-light {
|
|
118
|
+
--shoji-color-bg: #fafafa;
|
|
119
|
+
--shoji-color-fg: #111111;
|
|
120
|
+
--shoji-color-backdrop: #ffffff;
|
|
121
|
+
--shoji-color-toolbar-bg: rgba(255, 255, 255, 0.9);
|
|
122
|
+
--shoji-color-disabled: rgba(17, 17, 17, 0.35);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@media (prefers-reduced-motion: reduce) {
|
|
126
|
+
:root {
|
|
127
|
+
--shoji-duration: 0ms;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Narrows the caption from its mobile default (spans ~the full dialog
|
|
132
|
+
width) down to roughly a quarter of the viewport, floored at 14rem so
|
|
133
|
+
it stays legible in a smaller desktop window — reported from real
|
|
134
|
+
usage as an aesthetic complaint (a long caption reading as a bar
|
|
135
|
+
spanning the screen), desktop-only by request: a narrow/mobile
|
|
136
|
+
viewport keeps the original near-full-width behavior unchanged, since
|
|
137
|
+
there's much less width to spare there in the first place. 768px is an
|
|
138
|
+
ordinary phone/tablet breakpoint, not tied to any other Shoji default —
|
|
139
|
+
there's no matching JS-side concept of "mobile" this needs to stay
|
|
140
|
+
consistent with. */
|
|
141
|
+
@media (min-width: 768px) {
|
|
142
|
+
:root {
|
|
143
|
+
--shoji-caption-max-width: min(calc(100% - 2 * var(--shoji-spacing-md)), max(24vw, 14rem));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Lightbox structure (DESIGN.md §2.3 slides, §2.6 dialog). Built lazily on first open(). */
|
|
148
|
+
.shoji-outer {
|
|
149
|
+
display: none;
|
|
150
|
+
position: fixed;
|
|
151
|
+
inset: 0;
|
|
152
|
+
z-index: var(--shoji-z-base);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.shoji-outer.shoji-open {
|
|
156
|
+
display: block;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.shoji-backdrop {
|
|
160
|
+
position: absolute;
|
|
161
|
+
inset: 0;
|
|
162
|
+
z-index: var(--shoji-z-backdrop);
|
|
163
|
+
background: var(--shoji-color-backdrop);
|
|
164
|
+
opacity: var(--shoji-backdrop-opacity);
|
|
165
|
+
/* A real bug: without this, the backdrop had no transition of its own at
|
|
166
|
+
all — .shoji-outer (backdrop included) goes display:none in one
|
|
167
|
+
instant step once the zoom-out settles, so the still-fully-dark
|
|
168
|
+
backdrop behind the shrinking photo would snap straight to fully
|
|
169
|
+
visible-page-behind-it in a single frame, reading as a flash right as
|
|
170
|
+
the close finishes. Gallery.ts's beginClose() now starts this fade at
|
|
171
|
+
the same moment as the zoom-out and the controls fade, so all three
|
|
172
|
+
finish together instead of the backdrop cutting out abruptly at the end. */
|
|
173
|
+
transition: opacity var(--shoji-duration) var(--shoji-easing);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.shoji-dialog {
|
|
177
|
+
position: relative;
|
|
178
|
+
z-index: var(--shoji-z-slides);
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
height: 100%;
|
|
182
|
+
color: var(--shoji-color-fg);
|
|
183
|
+
font-family: var(--shoji-font-family);
|
|
184
|
+
outline: none;
|
|
185
|
+
/* DESIGN.md §2.4 — the gesture engine owns horizontal/vertical drag on
|
|
186
|
+
this element; without this, touch browsers intercept the same gesture
|
|
187
|
+
for native scroll/pull-to-refresh/back-navigation before JS ever sees
|
|
188
|
+
enough movement to lock a direction. Buttons/video inside remain
|
|
189
|
+
interactive regardless (this is about implicit browser gesture
|
|
190
|
+
handling, not pointer-events). */
|
|
191
|
+
touch-action: none;
|
|
192
|
+
/* DESIGN.md §2.4 — a real bug, reported from real usage: a drag over the
|
|
193
|
+
photo/video (navigate/close) also started the browser's own native
|
|
194
|
+
text/image selection — visible as the image or video highlighting blue
|
|
195
|
+
mid-drag. `GestureEngine.ts` only calls `preventDefault()` on
|
|
196
|
+
`pointermove` once a direction has locked past `lockThreshold`
|
|
197
|
+
(horizontal) or not at all (vertical, DESIGN.md's own gesture-engine
|
|
198
|
+
notes above) — by then, or for a vertical drag, ever, the browser's own
|
|
199
|
+
selection-start heuristic has already had its chance. `user-select:
|
|
200
|
+
none` stops it at the source instead of racing preventDefault's own
|
|
201
|
+
timing. `.shoji-caption` below re-enables it — selecting caption text
|
|
202
|
+
is a deliberate, still-supported gesture-exclusion (this section,
|
|
203
|
+
"ignore()"), not something this should also block. */
|
|
204
|
+
user-select: none;
|
|
205
|
+
/* Caps how tall the caption box (a descendant, DESIGN.md §2.3a) can grow
|
|
206
|
+
before it scrolls internally — grows naturally with its content up to
|
|
207
|
+
that point, no cap kicking in early. The real ceiling is this
|
|
208
|
+
element's own height (100%) minus the toolbar's footprint at the top
|
|
209
|
+
minus the caption's own `bottom` offset minus a small breathing gap —
|
|
210
|
+
i.e. "as tall as it can get without reaching up under the toolbar,"
|
|
211
|
+
not an arbitrary fixed figure. Declared *here*, not `:root`, on
|
|
212
|
+
purpose — a real bug caught testing this: a custom property's nested
|
|
213
|
+
`var()` references substitute at the element where that property is
|
|
214
|
+
declared, not where a descendant later uses it, so defining this same
|
|
215
|
+
formula at `:root` baked in whatever `--shoji-toolbar-height`
|
|
216
|
+
happened to resolve to *there* (never set, always the fallback)
|
|
217
|
+
regardless of the real, per-instance value Gallery.ts's
|
|
218
|
+
ResizeObserver sets right on this same `.shoji-dialog` element below.
|
|
219
|
+
Declaring it here instead means the substitution happens where that
|
|
220
|
+
inline-set value is actually visible. */
|
|
221
|
+
--shoji-caption-max-height: calc(
|
|
222
|
+
100% - var(--shoji-toolbar-height, var(--shoji-provider-video-top-inset)) -
|
|
223
|
+
var(--shoji-spacing-md) - var(--shoji-spacing-sm)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.shoji-toolbar {
|
|
228
|
+
position: absolute;
|
|
229
|
+
inset: 0 0 auto 0;
|
|
230
|
+
z-index: var(--shoji-z-toolbar);
|
|
231
|
+
display: flex;
|
|
232
|
+
justify-content: space-between;
|
|
233
|
+
padding: var(--shoji-spacing-sm);
|
|
234
|
+
pointer-events: none;
|
|
235
|
+
/* Reported from real usage: the Zoom plugin's frequent scale() updates on
|
|
236
|
+
the photo (a sibling, not a descendant) could show as GPU tile-seam
|
|
237
|
+
artifacts (thin lines) through the toolbar's own semi-transparent,
|
|
238
|
+
border-radius buttons — the same class of rendering quirk already noted
|
|
239
|
+
for progressive image decode. Promoting the toolbar to its own stable
|
|
240
|
+
compositing layer, decoupled from the photo's repaints, avoids it. */
|
|
241
|
+
will-change: transform;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* DESIGN.md §3 — ctx.ui.toolbar(slot, ...) inserts plugin buttons into these;
|
|
245
|
+
pointer-events re-enabled per-slot (not per-button) so gaps between slots
|
|
246
|
+
stay click-through even once a slot has content. `flex-wrap: wrap` (not
|
|
247
|
+
`overflow-x: auto`) is deliberate: several plugins loaded together on a
|
|
248
|
+
narrow/mobile viewport can add up to more 44px buttons than the width
|
|
249
|
+
allows (CLAUDE.md's own touch-target size, non-negotiable), and this
|
|
250
|
+
project treats horizontal scroll as something to design around, not
|
|
251
|
+
reach for (see the layout plugin's masonry `orientation: 'horizontal'`,
|
|
252
|
+
which was rebuilt specifically to avoid it) — wrapping to a second row
|
|
253
|
+
keeps every button reachable by a tap with no sideways scroll gesture
|
|
254
|
+
anywhere in the lightbox chrome either.
|
|
255
|
+
|
|
256
|
+
max-width was 45% until a real bug, reported from real usage: the
|
|
257
|
+
toolbar-overflow popover (DESIGN.md §3.1a) collapses plugin buttons down
|
|
258
|
+
to `minPinnedToolbarButtons` (default 2), but 2 pinned + close + the
|
|
259
|
+
caret itself (4 icons, ~200px at the 44px touch-target size plus gaps)
|
|
260
|
+
already exceeds 45% of a common ~380px mobile width — so this slot fell
|
|
261
|
+
back to wrapping to a second row (pushing close onto its own line) even
|
|
262
|
+
though the *whole point* of the popover is that a bounded, small icon
|
|
263
|
+
count should now reliably fit in one row. Raised to 60% — comfortably
|
|
264
|
+
covers the default floor plus close/caret at typical mobile widths,
|
|
265
|
+
confirmed directly — while still leaving `.shoji-toolbar-left`'s own
|
|
266
|
+
counter room; a host who raises `minPinnedToolbarButtons` well past the
|
|
267
|
+
default on a narrow viewport can still hit this same wrap, by design
|
|
268
|
+
(DESIGN.md §3.1a: never collapsing below that floor, even if it still
|
|
269
|
+
leaves a slot wrapped, is the documented tradeoff). */
|
|
270
|
+
.shoji-toolbar-slot {
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-wrap: wrap;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: var(--shoji-spacing-sm);
|
|
275
|
+
pointer-events: auto;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.shoji-toolbar-left {
|
|
279
|
+
justify-content: flex-start;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.shoji-toolbar-right {
|
|
283
|
+
justify-content: flex-end;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.shoji-close,
|
|
287
|
+
.shoji-nav,
|
|
288
|
+
.shoji-toolbar-button,
|
|
289
|
+
.shoji-caption-modal-close {
|
|
290
|
+
appearance: none;
|
|
291
|
+
border: none;
|
|
292
|
+
padding: 0;
|
|
293
|
+
width: 44px;
|
|
294
|
+
height: 44px;
|
|
295
|
+
border-radius: 50%;
|
|
296
|
+
display: flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
justify-content: center;
|
|
299
|
+
background: rgba(0, 0, 0, 0.4);
|
|
300
|
+
color: var(--shoji-color-fg);
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.shoji-close:hover,
|
|
305
|
+
.shoji-nav:hover,
|
|
306
|
+
.shoji-toolbar-button:hover,
|
|
307
|
+
.shoji-caption-modal-close:hover {
|
|
308
|
+
background: rgba(0, 0, 0, 0.6);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.shoji-close:focus-visible,
|
|
312
|
+
.shoji-nav:focus-visible,
|
|
313
|
+
.shoji-toolbar-button:focus-visible,
|
|
314
|
+
.shoji-caption-modal-close:focus-visible {
|
|
315
|
+
outline: var(--shoji-focus-ring);
|
|
316
|
+
outline-offset: 2px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.shoji-nav {
|
|
320
|
+
position: absolute;
|
|
321
|
+
top: 50%;
|
|
322
|
+
transform: translateY(-50%);
|
|
323
|
+
z-index: var(--shoji-z-toolbar);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.shoji-nav-prev {
|
|
327
|
+
left: var(--shoji-spacing-md);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.shoji-nav-next {
|
|
331
|
+
right: var(--shoji-spacing-md);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.shoji-nav:disabled {
|
|
335
|
+
opacity: 0.3;
|
|
336
|
+
cursor: default;
|
|
337
|
+
pointer-events: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.shoji-close[hidden],
|
|
341
|
+
.shoji-nav[hidden],
|
|
342
|
+
.shoji-toolbar-button[hidden] {
|
|
343
|
+
display: none;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* DESIGN.md §2.3 — while the active slide's own content is still loading,
|
|
347
|
+
feature/plugin toolbar buttons (zoom, rotate, fullscreen, autoplay, any
|
|
348
|
+
custom plugin's own button) dim and stop responding, since they'd
|
|
349
|
+
otherwise act on content about to be replaced. Deliberately scoped to
|
|
350
|
+
`.shoji-toolbar-button` only — `.shoji-close`/`.shoji-nav` are different
|
|
351
|
+
classes, so closing and navigating are never blocked by a slow image.
|
|
352
|
+
`pointer-events: none` here is the mouse half; Gallery.ts's matching
|
|
353
|
+
`tabIndex = -1` is the keyboard half (this alone wouldn't stop
|
|
354
|
+
Enter/Space activating an already-focused button). */
|
|
355
|
+
.shoji-outer.shoji-slide-loading .shoji-toolbar-button {
|
|
356
|
+
opacity: 0.3;
|
|
357
|
+
cursor: default;
|
|
358
|
+
pointer-events: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.shoji-slides {
|
|
362
|
+
position: relative;
|
|
363
|
+
flex: 1;
|
|
364
|
+
overflow: hidden;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.shoji-slide {
|
|
368
|
+
position: absolute;
|
|
369
|
+
inset: 0;
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
/* transform (translateX per pool offset) is set inline by SlideManager */
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.shoji-slide-media {
|
|
377
|
+
/* Definite size (not max-width/max-height alone) so the img's own
|
|
378
|
+
percentage-based max-width/max-height below has something concrete to
|
|
379
|
+
resolve against — otherwise it sizes to the image's natural dimensions
|
|
380
|
+
instead of being contained, and object-fit: contain never engages. */
|
|
381
|
+
width: 100%;
|
|
382
|
+
height: 100%;
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
justify-content: center;
|
|
386
|
+
/* Positioning context for this box's own absolutely-positioned children
|
|
387
|
+
(the spinner, the open placeholder) specifically, not implicitly
|
|
388
|
+
whichever positioned ancestor happens to be next up (currently
|
|
389
|
+
.shoji-slide, coincidentally the same size — explicit here so that
|
|
390
|
+
stays true even if that ever changes). */
|
|
391
|
+
position: relative;
|
|
392
|
+
/* DESIGN.md §4-video — a provider video's real shape (YouTube/Vimeo/
|
|
393
|
+
html5), as a plain number SlideManager sets from item.width/item.height
|
|
394
|
+
(`applyAspect()`); `calc(16 / 9)` here is only the fallback for an item
|
|
395
|
+
that never declares a size, kept exact via calc() rather than a rounded
|
|
396
|
+
decimal. `.shoji-slide-provider-poster` below reads this directly.
|
|
397
|
+
`.shoji-slide-provider-video > iframe`/`.shoji-video-mount` also read it,
|
|
398
|
+
through their own nearer `container-type: size` context (still on
|
|
399
|
+
`.shoji-slide-provider-video` itself, unchanged) — declared again here
|
|
400
|
+
too only so a poster, `.shoji-slide-provider-video`'s own *sibling*
|
|
401
|
+
rather than a descendant of it, has a container-type ancestor to
|
|
402
|
+
resolve cqw/cqh against at all. */
|
|
403
|
+
--shoji-provider-video-aspect: calc(16 / 9);
|
|
404
|
+
container-type: size;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.shoji-slide-img,
|
|
408
|
+
.shoji-slide-video {
|
|
409
|
+
display: block;
|
|
410
|
+
max-width: 100%;
|
|
411
|
+
max-height: 100%;
|
|
412
|
+
object-fit: contain;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* max-width/max-height above only ever *cap* size — a real photo is normally
|
|
416
|
+
bigger than the slide area, so that's correct. A low-res open() placeholder
|
|
417
|
+
(§2.3) is the opposite case: deliberately small, so it needs to be forced
|
|
418
|
+
up to fill the frame (blurry, on purpose) instead of rendering at its own
|
|
419
|
+
tiny natural size. object-fit: contain (inherited above) still letterboxes
|
|
420
|
+
it within that forced box, just per the thumbnail's own aspect ratio. */
|
|
421
|
+
.shoji-slide-open-placeholder {
|
|
422
|
+
width: 100%;
|
|
423
|
+
height: 100%;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* A provider video's poster (§4.3, `item.poster`/a renderer's own fallback)
|
|
427
|
+
is the same "usually a small thumbnail file, needs to be forced up to
|
|
428
|
+
fill the frame" case as the open placeholder above — a real bug, a small
|
|
429
|
+
poster rendering as a postage-stamp-sized image floating in the slide
|
|
430
|
+
instead of filling it. Sized to the *video's* real shape
|
|
431
|
+
(`--shoji-provider-video-aspect`, `.shoji-slide-media` above) via the same
|
|
432
|
+
cqw/cqh contain-within-either-axis formula the real embed below uses, not
|
|
433
|
+
plain width/height: 100% — so the poster occupies exactly the box the
|
|
434
|
+
embed will occupy once it swaps in, instead of the two visibly jumping
|
|
435
|
+
between a video-shaped box and a dialog-shaped one. */
|
|
436
|
+
.shoji-slide-provider-poster {
|
|
437
|
+
width: min(100cqw, calc(100cqh * var(--shoji-provider-video-aspect)));
|
|
438
|
+
height: min(100cqh, calc(100cqw / var(--shoji-provider-video-aspect)));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* DESIGN.md §4-video — a provider's embed (an <iframe>, typically) has no
|
|
442
|
+
natural size Shoji can measure the way a decoded image/video does. A real
|
|
443
|
+
bug, reported from real usage, found in two parts:
|
|
444
|
+
1. Stretching the embed to fill this box regardless of the video's own
|
|
445
|
+
aspect ratio pushed the bottom of the video (and a provider's own
|
|
446
|
+
controls, always along that edge) below the visible area whenever the
|
|
447
|
+
box wasn't already the same shape as the video.
|
|
448
|
+
2. Fixing that by fitting to the box's actual shape instead exposed a
|
|
449
|
+
second issue: neither YouTube's nor Vimeo's own player page renders
|
|
450
|
+
with a background matching Shoji's theme in the letterboxed gap
|
|
451
|
+
*inside* the iframe (both showed white) — a color this page has no
|
|
452
|
+
way to reach or override, since that content is cross-origin.
|
|
453
|
+
Assumes 16:9 (overwhelmingly the common case for both providers, and
|
|
454
|
+
exactly correct for the video this was diagnosed against) and *contains*
|
|
455
|
+
within whatever box is actually available — the same shape object-fit
|
|
456
|
+
already gives an <img>/<video> above, achieved differently here since
|
|
457
|
+
object-fit doesn't apply to an iframe (or the wrapper div Vimeo's own
|
|
458
|
+
SDK builds one inside) — so any remaining gap is outside the iframe
|
|
459
|
+
instead of inside it, showing Shoji's own dark backdrop through this
|
|
460
|
+
element's own transparent background rather than the provider's white. */
|
|
461
|
+
.shoji-slide-provider-video {
|
|
462
|
+
/* `position: absolute; inset: 0` instead of sizing this as a centered flex
|
|
463
|
+
item: a real bug, reported from real usage, traced to this box's parent
|
|
464
|
+
(`.shoji-slide-media`, `justify-content: center`) miscentering it once
|
|
465
|
+
`container-type: size` (below) is also in play — confirmed directly via
|
|
466
|
+
`getBoundingClientRect()` on the real reported case, rendering the box
|
|
467
|
+
shifted half its own width off-screen. Taking it out of flex flow
|
|
468
|
+
entirely sidesteps the centering pass for this element altogether: it
|
|
469
|
+
fills `.shoji-slide-media` directly (already `position: relative`), no
|
|
470
|
+
distribution/centering math involved. `.shoji-slide-media`'s own
|
|
471
|
+
`justify-content: center` is untouched — still needed for photo/HTML5-
|
|
472
|
+
video slides, where content can be smaller than the box. */
|
|
473
|
+
position: absolute;
|
|
474
|
+
inset: 0;
|
|
475
|
+
display: flex;
|
|
476
|
+
align-items: center;
|
|
477
|
+
justify-content: center;
|
|
478
|
+
box-sizing: border-box;
|
|
479
|
+
padding-top: var(--shoji-provider-video-top-inset);
|
|
480
|
+
container-type: size;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.shoji-slide-provider-video[hidden] {
|
|
484
|
+
display: none;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* `.shoji-video-mount` — src/plugins/video/vimeo.ts's own wrapper div,
|
|
488
|
+
which its SDK would otherwise size itself (a small fixed default
|
|
489
|
+
normally, or its own `responsive: true` option's aspect-ratio-from-width-
|
|
490
|
+
alone computation, ignoring how much height is actually available either
|
|
491
|
+
way) — `!important` discards whatever it tries to set inline. The direct
|
|
492
|
+
`> iframe` case is YouTube, which has no such wrapper at all.
|
|
493
|
+
`min(100cqw, 100cqh * ratio)`/`min(100cqh, 100cqw / ratio)` is the
|
|
494
|
+
contain-within-either-axis formula `object-fit: contain` gives an
|
|
495
|
+
<img>/<video> natively — plain `aspect-ratio` alone can't do this for a
|
|
496
|
+
non-replaced element like a div/iframe: it only ever derives *one*
|
|
497
|
+
dimension from the other, so whichever of width/height stays a fixed
|
|
498
|
+
`100%` never actually shrinks even once the derived dimension gets
|
|
499
|
+
capped by `max-width`/`max-height`, silently distorting the ratio in
|
|
500
|
+
whichever direction wasn't the binding constraint. Confirmed directly at
|
|
501
|
+
both a wide-short viewport (height the binding constraint) and a
|
|
502
|
+
narrow-tall one (width the binding constraint) — container query units
|
|
503
|
+
are what actually solve both at once, not `aspect-ratio` by itself.
|
|
504
|
+
`--shoji-provider-video-aspect` (`.shoji-slide-media` above) is the
|
|
505
|
+
item's own real `width`/`height` ratio when known — a real bug/gap this
|
|
506
|
+
replaces a hardcoded `16/9` to fix: every provider video rendered as if
|
|
507
|
+
it were 16:9 regardless of its declared shape, YouTube/Vimeo/html5 alike
|
|
508
|
+
(html5's own `<video>` element is fixed the same way, directly, in
|
|
509
|
+
`SlideManager.ts`'s `renderVideo()` — it doesn't go through this
|
|
510
|
+
container-query-based sizing at all, having a real intrinsic size of its
|
|
511
|
+
own once it loads). */
|
|
512
|
+
.shoji-slide-provider-video > iframe,
|
|
513
|
+
.shoji-video-mount {
|
|
514
|
+
width: min(100cqw, calc(100cqh * var(--shoji-provider-video-aspect))) !important;
|
|
515
|
+
height: min(100cqh, calc(100cqw / var(--shoji-provider-video-aspect))) !important;
|
|
516
|
+
margin: 0 !important;
|
|
517
|
+
padding: 0 !important;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.shoji-slide-provider-video iframe {
|
|
521
|
+
display: block;
|
|
522
|
+
border: 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* Once `.shoji-video-mount` above is already correctly contain-fitted, its
|
|
526
|
+
own iframe just needs to fill it completely — no `aspect-ratio` of its
|
|
527
|
+
own to fight the wrapper's. Same `!important`/static-not-absolute
|
|
528
|
+
reasoning as `.shoji-video-mount` — Vimeo's SDK may set its own inline
|
|
529
|
+
sizing/position on the iframe too, not just on the wrapper. */
|
|
530
|
+
.shoji-video-mount iframe {
|
|
531
|
+
position: static !important;
|
|
532
|
+
width: 100% !important;
|
|
533
|
+
height: 100% !important;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* DESIGN.md §2.5 — the temporary clone SlideTransition animates the
|
|
537
|
+
outgoing slide's content out on. Sits above the normal (unpositioned
|
|
538
|
+
z-index) pool slots by DOM order alone, but z-index is set explicitly
|
|
539
|
+
here rather than relied on implicitly, matching how every other overlay
|
|
540
|
+
layer in this file states its stacking intent up front. Not interactive
|
|
541
|
+
(it's a decorative clone mid-removal) and never receives its own size —
|
|
542
|
+
it fills `.shoji-slides` exactly like the real `.shoji-slide` it's a
|
|
543
|
+
snapshot of, so the cloned `.shoji-slide-media` inside it (already
|
|
544
|
+
`width/height: 100%`) lines up pixel-for-pixel with no extra CSS needed. */
|
|
545
|
+
.shoji-slide-ghost {
|
|
546
|
+
position: absolute;
|
|
547
|
+
inset: 0;
|
|
548
|
+
z-index: 1;
|
|
549
|
+
pointer-events: none;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.shoji-slide-placeholder {
|
|
553
|
+
color: var(--shoji-color-disabled);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* DESIGN.md §2.3 — shown in place of a slide's content while it's still
|
|
557
|
+
decoding/loading (outside the `preload` window, where nothing was ready
|
|
558
|
+
ahead of time). CSS-only rotation (no JS-driven layout, per CLAUDE.md);
|
|
559
|
+
disabled under prefers-reduced-motion the same as every other animation
|
|
560
|
+
here, rather than left spinning regardless. */
|
|
561
|
+
.shoji-slide-spinner {
|
|
562
|
+
width: var(--shoji-spinner-size, 40px);
|
|
563
|
+
height: var(--shoji-spinner-size, 40px);
|
|
564
|
+
border-radius: 50%;
|
|
565
|
+
border: var(--shoji-spinner-thickness, 3px) solid var(--shoji-color-disabled);
|
|
566
|
+
border-top-color: var(--shoji-color-accent);
|
|
567
|
+
animation: shoji-spin 0.8s linear infinite;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
@keyframes shoji-spin {
|
|
571
|
+
to {
|
|
572
|
+
transform: rotate(360deg);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
@media (prefers-reduced-motion: reduce) {
|
|
577
|
+
.shoji-slide-spinner {
|
|
578
|
+
animation: none;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* A normal flex item within .shoji-toolbar-left (not independently
|
|
583
|
+
positioned) — .shoji-slides still gets the full dialog height either way,
|
|
584
|
+
no reserved gutter at top or bottom, since the toolbar itself overlays the
|
|
585
|
+
slides rather than pushing them down. Flowing alongside whatever else is
|
|
586
|
+
in that slot (e.g. a plugin's ctx.ui.toolbar('left', ...) button) keeps it
|
|
587
|
+
from overlapping and stealing clicks the way independent absolute
|
|
588
|
+
positioning did. */
|
|
589
|
+
.shoji-counter {
|
|
590
|
+
padding: 0.25rem 0.6rem;
|
|
591
|
+
border-radius: 999px;
|
|
592
|
+
background: rgba(0, 0, 0, 0.4);
|
|
593
|
+
font-size: 0.875rem;
|
|
594
|
+
white-space: nowrap;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.shoji-counter[hidden] {
|
|
598
|
+
display: none;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
.shoji-caption {
|
|
602
|
+
position: absolute;
|
|
603
|
+
bottom: var(--shoji-spacing-md);
|
|
604
|
+
left: var(--shoji-spacing-md);
|
|
605
|
+
z-index: var(--shoji-z-toolbar);
|
|
606
|
+
/* Re-enables text selection `.shoji-dialog`'s own user-select: none
|
|
607
|
+
(above) turns off broadly — caption text staying selectable is a
|
|
608
|
+
deliberate, existing feature (this section's "ignore()"), unaffected
|
|
609
|
+
by that fix for the photo/video's own native-selection bug. */
|
|
610
|
+
user-select: text;
|
|
611
|
+
width: fit-content;
|
|
612
|
+
max-width: var(--shoji-caption-max-width);
|
|
613
|
+
max-height: min(var(--shoji-caption-max-height), var(--shoji-caption-collapsed-max-height));
|
|
614
|
+
/* DESIGN.md §2.3a — a clean clip, not a scrollbar: the collapsed cap is
|
|
615
|
+
now small (~1 line) by design, so almost any real caption exceeds it;
|
|
616
|
+
a visible scrollbar on that tiny a box would look broken. Reading the
|
|
617
|
+
rest is what `.shoji-caption-modal` (Gallery.ts) is for instead — its
|
|
618
|
+
own content area is where `overflow-y: auto` still applies. */
|
|
619
|
+
overflow: hidden;
|
|
620
|
+
padding: var(--shoji-spacing-sm) var(--shoji-spacing-md);
|
|
621
|
+
/* A real bug, caught by an e2e regression test: without this, max-height
|
|
622
|
+
only constrains the content box — the element's own padding (16px,
|
|
623
|
+
top+bottom) then renders on top of that, eating into the breathing
|
|
624
|
+
gap the height cap's own calc (--shoji-caption-max-height, above)
|
|
625
|
+
already budgeted for, enough to actually overlap the toolbar instead
|
|
626
|
+
of stopping just short of it. */
|
|
627
|
+
box-sizing: border-box;
|
|
628
|
+
border-radius: 8px;
|
|
629
|
+
background: var(--shoji-color-toolbar-bg);
|
|
630
|
+
text-align: left;
|
|
631
|
+
/* A real bug, caught testing the truncated-caption line-clamp
|
|
632
|
+
(DESIGN.md §2.3a): left unset, `line-height` computes to the literal
|
|
633
|
+
keyword "normal" rather than a resolved px value, so
|
|
634
|
+
`updateCaptionTruncation()`'s own line-height measurement (Gallery.ts)
|
|
635
|
+
could only ever approximate it — close enough to be off by a
|
|
636
|
+
fractional line, letting one extra partial line bleed in below the
|
|
637
|
+
clamp's own ellipsis instead of cutting cleanly. An explicit value
|
|
638
|
+
here makes that measurement exact instead of estimated. */
|
|
639
|
+
line-height: 1.4;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* A real bug: height-capping the caption (above) still isn't enough on its
|
|
643
|
+
own — a video that fills most of the dialog leaves little to no
|
|
644
|
+
letterboxing gap, so even a short caption's opaque background can land
|
|
645
|
+
directly on the native control bar underneath it (same corner). Letting
|
|
646
|
+
clicks pass straight through the caption on a video slide (Gallery.ts
|
|
647
|
+
toggles this class from `item.video`, HTML5 or a provider like YouTube
|
|
648
|
+
alike) is what actually guarantees the controls stay reachable, not just
|
|
649
|
+
"usually clear of them." Re-enabled on any real child element so a rich
|
|
650
|
+
HTML caption's own links/buttons (DESIGN.md §2.1) stay clickable — only
|
|
651
|
+
a plain-string caption (no element children at all) ever fully passes
|
|
652
|
+
through. Photo slides are untouched: nothing below them needs protecting,
|
|
653
|
+
so the caption keeps its normal scrollable/selectable behavior there. */
|
|
654
|
+
.shoji-caption--video {
|
|
655
|
+
pointer-events: none;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.shoji-caption--video > * {
|
|
659
|
+
pointer-events: auto;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/* DESIGN.md §2.3a — only ever added once content genuinely overflows the
|
|
663
|
+
collapsed clip (Gallery.ts measures scrollHeight vs. clientHeight), so a
|
|
664
|
+
caption that already fits never looks or behaves clickable. On a video
|
|
665
|
+
slide `.shoji-caption--video`'s own pointer-events: none (above) already
|
|
666
|
+
makes this a no-op for pointer input there, on purpose — reaching the
|
|
667
|
+
native video controls underneath still wins; a keyboard user can still
|
|
668
|
+
Tab to it and open the modal regardless, since that's not competing for
|
|
669
|
+
the same physical space a click would be. */
|
|
670
|
+
.shoji-caption--truncated {
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* A visible cue, not just a silent clip. `-webkit-line-clamp` (the
|
|
675
|
+
standard tool for this) was tried first but didn't reliably stop at a
|
|
676
|
+
clean line boundary here — regardless of how precisely the line count
|
|
677
|
+
fed into it was computed, a partial extra line kept bleeding through
|
|
678
|
+
below its own ellipsis. Gallery.ts's updateCaptionTruncation() instead
|
|
679
|
+
snaps `max-height` itself down to an exact whole-line multiple, so a
|
|
680
|
+
plain clip can never land mid-line — this "…" chip is what actually
|
|
681
|
+
supplies the visible cue for that, pinned over the last visible line's
|
|
682
|
+
corner, background-matched so it masks whatever text it sits on. */
|
|
683
|
+
.shoji-caption--truncated::after {
|
|
684
|
+
content: '…';
|
|
685
|
+
position: absolute;
|
|
686
|
+
right: var(--shoji-spacing-sm);
|
|
687
|
+
bottom: var(--shoji-spacing-sm);
|
|
688
|
+
padding-left: 0.5em;
|
|
689
|
+
background: var(--shoji-color-toolbar-bg);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* DESIGN.md §2.3a — the truncated caption's own expanded view. A real
|
|
693
|
+
backdrop over the whole dialog, not an in-place taller caption: growing
|
|
694
|
+
the caption itself tall enough to show everything would risk covering
|
|
695
|
+
the nav arrows again, exactly the bug this feature exists to avoid. */
|
|
696
|
+
.shoji-caption-modal {
|
|
697
|
+
position: absolute;
|
|
698
|
+
inset: 0;
|
|
699
|
+
z-index: var(--shoji-z-caption-modal);
|
|
700
|
+
display: flex;
|
|
701
|
+
align-items: center;
|
|
702
|
+
justify-content: center;
|
|
703
|
+
padding: var(--shoji-spacing-lg);
|
|
704
|
+
background: rgba(0, 0, 0, 0.6);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.shoji-caption-modal[hidden] {
|
|
708
|
+
display: none;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.shoji-caption-modal-panel {
|
|
712
|
+
position: relative;
|
|
713
|
+
max-width: 32rem;
|
|
714
|
+
max-height: 100%;
|
|
715
|
+
overflow-y: auto;
|
|
716
|
+
overscroll-behavior: contain;
|
|
717
|
+
padding: var(--shoji-spacing-lg);
|
|
718
|
+
padding-inline-end: calc(44px + var(--shoji-spacing-lg));
|
|
719
|
+
border-radius: 8px;
|
|
720
|
+
background: var(--shoji-color-bg);
|
|
721
|
+
color: var(--shoji-color-fg);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.shoji-caption-modal-close {
|
|
725
|
+
position: absolute;
|
|
726
|
+
top: var(--shoji-spacing-sm);
|
|
727
|
+
right: var(--shoji-spacing-sm);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.shoji-caption[hidden] {
|
|
731
|
+
display: none;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/* DESIGN.md §3.1a — hidden unless the toolbar actually overflows
|
|
735
|
+
(Gallery.ts's measureToolbarOverflow() reveals it), so it never adds a
|
|
736
|
+
permanent extra control to a toolbar that never needed one. */
|
|
737
|
+
.shoji-toolbar-overflow[hidden] {
|
|
738
|
+
display: none;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/* 3 columns, wrapping to as many rows as needed — requested directly.
|
|
742
|
+
Width is content-driven (3 * 44px + 2 gaps + padding), not clamped to a
|
|
743
|
+
fraction of the dialog, since it never grows past that regardless of how
|
|
744
|
+
many buttons collapse into it. Columns are 44px with `--shoji-spacing-sm`
|
|
745
|
+
gaps — deliberately the same pitch as a toolbar button's own size and
|
|
746
|
+
`.shoji-toolbar-slot`'s own gap, so the popover's icon columns actually
|
|
747
|
+
line up with the toolbar row's icons above them.
|
|
748
|
+
|
|
749
|
+
`right` here is only a pre-open fallback — Gallery.ts's
|
|
750
|
+
`positionToolbarOverflowPanel()` sets it inline every time the panel
|
|
751
|
+
opens, anchored to the caret's own live position (which shifts with
|
|
752
|
+
`minPinnedToolbarButtons` and viewport width) rather than this fixed
|
|
753
|
+
offset from the dialog's edge, which otherwise lined up under
|
|
754
|
+
`closeButton` instead of the caret that actually opens it. */
|
|
755
|
+
.shoji-toolbar-overflow-panel {
|
|
756
|
+
position: absolute;
|
|
757
|
+
top: var(--shoji-toolbar-height, 56px);
|
|
758
|
+
right: var(--shoji-spacing-md);
|
|
759
|
+
z-index: var(--shoji-z-toast);
|
|
760
|
+
display: grid;
|
|
761
|
+
grid-template-columns: repeat(3, 44px);
|
|
762
|
+
gap: var(--shoji-spacing-sm);
|
|
763
|
+
max-height: calc(100% - var(--shoji-toolbar-height, 56px) - var(--shoji-spacing-md));
|
|
764
|
+
overflow-y: auto;
|
|
765
|
+
padding: var(--shoji-spacing-sm);
|
|
766
|
+
border-radius: 8px;
|
|
767
|
+
background: var(--shoji-color-bg);
|
|
768
|
+
color: var(--shoji-color-fg);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.shoji-toolbar-overflow-panel[hidden] {
|
|
772
|
+
display: none;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/* DESIGN.md §2.8 — idle auto-hide. Opacity only, never display:none, never
|
|
776
|
+
removed from the tab order: a hidden-but-focused control stays interactive,
|
|
777
|
+
just visually faded (see Gallery's isControlFocused() guard). */
|
|
778
|
+
.shoji-toolbar,
|
|
779
|
+
.shoji-nav,
|
|
780
|
+
.shoji-counter,
|
|
781
|
+
.shoji-caption {
|
|
782
|
+
transition: opacity var(--shoji-duration) var(--shoji-easing);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.shoji-controls-hidden .shoji-toolbar,
|
|
786
|
+
.shoji-controls-hidden .shoji-nav,
|
|
787
|
+
.shoji-controls-hidden .shoji-counter,
|
|
788
|
+
.shoji-controls-hidden .shoji-caption {
|
|
789
|
+
opacity: 0;
|
|
790
|
+
pointer-events: none;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* !important: wins over any plugin's own cursor styling (e.g. zoom.css's
|
|
794
|
+
zoom-in/grab affordances) without core needing to know that plugin's
|
|
795
|
+
selectors — the hidden state means nothing should suggest interactivity. */
|
|
796
|
+
.shoji-controls-hidden,
|
|
797
|
+
.shoji-controls-hidden * {
|
|
798
|
+
cursor: none !important;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* autoHideDelay: 0 (Gallery.ts) — Shoji's own controls stay permanently
|
|
802
|
+
invisible, but the cursor itself should behave normally, not force-hidden
|
|
803
|
+
along with them. Higher specificity than the rule above (both compound
|
|
804
|
+
selectors carry two classes/one-class-plus-descendant vs. one), so this
|
|
805
|
+
wins despite both using !important. */
|
|
806
|
+
.shoji-cursor-visible.shoji-controls-hidden,
|
|
807
|
+
.shoji-cursor-visible.shoji-controls-hidden * {
|
|
808
|
+
cursor: auto !important;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* A real bug, reported from real usage: dragging past the close threshold
|
|
812
|
+
(GestureController's live controls-hide cue, §2.4/§2.8) reuses this same
|
|
813
|
+
`.shoji-controls-hidden` class, so the cursor vanished along with the
|
|
814
|
+
toolbar/nav/counter/caption — disorienting mid-drag, when the pointer is
|
|
815
|
+
actively moving and the viewer needs to see it. `cursor: none` only makes
|
|
816
|
+
sense for an idle/inactive hide; a drag is the opposite of idle. Not
|
|
817
|
+
reusing `.shoji-cursor-visible` here — that class is a *permanent* marker
|
|
818
|
+
for `autoHideDelay: 0`, and toggling it on/off with the drag would
|
|
819
|
+
incorrectly clear it once the drag ends even when that permanent mode is
|
|
820
|
+
also active. */
|
|
821
|
+
.shoji-controls-hidden-for-drag.shoji-controls-hidden,
|
|
822
|
+
.shoji-controls-hidden-for-drag.shoji-controls-hidden * {
|
|
823
|
+
cursor: auto !important;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.shoji-live-region {
|
|
827
|
+
position: absolute;
|
|
828
|
+
width: 1px;
|
|
829
|
+
height: 1px;
|
|
830
|
+
overflow: hidden;
|
|
831
|
+
clip: rect(0 0 0 0);
|
|
832
|
+
white-space: nowrap;
|
|
833
|
+
}
|