feat(grid): stop bars at the last symbol and shade today's column

- bar line begins/ends at a glyph centre — no stub past the first/last symbol
- glyph halos so the bar passes behind, not through, the symbols
- today's week is a shaded column instead of a vertical rule
- widen week columns so date labels stop colliding
- drop "slip" wording from the legend
This commit is contained in:
Julien Calixte
2026-06-17 01:01:35 +02:00
parent 7cd6a1f98e
commit d8ce9a052c

View File

@@ -78,7 +78,9 @@ function markerAt(row: FeatureRow, w: WeekId): MarkerKind | null {
} }
interface Cell { interface Cell {
inBar: boolean // how far the bar line runs within this cell: none, half-from-center-right,
// half-from-left-to-center, or full width
line: 'none' | 'right' | 'left' | 'full'
isStart: boolean isStart: boolean
glyph: string glyph: string
glyphCls: string glyphCls: string
@@ -90,8 +92,16 @@ const matrix = computed<Cell[][]>(() =>
weeks.value.map((w) => { weeks.value.map((w) => {
const m = markerAt(row, w) const m = markerAt(row, w)
const inBar = w >= row.startWeek && w <= row.barEndWeek const inBar = w >= row.startWeek && w <= row.barEndWeek
let line: Cell['line'] = 'none'
if (inBar) {
const isStart = w === row.startWeek
const isEnd = w === row.barEndWeek
// line runs center→edge so it begins and ends at a glyph centre,
// never stubbing past the first/last symbol
line = isStart && isEnd ? 'none' : isStart ? 'right' : isEnd ? 'left' : 'full'
}
return { return {
inBar, line,
isStart: inBar && w === row.startWeek, isStart: inBar && w === row.startWeek,
glyph: m ? GLYPH[m] : '', glyph: m ? GLYPH[m] : '',
glyphCls: m ? MARKER_CLASS[m] : '', glyphCls: m ? MARKER_CLASS[m] : '',
@@ -157,7 +167,7 @@ function milestoneTitle(ms: Plan['milestones']): string {
class="cell" class="cell"
:class="colClass(ci)" :class="colClass(ci)"
> >
<i v-if="cell.inBar" class="bar" :class="TONE_TEXT[tone(row)]"></i> <i v-if="cell.line !== 'none'" class="bar" :class="[cell.line, TONE_TEXT[tone(row)]]"></i>
<i v-if="cell.isStart" class="riser" :class="TONE_TEXT[tone(row)]"></i> <i v-if="cell.isStart" class="riser" :class="TONE_TEXT[tone(row)]"></i>
<span v-if="cell.glyph" class="glyph" :class="cell.glyphCls">{{ cell.glyph }}</span> <span v-if="cell.glyph" class="glyph" :class="cell.glyphCls">{{ cell.glyph }}</span>
</div> </div>
@@ -178,18 +188,18 @@ function milestoneTitle(ms: Plan['milestones']): string {
<!-- legend --> <!-- legend -->
<div class="legend"> <div class="legend">
<span><b class="text-base-content/50"></b> original estimate</span> <span><b class="text-base-content/50"></b> original estimate</span>
<span><b class="text-warning"></b> re-estimate (slip)</span> <span><b class="text-warning"></b> re-estimate</span>
<span><b class="text-success"></b> on time</span> <span><b class="text-success"></b> on time</span>
<span><b class="text-error"></b> late</span> <span><b class="text-error"></b> late</span>
<span><b></b> feature bar</span> <span><b></b> feature bar</span>
<span><b class="text-error"></b> milestone</span> <span><b class="text-error"></b> milestone</span>
<span>vertical rule = now</span> <span><b class="now-swatch"></b> today</span>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.plan-grid { .plan-grid {
--wk: 2.5rem; --wk: 3.5rem;
display: grid; display: grid;
width: max-content; width: max-content;
min-width: 100%; min-width: 100%;
@@ -219,6 +229,7 @@ function milestoneTitle(ms: Plan['milestones']): string {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
line-height: 1.15; line-height: 1.15;
padding-inline: 0.15rem;
} }
.wklabel { .wklabel {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@@ -271,20 +282,31 @@ function milestoneTitle(ms: Plan['milestones']): string {
} }
.bar { .bar {
position: absolute; position: absolute;
left: 0;
right: 0;
top: 50%; top: 50%;
height: 2px; height: 2px;
transform: translateY(-50%); transform: translateY(-50%);
background: currentColor; background: currentColor;
opacity: 0.55; opacity: 0.55;
} }
.bar.full {
left: 0;
right: 0;
}
.bar.left {
left: 0;
right: 50%;
} /* enters from the left, stops at the glyph */
.bar.right {
left: 50%;
right: 0;
} /* starts at the first glyph, runs right */
.riser { .riser {
position: absolute; position: absolute;
left: 0; left: 50%;
top: 28%; top: 28%;
bottom: 28%; bottom: 28%;
width: 2px; width: 2px;
transform: translateX(-50%);
background: currentColor; background: currentColor;
opacity: 0.55; opacity: 0.55;
} }
@@ -293,6 +315,10 @@ function milestoneTitle(ms: Plan['milestones']): string {
z-index: 1; z-index: 1;
font-size: 0.95rem; font-size: 0.95rem;
line-height: 1; line-height: 1;
padding: 0 0.18rem;
/* halo so the bar reads as passing behind, not through, the symbol */
background: var(--color-base-100);
border-radius: 0.3rem;
} }
.learncell { .learncell {
@@ -320,8 +346,12 @@ function milestoneTitle(ms: Plan['milestones']): string {
flex: none; flex: none;
} }
.col-now { .cell.col-now {
border-left: 2px solid var(--color-primary); background: color-mix(in oklch, var(--color-primary) 9%, var(--color-base-100));
}
.cell.col-now .glyph {
/* match the column tint so the halo doesn't punch a hole in it */
background: color-mix(in oklch, var(--color-primary) 9%, var(--color-base-100));
} }
.col-ms { .col-ms {
border-left: 2px dashed color-mix(in oklch, var(--color-base-content) 35%, transparent); border-left: 2px dashed color-mix(in oklch, var(--color-base-content) 35%, transparent);
@@ -338,4 +368,13 @@ function milestoneTitle(ms: Plan['milestones']): string {
.legend b { .legend b {
font-weight: 700; font-weight: 700;
} }
.now-swatch {
display: inline-block;
width: 0.8rem;
height: 0.8rem;
vertical-align: -0.1rem;
border-radius: 0.15rem;
background: color-mix(in oklch, var(--color-primary) 9%, var(--color-base-100));
border: 1px solid color-mix(in oklch, var(--color-primary) 30%, var(--color-base-100));
}
</style> </style>