feat(dashboard): red-dot weak-point map on /defects (T7)
Overlay each filed defect as a red dot inside its section box with a diagonal date label; cap visible dots and collapse the remainder to a "+N more" using the authoritative per-section counts (T6). Add an `overlay` scoped slot to BoardView (C2) so the dashboard can layer dots without forking the board definition (F1); the reporting view leaves it empty. DotMap groups the bounded feed by section and the /defects page wires both endpoints.
This commit is contained in:
@@ -40,6 +40,7 @@ const hoveredId = ref<string | null>(null);
|
||||
>
|
||||
<span class="section__label">{{ section.label }}</span>
|
||||
<span class="section__size">{{ section.size }}</span>
|
||||
<slot name="overlay" :section-id="section.id" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,6 +105,8 @@ const hoveredId = ref<string | null>(null);
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
/* Positioning context for the dashboard's absolutely-placed dots (C5). */
|
||||
position: relative;
|
||||
/* ISO 216: every A-series sheet is landscape √2 : 1 (width : height). */
|
||||
aspect-ratio: 1.41421356 / 1;
|
||||
}
|
||||
|
||||
107
app/components/DotMap.vue
Normal file
107
app/components/DotMap.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
// The weak-point dot map (C5, F4): overlays each defect as a red dot inside its
|
||||
// Section box with a diagonal date label. Visible dots are capped (~8–12) and
|
||||
// the remainder collapses to a "+N more" marker, so a hot Section stays legible
|
||||
// (DESIGN §5, ADR T7 — dot positions are cosmetic; the Section is the pin).
|
||||
//
|
||||
// Reuses BoardView (C2) via its `overlay` slot, and the feed + per-section
|
||||
// counts from T6: `defects` are the newest-first feed slice (bounded), `counts`
|
||||
// the true per-section totals that drive the "+N more" arithmetic.
|
||||
interface DotDefect {
|
||||
id: string
|
||||
sectionId: string
|
||||
createdAt: string | Date
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
defects: DotDefect[]
|
||||
counts: Record<string, number>
|
||||
cap?: number
|
||||
}>()
|
||||
|
||||
const cap = computed(() => props.cap ?? 10)
|
||||
|
||||
// Group the feed by section, preserving its newest-first order.
|
||||
const bySection = computed(() => {
|
||||
const map: Record<string, DotDefect[]> = {}
|
||||
for (const d of props.defects) (map[d.sectionId] ??= []).push(d)
|
||||
return map
|
||||
})
|
||||
|
||||
const dotsFor = (id: string) => (bySection.value[id] ?? []).slice(0, cap.value)
|
||||
|
||||
// Hidden defects = true total (counts, authoritative) minus what we drew.
|
||||
function overflowFor(id: string): number {
|
||||
const total = props.counts[id] ?? bySection.value[id]?.length ?? 0
|
||||
return Math.max(0, total - dotsFor(id).length)
|
||||
}
|
||||
|
||||
// Compact, locale-free date stamp (DD/MM, UTC) for the diagonal label.
|
||||
function formatDate(value: string | Date): string {
|
||||
const d = new Date(value)
|
||||
const dd = String(d.getUTCDate()).padStart(2, '0')
|
||||
const mm = String(d.getUTCMonth() + 1).padStart(2, '0')
|
||||
return `${dd}/${mm}`
|
||||
}
|
||||
|
||||
// Cosmetic scatter: a stable 5-wide grid inside the box (ADR T7).
|
||||
function dotStyle(index: number) {
|
||||
return {
|
||||
left: `${12 + (index % 5) * 17}%`,
|
||||
top: `${20 + Math.floor(index / 5) * 22}%`,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BoardView>
|
||||
<template #overlay="{ sectionId }">
|
||||
<span
|
||||
v-for="(defect, index) in dotsFor(sectionId)"
|
||||
:key="defect.id"
|
||||
class="dot"
|
||||
:style="dotStyle(index)"
|
||||
data-test="dot"
|
||||
>
|
||||
<span class="dot__date">{{ formatDate(defect.createdAt) }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="overflowFor(sectionId) > 0" class="dot-more" data-test="more">
|
||||
+{{ overflowFor(sectionId) }} more
|
||||
</span>
|
||||
</template>
|
||||
</BoardView>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
border-radius: 50%;
|
||||
background: #e11d48;
|
||||
/* Centre the dot on its grid point so labels splay outward consistently. */
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.dot__date {
|
||||
position: absolute;
|
||||
left: 0.7rem;
|
||||
bottom: 0.7rem;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
/* Diagonal label, as on the physical board. */
|
||||
transform: rotate(-45deg);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.dot-more {
|
||||
position: absolute;
|
||||
right: 0.4rem;
|
||||
bottom: 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #e11d48;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user