Fix translations, center logo/footer, add tile zoom control
Build and Deploy Verso / deploy (push) Successful in 14m8s

- i18n: unwrap webpack module object on dynamic JSON import (lang.default ?? lang)
  so French bundle keys are correctly registered in the i18next store
- Login logo: use flex centering on wrapper instead of display:block + margin:auto
- Footer (project list + login): align-items:center on .row for vertical centering
- Tile zoom: S/M/L control in header with CSS custom property (--lum-card-scale)
  that scales grid column width and card thumbnail height; persisted in localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-12 17:20:21 +00:00
parent d08e834f49
commit 592b4d3dad
5 changed files with 127 additions and 8 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ block content
.container
.row
.col-12
.text-center.mb-4
.lumiere-logo-center.mb-4
img.verso-login-logo(
src=buildImgPath('ol-brand/verso-logo.svg')
alt='Verso'
@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useRef } from 'react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useProjectListContext } from '../context/project-list-context'
import { Project } from '../../../../../types/project/dashboard/api'
@@ -28,6 +28,46 @@ import { CompileAndDownloadProjectPDFButtonTooltip } from './table/cells/action-
import { ArchiveProjectButtonTooltip } from './table/cells/action-buttons/archive-project-button'
import { TrashProjectButtonTooltip } from './table/cells/action-buttons/trash-project-button'
// ── Tile zoom ─────────────────────────────────────────────────────────────────
type ZoomLevel = 0.75 | 1 | 1.35
const ZOOM_OPTIONS: { value: ZoomLevel; label: string }[] = [
{ value: 0.75, label: 'S' },
{ value: 1, label: 'M' },
{ value: 1.35, label: 'L' },
]
const ZOOM_STORAGE_KEY = 'lumiere-card-scale'
function useLumiereCardScale(): [ZoomLevel, (z: ZoomLevel) => void] {
const [scale, setScale] = useState<ZoomLevel>(() => {
try {
const stored = localStorage.getItem(ZOOM_STORAGE_KEY)
if (stored) {
const val = parseFloat(stored)
if ((ZOOM_OPTIONS.map(o => o.value) as number[]).includes(val)) {
return val as ZoomLevel
}
}
} catch (_) {
// storage unavailable
}
return 1
})
const updateScale = useCallback((z: ZoomLevel) => {
setScale(z)
try {
localStorage.setItem(ZOOM_STORAGE_KEY, String(z))
} catch (_) {
// ignore
}
}, [])
return [scale, updateScale]
}
// ── Format helpers ─────────────────────────────────────────────────────────────
type FormatVariant = 'latex' | 'typst' | 'quarto' | 'quarto-slides'
function getFormatVariant(
@@ -137,6 +177,7 @@ export function ProjectListLumiere() {
const navbarProps = getMeta('ol-navbar')
const footerProps = getMeta('ol-footer')
const { t } = useTranslation()
const [cardScale, setCardScale] = useLumiereCardScale()
const {
error,
visibleProjects,
@@ -206,6 +247,23 @@ export function ProjectListLumiere() {
filter={filter}
selectedTag={selectedTag}
/>
<div
className="lumiere-zoom-control"
role="group"
aria-label="Taille des cartes"
>
{ZOOM_OPTIONS.map(({ value, label }) => (
<button
key={value}
type="button"
className={`lumiere-zoom-btn${cardScale === value ? ' active' : ''}`}
onClick={() => setCardScale(value)}
aria-pressed={cardScale === value}
>
{label}
</button>
))}
</div>
<NewProjectButton
id="lumiere-new-project-button"
showAddAffiliationWidget
@@ -241,7 +299,11 @@ export function ProjectListLumiere() {
{visibleProjects.length === 0 ? (
<p className="lumiere-empty">{t('no_projects')}</p>
) : (
<div className="lumiere-card-grid">
<div
className="lumiere-card-grid"
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
style={{ '--lum-card-scale': cardScale } as React.CSSProperties}
>
{visibleProjects.map(project => (
<ProjectCard key={project.id} project={project} />
))}
+4 -1
View File
@@ -57,7 +57,10 @@ i18n.use(initReactI18next).init({
const localesPromise = import(
/* webpackChunkName: "[request]" */ `../../locales/${LANG}.json`
).then(lang => {
i18n.addResourceBundle(LANG, 'translation', lang)
// webpack dynamic JSON imports return a module object { default: JSON },
// not the raw JSON — unwrap if needed.
const data = lang.default ?? lang
i18n.addResourceBundle(LANG, 'translation', data)
i18n.addResourceBundle(
LANG,
'writefull',
@@ -44,10 +44,17 @@
background-color: #f0faf8;
min-height: 100vh;
// Flex wrapper replaces .text-center so the block image centers reliably.
.lumiere-logo-center {
display: flex;
justify-content: center;
align-items: center;
}
.verso-login-logo {
display: block;
margin: 0 auto;
max-width: 520px;
width: 100%;
height: auto;
}
}
@@ -55,6 +62,10 @@
// Same light-teal treatment as the project page footer.
body:has(.login-page) footer.site-footer {
position: relative;
.site-footer-content > .row {
align-items: center;
}
background-color: #edf7f4 !important;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.28'/%3E%3C/svg%3E") !important;
background-size: 200px 200px !important;
@@ -398,12 +398,51 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
// ── Card grid ─────────────────────────────────────────────────────────────
.lumiere-card-grid {
--lum-card-scale: 1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(calc(180px * var(--lum-card-scale)), 1fr));
gap: 1.5rem;
margin-top: 0.5rem;
}
// ── Zoom control (S / M / L card-size picker) ─────────────────────────────
.lumiere-zoom-control {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.6);
border: 1px solid $lum-border;
border-radius: 7px;
padding: 2px;
gap: 1px;
flex-shrink: 0;
}
.lumiere-zoom-btn {
background: none;
border: none;
border-radius: 5px;
padding: 3px 9px;
font-size: 0.72rem;
font-weight: 700;
color: $lum-text-sub;
cursor: pointer;
line-height: 1;
transition: background-color 0.12s ease, color 0.12s ease;
letter-spacing: 0.05em;
&:hover {
background: rgba($lum-teal, 0.1);
color: $lum-teal;
}
&.active {
background: #fff;
color: $lum-teal;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}
}
// ── Card wrapper (checkbox + link) ────────────────────────────────────────
.lumiere-card-wrapper {
@@ -518,7 +557,7 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
.lumiere-card-thumb {
position: relative;
height: 130px;
height: calc(130px * var(--lum-card-scale, 1));
display: flex;
align-items: center;
justify-content: center;
@@ -728,6 +767,10 @@ $lum-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' wi
.project-list-lumiere footer.site-footer {
position: relative;
.site-footer-content > .row {
align-items: center;
}
background-color: #edf7f4 !important;
background-image: #{$lum-noise} !important;
background-size: 200px 200px !important;