From 592b4d3daddb775bb23e2a5ea667c85ca8470d66 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 12 Jun 2026 17:20:21 +0000 Subject: [PATCH] Fix translations, center logo/footer, add tile zoom control - 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 --- services/web/app/views/user/login.pug | 2 +- .../components/project-list-lumiere.tsx | 66 ++++++++++++++++++- services/web/frontend/js/i18n.ts | 5 +- .../stylesheets/pages/login-register.scss | 15 ++++- .../pages/project-list-lumiere.scss | 47 ++++++++++++- 5 files changed, 127 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/user/login.pug b/services/web/app/views/user/login.pug index a6425d077b..1d4b35651f 100644 --- a/services/web/app/views/user/login.pug +++ b/services/web/app/views/user/login.pug @@ -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' diff --git a/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx b/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx index 39617178c2..e76c6ba989 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx @@ -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(() => { + 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} /> +
+ {ZOOM_OPTIONS.map(({ value, label }) => ( + + ))} +
{t('no_projects')}

) : ( -
+
{visibleProjects.map(project => ( ))} diff --git a/services/web/frontend/js/i18n.ts b/services/web/frontend/js/i18n.ts index 283b223c11..484cd6c87d 100644 --- a/services/web/frontend/js/i18n.ts +++ b/services/web/frontend/js/i18n.ts @@ -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', diff --git a/services/web/frontend/stylesheets/pages/login-register.scss b/services/web/frontend/stylesheets/pages/login-register.scss index cba50d01f6..f6f5c4b7fb 100644 --- a/services/web/frontend/stylesheets/pages/login-register.scss +++ b/services/web/frontend/stylesheets/pages/login-register.scss @@ -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; diff --git a/services/web/frontend/stylesheets/pages/project-list-lumiere.scss b/services/web/frontend/stylesheets/pages/project-list-lumiere.scss index 09a602a0df..f4fe9aa612 100644 --- a/services/web/frontend/stylesheets/pages/project-list-lumiere.scss +++ b/services/web/frontend/stylesheets/pages/project-list-lumiere.scss @@ -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;