From bf672a0b2061ce094a7cdcc500fb35e382b947bd Mon Sep 17 00:00:00 2001 From: Yurii M Date: Wed, 5 Aug 2026 14:07:32 +0300 Subject: [PATCH] Settle the newsletter signup into the page rhythm The signup was capped at 720px inside a 960px column, so it sat narrower than the docs and support panels above and below it and read as a foreign block wedged between them. It was also the tallest thing on that stretch of the page for an optional action, and its button carried a "primary" class that Infima does not define, so it rendered unstyled. Drop the inline width cap and let the form be a direct child of main, so it inherits the same column as its neighbours. Lay it out as one horizontal strip -- copy on the left, field and button on the right -- and take the surface, border and light-theme treatment straight from SupportSection so the three blocks read as one family in both themes. The consent sentence is the bulk of the old height, and it only matters to someone about to submit: it now unfolds on the first focus of the field and stays reachable to screen readers via aria-describedby, which keeps the resting state a single line. The eyebrow duplicated the heading in a smaller font, so it goes, along with its now-dead key in all six locales. Verified against a production build: identical computed background, border and radius to the adjacent support panel in dark and light, no horizontal overflow at 375px, and a 16px field so iOS does not zoom. Co-Authored-By: Claude Opus 5 --- src/components/Newsletter/SubscribeForm.tsx | 39 ++++++-- src/components/Newsletter/styles.module.css | 104 +++++++++++++------- src/i18n/locales/de.ts | 1 - src/i18n/locales/en.ts | 1 - src/i18n/locales/es.ts | 1 - src/i18n/locales/et.ts | 1 - src/i18n/locales/ru.ts | 1 - src/i18n/locales/ua.ts | 1 - src/pages/index.tsx | 4 +- 9 files changed, 100 insertions(+), 53 deletions(-) diff --git a/src/components/Newsletter/SubscribeForm.tsx b/src/components/Newsletter/SubscribeForm.tsx index 687062e..eacf5f1 100644 --- a/src/components/Newsletter/SubscribeForm.tsx +++ b/src/components/Newsletter/SubscribeForm.tsx @@ -16,6 +16,7 @@ export default function SubscribeForm(): React.JSX.Element { const {t, locale} = useI18n(); const [email, setEmail] = useState(''); const [status, setStatus] = useState('idle'); + const [consentVisible, setConsentVisible] = useState(false); const handleSubmit = useCallback( (event: React.FormEvent) => { @@ -44,18 +45,21 @@ export default function SubscribeForm(): React.JSX.Element { // list — the form must not answer "is this person subscribed?". if (status === 'sent') { return ( -
-

{t('newsletter.checkInbox')}

-

{t('newsletter.confirmHint')}

+
+
+

{t('newsletter.checkInbox')}

+

{t('newsletter.confirmHint')}

+
); } return ( -
-

{t('newsletter.eyebrow')}

-

{t('newsletter.title')}

-

{t('newsletter.description')}

+ +
+

{t('newsletter.title')}

+

{t('newsletter.description')}

+
@@ -92,7 +102,18 @@ export default function SubscribeForm(): React.JSX.Element {

) : null} -

{t('newsletter.consent')}

+ {/* + The strip stays one line until the visitor engages with it, but the + consent text must be readable before they can submit — so it unfolds on + the first focus of the field, and screen readers reach it either way + through aria-describedby. + */} +
); } diff --git a/src/components/Newsletter/styles.module.css b/src/components/Newsletter/styles.module.css index 93dd17c..d7b0cf3 100644 --- a/src/components/Newsletter/styles.module.css +++ b/src/components/Newsletter/styles.module.css @@ -1,48 +1,62 @@ -.panel { +/* + * A single slim strip, not a card: the home page already stacks two full-width + * panels (docs, support) and a third heavy block between them read as an + * interruption. Same border, radius family and surface as those panels so it + * sits in the rhythm instead of announcing itself. + */ +.bar { display: flex; - flex-direction: column; - gap: 0.4rem; - padding: 1.25rem 1.5rem; - border: 1px solid var(--ifm-color-emphasis-200); + flex-wrap: wrap; + align-items: center; + gap: 0.6rem 1.5rem; + padding: 0.85rem 1.25rem; + border: 1px solid var(--card-border); border-radius: 14px; - background: var(--ifm-background-surface-color); + background: rgba(255, 255, 255, 0.02); } -.eyebrow { - margin: 0; - font-size: 0.72rem; - letter-spacing: 0.08em; - text-transform: uppercase; - opacity: 0.6; +.text { + flex: 1 1 17rem; + min-width: 0; } .title { margin: 0; - font-size: 1.15rem; + font-size: 0.95rem; + font-weight: 600; + line-height: 1.3; } .note { - margin: 0; - font-size: 0.9rem; - opacity: 0.8; + margin: 0.15rem 0 0; + font-size: 0.78rem; + line-height: 1.4; + opacity: 0.65; } .row { display: flex; - gap: 0.6rem; - flex-wrap: wrap; - margin-top: 0.6rem; + align-items: center; + gap: 0.5rem; + margin-left: auto; } .input { - flex: 1 1 16rem; + flex: 1 1 13rem; min-width: 0; - padding: 0.55rem 0.8rem; - border: 1px solid var(--ifm-color-emphasis-300); - border-radius: 9px; - background: var(--ifm-background-color); + padding: 0.4rem 0.7rem; + border: 1px solid var(--card-border); + border-radius: 8px; + background: transparent; color: inherit; - font-size: 0.95rem; + /* iOS zooms the page in when a focused field is under 16px. */ + font-size: 1rem; +} + +@media (min-width: 601px) { + .input { + font-size: 0.875rem; + } } .input:focus { @@ -54,22 +68,26 @@ border-color: var(--ifm-color-danger); } -.error { - margin: 0.4rem 0 0; - font-size: 0.85rem; - color: var(--ifm-color-danger); +.submit { + white-space: nowrap; } -.successTitle { +/* Full-width lines below the strip, so they never widen the row itself. */ +.error, +.consent { + flex: 1 0 100%; margin: 0; - font-weight: 600; + line-height: 1.45; } -.consent { - margin: 0.7rem 0 0; +.error { font-size: 0.78rem; - opacity: 0.65; - line-height: 1.5; + color: var(--ifm-color-danger); +} + +.consent { + font-size: 0.7rem; + opacity: 0.55; } .srOnly { @@ -83,3 +101,19 @@ white-space: nowrap; border: 0; } + +@media (max-width: 700px) { + .row { + width: 100%; + margin-left: 0; + } +} + +:global(html[data-theme='light']) .bar { + background: var(--light-surface); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06); +} + +:global(html[data-theme='light']) .input { + background: var(--light-surface-strong, #fff); +} diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 93c6dd8..14c59f6 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -84,7 +84,6 @@ export const de: TranslationDict = { error: 'Gespeicherte Berechnungen konnten nicht geladen werden. Bitte erneut versuchen.', }, newsletter: { - eyebrow: 'Auf dem Laufenden bleiben', title: 'Neue Tools, direkt per E-Mail', description: 'Eine gelegentliche Nachricht, wenn ein neues Ingenieur-Tool erscheint oder ein bestehendes erweitert wird. Höchstens einmal im Monat.', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 16e1a5c..92df722 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -82,7 +82,6 @@ export const en = { error: 'Unable to load your saved calculations. Please try again.', }, newsletter: { - eyebrow: 'Stay updated', title: 'New tools, in your inbox', description: 'An occasional note when a new engineering utility ships or an existing one gains a feature. No more than once a month.', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 3f6196c..99b1aa0 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -84,7 +84,6 @@ export const es: TranslationDict = { error: 'No se pudieron cargar tus cálculos guardados. Inténtalo de nuevo.', }, newsletter: { - eyebrow: 'Mantente al día', title: 'Nuevas herramientas, en tu correo', description: 'Un aviso ocasional cuando se publica una nueva utilidad de ingeniería o una existente gana una función. No más de una vez al mes.', diff --git a/src/i18n/locales/et.ts b/src/i18n/locales/et.ts index e9c7d1f..c21a048 100644 --- a/src/i18n/locales/et.ts +++ b/src/i18n/locales/et.ts @@ -84,7 +84,6 @@ export const et: TranslationDict = { error: 'Salvestatud arvutusi ei õnnestunud laadida. Palun proovi uuesti.', }, newsletter: { - eyebrow: 'Ole kursis', title: 'Uued tööriistad sinu postkasti', description: 'Harv kiri, kui ilmub uus inseneritööriist või olemasolev saab uue võimaluse. Mitte sagedamini kui kord kuus.', diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 8c0bc99..a47dbd5 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -84,7 +84,6 @@ export const ru: TranslationDict = { error: 'Не удалось загрузить сохранённые расчёты. Попробуйте снова.', }, newsletter: { - eyebrow: 'Будьте в курсе', title: 'Новые инструменты — на вашу почту', description: 'Редкое письмо, когда выходит новая инженерная утилита или у существующей появляется функция. Не чаще раза в месяц.', diff --git a/src/i18n/locales/ua.ts b/src/i18n/locales/ua.ts index 8100d15..a5b9eec 100644 --- a/src/i18n/locales/ua.ts +++ b/src/i18n/locales/ua.ts @@ -84,7 +84,6 @@ export const ua: TranslationDict = { error: 'Не вдалося завантажити збережені розрахунки. Спробуйте знову.', }, newsletter: { - eyebrow: 'Будьте в курсі', title: 'Нові інструменти — на вашу пошту', description: 'Рідкий лист, коли виходить нова інженерна утиліта або наявна отримує функцію. Не частіше разу на місяць.', diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 5597892..b1fa013 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -383,9 +383,7 @@ export default function Home(): ReactNode { -
- -
+