feat(faith): Bible fallback for 1962 propers with propers view toggle
- Auto-fill missing vernacular propers from Allioli (DE) or DRB (EN) when the 1962 missal bundle lacks a translation, mapped per Latin slot via romcal's scriptureRef blocks (compound refs split 1-to-1 when segment count matches slot count). - Strip Psalm superscriptions and trailing periods so lookups parse and Bible text aligns with the Latin antiphon. - Localize the section reference header (Marc → Mk, Vulgate→Hebrew psalm shift for DE) instead of showing raw Latin. - Add Latin / Parallel / Vernacular view toggle with localStorage persistence; hide Allioli/DRB badge in Latin-only view. - Latin column now takes primary text color; vernacular secondary, matching the Prayer.svelte convention.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.39.0",
|
||||
"version": "1.40.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -63,4 +63,7 @@ export interface ProperSection {
|
||||
key: string;
|
||||
la: string[];
|
||||
local: string[];
|
||||
refs?: string[];
|
||||
refLabel?: string;
|
||||
localFromBible?: boolean;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,9 @@ export function translateRefToTarget(ref: string, lang: TargetLang): string | nu
|
||||
const m = ref.trim().match(/^(\d?\s?[A-Za-z]+\.?)\s*(\d.*)$/);
|
||||
if (!m) return null;
|
||||
const bookNorm = normalizeLatinBook(m[1]);
|
||||
const rest = m[2].trim().replace(/;.*$/, '').trim();
|
||||
// Strip trailing punctuation ("Marc 16:1-7." → "16:1-7") — bible.ts's
|
||||
// parseReference regex anchors on digits and refuses trailing periods.
|
||||
const rest = m[2].trim().replace(/;.*$/, '').replace(/[.\s]+$/, '').trim();
|
||||
const map = lookupLatinBook(bookNorm);
|
||||
const target = map?.[lang];
|
||||
if (!target) return null;
|
||||
@@ -132,3 +134,27 @@ export function translateRefToTarget(ref: string, lang: TargetLang): string | nu
|
||||
}
|
||||
return `${target} ${clean}`;
|
||||
}
|
||||
|
||||
// Translate the book name only, preserving the rest of the citation (including
|
||||
// compound refs with semicolons, verse ranges, etc.) for display purposes.
|
||||
// Strips trailing periods but keeps the structure readable.
|
||||
export function translateRefLabel(ref: string, lang: TargetLang | 'la'): string {
|
||||
const trimmed = ref.trim().replace(/[.\s]+$/, '');
|
||||
if (lang === 'la') return trimmed;
|
||||
const m = trimmed.match(/^(\d?\s?[A-Za-z]+\.?)\s*(.*)$/);
|
||||
if (!m) return trimmed;
|
||||
const bookNorm = normalizeLatinBook(m[1]);
|
||||
const map = lookupLatinBook(bookNorm);
|
||||
const target = map?.[lang];
|
||||
if (!target) return trimmed;
|
||||
const rest = m[2].trim();
|
||||
// Psalm numbering differs between Vulgate and Allioli — shift each
|
||||
// chapter reference found in the remainder.
|
||||
if (bookNorm === 'ps' && lang === 'de') {
|
||||
const shifted = rest.replace(/(^|[\s;,])(\d+)(?=[:,\s-])/g, (_, pre, ch) => {
|
||||
return `${pre}${mapPsalmChapter(parseInt(ch, 10), 'de')}`;
|
||||
});
|
||||
return `${target} ${shifted}`;
|
||||
}
|
||||
return `${target} ${rest}`;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ import type {
|
||||
Rite1962Commem,
|
||||
Rite1962Detail
|
||||
} from '../calendarTypes';
|
||||
import { getProperRefs, getProperRefsPerSlot } from './romcal1962Refs';
|
||||
import { fetchLocalFromBible, type FallbackLang } from './properBibleFallback';
|
||||
import { translateRefLabel } from './bibleRefLatin';
|
||||
|
||||
// romcal's package.json isn't exposed via its exports map, so resolve the
|
||||
// main entry instead and walk up until we hit the package root.
|
||||
@@ -340,6 +343,40 @@ function adaptDay1962(
|
||||
const laProps = findPropersFor(d, laBundle);
|
||||
const localProps = localBundle ? findPropersFor(d, localBundle) : undefined;
|
||||
const propers = sectionsFromBundle(laProps, localProps);
|
||||
const properKey = d.key1962 ?? d.id;
|
||||
const fallbackLang: FallbackLang | null =
|
||||
lang === 'en' ? 'en' : lang === 'de' ? 'de' : null;
|
||||
const labelLang: 'en' | 'de' | 'la' = lang === 'en' ? 'en' : lang === 'de' ? 'de' : 'la';
|
||||
for (const section of propers) {
|
||||
const refs = getProperRefs(d.kind1962, properKey, section.key);
|
||||
if (refs.length) {
|
||||
section.refs = refs;
|
||||
section.refLabel = translateRefLabel(refs[0], labelLang);
|
||||
}
|
||||
if (fallbackLang && section.local.length === 0 && refs.length) {
|
||||
const perSlot = getProperRefsPerSlot(
|
||||
d.kind1962,
|
||||
properKey,
|
||||
section.key,
|
||||
section.la.length
|
||||
);
|
||||
const localArr: string[] = new Array(section.la.length).fill('');
|
||||
let any = false;
|
||||
for (let i = 0; i < section.la.length; i++) {
|
||||
const slotRefs = perSlot[i];
|
||||
if (!slotRefs || slotRefs.length === 0) continue;
|
||||
const text = fetchLocalFromBible(slotRefs, fallbackLang);
|
||||
if (text) {
|
||||
localArr[i] = text;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any) {
|
||||
section.local = localArr;
|
||||
section.localFromBible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const detail: Rite1962Detail = {
|
||||
class: classOf,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { resolve } from 'path';
|
||||
import { translateRefToTarget } from './bibleRefLatin';
|
||||
import { lookupReference } from './bible';
|
||||
|
||||
export type FallbackLang = 'en' | 'de';
|
||||
|
||||
const TSV_PATH: Record<FallbackLang, string> = {
|
||||
en: 'static/drb.tsv',
|
||||
de: 'static/allioli.tsv'
|
||||
};
|
||||
|
||||
// Latin propers often cite successive verses in compact form like
|
||||
// "Ps 80:2; 80:3; 80:4; 80:5" (book implicit after the first segment) or
|
||||
// "Ps 32:12 ; 32:6". Expand each segment into a standalone "Book Chap:Verse"
|
||||
// reference so downstream parsing can handle them one at a time.
|
||||
export function splitCompoundRef(ref: string): string[] {
|
||||
const parts = ref
|
||||
.split(/\s*;\s*/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length <= 1) return parts;
|
||||
const bookMatch = parts[0].match(/^(\d?\s?[A-Za-z.]+)/);
|
||||
if (!bookMatch) return parts;
|
||||
const book = bookMatch[0].trim();
|
||||
return parts.map((p, i) => (i === 0 || !/^\d/.test(p) ? p : `${book} ${p}`));
|
||||
}
|
||||
|
||||
// Allioli (and DRB) include the Psalm superscription ("Zum Ende, ein Psalm
|
||||
// Davids.", "Unto the end, a psalm of David.") as the opening of verse 1.
|
||||
// The Latin propers always skip the title phrase, so strip it when our
|
||||
// fallback lookup pulls a Psalm verse 1 in.
|
||||
const PS_TITLE_DE = /^(Zum Ende|Ein (Psalm|Gebet|Lied|Lobpsalm|Gesang|Hymnus)|Eine Unterweisung|Aufschrift|Am (ersten|Sabbat)|Loblied|Psalm Davids|Dem Sangmeister)/;
|
||||
const PS_TITLE_EN = /^(Unto the end|A psalm|A canticle|A song|To the end|For the end|An instruction|Of David)/i;
|
||||
function stripPsalmSuperscription(text: string, lang: FallbackLang): string {
|
||||
const re = lang === 'de' ? PS_TITLE_DE : PS_TITLE_EN;
|
||||
if (!re.test(text)) return text;
|
||||
// Drop the first sentence (ends at first period/exclamation/question).
|
||||
return text.replace(/^[^.!?]*[.!?]\s*/, '').trim();
|
||||
}
|
||||
|
||||
export function fetchLocalFromBible(refs: string[], lang: FallbackLang): string | null {
|
||||
if (!refs || refs.length === 0) return null;
|
||||
const tsvPath = resolve(TSV_PATH[lang]);
|
||||
const collected: string[] = [];
|
||||
for (const rawRef of refs) {
|
||||
for (const seg of splitCompoundRef(rawRef)) {
|
||||
const translated = translateRefToTarget(seg, lang);
|
||||
if (!translated) continue;
|
||||
const lookup = lookupReference(translated, tsvPath);
|
||||
if (!lookup) continue;
|
||||
const isPsalm = /^Ps\s/i.test(translated);
|
||||
const parts = lookup.verses.map((v) => {
|
||||
if (isPsalm && v.verse === 1) return stripPsalmSuperscription(v.text, lang);
|
||||
return v.text;
|
||||
});
|
||||
const text = parts.join(' ').trim();
|
||||
if (text) collected.push(text);
|
||||
}
|
||||
}
|
||||
if (collected.length === 0) return null;
|
||||
return collected.join(' ');
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { createRequire } from 'module';
|
||||
import { splitCompoundRef } from './properBibleFallback';
|
||||
|
||||
type Block =
|
||||
| { type: 'text'; slot?: number; role?: string }
|
||||
| { type: 'scriptureRef'; ref: string }
|
||||
| { type: 'directive'; value: string }
|
||||
| { type: 'rubric'; note: string }
|
||||
| { type: 'ref'; target: string }
|
||||
| { type: 'separator' };
|
||||
|
||||
type Entry = Record<string, Block[]>;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function loadStructure(name: 'tempora' | 'sancti' | 'commune'): Record<string, Entry> {
|
||||
try {
|
||||
const pkgEntry = require.resolve('romcal/1962');
|
||||
const baseIdx = pkgEntry.indexOf('/rites/roman1962/');
|
||||
if (baseIdx < 0) return {};
|
||||
const dataPath =
|
||||
pkgEntry.slice(0, baseIdx) +
|
||||
`/rites/roman1962/data/propers/_structure/${name}.json`;
|
||||
return JSON.parse(readFileSync(dataPath, 'utf-8')) as Record<string, Entry>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const structures: Record<'tempora' | 'sancti' | 'commune', Record<string, Entry>> = {
|
||||
tempora: loadStructure('tempora'),
|
||||
sancti: loadStructure('sancti'),
|
||||
commune: loadStructure('commune')
|
||||
};
|
||||
|
||||
export function getProperRefs(
|
||||
kind: string | null | undefined,
|
||||
key: string,
|
||||
section: string
|
||||
): string[] {
|
||||
if (!kind) return [];
|
||||
const entry = structures[kind as keyof typeof structures]?.[key];
|
||||
if (!entry) return [];
|
||||
const blocks = entry[section];
|
||||
if (!blocks) return [];
|
||||
const refs: string[] = [];
|
||||
for (const b of blocks) {
|
||||
if (b.type === 'scriptureRef' && b.ref) refs.push(b.ref);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
// Distribute scripture refs across Latin text slots so Bible fallback aligns
|
||||
// row-for-row with the Latin. Each scriptureRef block applies to the text
|
||||
// slots that follow it (up to the next scriptureRef). When a compound ref
|
||||
// like "Ps. 117:24; 117:1" has the same number of segments as following
|
||||
// slots, segments map 1-to-1. Otherwise all segments collapse into the first
|
||||
// following slot so multi-verse antiphons stay intact.
|
||||
export function getProperRefsPerSlot(
|
||||
kind: string | null | undefined,
|
||||
key: string,
|
||||
section: string,
|
||||
slotCount: number
|
||||
): string[][] {
|
||||
const perSlot: string[][] = Array.from({ length: slotCount }, () => []);
|
||||
if (!kind || slotCount <= 0) return perSlot;
|
||||
const entry = structures[kind as keyof typeof structures]?.[key];
|
||||
if (!entry) return perSlot;
|
||||
const blocks = entry[section];
|
||||
if (!blocks) return perSlot;
|
||||
|
||||
let i = 0;
|
||||
while (i < blocks.length) {
|
||||
const b = blocks[i];
|
||||
if (b.type !== 'scriptureRef' || !b.ref) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const segs = splitCompoundRef(b.ref);
|
||||
const slotIdxs: number[] = [];
|
||||
let j = i + 1;
|
||||
while (j < blocks.length && blocks[j].type !== 'scriptureRef') {
|
||||
const nb = blocks[j];
|
||||
if (nb.type === 'text' && typeof nb.slot === 'number') slotIdxs.push(nb.slot);
|
||||
j++;
|
||||
}
|
||||
if (slotIdxs.length > 0) {
|
||||
if (segs.length === slotIdxs.length) {
|
||||
for (let k = 0; k < segs.length; k++) {
|
||||
const s = slotIdxs[k];
|
||||
if (s >= 0 && s < slotCount) perSlot[s].push(segs[k]);
|
||||
}
|
||||
} else {
|
||||
const first = slotIdxs[0];
|
||||
if (first >= 0 && first < slotCount) perSlot[first].push(...segs);
|
||||
}
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
return perSlot;
|
||||
}
|
||||
+175
-92
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { page } from '$app/state';
|
||||
import { browser } from '$app/environment';
|
||||
import {
|
||||
formatLongDate,
|
||||
getMonthName,
|
||||
hexFor,
|
||||
properLabel,
|
||||
t,
|
||||
t1962,
|
||||
@@ -22,7 +22,16 @@
|
||||
const iso = $derived(data.iso);
|
||||
const todayIso = $derived(data.todayIso);
|
||||
|
||||
const dayHex = $derived(hexFor(day.colorKeys));
|
||||
type PropersView = 'la' | 'parallel' | 'local';
|
||||
const VIEW_KEY = 'litcal.propersView';
|
||||
let propersView = $state<PropersView>('parallel');
|
||||
if (browser) {
|
||||
const saved = localStorage.getItem(VIEW_KEY);
|
||||
if (saved === 'la' || saved === 'parallel' || saved === 'local') propersView = saved;
|
||||
}
|
||||
$effect(() => {
|
||||
if (browser) localStorage.setItem(VIEW_KEY, propersView);
|
||||
});
|
||||
|
||||
function pad(n: number) {
|
||||
return String(n).padStart(2, '0');
|
||||
@@ -77,7 +86,7 @@
|
||||
|
||||
{#if day.rite1962}
|
||||
{@const d = day.rite1962}
|
||||
<section class="detail" style="--accent: {dayHex}">
|
||||
{#if d.vigilOf || d.octave || d.transferredFrom}
|
||||
<dl class="detail-extras">
|
||||
{#if d.vigilOf}
|
||||
<div>
|
||||
@@ -98,48 +107,84 @@
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
{#if d.commemorations.length}
|
||||
<div class="commems">
|
||||
<h4>{t1962('commemorations', lang)}</h4>
|
||||
<ul>
|
||||
{#each d.commemorations as c (c.id)}
|
||||
<li>
|
||||
<span class="commem-name">{c.name}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if d.propers.length}
|
||||
<section class="propers">
|
||||
{/if}
|
||||
{#if d.propers.length}
|
||||
<section class="propers">
|
||||
<div class="propers-head">
|
||||
<h4>{t1962('propers', lang)}</h4>
|
||||
{#each d.propers as section (section.key)}
|
||||
{@const rows = Math.max(section.la.length, section.local.length)}
|
||||
<div class="proper-block">
|
||||
<div class="proper-label-row">
|
||||
<span class="proper-label">{properLabel(section.key, lang)}</span>
|
||||
</div>
|
||||
{#if lang !== 'la'}
|
||||
<div class="view-toggle" role="group" aria-label={t1962('propers', lang)}>
|
||||
<button
|
||||
type="button"
|
||||
class="view-btn"
|
||||
class:active={propersView === 'la'}
|
||||
aria-pressed={propersView === 'la'}
|
||||
onclick={() => (propersView = 'la')}
|
||||
>
|
||||
{t1962('viewLatin', lang)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="view-btn"
|
||||
class:active={propersView === 'parallel'}
|
||||
aria-pressed={propersView === 'parallel'}
|
||||
onclick={() => (propersView = 'parallel')}
|
||||
>
|
||||
{t1962('viewParallel', lang)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="view-btn"
|
||||
class:active={propersView === 'local'}
|
||||
aria-pressed={propersView === 'local'}
|
||||
onclick={() => (propersView = 'local')}
|
||||
>
|
||||
{t1962('viewVernacular', lang)}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#each d.propers as section (section.key)}
|
||||
{@const rows = Math.max(section.la.length, section.local.length)}
|
||||
<article class="proper-card">
|
||||
<header class="proper-card-head">
|
||||
<span class="proper-label">{properLabel(section.key, lang)}</span>
|
||||
{#if section.refLabel}
|
||||
<span class="proper-ref">({section.refLabel})</span>
|
||||
{/if}
|
||||
{#if section.localFromBible && lang !== 'la' && propersView !== 'la'}
|
||||
<span class="proper-fallback" title={t1962('fallbackHint', lang)}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/></svg>
|
||||
{t1962('fallbackBadge', lang)}
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
<div class="proper-content">
|
||||
{#each Array(rows) as _, i (i)}
|
||||
{@const la = section.la[i] ?? ''}
|
||||
{@const local = section.local[i] ?? ''}
|
||||
{#if la || local}
|
||||
{@const isLaOnly = lang === 'la' || propersView === 'la'}
|
||||
{@const isLocalOnly = lang !== 'la' && propersView === 'local'}
|
||||
{@const showLa = isLaOnly || propersView === 'parallel' || (isLocalOnly && !local)}
|
||||
{@const showLocal = !isLaOnly && local && (propersView === 'parallel' || isLocalOnly)}
|
||||
{#if (showLa && la) || (showLocal && local)}
|
||||
<div class="proper-segment">
|
||||
<div class="proper-cols" class:single={lang === 'la' || !local}>
|
||||
{#if la}
|
||||
<div class="proper-cols" class:single={!(showLa && la && showLocal && local)}>
|
||||
{#if showLa && la}
|
||||
<div class="proper-col proper-col-la" lang="la">{la}</div>
|
||||
{/if}
|
||||
{#if lang !== 'la' && local}
|
||||
<div class="proper-col proper-col-local" lang={lang}>{local}</div>
|
||||
{#if showLocal && local}
|
||||
<div class="proper-col proper-col-local" {lang}>{local}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<nav class="back-nav">
|
||||
@@ -219,17 +264,8 @@
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.detail {
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-top: 1.25rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
|
||||
.detail-extras {
|
||||
margin: 0 0 0.5rem;
|
||||
margin: 1.25rem 0 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 0.5rem 1rem;
|
||||
@@ -250,7 +286,6 @@
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.commems h4,
|
||||
.propers h4 {
|
||||
margin: 0.5rem 0 0.4rem;
|
||||
font-size: 0.72rem;
|
||||
@@ -259,52 +294,106 @@
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.commems {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.commems ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.commems li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.commem-name {
|
||||
flex: 1 1 auto;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.propers {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.proper-block {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.proper-label-row {
|
||||
.propers-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.propers-head h4 {
|
||||
margin: 0;
|
||||
}
|
||||
.view-toggle {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: var(--radius-pill);
|
||||
gap: 2px;
|
||||
}
|
||||
.view-btn {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: var(--radius-pill);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
.view-btn:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.view-btn.active {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.proper-card {
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
padding: 1.1rem 1.35rem 1.25rem;
|
||||
margin-bottom: 0.9rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.proper-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.proper-card-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35rem 0.4rem;
|
||||
margin-bottom: 0.6rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.proper-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-secondary);
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
font-size: 0.78rem;
|
||||
color: var(--nord11);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.proper-ref {
|
||||
margin-left: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-tertiary);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
.proper-fallback {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
padding: 0.15rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--nord12);
|
||||
background: color-mix(in srgb, var(--nord12) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--nord12) 35%, transparent);
|
||||
border-radius: var(--radius-pill);
|
||||
letter-spacing: 0.02em;
|
||||
cursor: help;
|
||||
}
|
||||
.proper-fallback svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.proper-content {
|
||||
min-width: 0;
|
||||
}
|
||||
.proper-segment {
|
||||
margin-top: 0.5rem;
|
||||
@@ -319,13 +408,12 @@
|
||||
.proper-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 0.75rem;
|
||||
column-gap: 1.25rem;
|
||||
row-gap: 0;
|
||||
align-items: start;
|
||||
}
|
||||
.proper-col-la {
|
||||
grid-column: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
.proper-col-local {
|
||||
grid-column: 2;
|
||||
@@ -339,10 +427,13 @@
|
||||
}
|
||||
.proper-col {
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.5;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.proper-cols:not(.single) .proper-col-local {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.back-nav {
|
||||
margin-top: 1.5rem;
|
||||
@@ -359,12 +450,4 @@
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.detail-hero {
|
||||
padding: 1.15rem 1.2rem;
|
||||
}
|
||||
.hero-name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -288,7 +288,16 @@ export const ui1962 = {
|
||||
transferredFrom: { en: 'Transferred from', de: 'Übertragen von', la: 'Translatum ex' },
|
||||
source: { en: 'Source', de: 'Quelle', la: 'Fons' },
|
||||
propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' },
|
||||
stationChurch: { en: 'Station church', de: 'Stationskirche', la: 'Statio' }
|
||||
stationChurch: { en: 'Station church', de: 'Stationskirche', la: 'Statio' },
|
||||
viewLatin: { en: 'Latin', de: 'Latein', la: 'Latine' },
|
||||
viewParallel: { en: 'Parallel', de: 'Parallel', la: 'Parallelum' },
|
||||
viewVernacular: { en: 'English', de: 'Deutsch', la: 'Vernacula' },
|
||||
fallbackBadge: { en: 'Douay-Rheims', de: 'Allioli', la: 'Vulgata' },
|
||||
fallbackHint: {
|
||||
en: 'Translation not provided in the missal. Text taken from the Douay-Rheims Bible at the cited reference.',
|
||||
de: 'Keine Übersetzung im Messbuch vorhanden. Text aus der Allioli-Bibelübersetzung an der angegebenen Stelle.',
|
||||
la: 'Interpretatio localis deest. Textus ex Biblia Sacra locis citatis.'
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string {
|
||||
|
||||
Reference in New Issue
Block a user