@leavepulse/ui 0.15.5 → 0.16.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.
@@ -3,7 +3,7 @@ import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, Di
3
3
  import { useZoomPan } from "../composables/useZoomPan.js";
4
4
  import { fileNameOf } from "../components/lightbox.js";
5
5
  import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
6
- const _hoisted_1 = { class: "flex shrink-0 items-center gap-1 p-3 text-sm text-white/70" };
6
+ const _hoisted_1 = { class: "flex shrink-0 items-center gap-1 p-3 text-sm text-white/70 [--chrome-from:-8px] animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_60ms_both] motion-reduce:animate-none" };
7
7
  const _hoisted_2 = {
8
8
  key: 0,
9
9
  class: "px-2 tabular-nums"
@@ -20,7 +20,7 @@ const _hoisted_6 = {
20
20
  };
21
21
  const _hoisted_7 = ["src", "alt"];
22
22
  const _hoisted_8 = ["disabled"];
23
- const _hoisted_9 = { class: "shrink-0 p-3 pt-2" };
23
+ const _hoisted_9 = { class: "shrink-0 p-3 pt-2 animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_60ms_both] motion-reduce:animate-none" };
24
24
  const _hoisted_10 = {
25
25
  key: 0,
26
26
  class: "mb-2 text-center"
@@ -55,9 +55,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
55
55
  const props = __props;
56
56
  const emit = __emit;
57
57
  const zoom = useZoomPan({ max: props.maxZoom });
58
- computed(() => zoom.scale.value);
59
- computed(() => zoom.zoomed.value);
60
- computed(() => zoom.panning.value);
58
+ const scale = computed(() => zoom.scale.value);
59
+ const isZoomed = computed(() => zoom.zoomed.value);
60
+ const isPanning = computed(() => zoom.panning.value);
61
61
  const current = computed(() => Math.min(Math.max(props.index, 0), Math.max(props.items.length - 1, 0)));
62
62
  const item = computed(() => props.items[current.value]);
63
63
  const total = computed(() => props.items.length);
@@ -68,10 +68,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
68
68
  const viewport = ref(null);
69
69
  const stripEl = ref(null);
70
70
  watch(viewport, (el) => zoom.setViewport(el));
71
+ const direction = ref(1);
71
72
  function go(next) {
72
73
  if (!total.value) return;
73
74
  const wrapped = props.loop ? (next + total.value) % total.value : Math.min(Math.max(next, 0), total.value - 1);
74
75
  if (wrapped === current.value) return;
76
+ direction.value = next > current.value ? 1 : -1;
75
77
  emit("update:index", wrapped);
76
78
  emit("change", wrapped, props.items[wrapped]);
77
79
  }
@@ -196,7 +198,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
196
198
  }
197
199
  const imageStyle = computed(() => ({
198
200
  ...zoom.style.value,
199
- rotate: `${rotation.value}deg`
201
+ rotate: `${rotation.value}deg`,
202
+ // Consumed by the lightbox-frame-in keyframe; negative when paging backwards.
203
+ "--frame-from": `${direction.value * 24}px`
200
204
  }));
201
205
  return (_ctx, _cache) => {
202
206
  return openBlock(), createBlock(unref(DialogRoot), {
@@ -208,7 +212,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
208
212
  default: withCtx(() => [
209
213
  createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/85 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
210
214
  createVNode(unref(DialogContent), {
211
- class: "fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]",
215
+ class: "fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none",
212
216
  onKeydown
213
217
  }, {
214
218
  default: withCtx(() => [
@@ -226,7 +230,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
226
230
  }),
227
231
  createElementVNode("div", _hoisted_1, [
228
232
  total.value > 1 ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(current.value + 1) + " / " + toDisplayString(total.value), 1)) : createCommentVNode("", true),
229
- unref(zoom).zoomed.value ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(Math.round(unref(zoom).scale.value * 100)) + "% ", 1)) : createCommentVNode("", true),
233
+ isZoomed.value ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(Math.round(scale.value * 100)) + "% ", 1)) : createCommentVNode("", true),
230
234
  createElementVNode("div", _hoisted_4, [
231
235
  renderSlot(_ctx.$slots, "actions", {
232
236
  item: item.value,
@@ -306,7 +310,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
306
310
  createElementVNode("div", {
307
311
  ref_key: "viewport",
308
312
  ref: viewport,
309
- class: normalizeClass(["relative flex min-h-0 flex-1 items-center justify-center overflow-hidden", unref(zoom).panning.value ? "cursor-grabbing" : unref(zoom).zoomed.value ? "cursor-grab" : ""]),
313
+ class: normalizeClass(["relative flex min-h-0 flex-1 items-center justify-center overflow-hidden", isPanning.value ? "cursor-grabbing" : isZoomed.value ? "cursor-grab" : ""]),
310
314
  onWheel: _cache[6] || (_cache[6] = //@ts-ignore
311
315
  (...args) => unref(zoom).onWheel && unref(zoom).onWheel(...args)),
312
316
  onPointerdown: onPointerDown,
@@ -341,7 +345,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
341
345
  "data-lightbox-image": "",
342
346
  src: item.value.src,
343
347
  alt: item.value.title ?? "",
344
- class: "max-h-full max-w-full select-none object-contain transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none",
348
+ class: "max-h-full max-w-full select-none object-contain transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] animate-[lightbox-frame-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none motion-reduce:transition-none",
345
349
  style: normalizeStyle(imageStyle.value),
346
350
  draggable: "false",
347
351
  onLoad: _cache[3] || (_cache[3] = ($event) => loading.value = false),
@@ -388,7 +392,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
388
392
  key: entry.src + i,
389
393
  "data-index": i,
390
394
  type: "button",
391
- class: normalizeClass(["size-14 shrink-0 overflow-hidden rounded-control outline-none ring-2 transition-[opacity,box-shadow] duration-[var(--duration-fast)] focus-visible:ring-ring", i === current.value ? "opacity-100 ring-brand" : "opacity-45 ring-transparent hover:opacity-80"]),
395
+ style: normalizeStyle({ "--thumb-delay": `${Math.min(i * 30, 240)}ms` }),
396
+ class: normalizeClass(["size-14 shrink-0 overflow-hidden rounded-control outline-none ring-2 transition-[opacity,box-shadow,scale] duration-[var(--duration-fast)] hover:scale-105 focus-visible:ring-ring animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_both] [animation-delay:var(--thumb-delay)] motion-reduce:animate-none", i === current.value ? "opacity-100 ring-brand" : "opacity-45 ring-transparent hover:opacity-80"]),
392
397
  "aria-label": entry.title ?? `Image ${i + 1}`,
393
398
  onClick: ($event) => go(i)
394
399
  }, [
@@ -398,7 +403,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
398
403
  class: "size-full object-cover",
399
404
  draggable: "false"
400
405
  }, null, 8, _hoisted_14)
401
- ], 10, _hoisted_13);
406
+ ], 14, _hoisted_13);
402
407
  }), 128))
403
408
  ], 512)) : createCommentVNode("", true)
404
409
  ])
@@ -1,4 +1,4 @@
1
- import { _ as _sfc_main } from "../chunks/LpLightbox-BYIA9qne.js";
1
+ import { _ as _sfc_main } from "../chunks/LpLightbox-CWdIsthW.js";
2
2
  export {
3
3
  _sfc_main as default
4
4
  };
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import { _ as _22 } from "./chunks/LpFormField-Bn-ZwC6z.js";
25
25
  import { _ as _23 } from "./chunks/LpIcon-CCnX5_2j.js";
26
26
  import { _ as _24 } from "./chunks/LpInfraNode-C_PHcthC.js";
27
27
  import { _ as _25 } from "./chunks/LpInput-BauZRyzm.js";
28
- import { _ as _26 } from "./chunks/LpLightbox-BYIA9qne.js";
28
+ import { _ as _26 } from "./chunks/LpLightbox-CWdIsthW.js";
29
29
  import { fileNameOf } from "./components/lightbox.js";
30
30
  import { _ as _27 } from "./chunks/LpLink-DHwOEp4e.js";
31
31
  import { _ as _28 } from "./chunks/LpLogViewer-BvzRZPjs.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.15.5",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -94,12 +94,19 @@ const stripEl = ref<HTMLElement | null>(null)
94
94
 
95
95
  watch(viewport, (el) => zoom.setViewport(el))
96
96
 
97
+ // Which way the last navigation went, so the incoming frame slides in from the
98
+ // side it travelled from instead of always drifting the same direction.
99
+ const direction = ref<1 | -1>(1)
100
+
97
101
  function go(next: number) {
98
102
  if (!total.value) return
99
103
  const wrapped = props.loop
100
104
  ? (next + total.value) % total.value
101
105
  : Math.min(Math.max(next, 0), total.value - 1)
102
106
  if (wrapped === current.value) return
107
+ // Compare against the raw target, not the wrapped one: stepping past the last
108
+ // image is still "forward" even though the index jumps back to 0.
109
+ direction.value = next > current.value ? 1 : -1
103
110
  emit("update:index", wrapped)
104
111
  emit("change", wrapped, props.items[wrapped])
105
112
  }
@@ -250,6 +257,8 @@ async function reencodeToPng(bitmap: ImageBitmap): Promise<Blob> {
250
257
  const imageStyle = computed(() => ({
251
258
  ...zoom.style.value,
252
259
  rotate: `${rotation.value}deg`,
260
+ // Consumed by the lightbox-frame-in keyframe; negative when paging backwards.
261
+ "--frame-from": `${direction.value * 24}px`,
253
262
  }))
254
263
 
255
264
  const BTN =
@@ -265,7 +274,7 @@ const ARROW =
265
274
  class="fixed inset-0 z-(--z-overlay) bg-black/85 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
266
275
  />
267
276
  <DialogContent
268
- class="fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
277
+ class="fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
269
278
  @keydown="onKeydown"
270
279
  >
271
280
  <!-- reka wants both for a labelled dialog; the caption is the title. -->
@@ -274,8 +283,11 @@ const ARROW =
274
283
  {{ item?.description ?? `Image ${current + 1} of ${total}` }}
275
284
  </DialogDescription>
276
285
 
277
- <!-- Toolbar -->
278
- <div class="flex shrink-0 items-center gap-1 p-3 text-sm text-white/70">
286
+ <!-- Toolbar. Chrome drifts in from its own edge, slightly behind the
287
+ image, so the picture is what arrives first. -->
288
+ <div
289
+ class="flex shrink-0 items-center gap-1 p-3 text-sm text-white/70 [--chrome-from:-8px] animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_60ms_both] motion-reduce:animate-none"
290
+ >
279
291
  <span v-if="total > 1" class="px-2 tabular-nums">{{ current + 1 }} / {{ total }}</span>
280
292
  <span v-if="isZoomed" class="px-2 tabular-nums">
281
293
  {{ Math.round(scale * 100) }}%
@@ -347,13 +359,16 @@ const ARROW =
347
359
  <LpIcon name="lucide:image-off" :size="40" />
348
360
  <p class="text-sm">This image couldn't be loaded.</p>
349
361
  </div>
362
+ <!-- Keyed on src so paging remounts the img and replays the slide;
363
+ without the key Vue patches it in place and the change is a hard
364
+ cut with no sense of direction. -->
350
365
  <img
351
366
  v-else-if="item"
352
367
  :key="item.src"
353
368
  data-lightbox-image
354
369
  :src="item.src"
355
370
  :alt="item.title ?? ''"
356
- class="max-h-full max-w-full select-none object-contain transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] motion-reduce:transition-none"
371
+ class="max-h-full max-w-full select-none object-contain transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] animate-[lightbox-frame-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none motion-reduce:transition-none"
357
372
  :style="imageStyle"
358
373
  draggable="false"
359
374
  @load="loading = false"
@@ -379,7 +394,9 @@ const ARROW =
379
394
  </div>
380
395
 
381
396
  <!-- Caption + filmstrip -->
382
- <div class="shrink-0 p-3 pt-2">
397
+ <div
398
+ class="shrink-0 p-3 pt-2 animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_60ms_both] motion-reduce:animate-none"
399
+ >
383
400
  <slot name="caption" :item="item!" :index="current" :total="total">
384
401
  <div v-if="item?.title || item?.description" class="mb-2 text-center">
385
402
  <p v-if="item.title" class="truncate text-sm font-medium text-white">
@@ -401,7 +418,8 @@ const ARROW =
401
418
  :key="entry.src + i"
402
419
  :data-index="i"
403
420
  type="button"
404
- class="size-14 shrink-0 overflow-hidden rounded-control outline-none ring-2 transition-[opacity,box-shadow] duration-[var(--duration-fast)] focus-visible:ring-ring"
421
+ :style="{ '--thumb-delay': `${Math.min(i * 30, 240)}ms` }"
422
+ class="size-14 shrink-0 overflow-hidden rounded-control outline-none ring-2 transition-[opacity,box-shadow,scale] duration-[var(--duration-fast)] hover:scale-105 focus-visible:ring-ring animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_both] [animation-delay:var(--thumb-delay)] motion-reduce:animate-none"
405
423
  :class="i === current ? 'opacity-100 ring-brand' : 'opacity-45 ring-transparent hover:opacity-80'"
406
424
  :aria-label="entry.title ?? `Image ${i + 1}`"
407
425
  @click="go(i)"