diff --git a/apps/admin/app/domains/home-grid/HomeGridEditor.tsx b/apps/admin/app/domains/home-grid/HomeGridEditor.tsx index 6a4fda0..4772e55 100644 --- a/apps/admin/app/domains/home-grid/HomeGridEditor.tsx +++ b/apps/admin/app/domains/home-grid/HomeGridEditor.tsx @@ -131,25 +131,10 @@ export default function HomeGridEditor() { return (
- Leave a field empty to use the translation from the message files. Anything you type here overrides it for - that language only. -
-+ Leave a text field empty to use the translation from the message files. Anything you type here overrides it + for that language only. +
{BLOCKS.map((meta) => { const b = config.blocks[meta.id]; @@ -346,8 +331,11 @@ function PreviewFrame({ }, [config, ready]); const frameWidth = width === 'desktop' ? 1280 : width === 'tablet' ? 900 : 390; - // Tall enough for the whole section once the page has scrolled to it. - const frameHeight = width === 'mobile' ? 1900 : 1300; + // Real device heights. The mobile frame used to be 1900px tall, which is no + // phone that exists — and every `vh` rule on the page then resolved against + // it, so the preview showed a hero and a spacing nobody would ever get. A + // preview whose viewport is invented previews an invented layout. + const frameHeight = width === 'mobile' ? 844 : width === 'tablet' ? 1180 : 900; // Scaled down so a 1280px page fits the panel while keeping the real // breakpoints — resizing the iframe instead would change which media query // applies and preview the wrong layout. diff --git a/apps/web/app/components/StatsGrid.tsx b/apps/web/app/components/StatsGrid.tsx index 8a93817..8a2906e 100644 --- a/apps/web/app/components/StatsGrid.tsx +++ b/apps/web/app/components/StatsGrid.tsx @@ -16,11 +16,24 @@ import { r2url, type NewsData } from '../lib/api'; const PLAN_URL = 'https://www.bible.com/organizations/3f885b9c-404e-48be-8ad7-3d4e399560e7?utm_source=yvapp&utm_medium=share&utm_content=partner-page'; +// TEMPORARY. The share image QuoteShareModal renders is still being designed, +// so the affordance that advertises it is off. Set to true to bring it back — +// the button and the modal are both untouched underneath. +const SHOW_VERSE_SAVE = false; + // Card headlines vary wildly in length — the verse alone runs 21 to 112 // characters, and a single long word like "Grundlagen" is wider than a narrow // card at the sketch's 64px. Largest rung first; the fitter takes the first // that fits both the height and the width of its card. -const HEADLINE_SIZES = [64, 56, 48, 42, 36, 32, 28, 24, 20, 17]; +// +// The ladder has to reach far enough for the WORST case, not the average one: +// it used to stop at 17px, and the 112-character verse in a 156px-wide card at +// 360px still needed 24px more room than that. The card then grew past its +// min-height, and because the mobile columns are balanced by those heights +// (240 + 12 + 192 = 444 = 216 + 12 + 216) the two columns stopped ending level. +// The misalignment only showed on the hours when a long verse was up, which is +// what made it look intermittent. +const HEADLINE_SIZES = [64, 56, 48, 42, 36, 32, 28, 24, 20, 17, 15, 14, 13]; /** The photo the reading-plan card ships with, used until one is uploaded. */ const PLAN_FALLBACK_PHOTO = '/youversion-plan.webp'; @@ -97,22 +110,30 @@ export default function StatsGrid({ newsData, grid }: { newsData?: NewsData; gri const card = text.closest{eyebrow}
} -{t('verse.label')}
} @@ -305,18 +325,31 @@ export default function StatsGrid({ newsData, grid }: { newsData?: NewsData; gri {blocks.book.visible && ({pick(blocks.book.text[lang].label, tr('book.label'))}
+{pick(blocks.book.text[lang].label, t('songs.label'))}
)}- {pick(blocks.book.text[lang].title, bookTitle)} -
+ {/* With a photo the picture says "songbooks" and a line of + type on top only competes with it, so the headline sits + out. Without one the card would be a label on an empty + rectangle — which is what the default config gives — so + the headline comes back. An override always wins. */} + {blocks.book.text[lang].title.trim() !== '' ? ( ++ {blocks.book.text[lang].title} +
+ ) : ( + !bookPhoto && ( ++ {t.rich('songs.title', { em, br })} +
+ ) + )} {blocks.book.showButton && blocks.book.text[lang].button && ( {blocks.book.text[lang].button} )} diff --git a/apps/web/app/styles/stats-grid.css b/apps/web/app/styles/stats-grid.css index 4b08c7c..5426f61 100644 --- a/apps/web/app/styles/stats-grid.css +++ b/apps/web/app/styles/stats-grid.css @@ -43,35 +43,6 @@ margin: 0 auto; } -.stats__head { - max-width: 800px; - margin: 0 auto 64px; - text-align: center; -} - -.stats__eyebrow { - margin: 0 0 24px; - font: - 600 12px/1 var(--stats-sans); - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--stats-muted); -} - -.stats__title { - margin: 0; - font: - 400 56px/1.1 var(--stats-serif); - color: var(--stats-text); - letter-spacing: -0.02em; - text-wrap: balance; -} - -.stats__title em { - color: var(--stats-accent); - font-style: italic; -} - /* ── Grid ── */ .stats__grid { display: grid; @@ -113,18 +84,27 @@ border-color: var(--stats-line-hover); } -/* Height modifiers — see the arithmetic in the header comment */ +/* Height modifiers — see the arithmetic in the header comment. + Each height is one number applied as both bounds, so a card is a container + the text must fit into rather than a floor the text can push past. A card + that grows drags its column out of line with the other two. */ .stats__card--tall { - min-height: 724px; + --stats-card-h: 724px; } .stats__card--media { - min-height: 420px; + --stats-card-h: 420px; } .stats__card--short { - min-height: 280px; + --stats-card-h: 280px; +} +.stats__card--mid { + --stats-card-h: 350px; } +.stats__card--tall, +.stats__card--media, +.stats__card--short, .stats__card--mid { - min-height: 350px; + height: var(--stats-card-h); } .stats__card-body { @@ -246,6 +226,11 @@ gap: 0.6em; margin-top: 28px; padding: 13px 24px; + /* A pill that wraps stops being a pill. With multi-word labels the text + broke over three lines inside the capsule on a 360px screen and the whole + shape grew past the card edge. The label stays on one line; keeping it + short enough to fit is the label's job, not the layout's. */ + white-space: nowrap; border: 1px solid var(--stats-line); border-radius: 999px; font: @@ -388,17 +373,16 @@ .stats__col:nth-child(3) { grid-column: auto; } - /* Banner needs to read as solid as the cards below it */ + /* Banner needs to read as solid as the cards below it. Sets the variable, + not min-height — a stray min-height outranks the fixed height whenever it + is the larger of the two and quietly puts the card back to variable. */ .stats__card--tall { - min-height: 350px; + --stats-card-h: 350px; } /* Wide and short — the phone in the photo needs to stay in frame */ .stats__card-photo { object-position: 58% 30%; } - .stats__title { - font-size: 48px; - } } /* ── Responsive — mobile: banner on top, two columns beneath ── */ @@ -406,6 +390,14 @@ .stats { padding: 48px 16px; } + /* Two cards sit side by side here, so a pill has roughly 140px to live in. + The desktop padding leaves no room for the label and the arrow. */ + .stats__btn { + margin-top: 18px; + padding: 10px 16px; + font-size: 13px; + gap: 0.45em; + } .stats__grid { grid-template-columns: repeat(2, 1fr); gap: 12px; @@ -421,22 +413,34 @@ grid-column: span 1; } - .stats__card--tall { - min-height: 280px; - } /* 240 + 12 + 192 = 444 = 216 + 12 + 216. The short card is 192 rather than the 180 it started at: at 180 column 2 came to 432 against column 3's 444, the grid stretched it to match, and its last card ended 12px above the neighbouring one. The same arithmetic that aligns the desktop columns has - to hold at every breakpoint. */ + to hold at every breakpoint. + + Each height is one number on --stats-card-h. min-height alone was a floor: + content that outgrew it — a 112-character verse in a 156px card — pushed + the card taller and the two columns stopped ending level. A fixed height + makes the card a container the text has to fit into, which is what the + fitter in StatsGrid is for. */ + .stats__card--tall { + --stats-card-h: 280px; + } .stats__card--media { - min-height: 240px; + --stats-card-h: 240px; } .stats__card--short { - min-height: 192px; + --stats-card-h: 192px; } .stats__card--mid { - min-height: 216px; + --stats-card-h: 216px; + } + .stats__card--tall, + .stats__card--media, + .stats__card--short, + .stats__card--mid { + height: var(--stats-card-h); } .stats__card { @@ -444,9 +448,6 @@ gap: 24px; } - .stats__title { - font-size: 32px; - } .stats__card-label { font-size: 11px; } diff --git a/docs/frontend.md b/docs/frontend.md index 95985f8..536e842 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -67,6 +67,18 @@ col 3 350 + 24 + 350 = 724 **Verse emphasis.** `splitVerse()` in [lib/verses.ts](../apps/web/app/lib/verses.ts) picks one word to mark: a concept (`Liebe`, `Gnade`, `Hirte`, `erquicken`, …) first, the divine name only if the verse carries no concept, and the longest word as a last resort. Concepts come first because `Gott` appears in most verses — marking it every hour marks nothing. Across the 32 German verses this picks 25 distinct words. The mark is a hand-drawn SVG stroke, not a rectangle. +**No section heading.** The five cards speak for themselves; there is no eyebrow and no `