@leavepulse/ui 0.32.0 → 0.33.0
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/chunks/{LpLightbox-Cixfxezw.js → LpLightbox-oLqyjOln.js} +1 -1
- package/dist/chunks/{LpMediaGallery-t1z3bQcY.js → LpMediaGallery-Cid7PGYx.js} +41 -14
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.d.ts +4 -0
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/lightbox.d.ts +10 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/components/LpLightbox.vue +14 -1
- package/src/components/LpMediaGallery.vue +97 -12
- package/src/components/lightbox.ts +10 -0
|
@@ -37,7 +37,7 @@ const _hoisted_13 = ["data-index", "aria-label", "onClick"];
|
|
|
37
37
|
const _hoisted_14 = ["src", "alt"];
|
|
38
38
|
const WHEEL_STEP = 60;
|
|
39
39
|
const BTN = "flex size-9 items-center justify-center rounded-control text-white/80 outline-none transition-colors duration-[var(--duration-fast)] hover:bg-white/15 hover:text-white focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40";
|
|
40
|
-
const ARROW = "absolute top-1/2 z-10 flex size-11 -translate-y-1/2 items-center justify-center rounded-pill bg-black/45 text-white/
|
|
40
|
+
const ARROW = "absolute top-1/2 z-10 flex size-11 -translate-y-1/2 items-center justify-center rounded-pill bg-black/45 text-white/75 ring-1 ring-white/0 outline-none backdrop-blur-sm transition-[background-color,scale,color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:bg-white/20 hover:text-white hover:ring-white/30 hover:scale-110 active:scale-95 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-0 motion-reduce:transition-none motion-reduce:hover:scale-100 motion-reduce:active:scale-100";
|
|
41
41
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
42
42
|
__name: "LpLightbox",
|
|
43
43
|
props: {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { defineComponent, ref, computed, openBlock, createElementBlock, renderSlot, mergeProps, unref, Fragment, renderList, normalizeClass, createElementVNode, toDisplayString, createVNode, createCommentVNode, createBlock } from "vue";
|
|
2
2
|
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
3
3
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
4
|
-
import { _ as _sfc_main$2 } from "./LpLightbox-
|
|
4
|
+
import { _ as _sfc_main$2 } from "./LpLightbox-oLqyjOln.js";
|
|
5
5
|
const _hoisted_1 = { key: 0 };
|
|
6
6
|
const _hoisted_2 = ["aria-label"];
|
|
7
7
|
const _hoisted_3 = ["aria-label", "onClick"];
|
|
8
8
|
const _hoisted_4 = ["src", "alt"];
|
|
9
9
|
const _hoisted_5 = {
|
|
10
10
|
key: 0,
|
|
11
|
-
class: "pointer-events-none absolute inset-0 flex items-center justify-center
|
|
11
|
+
class: "pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
12
12
|
};
|
|
13
13
|
const _hoisted_6 = {
|
|
14
14
|
key: 1,
|
|
15
|
+
class: "pointer-events-none absolute inset-0 flex items-center justify-center text-lg font-medium text-white"
|
|
16
|
+
};
|
|
17
|
+
const _hoisted_7 = {
|
|
18
|
+
key: 2,
|
|
15
19
|
class: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/0 opacity-0 transition-[background-color,opacity] duration-[var(--duration-fast)] group-hover:bg-black/25 group-hover:opacity-100 motion-reduce:transition-none",
|
|
16
20
|
"aria-hidden": "true"
|
|
17
21
|
};
|
|
@@ -28,7 +32,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
28
32
|
rotatable: { type: Boolean, default: true },
|
|
29
33
|
ariaLabel: { default: "Media" }
|
|
30
34
|
},
|
|
31
|
-
emits: ["select", "update:open"],
|
|
35
|
+
emits: ["select", "reveal", "update:open"],
|
|
32
36
|
setup(__props, { expose: __expose, emit: __emit }) {
|
|
33
37
|
const props = __props;
|
|
34
38
|
const emit = __emit;
|
|
@@ -111,16 +115,37 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
111
115
|
if (!item) return;
|
|
112
116
|
emit("select", item, i);
|
|
113
117
|
if (!props.openable) return;
|
|
114
|
-
|
|
118
|
+
const at = openIndexOf(i);
|
|
119
|
+
if (at === -1) return;
|
|
120
|
+
index.value = at;
|
|
115
121
|
open.value = true;
|
|
116
122
|
emit("update:open", true);
|
|
117
123
|
}
|
|
118
124
|
function openOverflow() {
|
|
119
125
|
activate(visible.value.length);
|
|
120
126
|
}
|
|
127
|
+
const revealed = ref(/* @__PURE__ */ new Set());
|
|
128
|
+
function isHidden(item) {
|
|
129
|
+
return Boolean(item.spoiler) && !revealed.value.has(item.src);
|
|
130
|
+
}
|
|
131
|
+
function reveal(item) {
|
|
132
|
+
revealed.value = new Set(revealed.value).add(item.src);
|
|
133
|
+
emit("reveal", item);
|
|
134
|
+
}
|
|
135
|
+
const openItems = computed(() => props.items.filter((item) => !isHidden(item)));
|
|
136
|
+
function openIndexOf(i) {
|
|
137
|
+
const item = props.items[i];
|
|
138
|
+
return item ? openItems.value.indexOf(item) : -1;
|
|
139
|
+
}
|
|
121
140
|
__expose({
|
|
122
141
|
/** Open the lightbox at an index — for a "view all" button outside the grid. */
|
|
123
|
-
openAt: (i) => activate(i)
|
|
142
|
+
openAt: (i) => activate(i),
|
|
143
|
+
/** Uncover every spoiler in the set. */
|
|
144
|
+
revealAll: () => {
|
|
145
|
+
const next = new Set(revealed.value);
|
|
146
|
+
for (const item of props.items) if (item.spoiler) next.add(item.src);
|
|
147
|
+
revealed.value = next;
|
|
148
|
+
}
|
|
124
149
|
});
|
|
125
150
|
return (_ctx, _cache) => {
|
|
126
151
|
return !__props.items.length ? (openBlock(), createElementBlock("div", _hoisted_1, [
|
|
@@ -141,8 +166,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
141
166
|
__props.openable ? "cursor-pointer" : "cursor-default",
|
|
142
167
|
"focus-visible:ring-2 focus-visible:ring-ring"
|
|
143
168
|
]),
|
|
144
|
-
"aria-label": hiddenCount.value && i === visible.value.length - 1 ? `Show ${hiddenCount.value} more` : item.title || `Image ${i + 1} of ${__props.items.length}`,
|
|
145
|
-
onClick: ($event) => hiddenCount.value && i === visible.value.length - 1 ? openOverflow() : activate(i)
|
|
169
|
+
"aria-label": isHidden(item) ? "Spoiler — show image" : hiddenCount.value && i === visible.value.length - 1 ? `Show ${hiddenCount.value} more` : item.title || `Image ${i + 1} of ${__props.items.length}`,
|
|
170
|
+
onClick: ($event) => isHidden(item) ? reveal(item) : hiddenCount.value && i === visible.value.length - 1 ? openOverflow() : activate(i)
|
|
146
171
|
}, [
|
|
147
172
|
renderSlot(_ctx.$slots, "item", {
|
|
148
173
|
item,
|
|
@@ -150,16 +175,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
150
175
|
}, () => [
|
|
151
176
|
createElementVNode("img", {
|
|
152
177
|
src: item.thumb || item.src,
|
|
153
|
-
alt: item.title || "",
|
|
178
|
+
alt: isHidden(item) ? "" : item.title || "",
|
|
154
179
|
loading: "lazy",
|
|
155
180
|
decoding: "async",
|
|
156
|
-
class: normalizeClass([
|
|
157
|
-
"
|
|
158
|
-
hiddenCount.value && i === visible.value.length - 1 ? "scale-[1.06] blur-[2px] brightness-[0.45] group-hover:scale-[1.06]" : ""
|
|
159
|
-
])
|
|
181
|
+
class: normalizeClass(["size-full object-cover transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] group-hover:scale-[1.03] motion-reduce:transition-none motion-reduce:group-hover:scale-100", [
|
|
182
|
+
isHidden(item) ? "scale-[1.12] blur-[18px] brightness-[0.6] saturate-[1.1] group-hover:scale-[1.12]" : "",
|
|
183
|
+
!isHidden(item) && hiddenCount.value && i === visible.value.length - 1 ? "scale-[1.06] blur-[2px] brightness-[0.45] group-hover:scale-[1.06]" : ""
|
|
184
|
+
]])
|
|
160
185
|
}, null, 10, _hoisted_4)
|
|
161
186
|
]),
|
|
162
|
-
|
|
187
|
+
isHidden(item) ? (openBlock(), createElementBlock("span", _hoisted_5, [..._cache[2] || (_cache[2] = [
|
|
188
|
+
createElementVNode("span", { class: "rounded-pill bg-black/55 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white" }, " Spoiler ", -1)
|
|
189
|
+
])])) : hiddenCount.value && i === visible.value.length - 1 ? (openBlock(), createElementBlock("span", _hoisted_6, " +" + toDisplayString(hiddenCount.value), 1)) : __props.openable ? (openBlock(), createElementBlock("span", _hoisted_7, [
|
|
163
190
|
createVNode(_sfc_main$1, {
|
|
164
191
|
name: "lucide:maximize-2",
|
|
165
192
|
size: 18,
|
|
@@ -174,7 +201,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
174
201
|
"onUpdate:open": _cache[0] || (_cache[0] = ($event) => open.value = $event),
|
|
175
202
|
index: index.value,
|
|
176
203
|
"onUpdate:index": _cache[1] || (_cache[1] = ($event) => index.value = $event),
|
|
177
|
-
items:
|
|
204
|
+
items: openItems.value,
|
|
178
205
|
downloadable: __props.downloadable,
|
|
179
206
|
rotatable: __props.rotatable
|
|
180
207
|
}, null, 8, ["open", "index", "items", "downloadable", "rotatable"])) : createCommentVNode("", true)
|
|
@@ -45,12 +45,16 @@ type __VLS_Slots = {
|
|
|
45
45
|
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {
|
|
46
46
|
/** Open the lightbox at an index — for a "view all" button outside the grid. */
|
|
47
47
|
openAt: (i: number) => void;
|
|
48
|
+
/** Uncover every spoiler in the set. */
|
|
49
|
+
revealAll: () => void;
|
|
48
50
|
}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
49
51
|
select: (item: LightboxItem, index: number) => any;
|
|
50
52
|
"update:open": (value: boolean) => any;
|
|
53
|
+
reveal: (item: LightboxItem) => any;
|
|
51
54
|
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
52
55
|
onSelect?: ((item: LightboxItem, index: number) => any) | undefined;
|
|
53
56
|
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
57
|
+
onReveal?: ((item: LightboxItem) => any) | undefined;
|
|
54
58
|
}>, {
|
|
55
59
|
columns: number | {
|
|
56
60
|
base?: number;
|
|
@@ -9,6 +9,16 @@ export interface LightboxItem {
|
|
|
9
9
|
description?: string;
|
|
10
10
|
/** Filename used when downloading; derived from `src` when absent. */
|
|
11
11
|
filename?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Hide this image behind a cover until someone asks for it — a spoiler, a
|
|
14
|
+
* screenshot of something not everyone in the channel has reached yet, a
|
|
15
|
+
* picture that should not appear in a scroll past.
|
|
16
|
+
*
|
|
17
|
+
* A property of the ITEM rather than of the grid showing it: which images are
|
|
18
|
+
* spoilers is a fact about the images, and the usual set is a mixed one, with
|
|
19
|
+
* a marked picture sitting beside plain ones.
|
|
20
|
+
*/
|
|
21
|
+
spoiler?: boolean;
|
|
12
22
|
}
|
|
13
23
|
/** Last path segment of a URL, or a fallback — used to name a download. */
|
|
14
24
|
export declare function fileNameOf(item: LightboxItem, fallback?: string): string;
|
package/dist/index.js
CHANGED
|
@@ -35,11 +35,11 @@ import { _ as _27 } from "./chunks/LpIcon-CCnX5_2j.js";
|
|
|
35
35
|
import { default as default5 } from "./components/LpImageEditor.vue.js";
|
|
36
36
|
import { _ as _28 } from "./chunks/LpInfraNode-Ds_8SZgN.js";
|
|
37
37
|
import { _ as _29 } from "./chunks/LpInput-9EXWUM9B.js";
|
|
38
|
-
import { _ as _30 } from "./chunks/LpLightbox-
|
|
38
|
+
import { _ as _30 } from "./chunks/LpLightbox-oLqyjOln.js";
|
|
39
39
|
import { fileNameOf } from "./components/lightbox.js";
|
|
40
40
|
import { _ as _31 } from "./chunks/LpLink-DCND-zyu.js";
|
|
41
41
|
import { _ as _32 } from "./chunks/LpLogViewer-y53ZqSVq.js";
|
|
42
|
-
import { _ as _33 } from "./chunks/LpMediaGallery-
|
|
42
|
+
import { _ as _33 } from "./chunks/LpMediaGallery-Cid7PGYx.js";
|
|
43
43
|
import { _ as _34 } from "./chunks/LpModal-D_o-56To.js";
|
|
44
44
|
import { _ as _35 } from "./chunks/LpNotificationBell-lHePf89W.js";
|
|
45
45
|
import { _ as _36 } from "./chunks/LpNumberField-CWuX08iX.js";
|
package/package.json
CHANGED
|
@@ -315,8 +315,21 @@ const imageStyle = computed(() => ({
|
|
|
315
315
|
|
|
316
316
|
const BTN =
|
|
317
317
|
"flex size-9 items-center justify-center rounded-control text-white/80 outline-none transition-colors duration-[var(--duration-fast)] hover:bg-white/15 hover:text-white focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40"
|
|
318
|
+
/*
|
|
319
|
+
* The arrows sit on a photograph, which is the hard surface to answer on: the
|
|
320
|
+
* resting chrome is a dark translucent disc, and a hover that only deepens that
|
|
321
|
+
* dark is a change from "nearly black" to "slightly more nearly black" over an
|
|
322
|
+
* image that may itself be dark. Measured, the old step moved the fill from
|
|
323
|
+
* 0.45 to 0.65 alpha and the disc by 5% of 44px — two pixels. It was doing
|
|
324
|
+
* something, and it read as doing nothing.
|
|
325
|
+
*
|
|
326
|
+
* So the hover changes the QUALITY of the surface, not just its depth: the fill
|
|
327
|
+
* goes lighter rather than darker, a hairline ring appears, the icon reaches
|
|
328
|
+
* full white and the disc grows by a tenth. Pressing it dips back below resting
|
|
329
|
+
* size, which is the part that makes a button feel connected to the click.
|
|
330
|
+
*/
|
|
318
331
|
const ARROW =
|
|
319
|
-
"absolute top-1/2 z-10 flex size-11 -translate-y-1/2 items-center justify-center rounded-pill bg-black/45 text-white/
|
|
332
|
+
"absolute top-1/2 z-10 flex size-11 -translate-y-1/2 items-center justify-center rounded-pill bg-black/45 text-white/75 ring-1 ring-white/0 outline-none backdrop-blur-sm transition-[background-color,scale,color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:bg-white/20 hover:text-white hover:ring-white/30 hover:scale-110 active:scale-95 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-0 motion-reduce:transition-none motion-reduce:hover:scale-100 motion-reduce:active:scale-100"
|
|
320
333
|
</script>
|
|
321
334
|
|
|
322
335
|
<template>
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
* straight from here into the lightbox with nothing in between. `thumb` is the
|
|
20
20
|
* small source when one exists; without it the grid shows `src`, which is
|
|
21
21
|
* correct but heavier, and that is the caller's call to make.
|
|
22
|
+
*
|
|
23
|
+
* An item marked `spoiler` arrives covered and takes a click to uncover. The
|
|
24
|
+
* cover is per image and the reveal is remembered per image, because a set is
|
|
25
|
+
* usually mixed — one marked screenshot among plain ones.
|
|
22
26
|
*/
|
|
23
27
|
import { computed, ref } from "vue"
|
|
24
28
|
import type { LightboxItem } from "./lightbox"
|
|
@@ -69,6 +73,8 @@ const props = withDefaults(
|
|
|
69
73
|
const emit = defineEmits<{
|
|
70
74
|
/** A thumbnail was activated, before the lightbox opens. */
|
|
71
75
|
(e: "select", item: LightboxItem, index: number): void
|
|
76
|
+
/** A spoiler was uncovered. */
|
|
77
|
+
(e: "reveal", item: LightboxItem): void
|
|
72
78
|
(e: "update:open", value: boolean): void
|
|
73
79
|
}>()
|
|
74
80
|
|
|
@@ -173,9 +179,15 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
173
179
|
function activate(i: number) {
|
|
174
180
|
const item = props.items[i]
|
|
175
181
|
if (!item) return
|
|
182
|
+
// `select` reports the position in the caller's own array, which is the one
|
|
183
|
+
// they can do anything with — the lightbox's filtered index is internal.
|
|
176
184
|
emit("select", item, i)
|
|
177
185
|
if (!props.openable) return
|
|
178
|
-
index
|
|
186
|
+
// Covered images are not in the lightbox's list, so the index has to be
|
|
187
|
+
// translated into it.
|
|
188
|
+
const at = openIndexOf(i)
|
|
189
|
+
if (at === -1) return
|
|
190
|
+
index.value = at
|
|
179
191
|
open.value = true
|
|
180
192
|
emit("update:open", true)
|
|
181
193
|
}
|
|
@@ -185,9 +197,57 @@ function openOverflow() {
|
|
|
185
197
|
activate(visible.value.length)
|
|
186
198
|
}
|
|
187
199
|
|
|
200
|
+
/*
|
|
201
|
+
* Which spoilers have been opened.
|
|
202
|
+
*
|
|
203
|
+
* Keyed by `src` rather than by index, because the index is not a name: a set
|
|
204
|
+
* that gains an image at the front, or is filtered, shifts every position under
|
|
205
|
+
* the cursor, and a grid keyed by index would then show the wrong picture as
|
|
206
|
+
* already-revealed — which for a spoiler is the one failure that matters.
|
|
207
|
+
*/
|
|
208
|
+
const revealed = ref(new Set<string>())
|
|
209
|
+
|
|
210
|
+
function isHidden(item: LightboxItem): boolean {
|
|
211
|
+
return Boolean(item.spoiler) && !revealed.value.has(item.src)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* A covered tile takes its click to uncover itself and STOPS there — it does
|
|
216
|
+
* not also open the lightbox. Revealing and viewing are two separate requests,
|
|
217
|
+
* and running them together puts a picture nobody has agreed to see yet
|
|
218
|
+
* full-screen on the first click. The second click opens it like any other.
|
|
219
|
+
*/
|
|
220
|
+
function reveal(item: LightboxItem) {
|
|
221
|
+
// A new Set, not .add(): a Set mutated in place is the same object, so a
|
|
222
|
+
// computed reading it never sees a change.
|
|
223
|
+
revealed.value = new Set(revealed.value).add(item.src)
|
|
224
|
+
emit("reveal", item)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* The lightbox never receives a covered image. Paging through the set with the
|
|
229
|
+
* arrow keys would otherwise walk straight into one full-screen, having asked
|
|
230
|
+
* nobody — the grid's cover would have been for nothing. Items already revealed
|
|
231
|
+
* pass through untouched, so opening the one you just uncovered still lands on
|
|
232
|
+
* it.
|
|
233
|
+
*/
|
|
234
|
+
const openItems = computed(() => props.items.filter((item) => !isHidden(item)))
|
|
235
|
+
|
|
236
|
+
/** Index within `openItems` of the grid item at `i`, or -1 when it is covered. */
|
|
237
|
+
function openIndexOf(i: number): number {
|
|
238
|
+
const item = props.items[i]
|
|
239
|
+
return item ? openItems.value.indexOf(item) : -1
|
|
240
|
+
}
|
|
241
|
+
|
|
188
242
|
defineExpose({
|
|
189
243
|
/** Open the lightbox at an index — for a "view all" button outside the grid. */
|
|
190
244
|
openAt: (i: number) => activate(i),
|
|
245
|
+
/** Uncover every spoiler in the set. */
|
|
246
|
+
revealAll: () => {
|
|
247
|
+
const next = new Set(revealed.value)
|
|
248
|
+
for (const item of props.items) if (item.spoiler) next.add(item.src)
|
|
249
|
+
revealed.value = next
|
|
250
|
+
},
|
|
191
251
|
})
|
|
192
252
|
</script>
|
|
193
253
|
|
|
@@ -208,11 +268,19 @@ defineExpose({
|
|
|
208
268
|
'focus-visible:ring-2 focus-visible:ring-ring',
|
|
209
269
|
]"
|
|
210
270
|
:aria-label="
|
|
211
|
-
|
|
212
|
-
?
|
|
213
|
-
:
|
|
271
|
+
isHidden(item)
|
|
272
|
+
? 'Spoiler — show image'
|
|
273
|
+
: hiddenCount && i === visible.length - 1
|
|
274
|
+
? `Show ${hiddenCount} more`
|
|
275
|
+
: item.title || `Image ${i + 1} of ${items.length}`
|
|
276
|
+
"
|
|
277
|
+
@click="
|
|
278
|
+
isHidden(item)
|
|
279
|
+
? reveal(item)
|
|
280
|
+
: hiddenCount && i === visible.length - 1
|
|
281
|
+
? openOverflow()
|
|
282
|
+
: activate(i)
|
|
214
283
|
"
|
|
215
|
-
@click="hiddenCount && i === visible.length - 1 ? openOverflow() : activate(i)"
|
|
216
284
|
>
|
|
217
285
|
<slot name="item" :item="item" :index="i">
|
|
218
286
|
<!-- decoding=async + loading=lazy: a gallery is often far down a page,
|
|
@@ -227,15 +295,18 @@ defineExpose({
|
|
|
227
295
|
little pushes that feathered edge outside the clip. -->
|
|
228
296
|
<img
|
|
229
297
|
:src="item.thumb || item.src"
|
|
230
|
-
:alt="item.title || ''"
|
|
298
|
+
:alt="isHidden(item) ? '' : item.title || ''"
|
|
231
299
|
loading="lazy"
|
|
232
300
|
decoding="async"
|
|
233
301
|
class="size-full object-cover transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] group-hover:scale-[1.03] motion-reduce:transition-none motion-reduce:group-hover:scale-100"
|
|
234
|
-
:class="
|
|
235
|
-
|
|
302
|
+
:class="[
|
|
303
|
+
isHidden(item)
|
|
304
|
+
? 'scale-[1.12] blur-[18px] brightness-[0.6] saturate-[1.1] group-hover:scale-[1.12]'
|
|
305
|
+
: '',
|
|
306
|
+
!isHidden(item) && hiddenCount && i === visible.length - 1
|
|
236
307
|
? 'scale-[1.06] blur-[2px] brightness-[0.45] group-hover:scale-[1.06]'
|
|
237
|
-
: ''
|
|
238
|
-
"
|
|
308
|
+
: '',
|
|
309
|
+
]"
|
|
239
310
|
>
|
|
240
311
|
</slot>
|
|
241
312
|
|
|
@@ -252,8 +323,22 @@ defineExpose({
|
|
|
252
323
|
and obvious zoomed. Dimming the image itself means the corner clips a
|
|
253
324
|
finished pixel, which fades toward transparent instead of toward
|
|
254
325
|
something that should never be on show. -->
|
|
326
|
+
<!-- A covered tile says so in words. The blur alone is ambiguous — a
|
|
327
|
+
heavily blurred photograph reads as one that failed to load, and the
|
|
328
|
+
label is the difference between "broken" and "click to see". -->
|
|
329
|
+
<span
|
|
330
|
+
v-if="isHidden(item)"
|
|
331
|
+
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
332
|
+
>
|
|
333
|
+
<span
|
|
334
|
+
class="rounded-pill bg-black/55 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white"
|
|
335
|
+
>
|
|
336
|
+
Spoiler
|
|
337
|
+
</span>
|
|
338
|
+
</span>
|
|
339
|
+
|
|
255
340
|
<span
|
|
256
|
-
v-if="hiddenCount && i === visible.length - 1"
|
|
341
|
+
v-else-if="hiddenCount && i === visible.length - 1"
|
|
257
342
|
class="pointer-events-none absolute inset-0 flex items-center justify-center text-lg font-medium text-white"
|
|
258
343
|
>
|
|
259
344
|
+{{ hiddenCount }}
|
|
@@ -272,7 +357,7 @@ defineExpose({
|
|
|
272
357
|
v-if="openable"
|
|
273
358
|
v-model:open="open"
|
|
274
359
|
v-model:index="index"
|
|
275
|
-
:items="
|
|
360
|
+
:items="openItems"
|
|
276
361
|
:downloadable="downloadable"
|
|
277
362
|
:rotatable="rotatable"
|
|
278
363
|
/>
|
|
@@ -14,6 +14,16 @@ export interface LightboxItem {
|
|
|
14
14
|
description?: string
|
|
15
15
|
/** Filename used when downloading; derived from `src` when absent. */
|
|
16
16
|
filename?: string
|
|
17
|
+
/**
|
|
18
|
+
* Hide this image behind a cover until someone asks for it — a spoiler, a
|
|
19
|
+
* screenshot of something not everyone in the channel has reached yet, a
|
|
20
|
+
* picture that should not appear in a scroll past.
|
|
21
|
+
*
|
|
22
|
+
* A property of the ITEM rather than of the grid showing it: which images are
|
|
23
|
+
* spoilers is a fact about the images, and the usual set is a mixed one, with
|
|
24
|
+
* a marked picture sitting beside plain ones.
|
|
25
|
+
*/
|
|
26
|
+
spoiler?: boolean
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
/** Last path segment of a URL, or a fallback — used to name a download. */
|