Restyle individual page: FAB edit button, uncropped profile pics, silhouette placeholders, dark mode tabs

- Override individual-page-images template to request 400x400 contain (no server-side crop)
- Move circle crop to CSS on a.gallery wrapper, fixing single-image profiles
- Add gender-colored circular silhouette placeholders matching family navigator
- Replace edit button with fixed FAB (bottom-right) using homepage pencil SVG
- Add page cloak to prevent FOUC from JS DOM manipulations
- Dark mode: white profile shadow, black active tab with light blue accent
- Remove carousel arrows and "add media" link at template level
This commit is contained in:
2026-03-15 21:15:30 +01:00
parent 02599e5876
commit 1531c075c4
3 changed files with 221 additions and 42 deletions
+11 -4
View File
@@ -70,12 +70,17 @@ class BockenTheme extends AbstractModule implements ModuleCustomInterface, Modul
return <<<HTML
<link rel="icon" href="{$faviconUrl}" type="image/svg+xml">
<style>
.bocken-cloak .wt-main-wrapper { opacity: 0; }
.bocken-ready .wt-main-wrapper { transition: opacity 0.05s ease; opacity: 1; }
</style>
<script>
(function() {
var saved = localStorage.getItem('bocken-theme');
if (saved === 'dark' || saved === 'light') {
document.documentElement.setAttribute('data-theme', saved);
}
document.documentElement.classList.add('bocken-cloak');
})();
</script>
HTML;
@@ -358,11 +363,8 @@ class BockenTheme extends AbstractModule implements ModuleCustomInterface, Modul
var iconSpan = editBtn.querySelector('.wt-icon-menu');
if (iconSpan) {
editBtn.innerHTML = '';
iconSpan.innerHTML = '<i data-lucide="pencil"></i>';
iconSpan.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1v32c0 8.8 7.2 16 16 16h32zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"/></svg>';
editBtn.appendChild(iconSpan);
if (typeof lucide !== 'undefined') {
lucide.createIcons({ nodes: [editBtn] });
}
}
}
}
@@ -400,6 +402,10 @@ class BockenTheme extends AbstractModule implements ModuleCustomInterface, Modul
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Reveal page after DOM manipulations are done
document.documentElement.classList.remove('bocken-cloak');
document.documentElement.classList.add('bocken-ready');
}
if (document.readyState === 'loading') {
@@ -426,6 +432,7 @@ class BockenTheme extends AbstractModule implements ModuleCustomInterface, Modul
View::registerCustomView('::modules/place-hierarchy/list', $this->name() . '::modules/place-hierarchy/list');
View::registerCustomView('::lists/individuals-table', $this->name() . '::lists/individuals-table');
View::registerCustomView('::lists/families-table', $this->name() . '::lists/families-table');
View::registerCustomView('::individual-page-images', $this->name() . '::individual-page-images');
View::registerCustomView('::individual-page-menu', $this->name() . '::individual-page-menu');
View::registerCustomView('::modules/family_nav/sidebar-family', $this->name() . '::modules/family_nav/sidebar-family');
}
+160 -38
View File
@@ -10,23 +10,30 @@
/* ---------- Profile photo: large circle, no border ---------- */
.wt-route-IndividualPage .col-sm-3 .img-thumbnail {
border-radius: 50%;
aspect-ratio: 1 / 1;
object-fit: cover;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
padding: 0;
transition: box-shadow 0.3s ease, transform 0.3s ease;
width: 100%;
max-width: 260px;
#individual-images .carousel-inner {
overflow: visible;
}
.wt-route-IndividualPage .col-sm-3 .img-thumbnail:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transform: translateY(-2px);
.wt-route-IndividualPage .col-sm-3 a.gallery {
display: block;
width: 200px;
height: 200px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.wt-route-IndividualPage .col-sm-3 a.gallery .img-thumbnail {
border-radius: 0;
width: 100%;
height: 100%;
object-fit: cover;
border: none;
box-shadow: none;
padding: 0;
}
/* Center the photo column */
.wt-route-IndividualPage .col-sm-3 {
display: flex;
@@ -34,26 +41,41 @@
align-items: center;
}
/* Silhouette placeholder: constrain to match photo size, circular background */
.wt-route-IndividualPage .col-sm-3 .wt-individual-silhouette {
display: block;
width: auto !important;
max-height: 260px;
aspect-ratio: 1 / 1;
object-fit: contain;
/* Silhouette placeholder: circular with gender-based background */
.wt-route-IndividualPage .col-sm-3 > .img-thumbnail {
border-radius: 50%;
width: 200px;
height: 200px;
padding: 2rem;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: none;
}
/* "Add a media object" link — subtle */
.wt-route-IndividualPage .col-sm-3 .text-center a {
font-size: 0.8rem;
opacity: 0.45;
transition: opacity 0.2s;
.wt-route-IndividualPage .col-sm-3 .wt-individual-silhouette {
display: block;
width: 100% !important;
height: 100%;
opacity: 0.15;
filter: brightness(0);
}
.wt-route-IndividualPage .col-sm-3 .text-center a:hover {
opacity: 1;
/* Gender-based silhouette container — light mode */
.wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-m) {
background-color: #d1dce7;
}
.wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-f) {
background-color: #ded7e2;
}
.wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-u) {
background-color: #ddd;
}
/* ---------- Individual header: photo + title + edit ---------- */
.wt-route-IndividualPage .bocken-individual-header {
@@ -68,7 +90,7 @@
width: auto;
max-width: 260px;
padding: 0;
overflow: hidden;
overflow: visible;
}
.wt-route-IndividualPage .wt-page-title {
@@ -100,20 +122,56 @@
opacity: 0.45;
}
/* Edit button: icon-only pencil */
.wt-route-IndividualPage .bocken-title-user a {
color: #2E3440;
}
/* Edit button: fixed FAB in bottom-right corner */
.wt-route-IndividualPage .wt-page-menu-button {
width: 2.25rem;
height: 2.25rem;
padding: 0;
display: inline-flex;
align-items: center;
position: fixed;
bottom: 0;
right: 0;
width: 1rem;
height: 1rem;
padding: 2rem;
margin: 2rem;
display: grid;
align-content: center;
justify-content: center;
border-radius: 50%;
background-color: #BF616A;
border: none;
z-index: 100;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
--shake-angle: 15deg;
}
.wt-route-IndividualPage .wt-page-menu-button:hover,
.wt-route-IndividualPage .wt-page-menu-button:focus {
background-color: #2E3440;
box-shadow: 0 0 0.4em 0.15em rgba(0, 0, 0, 0.15);
animation: bocken-shake 0.5s forwards;
}
.wt-route-IndividualPage .wt-page-menu-button .wt-icon-menu svg {
width: 16px;
height: 16px;
width: 2rem;
height: 2rem;
fill: white;
stroke: none;
}
@keyframes bocken-shake {
0% { transform: rotate(0) scale(1); }
25% { box-shadow: 0 0 0.5em 0.15em rgba(0, 0, 0, 0.25); transform: rotate(var(--shake-angle)) scale(1.2); }
50% { box-shadow: 0 0 0.5em 0.15em rgba(0, 0, 0, 0.25); transform: rotate(calc(-1 * var(--shake-angle))) scale(1.2); }
74% { box-shadow: 0 0 0.5em 0.15em rgba(0, 0, 0, 0.25); transform: rotate(var(--shake-angle)) scale(1.2); }
100% { transform: rotate(0) scale(1.2); }
}
@media screen and (max-width: 500px) {
.wt-route-IndividualPage .wt-page-menu-button {
margin: 1rem;
}
}
/* Hide dropdown caret on the round button */
@@ -516,9 +574,9 @@
font-size: 0.9rem;
}
.wt-route-IndividualPage .col-sm-3 .img-thumbnail {
max-width: 80px;
.wt-route-IndividualPage .col-sm-3 a.gallery {
width: 80px;
height: 80px;
}
/* Names accordion: full width */
@@ -571,6 +629,38 @@
background: var(--color-primary, #5E81AC);
}
/* Active tab: black background, light blue accent */
:root[data-theme=dark] .wt-tabs-individual .nav-tabs .nav-link.active {
background: #000;
border-bottom-color: #88C0D0;
}
/* Silhouette icon tint */
:root[data-theme=dark] .wt-route-IndividualPage .col-sm-3 .wt-individual-silhouette {
filter: brightness(0) invert(1);
}
/* Silhouette gender colors */
:root[data-theme=dark] .wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-m) {
background-color: #505f73;
}
:root[data-theme=dark] .wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-f) {
background-color: #5f596d;
}
:root[data-theme=dark] .wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-u) {
background-color: #4C566A;
}
/* Profile picture shadow */
:root[data-theme=dark] .wt-route-IndividualPage .col-sm-3 a.gallery {
box-shadow: 0 4px 8px rgba(255, 255, 255, 0.3);
}
/* Username link */
:root[data-theme=dark] .wt-route-IndividualPage .bocken-title-user a {
color: #ECEFF4;
}
/* Links */
:root[data-theme=dark] .wt-route-IndividualPage .wt-facts-table a,
:root[data-theme=dark] .wt-route-IndividualPage .wt-family-navigator-family a,
@@ -617,6 +707,38 @@
background: var(--color-primary, #5E81AC);
}
/* Active tab: black background, light blue accent */
:root:not([data-theme=light]) .wt-tabs-individual .nav-tabs .nav-link.active {
background: #000;
border-bottom-color: #88C0D0;
}
/* Silhouette icon tint */
:root:not([data-theme=light]) .wt-route-IndividualPage .col-sm-3 .wt-individual-silhouette {
filter: brightness(0) invert(1);
}
/* Silhouette gender colors */
:root:not([data-theme=light]) .wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-m) {
background-color: #505f73;
}
:root:not([data-theme=light]) .wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-f) {
background-color: #5f596d;
}
:root:not([data-theme=light]) .wt-route-IndividualPage .col-sm-3 > .img-thumbnail:has(.wt-individual-silhouette-u) {
background-color: #4C566A;
}
/* Profile picture shadow */
:root:not([data-theme=light]) .wt-route-IndividualPage .col-sm-3 a.gallery {
box-shadow: 0 4px 8px rgba(255, 255, 255, 0.3);
}
/* Username link */
:root:not([data-theme=light]) .wt-route-IndividualPage .bocken-title-user a {
color: #ECEFF4;
}
/* Links */
:root:not([data-theme=light]) .wt-route-IndividualPage .wt-facts-table a,
:root:not([data-theme=light]) .wt-route-IndividualPage .wt-family-navigator-family a,
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Http\RequestHandlers\AddNewFact;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Media;
use Fisharebest\Webtrees\Module\ModuleSidebarInterface;
use Fisharebest\Webtrees\Module\ModuleTabInterface;
use Fisharebest\Webtrees\Tree;
use Illuminate\Support\Collection;
/**
* @var string $age
* @var bool $can_upload_media
* @var Collection<int,Media> $individual_media
* @var Collection<int,Fact> $name_records
* @var Individual $record
* @var Collection<int,Fact> $sex_records
* @var Collection<int,string> $shares
* @var Collection<int,ModuleSidebarInterface> $sidebars
* @var Collection<int,ModuleTabInterface> $tabs
* @var Tree $tree
* @var string $user_link
*/
?>
<?php if ($individual_media->isNotEmpty() || $tree->getPreference('USE_SILHOUETTE') === '1') : ?>
<div class="col-sm-3">
<?php if ($individual_media->isEmpty()) : ?>
<div class="img-thumbnail">
<i class="wt-individual-silhouette wt-individual-silhouette-<?= strtolower($record->sex()) ?> wt-icon-flip-rtl w-100"></i>
</div>
<?php elseif ($individual_media->count() === 1) : ?>
<?= $individual_media->first()->displayImage(400, 400, 'contain', ['class' => 'img-thumbnail img-fluid w-100']) ?>
<?php else : ?>
<div id="individual-images" class="carousel slide" data-bs-interval="false">
<div class="carousel-inner">
<?php foreach ($individual_media as $n => $media_file) : ?>
<div class="carousel-item <?= $n === 0 ? 'active' : '' ?>">
<?= $media_file->displayImage(400, 400, 'contain', ['class' => 'img-thumbnail img-fluid w-100']) ?>
</div>
<?php endforeach ?>
</div>
</div>
<?php endif ?>
</div>
<?php endif ?>