refactor(documents): extract Y-axis and X-axis components (#385)
Felix's review named "TimelineAxes" as one of four split targets. The Y-axis and X-axis don't sit adjacent in the DOM — Y is a flex sibling of the bars+X column — so two single-purpose components beats a discriminator-prop component. tickIndicesFor and the omitTickYear derivation move to TimelineXAxis where they belong. Closes part 2 of Felix's component-split concern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,12 @@ import {
|
|||||||
clipBucketsToRange,
|
clipBucketsToRange,
|
||||||
selectionBoundaryFrom,
|
selectionBoundaryFrom,
|
||||||
selectionBoundaryTo,
|
selectionBoundaryTo,
|
||||||
tickIndicesFor,
|
|
||||||
formatTickLabel
|
formatTickLabel
|
||||||
} from '$lib/document/timeline';
|
} from '$lib/document/timeline';
|
||||||
import { getLocale } from '$lib/paraglide/runtime';
|
import { getLocale } from '$lib/paraglide/runtime';
|
||||||
import TimelineBars from '$lib/document/TimelineBars.svelte';
|
import TimelineBars from '$lib/document/TimelineBars.svelte';
|
||||||
|
import TimelineYAxis from '$lib/document/TimelineYAxis.svelte';
|
||||||
|
import TimelineXAxis from '$lib/document/TimelineXAxis.svelte';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
type MonthBucket = components['schemas']['MonthBucket'];
|
type MonthBucket = components['schemas']['MonthBucket'];
|
||||||
@@ -219,8 +220,6 @@ const dragWindowRightPct = $derived.by(() => {
|
|||||||
return ((filled.length - dragHighIndex - 1) / filled.length) * 100;
|
return ((filled.length - dragHighIndex - 1) / filled.length) * 100;
|
||||||
});
|
});
|
||||||
|
|
||||||
const tickIndices = $derived(tickIndicesFor(filled));
|
|
||||||
|
|
||||||
// While dragging, expose the live preview range to assistive tech via a
|
// While dragging, expose the live preview range to assistive tech via a
|
||||||
// polite live region. Empty text outside drag avoids announcing residual state.
|
// polite live region. Empty text outside drag avoids announcing residual state.
|
||||||
const dragLiveMessage = $derived.by(() => {
|
const dragLiveMessage = $derived.by(() => {
|
||||||
@@ -233,11 +232,6 @@ const dragLiveMessage = $derived.by(() => {
|
|||||||
to: formatTickLabel(toLabel, getLocale())
|
to: formatTickLabel(toLabel, getLocale())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const omitTickYear = $derived.by(() => {
|
|
||||||
if (filled.length === 0 || filled[0].month.length === 4) return false;
|
|
||||||
const firstYear = filled[0].month.slice(0, 4);
|
|
||||||
return filled.every((b) => b.month.slice(0, 4) === firstYear);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if density !== null}
|
{#if density !== null}
|
||||||
@@ -248,15 +242,7 @@ const omitTickYear = $derived.by(() => {
|
|||||||
class="relative rounded-sm border border-line bg-surface px-3 pt-3 pb-2 shadow-sm"
|
class="relative rounded-sm border border-line bg-surface px-3 pt-3 pb-2 shadow-sm"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div
|
<TimelineYAxis maxCount={maxCount} barAreaHeight={BAR_AREA_HEIGHT} />
|
||||||
class="flex flex-col justify-between pr-1.5 text-right font-sans text-xs leading-none text-ink-3"
|
|
||||||
style="height: {BAR_AREA_HEIGHT}px;"
|
|
||||||
aria-hidden="true"
|
|
||||||
data-testid="timeline-y-axis"
|
|
||||||
>
|
|
||||||
<span>{maxCount}</span>
|
|
||||||
<span>0</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<TimelineBars
|
<TimelineBars
|
||||||
@@ -274,22 +260,7 @@ const omitTickYear = $derived.by(() => {
|
|||||||
onbarclick={handleClick}
|
onbarclick={handleClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<TimelineXAxis filled={filled} />
|
||||||
class="relative mt-1 h-4 font-sans text-xs leading-none text-ink-3"
|
|
||||||
aria-hidden="true"
|
|
||||||
data-testid="timeline-x-axis"
|
|
||||||
>
|
|
||||||
{#each tickIndices as idx (filled[idx]?.month)}
|
|
||||||
{@const tickLeftPct = ((idx + 0.5) / filled.length) * 100}
|
|
||||||
<span
|
|
||||||
class="absolute -translate-x-1/2 whitespace-nowrap"
|
|
||||||
data-testid="timeline-x-tick"
|
|
||||||
style="left: {tickLeftPct}%;"
|
|
||||||
>
|
|
||||||
{formatTickLabel(filled[idx].month, getLocale(), omitTickYear)}
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
40
frontend/src/lib/document/TimelineXAxis.svelte
Normal file
40
frontend/src/lib/document/TimelineXAxis.svelte
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { formatTickLabel, tickIndicesFor } from '$lib/document/timeline';
|
||||||
|
import { getLocale } from '$lib/paraglide/runtime';
|
||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type MonthBucket = components['schemas']['MonthBucket'];
|
||||||
|
|
||||||
|
let {
|
||||||
|
filled
|
||||||
|
}: {
|
||||||
|
filled: MonthBucket[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const tickIndices = $derived(tickIndicesFor(filled));
|
||||||
|
|
||||||
|
// When all visible buckets share a year, the X-axis omits the year so a
|
||||||
|
// 12-month zoom reads as "Jan Feb Mär…" without repetition.
|
||||||
|
const omitTickYear = $derived.by(() => {
|
||||||
|
if (filled.length === 0 || filled[0].month.length === 4) return false;
|
||||||
|
const firstYear = filled[0].month.slice(0, 4);
|
||||||
|
return filled.every((b) => b.month.slice(0, 4) === firstYear);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative mt-1 h-4 font-sans text-xs leading-none text-ink-3"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-testid="timeline-x-axis"
|
||||||
|
>
|
||||||
|
{#each tickIndices as idx (filled[idx]?.month)}
|
||||||
|
{@const tickLeftPct = ((idx + 0.5) / filled.length) * 100}
|
||||||
|
<span
|
||||||
|
class="absolute -translate-x-1/2 whitespace-nowrap"
|
||||||
|
data-testid="timeline-x-tick"
|
||||||
|
style="left: {tickLeftPct}%;"
|
||||||
|
>
|
||||||
|
{formatTickLabel(filled[idx].month, getLocale(), omitTickYear)}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
19
frontend/src/lib/document/TimelineYAxis.svelte
Normal file
19
frontend/src/lib/document/TimelineYAxis.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
maxCount,
|
||||||
|
barAreaHeight
|
||||||
|
}: {
|
||||||
|
maxCount: number;
|
||||||
|
barAreaHeight: number;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between pr-1.5 text-right font-sans text-xs leading-none text-ink-3"
|
||||||
|
style="height: {barAreaHeight}px;"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-testid="timeline-y-axis"
|
||||||
|
>
|
||||||
|
<span>{maxCount}</span>
|
||||||
|
<span>0</span>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user