|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
<template>
|
|
|
|
|
<span class="fc-tag-chip" @mouseenter="onEnter" @mouseleave="onLeave">
|
|
|
|
|
<v-chip
|
|
|
|
|
size="small" closable
|
|
|
|
|
size="small" :closable="!unconfirmedAuto"
|
|
|
|
|
:color="store.colorFor(tag.kind)" variant="tonal"
|
|
|
|
|
class="fc-tag-chip__nav"
|
|
|
|
|
role="link"
|
|
|
|
@@ -16,20 +16,26 @@
|
|
|
|
|
>mdi-shield-outline</v-icon><span
|
|
|
|
|
v-if="tag.fandom_id" class="fc-tag-chip__fandom"
|
|
|
|
|
:title="tag.fandom_name ? `Fandom: ${tag.fandom_name}` : 'Has a fandom'"
|
|
|
|
|
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span><span
|
|
|
|
|
v-if="unconfirmedAuto" class="fc-tag-chip__auto"
|
|
|
|
|
title="Auto-applied — provisional: it won't train the model and can be retracted until you confirm it."
|
|
|
|
|
>auto</span>
|
|
|
|
|
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span>
|
|
|
|
|
<!-- Provisional auto-tag: an in-pill yes/no pair REPLACES the ✕ (the pair
|
|
|
|
|
itself signals "auto" — no separate label, operator 2026-07-07). Yes
|
|
|
|
|
confirms it (trains + shields from retraction); No removes it (records
|
|
|
|
|
a negative). Both hand focus back to the tag input via TagPanel. -->
|
|
|
|
|
<span v-if="unconfirmedAuto" class="fc-tag-chip__verdict">
|
|
|
|
|
<button
|
|
|
|
|
type="button" class="fc-tag-chip__yes"
|
|
|
|
|
:title="`Yes — keep “${tag.name}” (trains the model, won't be retracted)`"
|
|
|
|
|
:aria-label="`Confirm ${tag.name}`"
|
|
|
|
|
@click.stop="$emit('confirm', tag)"
|
|
|
|
|
><v-icon size="13">mdi-check</v-icon></button>
|
|
|
|
|
<button
|
|
|
|
|
type="button" class="fc-tag-chip__no"
|
|
|
|
|
:title="`No — remove “${tag.name}”`"
|
|
|
|
|
:aria-label="`Reject ${tag.name}`"
|
|
|
|
|
@click.stop="$emit('remove', tag.id)"
|
|
|
|
|
><v-icon size="13">mdi-close</v-icon></button>
|
|
|
|
|
</span>
|
|
|
|
|
</v-chip>
|
|
|
|
|
<!-- Keep/confirm an auto-applied tag: promotes it to a training positive and
|
|
|
|
|
shields it from the retraction sweep (milestone 139). Only shown for
|
|
|
|
|
provisional (unconfirmed) auto-tags. -->
|
|
|
|
|
<button
|
|
|
|
|
v-if="unconfirmedAuto" class="fc-tag-chip__confirm" type="button"
|
|
|
|
|
:title="`Keep “${tag.name}” — confirm this auto-tag so it trains the model and won't be retracted`"
|
|
|
|
|
:aria-label="`Confirm ${tag.name}`"
|
|
|
|
|
@click.stop="$emit('confirm', tag)"
|
|
|
|
|
><v-icon size="16">mdi-check</v-icon></button>
|
|
|
|
|
<!-- Modal-safe kebab is baked into KebabMenu (this chip lives in the
|
|
|
|
|
teleported image modal — #711). System tags hide it entirely: rename
|
|
|
|
|
is refused server-side (the hygiene machinery keys on the row) and
|
|
|
|
@@ -71,8 +77,8 @@ const store = useTagStore()
|
|
|
|
|
const api = useApi()
|
|
|
|
|
|
|
|
|
|
// An auto-applied tag the operator hasn't confirmed yet — provisional (milestone
|
|
|
|
|
// 139): it doesn't train the model and the retraction sweep can drop it. Shows
|
|
|
|
|
// the "auto" badge + a Keep/confirm button. `source`/`confirmed` come from the
|
|
|
|
|
// 139): it doesn't train the model and the retraction sweep can drop it. Shows an
|
|
|
|
|
// in-pill yes/no pair in place of the ✕. `source`/`confirmed` come from the
|
|
|
|
|
// applied-tags payload (list_for_image / get_image_with_tags).
|
|
|
|
|
const unconfirmedAuto = computed(() =>
|
|
|
|
|
AUTO_SOURCES.includes(props.tag.source) && !props.tag.confirmed
|
|
|
|
@@ -130,10 +136,11 @@ function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
/* A chip must never exceed the rail — a long character+fandom(+AUTO) chip used
|
|
|
|
|
to overflow the right edge and clip its close ✕ (operator-flagged 2026-07-06).
|
|
|
|
|
The name is the elastic part: it ellipsis-truncates so the ✕, fandom, and AUTO
|
|
|
|
|
badge stay reachable. Full name is still available via the chip's hover title. */
|
|
|
|
|
/* A chip must never exceed the rail — a long character+fandom chip used to
|
|
|
|
|
overflow the right edge and clip its trailing control (operator-flagged
|
|
|
|
|
2026-07-06). The name is the elastic part: it ellipsis-truncates so the ✕ (or
|
|
|
|
|
the auto-tag yes/no pair) and fandom stay reachable. Full name is on the
|
|
|
|
|
chip's hover title. */
|
|
|
|
|
.fc-tag-chip { display: inline-flex; align-items: center; gap: 1px; max-width: 100%; min-width: 0; }
|
|
|
|
|
.fc-tag-chip__nav { max-width: 100%; min-width: 0; }
|
|
|
|
|
.fc-tag-chip__nav :deep(.v-chip__content) { min-width: 0; overflow: hidden; }
|
|
|
|
@@ -152,28 +159,23 @@ function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
|
|
|
|
.fc-tag-chip__kebab { opacity: 0.7; }
|
|
|
|
|
.fc-tag-chip:hover .fc-tag-chip__kebab { opacity: 1; }
|
|
|
|
|
.fc-tag-chip__fandom { opacity: 0.7; font-size: 0.85em; }
|
|
|
|
|
/* "auto" = provisional (auto-applied, unconfirmed). Quiet pill inside the chip. */
|
|
|
|
|
.fc-tag-chip__auto {
|
|
|
|
|
display: inline-block; vertical-align: middle; margin-left: 4px;
|
|
|
|
|
font-size: 9px; font-weight: 700;
|
|
|
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
|
|
|
color: rgb(var(--v-theme-on-surface-variant));
|
|
|
|
|
background: rgb(var(--v-theme-surface-light));
|
|
|
|
|
border: 1px solid rgb(var(--v-theme-on-surface-variant), 0.3);
|
|
|
|
|
padding: 0 4px; border-radius: 999px;
|
|
|
|
|
/* Provisional auto-tag: a compact green ✓ / red ✗ pair in place of the ✕. The
|
|
|
|
|
yes/no is obvious enough on its own, so there's no "auto" label (operator-asked
|
|
|
|
|
2026-07-07). flex:0 0 auto keeps it visible while the name ellipsis-truncates. */
|
|
|
|
|
.fc-tag-chip__verdict {
|
|
|
|
|
flex: 0 0 auto; display: inline-flex; align-items: center; gap: 1px;
|
|
|
|
|
margin-left: 3px;
|
|
|
|
|
}
|
|
|
|
|
/* Keep/confirm — the SAME filled green ✓ circle as the suggestion accept button
|
|
|
|
|
(.fc-act--yes in SuggestionItem), so "accept this tag" reads identically
|
|
|
|
|
wherever it appears (operator-asked 2026-07-06). */
|
|
|
|
|
.fc-tag-chip__confirm {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
width: 26px; height: 26px; border-radius: 50%; border: none; cursor: pointer;
|
|
|
|
|
.fc-tag-chip__yes, .fc-tag-chip__no {
|
|
|
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
|
|
|
color: #fff; background: rgb(var(--v-theme-success));
|
|
|
|
|
opacity: 0.9; transition: transform 0.1s, opacity 0.1s;
|
|
|
|
|
width: 18px; height: 18px; padding: 0; border: none; border-radius: 50%;
|
|
|
|
|
background: transparent; cursor: pointer; transition: background 0.1s;
|
|
|
|
|
}
|
|
|
|
|
.fc-tag-chip__confirm:hover { opacity: 1; transform: scale(1.1); }
|
|
|
|
|
.fc-tag-chip__confirm:focus-visible {
|
|
|
|
|
.fc-tag-chip__yes { color: rgb(var(--v-theme-success)); }
|
|
|
|
|
.fc-tag-chip__no { color: rgb(var(--v-theme-error)); }
|
|
|
|
|
.fc-tag-chip__yes:hover { background: rgb(var(--v-theme-success), 0.16); }
|
|
|
|
|
.fc-tag-chip__no:hover { background: rgb(var(--v-theme-error), 0.16); }
|
|
|
|
|
.fc-tag-chip__yes:focus-visible, .fc-tag-chip__no:focus-visible {
|
|
|
|
|
outline: 2px solid rgb(var(--v-theme-accent)); outline-offset: 1px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|