Rework timeline: outside the card, thicker rail, day-first date
Addresses feedback that the previous timeline competed for attention with the year and felt visually trapped inside the event card. - The card surface (cream background, hairline border, rounded corners) is now built per-row from the avatar + content TDs. The timeline TD sits on the page background to the right of the card with a 16px gutter between them. - Rail bumped from 1px to 4px, in a warm grey #cdc7be that reads as a deliberate ribbon rather than a divider. - Dots are 14px (up from 10), with a 4px page-coloured halo so they punch through the rail. Single colour (nord10 blue) for every event — no more per-event-type tinting. - Each calendar day shows exactly one dot: rows are walked in upcoming-anniversary order and any row whose $fact->jd matches the previous row renders without a dot but keeps its date text, so two May-17 deaths share one marker on the rail. - Date display split into "17. MAI" (semi-bold 13px tracking) and "1759" (light 11px, muted) on a second line, so the day of the year reads as the primary axis and the year as supporting context. - Relationship label moved from inline "(your great-aunt)" to a separate italic muted line beneath the name, so long relationship strings don't crowd the event label.
This commit is contained in:
+192
-65
@@ -44,6 +44,8 @@ $palette = [
|
||||
'birth' => '#5E81AC', // nord10 — birth (blue)
|
||||
'death' => '#4C566A', // nord3 — death (graphite)
|
||||
'marr' => '#B48EAD', // nord15 — marriage (purple/pink)
|
||||
'rail' => '#cdc7be', // muted warm grey for the timeline rail
|
||||
'dot' => '#5E81AC', // single unified colour for every timeline dot
|
||||
];
|
||||
|
||||
$font_stack = "'Open Sans', Helvetica, Arial, 'Noto Sans', sans-serif";
|
||||
@@ -81,46 +83,63 @@ $is_detailed = static function (Fact $fact) use ($detailed_xrefs): bool {
|
||||
return false;
|
||||
};
|
||||
|
||||
$linked_name = static function (Individual $individual) use ($palette, $relationships): string {
|
||||
/**
|
||||
* Just the linked name (no relationship), used both inline and inside
|
||||
* the multi-line name+relationship block below.
|
||||
*/
|
||||
$linked_name = static function (Individual $individual) use ($palette): string {
|
||||
$name = strip_tags($individual->fullName());
|
||||
$url = $individual->url();
|
||||
$style = 'color:' . $palette['ink'] . ';text-decoration:none;'
|
||||
. 'border-bottom:1px solid ' . $palette['border'] . ';'
|
||||
. 'padding-bottom:1px;';
|
||||
|
||||
$html = '<a href="' . e($url) . '" style="' . $style . '">' . e($name) . '</a>';
|
||||
return '<a href="' . e($url) . '" style="' . $style . '">' . e($name) . '</a>';
|
||||
};
|
||||
|
||||
if (isset($relationships[$individual->xref()])) {
|
||||
$html .= ' <span style="color:' . $palette['ink3'] . ';font-style:italic;font-weight:400;font-size:13px;">('
|
||||
. e(strip_tags($relationships[$individual->xref()]))
|
||||
. ')</span>';
|
||||
/**
|
||||
* Inline list of names ("Hans Doe" or "Hans Doe & Lotte Doe") plus
|
||||
* the combined relationship line on a second line beneath.
|
||||
*/
|
||||
$record_label = static function (Fact $fact) use ($linked_name, $relationships, $palette): string {
|
||||
$record = $fact->record();
|
||||
|
||||
$names = [];
|
||||
$rels = [];
|
||||
|
||||
if ($record instanceof Individual) {
|
||||
$names[] = $linked_name($record);
|
||||
if (isset($relationships[$record->xref()])) {
|
||||
$rels[] = strip_tags($relationships[$record->xref()]);
|
||||
}
|
||||
} elseif ($record instanceof Family) {
|
||||
foreach ([$record->husband(), $record->wife()] as $spouse) {
|
||||
if ($spouse instanceof Individual) {
|
||||
$names[] = $linked_name($spouse);
|
||||
if (isset($relationships[$spouse->xref()])) {
|
||||
$rels[] = strip_tags($relationships[$spouse->xref()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return e($record->xref());
|
||||
}
|
||||
|
||||
$html = implode(' & ', $names);
|
||||
|
||||
if ($rels !== []) {
|
||||
// Drop near-duplicate labels ("your father", "your father")
|
||||
// so couples don't print the same relationship twice.
|
||||
$rels = array_values(array_unique(array_map(static fn ($r) => e($r), $rels)));
|
||||
$html .= '<div style="margin-top:2px;color:' . $palette['ink3']
|
||||
. ';font-style:italic;font-weight:400;font-size:13px;">'
|
||||
. implode(' & ', $rels)
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
};
|
||||
|
||||
$record_label = static function (Fact $fact) use ($linked_name): string {
|
||||
$record = $fact->record();
|
||||
|
||||
if ($record instanceof Individual) {
|
||||
return $linked_name($record);
|
||||
}
|
||||
|
||||
if ($record instanceof Family) {
|
||||
$parts = [];
|
||||
|
||||
foreach ([$record->husband(), $record->wife()] as $spouse) {
|
||||
if ($spouse instanceof Individual) {
|
||||
$parts[] = $linked_name($spouse);
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' & ', $parts);
|
||||
}
|
||||
|
||||
return e($record->xref());
|
||||
};
|
||||
|
||||
$event_date_display = static function (Fact $fact): string {
|
||||
$date = $fact->date();
|
||||
|
||||
@@ -131,6 +150,43 @@ $event_date_display = static function (Fact $fact): string {
|
||||
return strip_tags($date->display());
|
||||
};
|
||||
|
||||
/**
|
||||
* Split a Fact's event date into (day+month, year) for the timeline
|
||||
* rail where the day-of-the-anniversary is the meaningful sort key
|
||||
* and the year is supporting information.
|
||||
*
|
||||
* @return array{day_month:string,year:string}
|
||||
*/
|
||||
$date_parts = static function (Fact $fact): array {
|
||||
static $gregorian = null;
|
||||
static $months_de = [
|
||||
1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
|
||||
];
|
||||
static $months_en = [
|
||||
1 => 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
|
||||
];
|
||||
|
||||
$gregorian ??= new \Fisharebest\ExtCalendar\GregorianCalendar();
|
||||
|
||||
$date = $fact->date();
|
||||
|
||||
if (!$date->isOK()) {
|
||||
return ['day_month' => '', 'year' => ''];
|
||||
}
|
||||
|
||||
[$year, $month, $day] = $gregorian->jdToYmd($date->minimumJulianDay());
|
||||
|
||||
if (str_starts_with(I18N::languageTag(), 'de')) {
|
||||
$day_month = $day . '. ' . mb_strtoupper($months_de[$month] ?? '');
|
||||
} else {
|
||||
$day_month = mb_strtoupper(($months_en[$month] ?? '') . ' ' . $day);
|
||||
}
|
||||
|
||||
return ['day_month' => $day_month, 'year' => (string) $year];
|
||||
};
|
||||
|
||||
$event_kind = static function (Fact $fact): string {
|
||||
$parts = explode(':', $fact->tag());
|
||||
|
||||
@@ -319,22 +375,35 @@ $section_kicker_style = 'margin:0 0 18px;'
|
||||
. 'font-style:italic;font-weight:300;font-size:14px;'
|
||||
. 'color:' . $palette['ink3'] . ';';
|
||||
|
||||
$divider_style = 'border-bottom:1px solid ' . $palette['border'] . ';';
|
||||
// The card surface is built per-row out of avatar+content TDs (the
|
||||
// outer table itself stays transparent). The timeline TD lives on the
|
||||
// page background and carries a thick rail on its left edge.
|
||||
$card_padding_y = 18;
|
||||
$card_corner_radius = 8;
|
||||
|
||||
$avatar_cell_style = 'width:72px;vertical-align:middle;padding:18px 0 18px 18px;';
|
||||
$avatar_cell_base = 'width:72px;vertical-align:middle;'
|
||||
. 'padding:' . $card_padding_y . 'px 0 ' . $card_padding_y . 'px 18px;'
|
||||
. 'background:' . $palette['surface'] . ';'
|
||||
. 'border-left:1px solid ' . $palette['border'] . ';';
|
||||
|
||||
$content_cell_style = 'vertical-align:middle;'
|
||||
. 'padding:18px 18px 18px 16px;'
|
||||
$content_cell_base = 'vertical-align:middle;'
|
||||
. 'padding:' . $card_padding_y . 'px 18px ' . $card_padding_y . 'px 16px;'
|
||||
. 'background:' . $palette['surface'] . ';'
|
||||
. 'border-right:1px solid ' . $palette['border'] . ';'
|
||||
. 'font-family:' . $font_stack . ';font-size:15px;line-height:1.4;'
|
||||
. 'font-weight:300;color:' . $palette['ink'] . ';';
|
||||
|
||||
$timeline_cell_style = 'width:170px;vertical-align:middle;'
|
||||
. 'padding:18px 18px 18px 26px;'
|
||||
. 'border-left:1px solid ' . $palette['border'] . ';'
|
||||
$timeline_cell_base = 'width:140px;vertical-align:middle;'
|
||||
. 'padding:' . $card_padding_y . 'px 14px ' . $card_padding_y . 'px 24px;'
|
||||
. 'border-left:4px solid ' . $palette['rail'] . ';'
|
||||
. 'background:' . $palette['bg'] . ';'
|
||||
. 'font-family:' . $font_stack . ';'
|
||||
. 'font-weight:500;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;'
|
||||
. 'color:' . $palette['ink3'] . ';white-space:nowrap;';
|
||||
|
||||
// A 16px wide gutter cell sits between the card and the timeline so
|
||||
// the rail visibly separates from the card edge.
|
||||
$gutter_cell_style = 'width:16px;background:' . $palette['bg'] . ';';
|
||||
|
||||
// Renders one event row in the consistent 3-column layout shared by
|
||||
// every section (avatar | content | date on the timeline rail).
|
||||
$summary_kicker_style = 'margin:18px 0 8px;'
|
||||
@@ -349,36 +418,85 @@ $summary_list_style = 'list-style:none;margin:0;padding:0;'
|
||||
|
||||
$summary_item_style = 'padding:3px 0;';
|
||||
|
||||
$event_row = static function (Fact $fact, string $body_html, string $dot_color)
|
||||
// Renders one event row in the 4-column layout
|
||||
// [avatar] [content] [gutter] [timeline rail + date]
|
||||
//
|
||||
// The avatar+content TDs carry the card surface (background + side
|
||||
// borders + corner radii on the first/last rows so the visual card
|
||||
// wraps just those two cells). The timeline TD sits on the page
|
||||
// background with a thick coloured rail on its left edge.
|
||||
//
|
||||
// $position is 'first', 'middle', or 'last' — used to add corner
|
||||
// radii and outer borders only where they belong.
|
||||
//
|
||||
// $show_dot suppresses the dot on rows whose upcoming-day matches
|
||||
// the previous row, so each calendar day has exactly one dot.
|
||||
$event_row = static function (
|
||||
Fact $fact,
|
||||
string $body_html,
|
||||
bool $is_first,
|
||||
bool $is_last,
|
||||
bool $show_dot,
|
||||
)
|
||||
use (
|
||||
$record_avatars,
|
||||
$event_date_display,
|
||||
$avatar_cell_style,
|
||||
$content_cell_style,
|
||||
$timeline_cell_style,
|
||||
$divider_style,
|
||||
$date_parts,
|
||||
$avatar_cell_base,
|
||||
$content_cell_base,
|
||||
$timeline_cell_base,
|
||||
$gutter_cell_style,
|
||||
$card_corner_radius,
|
||||
$palette,
|
||||
): string {
|
||||
return '<tr style="' . $divider_style . '">'
|
||||
. '<td style="' . $avatar_cell_style . '">' . $record_avatars($fact) . '</td>'
|
||||
. '<td style="' . $content_cell_style . '">' . $body_html . '</td>'
|
||||
. '<td style="' . $timeline_cell_style . '">'
|
||||
. '<span style="display:inline-block;width:10px;height:10px;background:'
|
||||
. $dot_color . ';border-radius:50%;'
|
||||
. 'margin-left:-32px;margin-right:14px;vertical-align:middle;"></span>'
|
||||
. e($event_date_display($fact))
|
||||
. '</td>'
|
||||
$avatar_extra = '';
|
||||
$content_extra = '';
|
||||
|
||||
if ($is_first) {
|
||||
$avatar_extra .= 'border-top:1px solid ' . $palette['border'] . ';'
|
||||
. 'border-top-left-radius:' . $card_corner_radius . 'px;';
|
||||
$content_extra .= 'border-top:1px solid ' . $palette['border'] . ';'
|
||||
. 'border-top-right-radius:' . $card_corner_radius . 'px;';
|
||||
}
|
||||
|
||||
if ($is_last) {
|
||||
$avatar_extra .= 'border-bottom:1px solid ' . $palette['border'] . ';'
|
||||
. 'border-bottom-left-radius:' . $card_corner_radius . 'px;';
|
||||
$content_extra .= 'border-bottom:1px solid ' . $palette['border'] . ';'
|
||||
. 'border-bottom-right-radius:' . $card_corner_radius . 'px;';
|
||||
} else {
|
||||
$avatar_extra .= 'border-bottom:1px solid ' . $palette['border'] . ';';
|
||||
$content_extra .= 'border-bottom:1px solid ' . $palette['border'] . ';';
|
||||
}
|
||||
|
||||
$parts = $date_parts($fact);
|
||||
|
||||
$dot_html = $show_dot
|
||||
? '<span style="display:inline-block;width:14px;height:14px;background:'
|
||||
. $palette['dot']
|
||||
. ';border-radius:50%;margin-left:-31px;margin-right:14px;vertical-align:middle;'
|
||||
. 'box-shadow:0 0 0 4px ' . $palette['bg'] . ';"></span>'
|
||||
: '<span style="display:inline-block;width:14px;margin-left:-31px;margin-right:14px;vertical-align:middle;"></span>';
|
||||
|
||||
$date_html =
|
||||
'<span style="display:inline-block;vertical-align:middle;">'
|
||||
. '<div style="font-weight:600;font-size:13px;letter-spacing:0.12em;color:'
|
||||
. $palette['ink'] . ';">' . e($parts['day_month']) . '</div>'
|
||||
. '<div style="font-weight:300;font-size:11px;color:'
|
||||
. $palette['mute'] . ';margin-top:1px;">' . e($parts['year']) . '</div>'
|
||||
. '</span>';
|
||||
|
||||
return '<tr>'
|
||||
. '<td style="' . $avatar_cell_base . $avatar_extra . '">' . $record_avatars($fact) . '</td>'
|
||||
. '<td style="' . $content_cell_base . $content_extra . '">' . $body_html . '</td>'
|
||||
. '<td style="' . $gutter_cell_style . '"></td>'
|
||||
. '<td style="' . $timeline_cell_base . '">' . $dot_html . $date_html . '</td>'
|
||||
. '</tr>';
|
||||
};
|
||||
|
||||
// Wraps a list of <tr> rows in the "card" surface — rounded background
|
||||
// patch matching .card on the website.
|
||||
// Outer section table is transparent. Each event row contributes its
|
||||
// own card-surfaced avatar+content TDs plus the timeline TD.
|
||||
$card_open = '<table role="presentation" cellpadding="0" cellspacing="0" border="0" '
|
||||
. 'style="width:100%;border-collapse:collapse;'
|
||||
. 'background:' . $palette['surface'] . ';'
|
||||
. 'border:1px solid ' . $palette['border'] . ';'
|
||||
. 'border-radius:8px;'
|
||||
. 'box-shadow:0 1px 3px rgba(0,0,0,0.04);'
|
||||
. 'overflow:hidden;">';
|
||||
. 'style="width:100%;border-collapse:separate;border-spacing:0;">';
|
||||
$card_close = '</table>';
|
||||
|
||||
?><!doctype html>
|
||||
@@ -436,7 +554,8 @@ $card_close = '</table>';
|
||||
</p>
|
||||
<?php if ($detailed !== []) : ?>
|
||||
<?= $card_open ?>
|
||||
<?php foreach ($detailed as $fact) : ?>
|
||||
<?php $prev_jd = null; $total = count($detailed); ?>
|
||||
<?php foreach ($detailed as $i => $fact) : ?>
|
||||
<?php
|
||||
$age = $upcoming_age($fact);
|
||||
$body = '<span style="display:inline-block;vertical-align:middle;margin-right:12px;">' . $event_icon('BIRT') . '</span>'
|
||||
@@ -444,7 +563,9 @@ $card_close = '</table>';
|
||||
. '<span style="font-weight:600;color:' . $palette['ink'] . ';">' . $record_label($fact) . '</span>'
|
||||
. ' <span style="color:' . $palette['ink2'] . ';font-weight:300;">— ' . e($birthday_label($age)) . '</span>'
|
||||
. '</span>';
|
||||
echo $event_row($fact, $body, $palette['birth']);
|
||||
$show_dot = ($fact->jd ?? 0) !== $prev_jd;
|
||||
$prev_jd = $fact->jd ?? 0;
|
||||
echo $event_row($fact, $body, $i === 0, $i === $total - 1, $show_dot);
|
||||
?>
|
||||
<?php endforeach ?>
|
||||
<?= $card_close ?>
|
||||
@@ -482,7 +603,8 @@ $card_close = '</table>';
|
||||
</p>
|
||||
<?php if ($detailed !== []) : ?>
|
||||
<?= $card_open ?>
|
||||
<?php foreach ($detailed as $fact) : ?>
|
||||
<?php $prev_jd = null; $total = count($detailed); ?>
|
||||
<?php foreach ($detailed as $i => $fact) : ?>
|
||||
<?php
|
||||
$age = $upcoming_age($fact);
|
||||
$body = '<span style="display:inline-block;vertical-align:middle;margin-right:12px;">' . $event_icon('MARR') . '</span>'
|
||||
@@ -490,7 +612,9 @@ $card_close = '</table>';
|
||||
. '<span style="font-weight:600;color:' . $palette['ink'] . ';">' . $record_label($fact) . '</span>'
|
||||
. ' <span style="color:' . $palette['ink2'] . ';font-weight:300;">— ' . e($anniversary_label($age)) . '</span>'
|
||||
. '</span>';
|
||||
echo $event_row($fact, $body, $palette['marr']);
|
||||
$show_dot = ($fact->jd ?? 0) !== $prev_jd;
|
||||
$prev_jd = $fact->jd ?? 0;
|
||||
echo $event_row($fact, $body, $i === 0, $i === $total - 1, $show_dot);
|
||||
?>
|
||||
<?php endforeach ?>
|
||||
<?= $card_close ?>
|
||||
@@ -528,7 +652,8 @@ $card_close = '</table>';
|
||||
</p>
|
||||
<?php if ($detailed !== []) : ?>
|
||||
<?= $card_open ?>
|
||||
<?php foreach ($detailed as $fact) : ?>
|
||||
<?php $prev_jd = null; $total = count($detailed); ?>
|
||||
<?php foreach ($detailed as $i => $fact) : ?>
|
||||
<?php
|
||||
$kind = $event_kind($fact);
|
||||
$body = '<span style="display:inline-block;vertical-align:middle;margin-right:12px;">' . $event_icon($kind) . '</span>'
|
||||
@@ -536,7 +661,9 @@ $card_close = '</table>';
|
||||
. '<span style="font-weight:600;color:' . $palette['ink'] . ';">' . $record_label($fact) . '</span>'
|
||||
. ' <span style="color:' . $palette['ink2'] . ';font-weight:300;">— ' . e($fact->label()) . '</span>'
|
||||
. '</span>';
|
||||
echo $event_row($fact, $body, $event_color($kind));
|
||||
$show_dot = ($fact->jd ?? 0) !== $prev_jd;
|
||||
$prev_jd = $fact->jd ?? 0;
|
||||
echo $event_row($fact, $body, $i === 0, $i === $total - 1, $show_dot);
|
||||
?>
|
||||
<?php endforeach ?>
|
||||
<?= $card_close ?>
|
||||
|
||||
Reference in New Issue
Block a user