fix(portal): review display, hover overlay, and drag ghost for complex field types

- Extract formatFieldValue helper for shared use between review step
  and confirmation page — one source of truth for TAG_PICKER,
  AVAILABILITY_PICKER, and SECTION_PRIORITY display, so the raw-ID
  and [object Object] leaks from two parallel stringifiers can't
  regress on either side.
- TAG_PICKER: lookup via field.available_tags (server-inlined).
- AVAILABILITY_PICKER: lookup via usePublicFormTimeSlots, strip
  seconds. "Laden…" while the cache warms.
- SECTION_PRIORITY: defensive shape-guard prevents [object Object]
  leaks, sorted priority-prefixed rendering ("1. Bar, 2. Hospitality").
- Subtle primary-tinted hover (4% primary, primary border) replacing
  the near-black Vuetify default overlay on unranked section cards.
- Explicit ghost-class / drag-class / chosen-class on vuedraggable
  with solid drag-clone + elevation shadow and a 30%-opacity silhouette
  at the origin, so mid-drag text no longer overlaps.
- 17 new formatFieldValue unit assertions + 2 new FieldSectionPriority
  assertions locking in the draggable classes and the disabled-card
  toggle at max.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 22:12:47 +02:00
parent 6f032a0311
commit e95f9a75f6
7 changed files with 421 additions and 24 deletions

View File

@@ -187,7 +187,10 @@ function sectionNameFor(id: string): string {
v-model="ranked"
item-key="section_id"
handle=".section-priority-handle"
:animation="180"
ghost-class="section-priority-ghost"
chosen-class="section-priority-chosen"
drag-class="section-priority-drag"
:animation="150"
:delay="100"
:delay-on-touch-only="true"
class="section-priority-ranked mb-4"
@@ -318,12 +321,13 @@ function sectionNameFor(id: string): string {
assistive tech, which Vuetify's :disabled would do. */
.section-priority-unranked-card {
cursor: pointer;
transition: background-color 120ms;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.section-priority-unranked-card:hover,
.section-priority-unranked-card:focus-visible {
background-color: rgb(var(--v-theme-surface-variant));
.section-priority-unranked-card:hover:not(.section-priority-unranked-disabled),
.section-priority-unranked-card:focus-visible:not(.section-priority-unranked-disabled) {
background-color: rgb(var(--v-theme-primary) / 0.04);
border-color: rgb(var(--v-theme-primary));
}
.section-priority-unranked-disabled {
@@ -331,13 +335,29 @@ function sectionNameFor(id: string): string {
opacity: 0.6;
}
.section-priority-unranked-disabled:hover,
.section-priority-unranked-disabled:focus-visible {
background-color: inherit;
}
.section-priority-rank {
min-inline-size: 1.5rem;
text-align: center;
}
/* vuedraggable drag states — SortableJS applies these classes. The
defaults leave the drag-clone semi-transparent while the ghost
stays solid at the origin, which produces text overlap mid-drag. */
.section-priority-ghost {
opacity: 0.3;
background: rgb(var(--v-theme-surface-bright));
}
.section-priority-drag {
/* !important overrides SortableJS's inline opacity: 0.8 default so
the drag-clone reads as solid + elevated. */
opacity: 1 !important;
background: rgb(var(--v-theme-surface));
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
cursor: grabbing;
}
.section-priority-chosen {
cursor: grabbing;
}
</style>