@wssto2/vue-core 0.4.24 → 0.4.25

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.25 — 2026-10-12
4
+
5
+ The photo viewer's actions on a phone. From ARV cutover g (wave 1, the vehicle gallery). Two items, one commit each.
6
+
7
+ ### Requires
8
+ - go-core unchanged: **`v1.7.0`**; **`vue` 3.5.19 or later**.
9
+
10
+ ### Added
11
+ - **`PhotoViewerAction`** (`@wssto2/vue-core/overlay`): the content of `PhotoViewer`'s `#actions` slot. `label`, `tone?: "critical"` (the destructive one, in the danger colour), `disabled?` (stays focusable, `aria-disabled`, emits nothing); emits `select`. A 44 px text button made for the viewer's dark surface, contrast-tested. ARV: `VehiclePhotoGallery.vue` replaces its hand-styled `<button>`s ("Postavi kao naslovnu", "Obriši") and their `text-xs` with two `PhotoViewerAction`s, `data-test` kept.
12
+
13
+ ### Changes to existing behaviour
14
+ - **On a phone the `#actions` move to a bottom toolbar.** Where `useCompactPresentation()` is true the slot renders once, in a centred toolbar (`data-test="photo-toolbar"`, safe-area aware) between the photo and the thumbnail strip; the header keeps the counter, rotate, download and close. On a wide screen nothing changes (the actions stay in the header). No `#actions`, no toolbar. ARV: the vehicle gallery's viewer (390 px: the counter is no longer cut to "2." and the actions no longer shrink).
15
+ - **The counter is never cut.** In the header only the title truncates; the counter stays whole.
16
+ - **Pinned by tests that were updated:** none.
17
+
3
18
  ## 0.4.24 — 2026-10-11
4
19
 
5
20
  A refusal in the server's words, a dialog that only shows, a new record with a start. From ARV cutover e8a (both lanes). Three items, one commit each.
@@ -0,0 +1,433 @@
1
+ import { defineComponent as Ne, useModel as Xe, ref as k, useTemplateRef as se, watch as Q, computed as g, onBeforeUnmount as Ve, nextTick as ye, onMounted as Ye, openBlock as h, createBlock as Ze, Teleport as je, createElementBlock as m, createVNode as C, Transition as Ee, withCtx as Ke, unref as s, createElementVNode as p, toDisplayString as ee, createCommentVNode as w, renderSlot as be, normalizeClass as M, withModifiers as re, normalizeStyle as te, Fragment as We, renderList as Ue, mergeModels as _e } from "vue";
2
+ import { useI18n as Fe } from "vue-i18n";
3
+ import { _ as I } from "./Icon.vue_vue_type_script_lang-D7oC-d-S.js";
4
+ import { u as He } from "./mediaQuery-CygcXmFW.js";
5
+ import { u as qe } from "./useDialog-CXR3hN0G.js";
6
+ const ke = [1, 1.5, 2, 3, 4, 5], Me = 1, ue = 5, Ge = (a) => Math.min(ue, Math.max(Me, a)), ze = (a, l) => l % 2 === 0 ? a : { width: a.height, height: a.width };
7
+ function Je(a, l, r) {
8
+ const i = ze(a, r);
9
+ if (i.width <= 0 || i.height <= 0 || l.width <= 0 || l.height <= 0) return { width: 0, height: 0 };
10
+ const o = Math.min(l.width / i.width, l.height / i.height);
11
+ return { width: a.width * o, height: a.height * o };
12
+ }
13
+ function Qe(a, l, r) {
14
+ return { x: Math.max(0, (a.width * r - l.width) / 2), y: Math.max(0, (a.height * r - l.height) / 2) };
15
+ }
16
+ function X(a, l, r, i) {
17
+ const o = Qe(l, r, i);
18
+ return { x: Math.min(o.x, Math.max(-o.x, a.x)) + 0, y: Math.min(o.y, Math.max(-o.y, a.y)) + 0 };
19
+ }
20
+ function et(a, l, r, i, o) {
21
+ const z = Ge(l), u = z / a.zoom, $ = { x: r.x - (r.x - a.pan.x) * u, y: r.y - (r.y - a.pan.y) * u };
22
+ return { zoom: z, pan: X($, i, o, z) };
23
+ }
24
+ function tt(a, l) {
25
+ return l === 1 ? ke.find((i) => i > a + 1e-3) ?? ue : [...ke].reverse().find((i) => i < a - 1e-3) ?? Me;
26
+ }
27
+ function at(a, l, r, i, o) {
28
+ const z = Math.min(o.width / a.width, o.height / a.height), u = { width: a.width * z, height: a.height * z }, $ = { width: a.width * r, height: a.height * r }, T = { width: Math.min(1, l.width / $.width), height: Math.min(1, l.height / $.height) }, O = { x: 0.5 - i.x / $.width, y: 0.5 - i.y / $.height };
29
+ return {
30
+ image: u,
31
+ view: { x: (O.x - T.width / 2) * u.width, y: (O.y - T.height / 2) * u.height, width: T.width * u.width, height: T.height * u.height }
32
+ };
33
+ }
34
+ function it(a, l, r, i) {
35
+ return X({ x: (0.5 - a.x) * l.width * i, y: (0.5 - a.y) * l.height * i }, l, r, i);
36
+ }
37
+ const ot = {
38
+ key: 0,
39
+ class: "relative z-9999"
40
+ }, nt = ["aria-label"], lt = {
41
+ "data-test": "photo-header",
42
+ class: "flex items-center gap-2 px-4 pt-[max(0.875rem,var(--app-safe-top))] pb-3.5 md:px-4.5"
43
+ }, st = { class: "flex min-w-0 text-headline font-semibold" }, rt = {
44
+ key: 0,
45
+ class: "truncate"
46
+ }, ut = {
47
+ key: 1,
48
+ class: "shrink-0 whitespace-pre"
49
+ }, ct = {
50
+ class: "shrink-0 whitespace-nowrap",
51
+ "aria-live": "polite",
52
+ "data-test": "photo-counter"
53
+ }, dt = ["aria-label", "aria-disabled"], ht = {
54
+ class: "w-13 text-center text-subheadline tabular-nums compact:hidden",
55
+ "data-test": "zoom-percent"
56
+ }, vt = ["aria-label", "aria-disabled"], mt = ["aria-label"], pt = ["href", "download", "aria-label"], ft = ["aria-label"], gt = { class: "flex min-h-0 grow items-center gap-4 px-4 md:px-4.5" }, wt = ["aria-label", "aria-disabled"], xt = ["src", "alt"], yt = ["src"], bt = ["aria-label", "aria-disabled"], _t = {
57
+ key: 0,
58
+ "data-test": "photo-toolbar",
59
+ class: "flex shrink-0 items-center justify-center gap-2 px-4 pt-3 pb-1"
60
+ }, kt = ["aria-label", "aria-current", "onClick"], Mt = ["src"], zt = {
61
+ key: 2,
62
+ class: "h-[max(1.25rem,var(--app-safe-bottom))] shrink-0"
63
+ }, $t = {
64
+ class: "sr-only",
65
+ "aria-live": "polite"
66
+ }, Pt = 8, Ct = 300, B = "flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-full bg-white/8 text-[#f1f5f3] transition-colors duration-motion-fast hover:bg-white/16 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6ee7b7] aria-disabled:cursor-default aria-disabled:opacity-30 aria-disabled:hover:bg-white/8", Bt = /* @__PURE__ */ Ne({
67
+ __name: "PhotoViewer",
68
+ props: /* @__PURE__ */ _e({
69
+ items: {},
70
+ title: { default: void 0 }
71
+ }, {
72
+ index: { default: 0 },
73
+ indexModifiers: {}
74
+ }),
75
+ emits: /* @__PURE__ */ _e(["presented", "dismissed"], ["update:index"]),
76
+ setup(a, { expose: l, emit: r }) {
77
+ const i = a, o = Xe(a, "index"), z = r, { t: u } = Fe(), $ = He(), T = k(null), O = se("stage"), $e = se("image"), ce = se("strip"), E = qe(T, { initialFocus: "panel", onPresented: () => z("presented"), onDismissed: () => z("dismissed") }), { isOpen: K } = E, ae = k(!1);
78
+ Q(K, (e) => {
79
+ e && (ae.value = !0);
80
+ }, { flush: "sync" });
81
+ const Pe = () => {
82
+ K.value || (ae.value = !1);
83
+ }, d = k(1), c = k({ x: 0, y: 0 }), P = k(0), W = k({ width: 0, height: 0 }), x = k({ width: 0, height: 0 }), b = k(0), ie = k(!1), V = k(null), v = g(() => i.items[o.value]), D = g(() => Je(W.value, x.value, P.value)), L = g(() => ze(D.value, P.value)), de = g(() => D.value.width > 0), Ce = g(() => Math.round(d.value * 100)), A = g(() => d.value > 1.001), he = g(() => o.value > 0), ve = g(() => o.value < i.items.length - 1), Te = g(() => ({
84
+ width: `${D.value.width}px`,
85
+ height: `${D.value.height}px`,
86
+ marginLeft: `${-D.value.width / 2}px`,
87
+ marginTop: `${-D.value.height / 2}px`,
88
+ transform: `translate(${c.value.x + b.value}px, ${c.value.y}px) scale(${d.value}) rotate(${P.value * 90}deg)`
89
+ }));
90
+ function U() {
91
+ d.value = 1, c.value = { x: 0, y: 0 }, b.value = 0;
92
+ }
93
+ function F() {
94
+ const e = O.value;
95
+ e && (x.value = { width: e.clientWidth, height: e.clientHeight }), oe();
96
+ }
97
+ function oe() {
98
+ const e = $e.value;
99
+ e && e.naturalWidth > 0 && (W.value = { width: e.naturalWidth, height: e.naturalHeight });
100
+ }
101
+ let H = null;
102
+ Q(O, (e) => {
103
+ H?.disconnect(), e && (F(), typeof ResizeObserver < "u" && (H = new ResizeObserver(() => {
104
+ F(), c.value = X(c.value, L.value, x.value, d.value);
105
+ }), H.observe(e)));
106
+ }), Ve(() => H?.disconnect()), Q(o, () => {
107
+ U(), P.value = 0, W.value = { width: 0, height: 0 }, ye(() => {
108
+ oe(), ce.value?.querySelector("[aria-current=true]")?.scrollIntoView?.({ inline: "center", block: "nearest" });
109
+ });
110
+ }), Q(() => i.items.length, (e) => {
111
+ e === 0 ? E.dismiss() : o.value > e - 1 && (o.value = e - 1);
112
+ });
113
+ function Oe(e = 0) {
114
+ o.value = Math.min(Math.max(0, e), Math.max(0, i.items.length - 1)), U(), P.value = 0, W.value = { width: 0, height: 0 }, V.value = null, E.present(), ye(F);
115
+ }
116
+ const me = () => E.dismiss();
117
+ l({ present: Oe, dismiss: me });
118
+ function Y(e) {
119
+ const t = o.value + e;
120
+ t < 0 || t > i.items.length - 1 || (V.value = e === 1 ? "forward" : "back", o.value = t);
121
+ }
122
+ function R(e) {
123
+ q(tt(d.value, e), { x: 0, y: 0 });
124
+ }
125
+ function q(e, t) {
126
+ const n = et({ zoom: d.value, pan: c.value }, e, t, L.value, x.value);
127
+ d.value = n.zoom, c.value = n.pan;
128
+ }
129
+ function ne() {
130
+ P.value = (P.value + 3) % 4, c.value = X(c.value, L.value, x.value, d.value);
131
+ }
132
+ function Le(e) {
133
+ if (e.altKey || e.ctrlKey || e.metaKey) return;
134
+ const n = {
135
+ ArrowLeft: () => Y(-1),
136
+ ArrowRight: () => Y(1),
137
+ "+": () => R(1),
138
+ "=": () => R(1),
139
+ "-": () => R(-1),
140
+ _: () => R(-1),
141
+ 0: U,
142
+ r: ne,
143
+ R: ne
144
+ }[e.key];
145
+ n && (e.preventDefault(), n());
146
+ }
147
+ const le = (e) => {
148
+ const t = O.value?.getBoundingClientRect();
149
+ return { x: e.clientX - (t ? t.left + t.width / 2 : 0), y: e.clientY - (t ? t.top + t.height / 2 : 0) };
150
+ }, _ = /* @__PURE__ */ new Map();
151
+ let Z = !1, pe = 0, N = { x: 0, y: 0 }, j = null;
152
+ const fe = () => {
153
+ const [e, t] = [..._.values()];
154
+ return { x: (e.x + t.x) / 2, y: (e.y + t.y) / 2 };
155
+ }, ge = () => {
156
+ const [e, t] = [..._.values()];
157
+ return Math.hypot(e.x - t.x, e.y - t.y);
158
+ };
159
+ function Se(e) {
160
+ e.pointerType === "mouse" && e.button !== 0 || (O.value?.setPointerCapture?.(e.pointerId), _.set(e.pointerId, { x: e.clientX, y: e.clientY }), ie.value = !0, _.size === 1 ? (Z = !1, pe = Date.now(), N = { x: 0, y: 0 }) : Z = !0);
161
+ }
162
+ function Ie(e) {
163
+ const t = _.get(e.pointerId);
164
+ if (!t) return;
165
+ const n = { x: e.clientX, y: e.clientY };
166
+ if (_.size === 2) {
167
+ const [G, J] = [ge(), fe()];
168
+ _.set(e.pointerId, n);
169
+ const S = fe();
170
+ q(d.value * (ge() / (G || 1)), le({ clientX: S.x, clientY: S.y })), c.value = X({ x: c.value.x + S.x - J.x, y: c.value.y + S.y - J.y }, L.value, x.value, d.value);
171
+ return;
172
+ }
173
+ _.set(e.pointerId, n);
174
+ const f = { x: n.x - t.x, y: n.y - t.y };
175
+ N = { x: N.x + f.x, y: N.y + f.y }, !(!Z && Math.hypot(N.x, N.y) < Pt) && (Z = !0, A.value ? c.value = X({ x: c.value.x + f.x, y: c.value.y + f.y }, L.value, x.value, d.value) : b.value = (b.value + f.x) * (b.value + f.x < 0 && !ve.value || b.value + f.x > 0 && !he.value ? 0.6 : 1));
176
+ }
177
+ function we(e, t = !1) {
178
+ if (!_.delete(e.pointerId) || _.size > 0) return;
179
+ ie.value = !1;
180
+ const n = le(e);
181
+ if (!Z && !t) {
182
+ const S = j && Date.now() - j.at < Ct && Math.hypot(n.x - j.point.x, n.y - j.point.y) < 32;
183
+ j = S ? null : { at: Date.now(), point: n }, S && (A.value ? U() : q(2, n));
184
+ return;
185
+ }
186
+ const f = Date.now() - pe < 250 && Math.abs(b.value) > 30, G = Math.abs(b.value) > Math.max(50, x.value.width * 0.15), J = b.value;
187
+ b.value = 0, !t && !A.value && (G || f) && Y(J < 0 ? 1 : -1);
188
+ }
189
+ function Be(e) {
190
+ const t = e.deltaMode === 1 ? 16 : 1;
191
+ q(d.value * Math.exp(-e.deltaY * t * (e.ctrlKey ? 0.01 : 15e-4)), le(e));
192
+ }
193
+ const De = { width: 120, height: 80 }, y = g(() => de.value ? at(L.value, x.value, d.value, c.value, De) : null);
194
+ function xe(e) {
195
+ if (e.type === "pointermove" && e.buttons === 0) return;
196
+ const t = e.currentTarget.getBoundingClientRect();
197
+ t.width === 0 || t.height === 0 || (c.value = it({ x: (e.clientX - t.left) / t.width, y: (e.clientY - t.top) / t.height }, L.value, x.value, d.value));
198
+ }
199
+ const Ae = g(() => {
200
+ if (!y.value) return {};
201
+ const { width: e, height: t } = P.value % 2 === 0 ? y.value.image : { width: y.value.image.height, height: y.value.image.width };
202
+ return { width: `${e}px`, height: `${t}px`, marginLeft: `${-e / 2}px`, marginTop: `${-t / 2}px`, transform: `rotate(${P.value * 90}deg)` };
203
+ });
204
+ Ye(() => {
205
+ K.value && F();
206
+ });
207
+ const Re = g(() => i.title ?? u("core.viewer.label"));
208
+ return (e, t) => (h(), Ze(je, { to: "body" }, [
209
+ ae.value ? (h(), m("div", ot, [
210
+ C(Ee, {
211
+ appear: "",
212
+ "enter-active-class": "transition-opacity duration-motion-normal ease-motion-standard",
213
+ "enter-from-class": "opacity-0",
214
+ "leave-active-class": "pointer-events-none transition-opacity duration-motion-normal ease-motion-standard",
215
+ "leave-to-class": "opacity-0",
216
+ onAfterLeave: Pe
217
+ }, {
218
+ default: Ke(() => [
219
+ s(K) ? (h(), m("div", {
220
+ key: 0,
221
+ ref_key: "panel",
222
+ ref: T,
223
+ role: "dialog",
224
+ "aria-modal": "true",
225
+ "aria-label": Re.value,
226
+ tabindex: "-1",
227
+ "data-part": "panel",
228
+ "data-test": "photo-viewer",
229
+ class: "fixed inset-0 flex flex-col bg-[#0b1210] text-[#f1f5f3] outline-none",
230
+ onKeydown: Le
231
+ }, [
232
+ p("div", lt, [
233
+ p("h2", st, [
234
+ i.title ? (h(), m("span", rt, ee(i.title), 1)) : w("", !0),
235
+ i.title ? (h(), m("span", ut, " · ")) : w("", !0),
236
+ p("span", ct, ee(s(u)("core.viewer.counter", { current: o.value + 1, total: i.items.length })), 1)
237
+ ]),
238
+ t[6] || (t[6] = p("div", { class: "grow" }, null, -1)),
239
+ v.value && !s($) ? be(e.$slots, "actions", {
240
+ item: v.value,
241
+ index: o.value
242
+ }, void 0, void 0, 0) : w("", !0),
243
+ p("button", {
244
+ type: "button",
245
+ class: M([B, "compact:hidden"]),
246
+ "aria-label": s(u)("core.viewer.zoom_out"),
247
+ "aria-disabled": !A.value,
248
+ "data-test": "zoom-out",
249
+ onClick: t[0] || (t[0] = (n) => R(-1))
250
+ }, [
251
+ C(s(I), {
252
+ name: "zoomOut",
253
+ size: 18
254
+ })
255
+ ], 10, dt),
256
+ p("p", ht, ee(s(u)("core.viewer.zoom_percent", { percent: Ce.value })), 1),
257
+ p("button", {
258
+ type: "button",
259
+ class: M([B, "compact:hidden"]),
260
+ "aria-label": s(u)("core.viewer.zoom_in"),
261
+ "aria-disabled": d.value >= s(ue),
262
+ "data-test": "zoom-in",
263
+ onClick: t[1] || (t[1] = (n) => R(1))
264
+ }, [
265
+ C(s(I), {
266
+ name: "zoomIn",
267
+ size: 18
268
+ })
269
+ ], 10, vt),
270
+ p("button", {
271
+ type: "button",
272
+ class: M(B),
273
+ "aria-label": s(u)("core.viewer.rotate"),
274
+ "data-test": "rotate",
275
+ onClick: ne
276
+ }, [
277
+ C(s(I), {
278
+ name: "rotateCounterclockwise",
279
+ size: 18
280
+ })
281
+ ], 8, mt),
282
+ v.value ? (h(), m("a", {
283
+ key: 1,
284
+ href: v.value.src,
285
+ download: v.value.downloadName ?? "",
286
+ target: "_blank",
287
+ rel: "noopener",
288
+ class: M(B),
289
+ "aria-label": s(u)("core.viewer.download"),
290
+ "data-test": "download"
291
+ }, [
292
+ C(s(I), {
293
+ name: "download",
294
+ size: 18
295
+ })
296
+ ], 8, pt)) : w("", !0),
297
+ p("button", {
298
+ type: "button",
299
+ class: M([B, "bg-white/16"]),
300
+ "aria-label": s(u)("core.actions.close"),
301
+ "data-test": "close",
302
+ onClick: me
303
+ }, [
304
+ C(s(I), {
305
+ name: "close",
306
+ size: 18
307
+ })
308
+ ], 10, ft)
309
+ ]),
310
+ p("div", gt, [
311
+ i.items.length > 1 ? (h(), m("button", {
312
+ key: 0,
313
+ type: "button",
314
+ class: M([B, "size-12 compact:hidden"]),
315
+ "aria-label": s(u)("core.viewer.previous"),
316
+ "aria-disabled": !he.value,
317
+ "data-test": "previous",
318
+ onClick: t[2] || (t[2] = (n) => Y(-1))
319
+ }, [
320
+ C(s(I), {
321
+ name: "arrowLeftSLine",
322
+ size: 22
323
+ })
324
+ ], 10, wt)) : w("", !0),
325
+ p("div", {
326
+ ref_key: "stage",
327
+ ref: x,
328
+ "data-test": "photo-stage",
329
+ class: M(["relative h-full min-w-0 grow touch-none overflow-hidden rounded-md select-none", A.value ? "cursor-grab active:cursor-grabbing" : ""]),
330
+ onPointerdown: Se,
331
+ onPointermove: Ie,
332
+ onPointerup: t[3] || (t[3] = (n) => we(n)),
333
+ onPointercancel: t[4] || (t[4] = (n) => we(n, !0)),
334
+ onWheel: re(Be, ["prevent"])
335
+ }, [
336
+ v.value ? (h(), m("img", {
337
+ key: v.value.src,
338
+ ref: "image",
339
+ src: v.value.src,
340
+ alt: v.value.alt,
341
+ draggable: "false",
342
+ "data-test": "photo",
343
+ class: M(["absolute top-1/2 left-1/2 max-w-none", [de.value ? "" : "opacity-0", ie.value ? "" : "transition-transform duration-motion-normal ease-motion-standard", V.value === "back" ? "animate-step-back" : V.value === "forward" ? "animate-step-in" : ""]]),
344
+ style: te(Te.value),
345
+ onLoad: oe
346
+ }, null, 46, xt)) : w("", !0),
347
+ A.value && y.value ? (h(), m("div", {
348
+ key: 1,
349
+ "data-test": "mini-map",
350
+ class: "absolute right-3.5 bottom-3.5 overflow-hidden rounded-md bg-black/50 ring-1 ring-white/20",
351
+ style: te({ width: `${y.value.image.width}px`, height: `${y.value.image.height}px` }),
352
+ onPointerdown: re(xe, ["stop"]),
353
+ onPointermove: re(xe, ["stop"])
354
+ }, [
355
+ v.value ? (h(), m("img", {
356
+ key: 0,
357
+ src: v.value.thumb ?? v.value.src,
358
+ alt: "",
359
+ draggable: "false",
360
+ class: "pointer-events-none absolute top-1/2 left-1/2 max-w-none opacity-80",
361
+ style: te(Ae.value)
362
+ }, null, 12, yt)) : w("", !0),
363
+ p("span", {
364
+ class: "pointer-events-none absolute rounded-xs ring-2 ring-[#6ee7b7]",
365
+ "data-test": "mini-map-view",
366
+ style: te({ left: `${y.value.view.x}px`, top: `${y.value.view.y}px`, width: `${y.value.view.width}px`, height: `${y.value.view.height}px` })
367
+ }, null, 4)
368
+ ], 36)) : w("", !0)
369
+ ], 34),
370
+ i.items.length > 1 ? (h(), m("button", {
371
+ key: 1,
372
+ type: "button",
373
+ class: M([B, "size-12 compact:hidden"]),
374
+ "aria-label": s(u)("core.viewer.next"),
375
+ "aria-disabled": !ve.value,
376
+ "data-test": "next",
377
+ onClick: t[5] || (t[5] = (n) => Y(1))
378
+ }, [
379
+ C(s(I), {
380
+ name: "arrowRightSLine",
381
+ size: 22
382
+ })
383
+ ], 10, bt)) : w("", !0)
384
+ ]),
385
+ s($) && v.value && e.$slots.actions ? (h(), m("div", _t, [
386
+ be(e.$slots, "actions", {
387
+ item: v.value,
388
+ index: o.value
389
+ })
390
+ ])) : w("", !0),
391
+ i.items.length > 1 ? (h(), m("ul", {
392
+ key: 1,
393
+ ref_key: "strip",
394
+ ref: ce,
395
+ "data-test": "photo-strip",
396
+ class: "flex shrink-0 justify-start gap-2 overflow-x-auto px-4 pt-4 pb-[max(1.25rem,var(--app-safe-bottom))] scrollbar-hide md:justify-center md:px-4.5"
397
+ }, [
398
+ (h(!0), m(We, null, Ue(i.items, (n, f) => (h(), m("li", {
399
+ key: n.src,
400
+ class: "shrink-0"
401
+ }, [
402
+ p("button", {
403
+ type: "button",
404
+ "aria-label": n.alt,
405
+ "aria-current": f === o.value ? "true" : void 0,
406
+ "data-test": "thumbnail",
407
+ class: M(["block h-11 w-16 cursor-pointer overflow-hidden rounded-[5px] bg-[#2a3431] transition-opacity duration-motion-fast focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6ee7b7]", f === o.value ? "ring-2 ring-[#6ee7b7]" : "opacity-60 hover:opacity-100"]),
408
+ onClick: (G) => {
409
+ V.value = f > o.value ? "forward" : "back", o.value = f;
410
+ }
411
+ }, [
412
+ p("img", {
413
+ src: n.thumb ?? n.src,
414
+ alt: "",
415
+ loading: "lazy",
416
+ draggable: "false",
417
+ class: "size-full object-cover"
418
+ }, null, 8, Mt)
419
+ ], 10, kt)
420
+ ]))), 128))
421
+ ], 512)) : (h(), m("div", zt)),
422
+ p("span", $t, ee(v.value?.alt), 1)
423
+ ], 40, nt)) : w("", !0)
424
+ ]),
425
+ _: 3
426
+ })
427
+ ])) : w("", !0)
428
+ ]));
429
+ }
430
+ });
431
+ export {
432
+ Bt as _
433
+ };
@@ -36,7 +36,7 @@ import { u as It, a as We } from "../SelectField.vue_vue_type_script_setup_true_
36
36
  import { _ as $n, b as _n } from "../SelectField.vue_vue_type_script_setup_true_lang-C3FbRQW7.js";
37
37
  import { g as Cn, m as Fn } from "../options-Ba_0uLWN.js";
38
38
  import { _ as Ln } from "../OtpInput.vue_vue_type_script_setup_true_lang-gp4FfWAH.js";
39
- import { _ as Je } from "../PhotoViewer.vue_vue_type_script_setup_true_lang-QPCW3Xi6.js";
39
+ import { _ as Je } from "../PhotoViewer.vue_vue_type_script_setup_true_lang-CY5EZiyK.js";
40
40
  import { _ as zn } from "../SegmentedField.vue_vue_type_script_setup_true_lang-Dg-_ZjgR.js";
41
41
  import { _ as Vn } from "../SwitchField.vue_vue_type_script_setup_true_lang-ALvJisiP.js";
42
42
  import { _ as Rn } from "../TextareaField.vue_vue_type_script_setup_true_lang-BJ4__cvJ.js";
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The version of @wssto2/vue-core (kept equal to package.json by index.test.ts). */
2
- export declare const version = "0.4.24";
2
+ export declare const version = "0.4.25";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const o = "0.4.24";
1
+ const o = "0.4.25";
2
2
  export {
3
3
  o as version
4
4
  };
@@ -15,8 +15,9 @@ export interface PhotoViewerItem {
15
15
  * and a strip of thumbnails. It is a dialog like `Modal`: the page behind is inert, Tab stays inside, Escape closes and focus goes back
16
16
  * to what opened it. Under reduced motion nothing animates.
17
17
  *
18
- * Keys: ← → photos, + − zoom, 0 fits the screen again, R rotates, Esc closes. `#actions` (given the photo on show) adds your own buttons to the header:
19
- * "Make cover", "Delete".
18
+ * Keys: ← → photos, + − zoom, 0 fits the screen again, R rotates, Esc closes. `#actions` (given the photo on show) adds your own buttons:
19
+ * "Make cover", "Delete". They sit in the header on a wide screen and in a toolbar under the photo on a phone (the header keeps the counter, rotate,
20
+ * download and close); the slot renders in one place at a time.
20
21
  *
21
22
  * <PhotoViewer ref="viewer" :items="photos" title="VW Golf 8" />
22
23
  * <button @click="viewer?.present(index)">…</button> viewer.value?.dismiss()
@@ -27,7 +28,7 @@ type __VLS_Props = {
27
28
  title?: string;
28
29
  };
29
30
  type __VLS_Slots = {
30
- /** Next to the zoom buttons: what the app does with the photo on show (make it the cover, delete it). */
31
+ /** What the app does with the photo on show (make it the cover, delete it): in the header next to the zoom buttons, in a bottom toolbar on a phone. */
31
32
  actions?: (scope: {
32
33
  item: PhotoViewerItem;
33
34
  index: number;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * One of the app's own actions in `PhotoViewer`'s `#actions` slot ("Make cover", "Delete"), styled for the viewer's dark surface in the
3
+ * header and in the phone toolbar (44 px tall). `tone="critical"` is the destructive one, in the danger colour. A disabled action stays
4
+ * focusable (`aria-disabled`), like the viewer's own buttons at the end of their range, so focus does not fall out of the dialog.
5
+ *
6
+ * <PhotoViewer :items="photos"><template #actions="{ index }">
7
+ * <PhotoViewerAction :label="t('makeCover')" @select="makeCover(index)" />
8
+ * <PhotoViewerAction :label="t('delete')" tone="critical" @select="remove(index)" />
9
+ * </template></PhotoViewer>
10
+ */
11
+ type __VLS_Props = {
12
+ label: string;
13
+ /** `critical` for a destructive action (Delete). */
14
+ tone?: "critical";
15
+ disabled?: boolean;
16
+ };
17
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
+ select: () => any;
19
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
20
+ onSelect?: (() => any) | undefined;
21
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
+ export default _default;
@@ -8,3 +8,4 @@ export { INERT_SKIP_ATTR, useOpenDialogCount } from "./dialogStack";
8
8
  export { INITIAL_FOCUS_SKIP_ATTR, useDialog, type DialogOptions } from "./useDialog";
9
9
  export { COMPACT_MEDIA_QUERY, useCompactPresentation } from "../internal/mediaQuery";
10
10
  export { default as PhotoViewer, type PhotoViewerItem } from "./PhotoViewer.vue";
11
+ export { default as PhotoViewerAction } from "./PhotoViewerAction.vue";
@@ -1,25 +1,49 @@
1
- import { _ as e } from "../AlertDialog.vue_vue_type_script_setup_true_lang-whA4WLdl.js";
2
- import { _ as a } from "../Menu.vue_vue_type_script_setup_true_lang-DsFOeCVe.js";
3
- import { _ as p } from "../Popover.vue_vue_type_script_setup_true_lang-C70GrCP7.js";
4
- import { _ as m } from "../Toaster.vue_vue_type_script_setup_true_lang-55boRLAj.js";
5
- import { _ as x } from "../Tooltip.vue_vue_type_script_setup_true_lang-mErNPt8e.js";
6
- import { t as T } from "../toast-CqSDTWRX.js";
7
- import { I as i, u as A } from "../dialogStack-DZZg55w0.js";
8
- import { I as P, u as l } from "../useDialog-CXR3hN0G.js";
9
- import { C as D, u as R } from "../mediaQuery-CygcXmFW.js";
10
- import { _ as E } from "../PhotoViewer.vue_vue_type_script_setup_true_lang-QPCW3Xi6.js";
1
+ import { _ as x } from "../AlertDialog.vue_vue_type_script_setup_true_lang-whA4WLdl.js";
2
+ import { _ as w } from "../Menu.vue_vue_type_script_setup_true_lang-DsFOeCVe.js";
3
+ import { _ as I } from "../Popover.vue_vue_type_script_setup_true_lang-C70GrCP7.js";
4
+ import { _ as v } from "../Toaster.vue_vue_type_script_setup_true_lang-55boRLAj.js";
5
+ import { _ as P } from "../Tooltip.vue_vue_type_script_setup_true_lang-mErNPt8e.js";
6
+ import { t as y } from "../toast-CqSDTWRX.js";
7
+ import { I as k, u as E } from "../dialogStack-DZZg55w0.js";
8
+ import { I as S, u as B } from "../useDialog-CXR3hN0G.js";
9
+ import { C as O, u as V } from "../mediaQuery-CygcXmFW.js";
10
+ import { _ as N } from "../PhotoViewer.vue_vue_type_script_setup_true_lang-CY5EZiyK.js";
11
+ import { defineComponent as s, openBlock as r, createElementBlock as n, normalizeClass as l, toDisplayString as c } from "vue";
12
+ const p = ["aria-disabled"], m = /* @__PURE__ */ s({
13
+ __name: "PhotoViewerAction",
14
+ props: {
15
+ label: {},
16
+ tone: {},
17
+ disabled: { type: Boolean }
18
+ },
19
+ emits: ["select"],
20
+ setup(o, { emit: t }) {
21
+ const e = o, i = t;
22
+ function a() {
23
+ e.disabled || i("select");
24
+ }
25
+ return (f, u) => (r(), n("button", {
26
+ type: "button",
27
+ "aria-disabled": e.disabled || void 0,
28
+ "data-part": "photo-viewer-action",
29
+ class: l(["flex min-h-11 shrink-0 cursor-pointer items-center justify-center rounded-full bg-white/8 px-4 text-subheadline font-medium whitespace-nowrap transition-colors duration-motion-fast hover:bg-white/16 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#6ee7b7] aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:bg-white/8", e.tone === "critical" ? "text-[#ff8a80]" : "text-[#f1f5f3]"]),
30
+ onClick: a
31
+ }, c(e.label), 11, p));
32
+ }
33
+ });
11
34
  export {
12
- e as AlertDialog,
13
- D as COMPACT_MEDIA_QUERY,
14
- i as INERT_SKIP_ATTR,
15
- P as INITIAL_FOCUS_SKIP_ATTR,
16
- a as Menu,
17
- E as PhotoViewer,
18
- p as Popover,
19
- m as Toaster,
20
- x as Tooltip,
21
- T as toast,
22
- R as useCompactPresentation,
23
- l as useDialog,
24
- A as useOpenDialogCount
35
+ x as AlertDialog,
36
+ O as COMPACT_MEDIA_QUERY,
37
+ k as INERT_SKIP_ATTR,
38
+ S as INITIAL_FOCUS_SKIP_ATTR,
39
+ w as Menu,
40
+ N as PhotoViewer,
41
+ m as PhotoViewerAction,
42
+ I as Popover,
43
+ v as Toaster,
44
+ P as Tooltip,
45
+ y as toast,
46
+ V as useCompactPresentation,
47
+ B as useDialog,
48
+ E as useOpenDialogCount
25
49
  };