mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Frontend: Highlight matching face marker on sidebar hover #4966
Hovering a person row in the lightbox sidebar's People section now sets a thicker purple stroke + glow on the matching face-marker rect on the image, making the visual link between the row and the box unambiguous in both display and edit modes. Implementation routes through a new hoveredMarkerUid field on the $faceMarkers singleton that the lightbox forwards as a prop to the overlay. Reverse direction (rect hover → sidebar row) isn't wired since rects carry pointer-events: none — recorded for future work in the singleton + CSS comments. Also drops two redundant text-align: start declarations on .meta-title / .meta-caption (defaults already match).
This commit is contained in:
parent
30e41c12d5
commit
15dabc5dcd
6 changed files with 65 additions and 3 deletions
|
|
@ -55,6 +55,7 @@ export class FaceMarkers {
|
|||
this.mode = null;
|
||||
this.busy = false;
|
||||
this.pendingNameMarkerUid = "";
|
||||
this.hoveredMarkerUid = "";
|
||||
}
|
||||
|
||||
// active reports whether any face-marker mode (display or draw) is on.
|
||||
|
|
@ -110,6 +111,18 @@ export class FaceMarkers {
|
|||
this.pendingNameMarkerUid = typeof uid === "string" ? uid : "";
|
||||
}
|
||||
|
||||
// setHoveredMarkerUid records the UID of the marker currently being
|
||||
// hovered. Drives the sidebar → image highlight: sidebar people
|
||||
// rows set it on @mouseenter (and clear it on @mouseleave) so the
|
||||
// matching overlay rect picks up the `--hovered` modifier class.
|
||||
// The reverse direction (rect → sidebar row) is not wired today
|
||||
// because rects carry `pointer-events: none`; a future
|
||||
// implementation would set this from a parent-level pointermove
|
||||
// hit-test on the overlay.
|
||||
setHoveredMarkerUid(uid) {
|
||||
this.hoveredMarkerUid = typeof uid === "string" ? uid : "";
|
||||
}
|
||||
|
||||
// reset returns every field to its default. Called when the lightbox
|
||||
// closes or the user navigates to a different photo so a fresh slide
|
||||
// never inherits stale state.
|
||||
|
|
@ -117,6 +130,7 @@ export class FaceMarkers {
|
|||
this.mode = null;
|
||||
this.busy = false;
|
||||
this.pendingNameMarkerUid = "";
|
||||
this.hoveredMarkerUid = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
:markers="markers"
|
||||
:pswp="pswp()"
|
||||
:busy="faceMarkers.busy"
|
||||
:hovered-uid="faceMarkers.hoveredMarkerUid"
|
||||
@create="onCreateFaceMarker"
|
||||
@cancel="exitFaceMarkerMode"
|
||||
@remove="onRemoveFaceMarker"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
:class="{
|
||||
'p-face-markers__rect--named': !!m.Name,
|
||||
'p-face-markers__rect--removing': removingMarker && removingMarker.UID === m.UID,
|
||||
'p-face-markers__rect--hovered': hoveredUid && hoveredUid === m.UID,
|
||||
}"
|
||||
:x="m.X * bounds.width"
|
||||
:y="m.Y * bounds.height"
|
||||
|
|
@ -138,6 +139,14 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// hoveredUid is the UID of the marker that should render with the
|
||||
// `--hovered` highlight (thicker, accent-colored stroke). Forwarded
|
||||
// from `$faceMarkers.hoveredMarkerUid` by the lightbox so sidebar
|
||||
// people-row hover and direct rect hover stay in sync.
|
||||
hoveredUid: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["create", "cancel", "remove"],
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,14 @@
|
|||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-for="m in people" :key="m.UID || m.CropID" :data-marker-uid="m.UID" class="metadata__item metadata__person-row">
|
||||
<v-list-item
|
||||
v-for="m in people"
|
||||
:key="m.UID || m.CropID"
|
||||
:data-marker-uid="m.UID"
|
||||
class="metadata__item metadata__person-row"
|
||||
@mouseenter="faceMarkers.active && faceMarkers.setHoveredMarkerUid(m.UID)"
|
||||
@mouseleave="faceMarkers.active && faceMarkers.setHoveredMarkerUid('')"
|
||||
>
|
||||
<template #prepend>
|
||||
<img
|
||||
:src="m.thumbnailUrl('tile_160')"
|
||||
|
|
|
|||
|
|
@ -121,6 +121,24 @@
|
|||
stroke-width: 2.5px;
|
||||
}
|
||||
|
||||
/* Hover highlight driven by sidebar People-row hover: when the user
|
||||
hovers a person row in the sidebar, the matching rect picks up a
|
||||
thicker accent stroke + soft glow so the visual link between the
|
||||
image and the People list is unambiguous. Listed AFTER --named so
|
||||
it overrides the named-marker stroke color too. The drop-shadow is
|
||||
non-scaling-stroke-aware (it keys off the rendered stroke, not the
|
||||
SVG userspace), so it stays consistent across PhotoSwipe zoom.
|
||||
Reverse direction (rect → sidebar row) is not wired today: the
|
||||
rect carries `pointer-events: none` so native @mouseenter never
|
||||
fires; a future implementation would need parent-level pointermove
|
||||
+ hit-testing against marker bounds. */
|
||||
.p-face-markers__rect--hovered {
|
||||
stroke: rgba(196, 167, 255, 1);
|
||||
stroke-width: 4px;
|
||||
fill: rgba(167, 139, 250, 0);
|
||||
filter: drop-shadow(0 0 4px rgba(196, 167, 255, 0.7));
|
||||
}
|
||||
|
||||
.p-face-markers__handle {
|
||||
fill: rgba(167, 139, 250, 1);
|
||||
stroke: rgba(70, 40, 140, 0.95);
|
||||
|
|
@ -270,7 +288,6 @@
|
|||
font-weight: 700;
|
||||
word-wrap: normal;
|
||||
word-break: break-word;
|
||||
text-align: start;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +297,6 @@
|
|||
overflow-wrap: normal;
|
||||
word-wrap: normal;
|
||||
word-break: break-word;
|
||||
text-align: start;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ describe("common/face-markers", () => {
|
|||
expect(fm.mode).toBeNull();
|
||||
expect(fm.busy).toBe(false);
|
||||
expect(fm.pendingNameMarkerUid).toBe("");
|
||||
expect(fm.hoveredMarkerUid).toBe("");
|
||||
expect(fm.active).toBe(false);
|
||||
expect(fm.isDisplay).toBe(false);
|
||||
expect(fm.isEdit).toBe(false);
|
||||
|
|
@ -68,15 +69,29 @@ describe("common/face-markers", () => {
|
|||
expect(fm.pendingNameMarkerUid).toBe("");
|
||||
});
|
||||
|
||||
it("setHoveredMarkerUid only accepts strings; non-strings clear it", () => {
|
||||
const fm = new FaceMarkers();
|
||||
fm.setHoveredMarkerUid("uid42");
|
||||
expect(fm.hoveredMarkerUid).toBe("uid42");
|
||||
fm.setHoveredMarkerUid("");
|
||||
expect(fm.hoveredMarkerUid).toBe("");
|
||||
fm.setHoveredMarkerUid(null);
|
||||
expect(fm.hoveredMarkerUid).toBe("");
|
||||
fm.setHoveredMarkerUid(123);
|
||||
expect(fm.hoveredMarkerUid).toBe("");
|
||||
});
|
||||
|
||||
it("reset returns every field to its default", () => {
|
||||
const fm = new FaceMarkers();
|
||||
fm.setMode(FaceMarkerEdit);
|
||||
fm.setBusy(true);
|
||||
fm.setPendingNameMarkerUid("uid1");
|
||||
fm.setHoveredMarkerUid("uid2");
|
||||
fm.reset();
|
||||
expect(fm.mode).toBeNull();
|
||||
expect(fm.busy).toBe(false);
|
||||
expect(fm.pendingNameMarkerUid).toBe("");
|
||||
expect(fm.hoveredMarkerUid).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue