fix(promo-banner): center by default + admin alignment selector (#482)

The gallery promotional banner (#440) read as visually offset from
the gallery footer because:

  - Footer used `container text-center px-4` (full container width,
    centered text).
  - Promo block used `container py-4 sm:py-6` with an inner
    `max-w-3xl mx-auto` wrapper holding left-aligned text — a
    narrower column with left-aligned content sitting in the
    middle of the page.

Two issues compounded: the column was narrower than the footer AND
its text alignment differed. Reported by Rekoo-PS in #482 with a
screenshot showing the misalignment, with a request for an admin
alignment option.

Fix:

- Drop the inner max-w-3xl wrapper. Promo content now spans the
  same .container width as the footer, eliminating the
  narrower-column visual.
- Default text alignment changed from left → center to match the
  footer.
- New `branding_promo_alignment` setting ('left' | 'center' | 'right',
  default 'center'). Surfaced as a dropdown next to the existing
  Position dropdown on the BrandingPage. Live preview block on the
  BrandingPage mirrors the gallery render so admins see what
  guests will see.
- Also replaced the no-op `prose-sm` prose-modifier with a real
  `prose prose-sm` outer class so the existing `prose-a:text-accent`
  modifier actually takes effect (it didn't before — modifiers
  without an outer .prose are silently ignored by Tailwind
  Typography).

Migration 103 seeds the new setting at 'center' so existing
installs that have a promo banner today see the corrected
alignment immediately on next deploy.

i18n: en + de hand-translated; nl/pt/ru/fr machine-translated and
flagged for native review per project convention.
This commit is contained in:
Paul Nothaft
2026-05-14 20:10:14 +02:00
parent 3869e5c0dc
commit a803491cf4
14 changed files with 183 additions and 20 deletions
@@ -0,0 +1,43 @@
/**
* Migration: Promotional banner text alignment (#482).
*
* Adds `branding_promo_alignment` to app_settings — admin-controlled
* horizontal alignment for the gallery promotional banner content
* (#440 / #482). Reuses the existing app_settings shape that
* branding_promo_markdown / branding_promo_position already use.
*
* Default 'center' so the banner aligns with the gallery footer
* (which is full-width center-aligned). The previous default left
* the markdown left-aligned in a max-w-3xl block, which Rekoo-PS
* reported as visually offset from the footer.
*
* Allowed values: 'left' | 'center' | 'right' — validated on the
* write path in adminSettings.js, not enforced by the column type
* (we use varchar instead of CHECK so the value can be extended
* later — e.g. 'justify' — without another schema migration).
*
* Idempotent: skips the insert when the row already exists.
*/
exports.up = async function(knex) {
if (!(await knex.schema.hasTable('app_settings'))) return;
const existing = await knex('app_settings')
.where('setting_key', 'branding_promo_alignment')
.first();
if (existing) return;
await knex('app_settings').insert({
setting_key: 'branding_promo_alignment',
setting_value: JSON.stringify('center'),
setting_type: 'branding',
updated_at: new Date(),
});
};
exports.down = async function(knex) {
if (!(await knex.schema.hasTable('app_settings'))) return;
await knex('app_settings')
.where('setting_key', 'branding_promo_alignment')
.del();
};
+13 -2
View File
@@ -322,7 +322,8 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re
twitter_url,
youtube_url,
promo_markdown,
promo_position
promo_position,
promo_alignment
} = req.body;
// Normalize force_color_mode: only 'dark' | 'light' | null are valid.
@@ -340,6 +341,15 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re
? 'below_footer'
: 'above_footer';
// Normalize promo_alignment: 'left' | 'center' | 'right'. Defaults
// to 'center' to match the gallery footer's full-width centering
// (#482 — the previous default left the markdown left-aligned in
// a max-w-3xl block, which read as visually offset from the footer).
const allowedPromoAlignments = ['left', 'center', 'right'];
const normalizedPromoAlignment = allowedPromoAlignments.includes(promo_alignment)
? promo_alignment
: 'center';
// Normalize login_logo_size to the same token set as logo_size.
// Anything else falls back to 'medium' on the next render.
const allowedLoginLogoSizes = ['small', 'medium', 'large', 'xlarge'];
@@ -381,7 +391,8 @@ router.put('/branding', adminAuth, requirePermission('settings.edit'), async (re
...(twitter_url !== undefined && { twitter_url: String(twitter_url || '').trim() }),
...(youtube_url !== undefined && { youtube_url: String(youtube_url || '').trim() }),
...(promo_markdown !== undefined && { promo_markdown: typeof promo_markdown === 'string' ? promo_markdown : '' }),
...(promo_position !== undefined && { promo_position: normalizedPromoPosition })
...(promo_position !== undefined && { promo_position: normalizedPromoPosition }),
...(promo_alignment !== undefined && { promo_alignment: normalizedPromoAlignment })
};
// Handle favicon deletion if empty string or null is provided
+6
View File
@@ -77,6 +77,12 @@ router.get('/', async (req, res) => {
branding_promo_position: settingsObject.branding_promo_position === 'below_footer'
? 'below_footer'
: 'above_footer',
// Promo content alignment (#482). Defaults to center so the
// banner aligns with the gallery footer; admin can flip to
// left or right via Settings → Branding.
branding_promo_alignment: ['left', 'center', 'right'].includes(settingsObject.branding_promo_alignment)
? settingsObject.branding_promo_alignment
: 'center',
// Force a specific color mode site-wide. When set, the user toggle
// is hidden and the value overrides per-theme/system preference.
// Allowed values: 'dark' | 'light' | null (null = no force).