feat(faith): adopt flat-id romcal fork and simplify 1962 calendar rendering
Switch the romcal dependency to AlexBocken/romcal (monorepo fork with
collapsed bucket prefixes) and strip the runtime prefix-fallback chain
from liturgicalCalendar.ts — name/propers lookups now use a single
flat id. The 1962 data model shrinks to just what the rendering uses
(commem {id,name}, detail carrying propers as {key, la[], local[]})
and the detail + overview pages drop the rubrics/octave/properSource
fields that never got wired in.
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.35.0",
|
||||
"version": "1.35.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -62,7 +62,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"mongoose": "^9.4.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"romcal": "github:AlexBocken/romcal1962#dev",
|
||||
"romcal": "github:AlexBocken/romcal#dev",
|
||||
"sharp": "^0.34.5",
|
||||
"web-haptics": "^0.0.6"
|
||||
},
|
||||
|
||||
Generated
+8
-8
@@ -22,7 +22,7 @@ importers:
|
||||
version: 2.1.8-no-fsevents.3
|
||||
'@romcal/calendar.general-roman':
|
||||
specifier: 3.0.0-dev.125
|
||||
version: 3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2))
|
||||
version: 3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2))
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4(@sveltejs/kit@2.56.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@types/node@22.18.0)(esbuild@0.27.3)(terser@5.46.0)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.1)(@types/node@22.18.0)(esbuild@0.27.3)(terser@5.46.0)))
|
||||
@@ -54,8 +54,8 @@ importers:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
romcal:
|
||||
specifier: github:AlexBocken/romcal1962#dev
|
||||
version: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2)
|
||||
specifier: github:AlexBocken/romcal#dev
|
||||
version: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2)
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
@@ -1782,8 +1782,8 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d:
|
||||
resolution: {tarball: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d}
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde:
|
||||
resolution: {tarball: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde}
|
||||
version: 3.0.0-dev.125
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
@@ -2683,9 +2683,9 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@romcal/calendar.general-roman@3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2))':
|
||||
'@romcal/calendar.general-roman@3.0.0-dev.125(romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2))':
|
||||
dependencies:
|
||||
romcal: https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2)
|
||||
romcal: https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2)
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
@@ -3545,7 +3545,7 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal1962/tar.gz/c353a43b08514f4af99e3a6796042155557f023d(typescript@6.0.2):
|
||||
romcal@https://codeload.github.com/AlexBocken/romcal/tar.gz/efbaab5813ecbdd4956d357c3f3a9bf06b6e1dde(typescript@6.0.2):
|
||||
dependencies:
|
||||
i18next: 26.0.4(typescript@6.0.2)
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -35,52 +35,25 @@ export interface SeasonArc {
|
||||
}
|
||||
|
||||
export interface Rite1962Commem {
|
||||
key: string;
|
||||
id: string;
|
||||
name: string;
|
||||
rankName: string;
|
||||
kind: 'tempora' | 'sancti';
|
||||
colorNames: string[];
|
||||
colorKeys: string[];
|
||||
}
|
||||
|
||||
export interface Rite1962Detail {
|
||||
class: 1 | 2 | 3 | 4;
|
||||
kind: 'tempora' | 'sancti';
|
||||
commemorations: Rite1962Commem[];
|
||||
rubrics: {
|
||||
gloria: boolean;
|
||||
credo: boolean;
|
||||
preface?: string;
|
||||
lastGospel?: string;
|
||||
ite?: string;
|
||||
};
|
||||
octave?: {
|
||||
id: string;
|
||||
parentFeastId: string;
|
||||
ofId: string;
|
||||
day: number;
|
||||
rank: string;
|
||||
};
|
||||
vigilOf?: string;
|
||||
transferredFrom?: string;
|
||||
properSource: string;
|
||||
communeSlug?: string;
|
||||
propers: ProperSection[];
|
||||
extraSections: ProperSection[];
|
||||
}
|
||||
|
||||
export interface ProperSegment {
|
||||
refs: string[];
|
||||
la: string;
|
||||
local?: string;
|
||||
// When true, `local` text comes from the Bible translation lookup because
|
||||
// the propers dataset had no localized text for this segment.
|
||||
fromBible?: boolean;
|
||||
}
|
||||
|
||||
export interface ProperSection {
|
||||
key: string;
|
||||
segments: ProperSegment[];
|
||||
// Aggregate list of refs across segments (for quick checks)
|
||||
refs: string[];
|
||||
fromBible?: boolean;
|
||||
la: string[];
|
||||
local: string[];
|
||||
}
|
||||
|
||||
@@ -19,14 +19,11 @@ import {
|
||||
Switzerland_Saint_Maurice_Abbey,
|
||||
Switzerland_Sankt_Gallen,
|
||||
Switzerland_Sion,
|
||||
resolvePropersBlocks
|
||||
} from 'romcal/1962';
|
||||
import type {
|
||||
LiturgicalDay1962,
|
||||
MassPropersBlocks,
|
||||
MassSectionField,
|
||||
PropersBlock
|
||||
createI18n1962
|
||||
} from 'romcal/1962';
|
||||
import type { LiturgicalDay1962, RomcalBundle1962 } from 'romcal/1962';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import {
|
||||
colorLabel1962,
|
||||
rank1962Label,
|
||||
@@ -38,13 +35,9 @@ import {
|
||||
import type {
|
||||
CalendarDay,
|
||||
ProperSection,
|
||||
ProperSegment,
|
||||
Rite1962Commem,
|
||||
Rite1962Detail
|
||||
} from '../calendarTypes';
|
||||
import { lookupReference } from '$lib/server/bible';
|
||||
import { translateRefToTarget } from '$lib/server/bibleRefLatin';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
const bundles1969: Record<Diocese1969, Record<CalendarLang, RomcalBundleObject>> = {
|
||||
general: { en: GeneralRoman_En, de: GeneralRoman_De, la: GeneralRoman_La },
|
||||
@@ -56,7 +49,7 @@ function getRomcal(lang: CalendarLang, diocese: Diocese1969): Romcal {
|
||||
const key = `${diocese}|${lang}`;
|
||||
let r = romcalByKey.get(key);
|
||||
if (r) return r;
|
||||
r = new Romcal({ localizedCalendar: bundles1969[diocese][lang] });
|
||||
r = new Romcal({ localizedCalendar: bundles1969[diocese][lang], scope: 'liturgical' });
|
||||
romcalByKey.set(key, r);
|
||||
return r;
|
||||
}
|
||||
@@ -111,307 +104,183 @@ const calendars1962 = {
|
||||
sion: Switzerland_Sion
|
||||
} as const satisfies Record<Diocese1962, unknown>;
|
||||
|
||||
const romcal1962ByKey = new Map<string, Romcal1962>();
|
||||
function getRomcal1962(lang: CalendarLang, diocese: Diocese1962): Romcal1962 {
|
||||
const key = `${diocese}|${lang}`;
|
||||
let r = romcal1962ByKey.get(key);
|
||||
if (r) return r;
|
||||
// Package localizes celebration.name via i18next; structured proper
|
||||
// blocks are fetched lazily via resolvePropersBlocks (below), so we
|
||||
// don't ask attachPropers to pre-concatenate locale text.
|
||||
const calendar = calendars1962[diocese];
|
||||
r = calendar
|
||||
? new Romcal1962({ localeId: lang, calendar })
|
||||
: new Romcal1962({ localeId: lang });
|
||||
romcal1962ByKey.set(key, r);
|
||||
return r;
|
||||
// Names & propers for the 1962 rite ship as bundled JS files outside the
|
||||
// package's `exports` map, so resolve them via an absolute file URL.
|
||||
const bundle1962Cache = new Map<CalendarLang, Promise<RomcalBundle1962>>();
|
||||
function loadBundle1962(lang: CalendarLang): Promise<RomcalBundle1962> {
|
||||
let p = bundle1962Cache.get(lang);
|
||||
if (p) return p;
|
||||
const abs = resolvePath(
|
||||
`node_modules/romcal/rites/roman1962/dist/bundles/${lang}/esm/index.js`
|
||||
);
|
||||
p = import(/* @vite-ignore */ pathToFileURL(abs).href).then(
|
||||
(m) => m.default as RomcalBundle1962
|
||||
);
|
||||
bundle1962Cache.set(lang, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
const PROPER_ORDER: MassSectionField[] = [
|
||||
'introit',
|
||||
'collect',
|
||||
'epistle',
|
||||
'gradual',
|
||||
'alleluia',
|
||||
'tract',
|
||||
'sequence',
|
||||
'gospel',
|
||||
'offertory',
|
||||
'secret',
|
||||
'preface',
|
||||
'communion',
|
||||
'postcommunion'
|
||||
];
|
||||
const romcal1962ByKey = new Map<string, Promise<Romcal1962>>();
|
||||
function getRomcal1962(lang: CalendarLang, diocese: Diocese1962): Promise<Romcal1962> {
|
||||
const key = `${diocese}|${lang}`;
|
||||
let p = romcal1962ByKey.get(key);
|
||||
if (p) return p;
|
||||
const calendar = calendars1962[diocese];
|
||||
// `localizedCalendar` must be a 1969-shape bundle; the 1962 names live on
|
||||
// the 1962 propers bundle and are injected via createI18n1962 extraNames.
|
||||
const base1969 = bundles1969.general[lang];
|
||||
p = loadBundle1962(lang).then((b) => {
|
||||
const i18next = createI18n1962(lang, { [lang]: b.i18n.names });
|
||||
// `i18next` is part of Romcal's runtime config but absent from the
|
||||
// published input type. Build via a permissive record so TS accepts it.
|
||||
const base: Record<string, unknown> = {
|
||||
i18next,
|
||||
localizedCalendar: base1969,
|
||||
scope: 'liturgical'
|
||||
};
|
||||
if (calendar) base.particularCalendar = calendar;
|
||||
return new Romcal1962(base as ConstructorParameters<typeof Romcal1962>[0]);
|
||||
});
|
||||
romcal1962ByKey.set(key, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
const COLOR_KEY_1962: Record<string, string> = {
|
||||
White: 'WHITE',
|
||||
Red: 'RED',
|
||||
Green: 'GREEN',
|
||||
Violet: 'PURPLE',
|
||||
Black: 'BLACK',
|
||||
Rose: 'ROSE'
|
||||
};
|
||||
// Section order follows the flow of the Mass. The new propers bundles key
|
||||
// sections by their Latin section names.
|
||||
const PROPER_ORDER = [
|
||||
'Introitus',
|
||||
'Oratio',
|
||||
'Lectio',
|
||||
'Graduale',
|
||||
'GradualeF',
|
||||
'Tractus',
|
||||
'Sequentia',
|
||||
'Evangelium',
|
||||
'Offertorium',
|
||||
'Secreta',
|
||||
'Communio',
|
||||
'Postcommunio'
|
||||
] as const;
|
||||
const PROPER_ORDER_SET: ReadonlySet<string> = new Set<string>(PROPER_ORDER);
|
||||
|
||||
const RANK_FROM_CLASS_1962: Record<1 | 2 | 3 | 4, string> = {
|
||||
1: 'SOLEMNITY',
|
||||
2: 'FEAST',
|
||||
3: 'MEMORIAL',
|
||||
4: 'WEEKDAY'
|
||||
1: 'ClassI',
|
||||
2: 'ClassII',
|
||||
3: 'ClassIII',
|
||||
4: 'ClassIV'
|
||||
};
|
||||
|
||||
function colorKeysFrom(c: LiturgicalDay1962): string[] {
|
||||
return c.colors.map((col) => COLOR_KEY_1962[col] ?? col.toUpperCase());
|
||||
// romcal 3 returns SCREAMING_SNAKE color keys ("WHITE", "ROSE", ...) which
|
||||
// already match our legend/CSS tokens.
|
||||
return c.colors ? [...c.colors] : [];
|
||||
}
|
||||
|
||||
function adaptCommem(c: LiturgicalDay1962, lang: CalendarLang): Rite1962Commem {
|
||||
const colorKeys = colorKeysFrom(c);
|
||||
return {
|
||||
key: c.key,
|
||||
name: c.name,
|
||||
rankName: rank1962Label(c.rank1962, lang),
|
||||
kind: c.kind,
|
||||
colorKeys,
|
||||
colorNames: colorKeys.map((k) => colorLabel1962(k, lang))
|
||||
};
|
||||
function buildCommemorations(d: LiturgicalDay1962): Rite1962Commem[] {
|
||||
return (d.commemorations ?? []).map((c) => ({ id: c.id, name: c.name }));
|
||||
}
|
||||
|
||||
function bibleTextFor(ref: string, targetLang: 'en' | 'de'): string | null {
|
||||
const tsvPath = resolvePath(targetLang === 'de' ? 'static/allioli.tsv' : 'static/drb.tsv');
|
||||
const segments = ref.split(';').map((s) => s.trim()).filter(Boolean);
|
||||
if (!segments.length) return null;
|
||||
|
||||
let lastBook: string | null = null;
|
||||
let lastChapter: string | null = null;
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const seg of segments) {
|
||||
// Detect optional leading book (letters, optional leading digit like "1 Cor")
|
||||
const bookMatch = seg.match(/^(\d?\s?[A-Za-z]+\.?)\s+(.*)$/);
|
||||
let book: string | null = null;
|
||||
let rest = seg;
|
||||
if (bookMatch) {
|
||||
book = bookMatch[1];
|
||||
rest = bookMatch[2].trim();
|
||||
}
|
||||
if (book) lastBook = book;
|
||||
if (!lastBook) continue;
|
||||
|
||||
let chapter: string;
|
||||
let verseRange: string;
|
||||
// Accept "118:85", "118, 85", "118:85-90", or bare "85" (inherit chapter)
|
||||
const normalized = rest.replace(/\s*,\s*/, ':').replace(/\s+/g, ' ').trim();
|
||||
if (normalized.includes(':')) {
|
||||
const [c, v] = normalized.split(':');
|
||||
chapter = c.trim();
|
||||
verseRange = v.trim();
|
||||
lastChapter = chapter;
|
||||
} else if (lastChapter) {
|
||||
chapter = lastChapter;
|
||||
verseRange = normalized;
|
||||
} else continue;
|
||||
|
||||
const fullRef = `${lastBook} ${chapter}:${verseRange}`;
|
||||
const translated = translateRefToTarget(fullRef, targetLang);
|
||||
if (!translated) continue;
|
||||
|
||||
try {
|
||||
const result = lookupReference(translated, tsvPath);
|
||||
if (result && result.verses.length) {
|
||||
parts.push(result.verses.map((v) => `${v.verse}. ${v.text}`).join(' '));
|
||||
}
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length ? parts.join(' ') : null;
|
||||
}
|
||||
|
||||
interface RawSegment {
|
||||
refs: string[];
|
||||
la: string;
|
||||
local: string;
|
||||
}
|
||||
|
||||
// Zip la / local text streams by index so la[i] and local[i] land in
|
||||
// the same segment. Scripture refs attach to the la block that follows
|
||||
// them; trailing refs with no following la block attach to the last
|
||||
// segment so they still render.
|
||||
function buildSegments(items: PropersBlock, localLang: CalendarLang): RawSegment[] {
|
||||
const la: string[] = [];
|
||||
const local: string[] = [];
|
||||
const refsByIdx = new Map<number, string[]>();
|
||||
let pendingRefs: string[] = [];
|
||||
|
||||
for (const it of items) {
|
||||
if (it.type === 'scriptureRef') {
|
||||
pendingRefs.push(it.ref);
|
||||
} else if (it.type === 'text') {
|
||||
const val = it.value.trim();
|
||||
if (!val) continue;
|
||||
if (it.lang === 'la') {
|
||||
if (pendingRefs.length) {
|
||||
refsByIdx.set(la.length, pendingRefs);
|
||||
pendingRefs = [];
|
||||
}
|
||||
la.push(val);
|
||||
} else if (it.lang === localLang) {
|
||||
local.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingRefs.length && la.length) {
|
||||
const lastIdx = la.length - 1;
|
||||
const existing = refsByIdx.get(lastIdx) ?? [];
|
||||
refsByIdx.set(lastIdx, [...existing, ...pendingRefs]);
|
||||
}
|
||||
|
||||
const count = Math.max(la.length, local.length);
|
||||
const segs: RawSegment[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
segs.push({ refs: refsByIdx.get(i) ?? [], la: la[i] ?? '', local: local[i] ?? '' });
|
||||
}
|
||||
return segs;
|
||||
}
|
||||
|
||||
function propersOf(sections: MassPropersBlocks, lang: CalendarLang): ProperSection[] {
|
||||
const out: ProperSection[] = [];
|
||||
for (const key of PROPER_ORDER) {
|
||||
const block = sections[key];
|
||||
if (!block || !block.length) continue;
|
||||
const rawSegs = buildSegments(block, lang);
|
||||
if (!rawSegs.length) continue;
|
||||
|
||||
const segments: ProperSegment[] = [];
|
||||
const allRefs: string[] = [];
|
||||
let sectionFromBible = false;
|
||||
|
||||
for (const raw of rawSegs) {
|
||||
const seg: ProperSegment = { refs: raw.refs, la: raw.la };
|
||||
if (lang !== 'la' && raw.local) seg.local = raw.local;
|
||||
|
||||
// Bible fallback: only for this segment, using only its own refs
|
||||
if (!seg.local && raw.refs.length && lang !== 'la') {
|
||||
const bible = bibleTextFor(raw.refs.join('; '), lang);
|
||||
if (bible) {
|
||||
seg.local = bible;
|
||||
seg.fromBible = true;
|
||||
sectionFromBible = true;
|
||||
}
|
||||
}
|
||||
|
||||
allRefs.push(...raw.refs);
|
||||
if (seg.la || seg.local || seg.refs.length) segments.push(seg);
|
||||
}
|
||||
|
||||
if (!segments.length) continue;
|
||||
const section: ProperSection = { key, segments, refs: allRefs };
|
||||
if (sectionFromBible) section.fromBible = true;
|
||||
out.push(section);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extraSectionsOf(
|
||||
extras: Record<string, PropersBlock>,
|
||||
lang: CalendarLang
|
||||
function sectionsFromBundle(
|
||||
laPropers: Record<string, string[]> | undefined,
|
||||
localPropers: Record<string, string[]> | undefined
|
||||
): ProperSection[] {
|
||||
const out: ProperSection[] = [];
|
||||
for (const [key, block] of Object.entries(extras)) {
|
||||
const buckets: Record<string, string[]> = {};
|
||||
const refs: string[] = [];
|
||||
for (const item of block) {
|
||||
if (item.type === 'text') (buckets[item.lang] ??= []).push(item.value);
|
||||
else if (item.type === 'scriptureRef') refs.push(item.ref);
|
||||
}
|
||||
const la = (buckets['la'] ?? []).join('\n\n').trim();
|
||||
const local = lang === 'la' ? '' : (buckets[lang] ?? []).join('\n\n').trim();
|
||||
if (!la && !local && refs.length === 0) continue;
|
||||
const segment: ProperSegment = { refs, la };
|
||||
if (local) segment.local = local;
|
||||
out.push({ key, segments: [segment], refs });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Primary celebration may only carry the sections that override the commune
|
||||
// (e.g. only `gospel` for a confessor). Merge commune sections under the
|
||||
// primary so every liturgical slot has a text source.
|
||||
function mergeCommunePropers(
|
||||
p: LiturgicalDay1962,
|
||||
sections: MassPropersBlocks,
|
||||
extraSections: Record<string, PropersBlock>
|
||||
): { sections: MassPropersBlocks; extraSections: Record<string, PropersBlock> } {
|
||||
const slug = p.properRef.communeSlug;
|
||||
if (!slug) return { sections, extraSections };
|
||||
const communeCelebration = {
|
||||
...p,
|
||||
properRef: { source: `commune/${slug}` }
|
||||
} as LiturgicalDay1962;
|
||||
let communeResolved;
|
||||
try {
|
||||
communeResolved = resolvePropersBlocks(communeCelebration);
|
||||
} catch {
|
||||
return { sections, extraSections };
|
||||
}
|
||||
const mergedSections = { ...communeResolved.sections, ...sections } as MassPropersBlocks;
|
||||
const mergedExtras = { ...communeResolved.extraSections, ...extraSections };
|
||||
return { sections: mergedSections, extraSections: mergedExtras };
|
||||
}
|
||||
|
||||
function adaptDay1962(entries: LiturgicalDay1962[], lang: CalendarLang): CalendarDay {
|
||||
const p = entries[0];
|
||||
const commemorations = entries.slice(1);
|
||||
const colorKeys = colorKeysFrom(p);
|
||||
const colorNames = colorKeys.map((k) => colorLabel1962(k, lang));
|
||||
const resolved = resolvePropersBlocks(p);
|
||||
const { sections, extraSections } = mergeCommunePropers(
|
||||
p,
|
||||
resolved.sections,
|
||||
resolved.extraSections
|
||||
);
|
||||
const detail: Rite1962Detail = {
|
||||
class: p.classOf1962,
|
||||
kind: p.kind,
|
||||
commemorations: commemorations.map((c) => adaptCommem(c, lang)),
|
||||
rubrics: {
|
||||
gloria: p.rubrics.gloria,
|
||||
credo: p.rubrics.credo,
|
||||
preface: p.rubrics.preface,
|
||||
lastGospel: p.rubrics.lastGospel,
|
||||
ite: p.rubrics.ite
|
||||
},
|
||||
...(p.octave
|
||||
? {
|
||||
octave: {
|
||||
id: p.octave.id,
|
||||
parentFeastId: p.octave.parentFeastId,
|
||||
day: p.octave.day,
|
||||
rank: p.octave.rank
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(p.vigil ? { vigilOf: p.vigil.of } : {}),
|
||||
...(p.transferredFromDate ? { transferredFrom: p.transferredFromDate } : {}),
|
||||
properSource: p.properRef.source,
|
||||
...(p.properRef.communeSlug ? { communeSlug: p.properRef.communeSlug } : {}),
|
||||
propers: propersOf(sections, lang),
|
||||
extraSections: extraSectionsOf(extraSections, lang)
|
||||
if (!laPropers && !localPropers) return [];
|
||||
const sections: ProperSection[] = [];
|
||||
const seen = new Set<string>();
|
||||
const emit = (key: string) => {
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
const la = laPropers?.[key];
|
||||
const local = localPropers?.[key];
|
||||
if ((!la || !la.length) && (!local || !local.length)) return;
|
||||
sections.push({ key, la: la ? [...la] : [], local: local ? [...local] : [] });
|
||||
};
|
||||
// Pentecost octave (Pentecost Sunday + 6 days) is carved out of Paschaltide so
|
||||
// it shows as its own arc in the year ring, mirroring the Easter Week octave.
|
||||
const isPentecostWeek = typeof p.key === 'string' && p.key.startsWith('easter_time_7_');
|
||||
const seasonKey = isPentecostWeek ? 'Pentecost' : p.season ?? null;
|
||||
const seasonNames = seasonKey ? [season1962Label(seasonKey, lang)] : [];
|
||||
for (const key of PROPER_ORDER) emit(key);
|
||||
// Emit any remaining sections (ember-day readings, extras) in source order.
|
||||
const extraKeys = new Set<string>();
|
||||
for (const k of Object.keys(laPropers ?? {})) if (!PROPER_ORDER_SET.has(k)) extraKeys.add(k);
|
||||
for (const k of Object.keys(localPropers ?? {})) if (!PROPER_ORDER_SET.has(k)) extraKeys.add(k);
|
||||
for (const k of extraKeys) emit(k);
|
||||
return sections;
|
||||
}
|
||||
|
||||
function findPropersFor(
|
||||
d: LiturgicalDay1962,
|
||||
bundle: RomcalBundle1962
|
||||
): Record<string, string[]> | undefined {
|
||||
const kind = d.kind1962;
|
||||
const key = d.key1962 ?? d.id;
|
||||
if (!kind) return undefined;
|
||||
return bundle.propers[kind]?.[key];
|
||||
}
|
||||
|
||||
function humanizeId(id: string): string {
|
||||
return id
|
||||
.split(/[_\s]+/)
|
||||
.filter(Boolean)
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// When romcal's own i18next lookup misses (stale bundle, unknown id), it
|
||||
// returns the raw key — detect that and fall back to a direct bundle name
|
||||
// lookup, then a humanized id.
|
||||
function resolveName1962(
|
||||
d: LiturgicalDay1962,
|
||||
localBundle: RomcalBundle1962 | null,
|
||||
laBundle: RomcalBundle1962
|
||||
): string {
|
||||
const raw = d.name ?? '';
|
||||
const id = d.id;
|
||||
const key = d.key1962 ?? d.id;
|
||||
const looksUnresolved =
|
||||
!raw || raw === id || raw === key || /^[a-z][a-z0-9_]*\.[a-z_]+$/.test(raw);
|
||||
if (!looksUnresolved) return raw;
|
||||
const bundles = [localBundle, laBundle].filter(
|
||||
(b): b is RomcalBundle1962 => b != null
|
||||
);
|
||||
for (const b of bundles) {
|
||||
const names = b.i18n.names;
|
||||
const v = names?.[key] ?? names?.[id];
|
||||
if (v && v !== key && v !== id) return v;
|
||||
}
|
||||
return humanizeId(id);
|
||||
}
|
||||
|
||||
function adaptDay1962(
|
||||
d: LiturgicalDay1962,
|
||||
lang: CalendarLang,
|
||||
laBundle: RomcalBundle1962,
|
||||
localBundle: RomcalBundle1962 | null
|
||||
): CalendarDay {
|
||||
const colorKeys = colorKeysFrom(d);
|
||||
const colorNames = colorKeys.map((k) => colorLabel1962(k, lang));
|
||||
const classOf = (d.classOf1962 ?? 4) as 1 | 2 | 3 | 4;
|
||||
const classKey = RANK_FROM_CLASS_1962[classOf];
|
||||
|
||||
const laProps = findPropersFor(d, laBundle);
|
||||
const localProps = localBundle ? findPropersFor(d, localBundle) : undefined;
|
||||
const propers = sectionsFromBundle(laProps, localProps);
|
||||
|
||||
const detail: Rite1962Detail = {
|
||||
class: classOf,
|
||||
kind: d.kind1962 ?? 'tempora',
|
||||
commemorations: buildCommemorations(d),
|
||||
propers
|
||||
};
|
||||
if (d.octaveOf) detail.octave = { ofId: d.octaveOf.ofId, day: d.octaveOf.day };
|
||||
if (d.vigilOf) detail.vigilOf = d.vigilOf;
|
||||
if (d.transferredFromDate) detail.transferredFrom = d.transferredFromDate;
|
||||
|
||||
const seasonKey = d.seasons?.[0] ?? null;
|
||||
return {
|
||||
iso: p.date,
|
||||
id: p.key,
|
||||
name: p.name,
|
||||
rankName: rank1962Label(p.rank1962, lang),
|
||||
rank: RANK_FROM_CLASS_1962[p.classOf1962],
|
||||
iso: d.date,
|
||||
id: d.id,
|
||||
name: resolveName1962(d, localBundle, laBundle),
|
||||
rankName: rank1962Label(classKey, lang),
|
||||
rank: classKey,
|
||||
seasonKey,
|
||||
seasonNames,
|
||||
seasonNames: d.seasonNames ? [...d.seasonNames] : [],
|
||||
colorNames,
|
||||
colorKeys,
|
||||
psalterWeek: null,
|
||||
@@ -430,13 +299,23 @@ export async function getYear1962(
|
||||
const cacheKey = `${diocese}|${lang}|${year}`;
|
||||
const cached = yearCache1962.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
const resolved = await getRomcal1962(lang, diocese).generateCalendar(year);
|
||||
|
||||
const [romcal, laBundle, localBundle] = await Promise.all([
|
||||
getRomcal1962(lang, diocese),
|
||||
loadBundle1962('la'),
|
||||
lang === 'la' ? Promise.resolve<RomcalBundle1962 | null>(null) : loadBundle1962(lang)
|
||||
]);
|
||||
const resolved = await romcal.generateCalendar(year);
|
||||
const map = new Map<string, CalendarDay>();
|
||||
for (const [iso, entries] of Object.entries(resolved)) {
|
||||
if (!entries.length) continue;
|
||||
map.set(iso, adaptDay1962(entries, lang));
|
||||
const principal = entries[0];
|
||||
if (!principal) continue;
|
||||
map.set(iso, adaptDay1962(principal, lang, laBundle, localBundle));
|
||||
}
|
||||
yearCache1962.set(cacheKey, map);
|
||||
// keep season1962Label referenced so tree-shakers don't drop it while the
|
||||
// detail view gradually takes over rendering its own labels.
|
||||
void season1962Label;
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -23,7 +23,6 @@ import type { CalendarDay, SeasonArc, YearDay } from '$lib/calendarTypes';
|
||||
export type {
|
||||
CalendarDay,
|
||||
ProperSection,
|
||||
ProperSegment,
|
||||
Rite1962Commem,
|
||||
Rite1962Detail,
|
||||
SeasonArc,
|
||||
|
||||
+54
-85
@@ -238,22 +238,11 @@
|
||||
<span class="tc-tag">{t('cycle', lang)}: {humanizeSundayCycle(hero.sundayCycle)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if hero.rite1962}
|
||||
{@const r = hero.rite1962.rubrics}
|
||||
<div class="tc-rubrics">
|
||||
<span class="tc-rubric" class:on={r.gloria}>
|
||||
<span class="tc-rubric-dot"></span>
|
||||
<b>{t1962('gloria', lang)}</b>
|
||||
<span class="tc-state">{r.gloria ? t1962('yes', lang) : t1962('no', lang)}</span>
|
||||
</span>
|
||||
<span class="tc-rubric" class:on={r.credo}>
|
||||
<span class="tc-rubric-dot"></span>
|
||||
<b>{t1962('credo', lang)}</b>
|
||||
<span class="tc-state">{r.credo ? t1962('yes', lang) : t1962('no', lang)}</span>
|
||||
</span>
|
||||
{#if r.preface}
|
||||
<span class="tc-preface"><em>{t1962('preface', lang)}:</em> {r.preface}</span>
|
||||
{/if}
|
||||
{#if hero.rite1962 && hero.rite1962.commemorations.length}
|
||||
<div class="tc-commems">
|
||||
{#each hero.rite1962.commemorations as c (c.id)}
|
||||
<span class="tc-commem">{c.name}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="tc-arrow" aria-hidden="true">→</span>
|
||||
@@ -281,19 +270,30 @@
|
||||
▦ {lang === 'de' ? 'Monat' : lang === 'la' ? 'Mensis' : 'Month'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="legend" aria-hidden={!Object.keys(LIT_COLOR_VAR).length}>
|
||||
{#each Object.keys(LIT_COLOR_VAR) as key (key)}
|
||||
{@const label =
|
||||
lang === 'de'
|
||||
? { WHITE: 'Weiß', RED: 'Rot', GREEN: 'Grün', PURPLE: 'Violett', ROSE: 'Rosa', BLACK: 'Schwarz', GOLD: 'Gold' }[key]
|
||||
: lang === 'la'
|
||||
? { WHITE: 'Albus', RED: 'Ruber', GREEN: 'Viridis', PURPLE: 'Violaceus', ROSE: 'Rosaceus', BLACK: 'Niger', GOLD: 'Aureus' }[key]
|
||||
: { WHITE: 'White', RED: 'Red', GREEN: 'Green', PURPLE: 'Violet', ROSE: 'Rose', BLACK: 'Black', GOLD: 'Gold' }[key]}
|
||||
<span class="swatch">
|
||||
<span class="sq" style="background: {litBg(key)}"></span>
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
{/each}
|
||||
<div class="overview-right">
|
||||
<div class="legend" aria-hidden={!Object.keys(LIT_COLOR_VAR).length}>
|
||||
{#each Object.keys(LIT_COLOR_VAR) as key (key)}
|
||||
{@const label =
|
||||
lang === 'de'
|
||||
? { WHITE: 'Weiß', RED: 'Rot', GREEN: 'Grün', PURPLE: 'Violett', ROSE: 'Rosa', BLACK: 'Schwarz' }[key]
|
||||
: lang === 'la'
|
||||
? { WHITE: 'Albus', RED: 'Ruber', GREEN: 'Viridis', PURPLE: 'Violaceus', ROSE: 'Rosaceus', BLACK: 'Niger' }[key]
|
||||
: { WHITE: 'White', RED: 'Red', GREEN: 'Green', PURPLE: 'Violet', ROSE: 'Rose', BLACK: 'Black' }[key]}
|
||||
<span class="swatch">
|
||||
<span class="sq" style="background: {litBg(key)}"></span>
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
<a
|
||||
class="jump-btn jump-btn-gold"
|
||||
href={todayHref}
|
||||
data-sveltekit-noscroll
|
||||
data-sveltekit-replacestate
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{t('jumpToToday', lang)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -333,13 +333,6 @@
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="today-jump-row">
|
||||
<a class="jump-btn" href={todayHref} data-sveltekit-noscroll>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
{t('jumpToToday', lang)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid-header" role="row">
|
||||
{#each weekdayLabels as wd (wd)}
|
||||
@@ -642,60 +635,21 @@
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.tc-rubrics {
|
||||
.tc-commems {
|
||||
margin-top: 1.4rem;
|
||||
padding-top: 1.1rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.22);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1.1rem;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.6rem;
|
||||
}
|
||||
.tc-rubric {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.35rem 0.75rem 0.35rem 0.6rem;
|
||||
.tc-commem {
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.tc-rubric b {
|
||||
font-weight: 700;
|
||||
}
|
||||
.tc-rubric .tc-state {
|
||||
font-size: 0.72rem;
|
||||
opacity: 0.72;
|
||||
font-style: italic;
|
||||
}
|
||||
.tc-rubric-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.tc-rubric.on .tc-rubric-dot {
|
||||
background: #8be78b;
|
||||
box-shadow: 0 0 0 2px rgba(139, 231, 139, 0.22);
|
||||
}
|
||||
.tc-rubric:not(.on) b {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.tc-preface {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.tc-preface em {
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-right: 0.4rem;
|
||||
opacity: 0.72;
|
||||
}
|
||||
.tc-arrow {
|
||||
position: absolute;
|
||||
bottom: 1.1rem;
|
||||
@@ -760,10 +714,17 @@
|
||||
.view-switcher button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.overview-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.legend .swatch {
|
||||
display: inline-flex;
|
||||
@@ -837,11 +798,6 @@
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.today-jump-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.jump-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -851,12 +807,25 @@
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: var(--text-sm);
|
||||
transition: background var(--transition-fast), transform var(--transition-fast);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background var(--transition-fast), transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
.jump-btn:hover {
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text-primary);
|
||||
transform: scale(1.03);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
.jump-btn-gold {
|
||||
background: var(--lit-gold);
|
||||
color: var(--lit-gold-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.jump-btn-gold:hover {
|
||||
background: var(--lit-gold);
|
||||
color: var(--lit-gold-ink);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.grid {
|
||||
|
||||
+22
-133
@@ -12,6 +12,12 @@
|
||||
t1962,
|
||||
type CalendarLang
|
||||
} from '../../../../../calendarI18n';
|
||||
|
||||
function kindLabel(kind: 'tempora' | 'sancti', l: CalendarLang): string {
|
||||
if (kind === 'tempora')
|
||||
return l === 'de' ? 'Temporale' : l === 'la' ? 'Temporale' : 'Temporal';
|
||||
return l === 'de' ? 'Sanktorale' : l === 'la' ? 'Sanctorale' : 'Sanctoral';
|
||||
}
|
||||
import { litBg, litInk } from '../../../../../calendarColors';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -115,7 +121,7 @@
|
||||
<dl class="detail-extras">
|
||||
<div>
|
||||
<dt>{t1962('source', lang)}</dt>
|
||||
<dd>{d.kind}{d.properSource ? ` · ${d.properSource}` : ''}{d.communeSlug ? ` (${d.communeSlug})` : ''}</dd>
|
||||
<dd>{kindLabel(d.kind, lang)}</dd>
|
||||
</div>
|
||||
{#if d.vigilOf}
|
||||
<div>
|
||||
@@ -126,7 +132,7 @@
|
||||
{#if d.octave}
|
||||
<div>
|
||||
<dt>{t1962('octave', lang)}</dt>
|
||||
<dd>{d.octave.id} · {t1962('octaveDay', lang)} {d.octave.day} · {d.octave.rank}</dd>
|
||||
<dd>{d.octave.ofId} · {t1962('octaveDay', lang)} {d.octave.day}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if d.transferredFrom}
|
||||
@@ -136,32 +142,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
<div class="rubrics-grid">
|
||||
<h4>{t1962('rubrics', lang)}</h4>
|
||||
<div class="rubric-row">
|
||||
<span class="rubric-chip" class:on={d.rubrics.gloria}>{t1962('gloria', lang)}: {d.rubrics.gloria ? t1962('yes', lang) : t1962('no', lang)}</span>
|
||||
<span class="rubric-chip" class:on={d.rubrics.credo}>{t1962('credo', lang)}: {d.rubrics.credo ? t1962('yes', lang) : t1962('no', lang)}</span>
|
||||
{#if d.rubrics.preface}
|
||||
<span class="rubric-chip on">{t1962('preface', lang)}: {d.rubrics.preface}</span>
|
||||
{/if}
|
||||
{#if d.rubrics.lastGospel}
|
||||
<span class="rubric-chip on">{t1962('lastGospel', lang)}: {d.rubrics.lastGospel}</span>
|
||||
{/if}
|
||||
{#if d.rubrics.ite}
|
||||
<span class="rubric-chip on">{t1962('ite', lang)}: {d.rubrics.ite}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if d.commemorations.length}
|
||||
<div class="commems">
|
||||
<h4>{t1962('commemorations', lang)}</h4>
|
||||
<ul>
|
||||
{#each d.commemorations as c (c.key)}
|
||||
{@const cHex = hexFor(c.colorKeys)}
|
||||
{#each d.commemorations as c (c.id)}
|
||||
<li>
|
||||
<span class="color-swatch" style="background: {cHex}"></span>
|
||||
<span class="commem-name">{c.name}</span>
|
||||
<span class="commem-rank">{c.rankName}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -171,62 +158,26 @@
|
||||
<section class="propers">
|
||||
<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>
|
||||
{#each section.segments as seg, segIdx (segIdx)}
|
||||
<div class="proper-segment">
|
||||
{#if seg.refs && seg.refs.length}
|
||||
<div class="proper-segment-refs">
|
||||
{#each seg.refs as r (r)}
|
||||
<span class="proper-ref">{r}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if seg.la || seg.local}
|
||||
<div class="proper-cols" class:single={lang === 'la' || !seg.local}>
|
||||
{#if lang !== 'la' && seg.local && seg.fromBible}
|
||||
<p class="proper-fallback-note">{t1962('bibleFallbackNote', lang)}</p>
|
||||
{#each Array(rows) as _, i (i)}
|
||||
{@const la = section.la[i] ?? ''}
|
||||
{@const local = section.local[i] ?? ''}
|
||||
{#if la || local}
|
||||
<div class="proper-segment">
|
||||
<div class="proper-cols" class:single={lang === 'la' || !local}>
|
||||
{#if la}
|
||||
<div class="proper-col proper-col-la" lang="la">{la}</div>
|
||||
{/if}
|
||||
{#if seg.la}
|
||||
<div class="proper-col proper-col-la" lang="la">{seg.la}</div>
|
||||
{/if}
|
||||
{#if lang !== 'la' && seg.local}
|
||||
<div class="proper-col proper-col-local" lang={lang}>{seg.local}</div>
|
||||
{#if lang !== 'la' && local}
|
||||
<div class="proper-col proper-col-local" lang={lang}>{local}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{#if d.extraSections.length}
|
||||
<section class="propers">
|
||||
<h4>{t1962('extraSections', lang)}</h4>
|
||||
{#each d.extraSections as section (section.key)}
|
||||
<div class="proper-block">
|
||||
<div class="proper-label-row">
|
||||
<span class="proper-label">{properLabel(section.key, lang)}</span>
|
||||
</div>
|
||||
{#each section.segments as seg, segIdx (segIdx)}
|
||||
<div class="proper-segment">
|
||||
{#if seg.refs && seg.refs.length}
|
||||
<div class="proper-segment-refs">
|
||||
{#each seg.refs as r (r)}
|
||||
<span class="proper-ref">{r}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="proper-cols" class:single={lang === 'la' || !seg.local}>
|
||||
<div class="proper-col proper-col-la" lang="la">{seg.la}</div>
|
||||
{#if lang !== 'la' && seg.local}
|
||||
<div class="proper-col proper-col-local" lang={lang}>{seg.local}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -399,10 +350,6 @@
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.rubrics-grid {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.rubrics-grid h4,
|
||||
.commems h4,
|
||||
.propers h4 {
|
||||
margin: 0.5rem 0 0.4rem;
|
||||
@@ -412,24 +359,6 @@
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rubric-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.rubric-chip {
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.rubric-chip.on {
|
||||
background: color-mix(in srgb, var(--accent) 15%, var(--color-bg-tertiary));
|
||||
color: var(--color-text-primary);
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, var(--color-border));
|
||||
}
|
||||
.commems {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@@ -450,18 +379,10 @@
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.commems .color-swatch {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
.commem-name {
|
||||
flex: 1 1 auto;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.commem-rank {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.propers {
|
||||
margin-top: 1rem;
|
||||
@@ -485,15 +406,6 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.proper-ref {
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 0.72rem;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
.proper-segment {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
@@ -504,24 +416,6 @@
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
}
|
||||
.proper-segment-refs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
.proper-fallback-note {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1;
|
||||
margin: 0 0 0.25rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-left: 2px solid color-mix(in srgb, var(--orange) 55%, transparent);
|
||||
background: color-mix(in srgb, var(--orange) 8%, transparent);
|
||||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.proper-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -531,12 +425,10 @@
|
||||
}
|
||||
.proper-col-la {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
font-style: italic;
|
||||
}
|
||||
.proper-col-local {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
.proper-cols.single {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -544,7 +436,6 @@
|
||||
.proper-cols.single .proper-col-la,
|
||||
.proper-cols.single .proper-col-local {
|
||||
grid-column: 1;
|
||||
grid-row: auto;
|
||||
}
|
||||
.proper-col {
|
||||
white-space: pre-wrap;
|
||||
@@ -564,10 +455,8 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.proper-cols .proper-col-la,
|
||||
.proper-cols .proper-col-local,
|
||||
.proper-cols .proper-fallback-note {
|
||||
.proper-cols .proper-col-local {
|
||||
grid-column: 1;
|
||||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// Liturgical color tokens used by the overview design.
|
||||
// Maps romcal color keys to the CSS variables defined on the calendar page.
|
||||
|
||||
// Romcal never emits GOLD for either rite, so it is excluded from the legend
|
||||
// and lookup here. The --lit-gold token still exists for today-pin styling.
|
||||
export const LIT_COLOR_VAR: Record<string, string> = {
|
||||
WHITE: '--lit-white',
|
||||
RED: '--lit-red',
|
||||
GREEN: '--lit-green',
|
||||
PURPLE: '--lit-violet',
|
||||
ROSE: '--lit-rose',
|
||||
BLACK: '--lit-black',
|
||||
GOLD: '--lit-gold'
|
||||
BLACK: '--lit-black'
|
||||
};
|
||||
|
||||
export const LIT_INK_VAR: Record<string, string> = {
|
||||
@@ -17,8 +18,7 @@ export const LIT_INK_VAR: Record<string, string> = {
|
||||
GREEN: '--lit-green-ink',
|
||||
PURPLE: '--lit-violet-ink',
|
||||
ROSE: '--lit-rose-ink',
|
||||
BLACK: '--lit-black-ink',
|
||||
GOLD: '--lit-gold-ink'
|
||||
BLACK: '--lit-black-ink'
|
||||
};
|
||||
|
||||
export function litBg(colorKey: string | undefined): string {
|
||||
|
||||
@@ -59,11 +59,18 @@ export function hexFor(colorKeys: string[]): string {
|
||||
return colorHex[first] ?? '#27AE60';
|
||||
}
|
||||
|
||||
// Rank emphasis for visual weighting of cells
|
||||
// Rank emphasis for visual weighting of cells. Accepts both 1969 rank keys and
|
||||
// 1962 class labels (ClassI..IV), since 1962 days are emphasized similarly.
|
||||
export function rankEmphasis(rank: string): number {
|
||||
if (rank === 'SOLEMNITY') return 3;
|
||||
if (rank === 'FEAST' || rank === 'SUNDAY' || rank === 'HOLY_DAY_OF_OBLIGATION') return 2;
|
||||
if (rank === 'MEMORIAL') return 1;
|
||||
if (rank === 'SOLEMNITY' || rank === 'ClassI') return 3;
|
||||
if (
|
||||
rank === 'FEAST' ||
|
||||
rank === 'SUNDAY' ||
|
||||
rank === 'HOLY_DAY_OF_OBLIGATION' ||
|
||||
rank === 'ClassII'
|
||||
)
|
||||
return 2;
|
||||
if (rank === 'MEMORIAL' || rank === 'ClassIII') return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -245,7 +252,14 @@ const SEASON_LABEL: Record<string, Record<CalendarLang, string>> = {
|
||||
EasterWeek: { en: 'Easter Week', de: 'Osteroktav', la: 'Hebdomada Paschae' },
|
||||
Paschaltide: { en: 'Eastertide', de: 'Osterzeit', la: 'Tempus Paschale' },
|
||||
Pentecost: { en: 'Pentecost Week', de: 'Pfingstoktav', la: 'Hebdomada Pentecostes' },
|
||||
TimeAfterPentecost: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' }
|
||||
TimeAfterPentecost: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' },
|
||||
// romcal 3 emits 1969-style SCREAMING_SNAKE season keys even for the 1962 calendar.
|
||||
ADVENT: { en: 'Advent', de: 'Advent', la: 'Adventus' },
|
||||
CHRISTMAS_TIME: { en: 'Christmastide', de: 'Weihnachtszeit', la: 'Tempus Nativitatis' },
|
||||
LENT: { en: 'Lent', de: 'Fastenzeit', la: 'Quadragesima' },
|
||||
PASCHAL_TRIDUUM: { en: 'Paschal Triduum', de: 'Ostertriduum', la: 'Triduum Paschale' },
|
||||
EASTER_TIME: { en: 'Eastertide', de: 'Osterzeit', la: 'Tempus Paschale' },
|
||||
ORDINARY_TIME: { en: 'after Pentecost', de: 'nach Pfingsten', la: 'post Pentecosten' }
|
||||
};
|
||||
|
||||
export function season1962Label(season: string, lang: CalendarLang): string {
|
||||
@@ -268,26 +282,12 @@ export function colorLabel1962(colorKey: string, lang: CalendarLang): string {
|
||||
|
||||
export const ui1962 = {
|
||||
commemorations: { en: 'Commemorations', de: 'Kommemorationen', la: 'Commemorationes' },
|
||||
rubrics: { en: 'Rubrics', de: 'Rubriken', la: 'Rubricæ' },
|
||||
gloria: { en: 'Gloria', de: 'Gloria', la: 'Gloria' },
|
||||
credo: { en: 'Credo', de: 'Credo', la: 'Credo' },
|
||||
preface: { en: 'Preface', de: 'Präfation', la: 'Præfatio' },
|
||||
lastGospel: { en: 'Last Gospel', de: 'Letztes Evangelium', la: 'Ultimum Evangelium' },
|
||||
ite: { en: 'Dismissal', de: 'Entlassung', la: 'Dimissio' },
|
||||
octave: { en: 'Octave', de: 'Oktav', la: 'Octava' },
|
||||
octaveDay: { en: 'day', de: 'Tag', la: 'dies' },
|
||||
vigilOf: { en: 'Vigil of', de: 'Vigil von', la: 'Vigilia' },
|
||||
transferredFrom: { en: 'Transferred from', de: 'Übertragen von', la: 'Translatum ex' },
|
||||
source: { en: 'Source', de: 'Quelle', la: 'Fons' },
|
||||
yes: { en: 'yes', de: 'ja', la: 'sic' },
|
||||
no: { en: 'no', de: 'nein', la: 'non' },
|
||||
properRef: { en: 'Proper', de: 'Proprium', la: 'Proprium' },
|
||||
propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' },
|
||||
extraSections: { en: 'Additional readings', de: 'Zusätzliche Lesungen', la: 'Lectiones additae' },
|
||||
bibleFallbackNote: {
|
||||
en: 'Translation taken from the Douay-Rheims Bible, since no translated proper is published for this section. Wording will differ from authoritative missals.',
|
||||
de: 'Übersetzung stammt aus der Allioli-Bibel, da für diesen Abschnitt kein übersetztes Proprium veröffentlicht ist. Wortlaut weicht von maßgeblichen Messbüchern ab.'
|
||||
}
|
||||
propers: { en: 'Mass propers', de: 'Messproprium', la: 'Propria Missæ' }
|
||||
} as const;
|
||||
|
||||
export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string {
|
||||
@@ -296,19 +296,18 @@ export function t1962(key: keyof typeof ui1962, lang: CalendarLang): string {
|
||||
}
|
||||
|
||||
const PROPER_LABEL: Record<string, Record<CalendarLang, string>> = {
|
||||
introit: { en: 'Introit', de: 'Introitus', la: 'Introitus' },
|
||||
collect: { en: 'Collect', de: 'Kollekte', la: 'Collecta' },
|
||||
epistle: { en: 'Epistle', de: 'Epistel', la: 'Epistola' },
|
||||
gradual: { en: 'Gradual', de: 'Graduale', la: 'Graduale' },
|
||||
alleluia: { en: 'Alleluia', de: 'Alleluja', la: 'Alleluia' },
|
||||
tract: { en: 'Tract', de: 'Tractus', la: 'Tractus' },
|
||||
sequence: { en: 'Sequence', de: 'Sequenz', la: 'Sequentia' },
|
||||
gospel: { en: 'Gospel', de: 'Evangelium', la: 'Evangelium' },
|
||||
offertory: { en: 'Offertory', de: 'Offertorium', la: 'Offertorium' },
|
||||
secret: { en: 'Secret', de: 'Stillgebet', la: 'Secreta' },
|
||||
preface: { en: 'Preface', de: 'Präfation', la: 'Præfatio' },
|
||||
communion: { en: 'Communion', de: 'Kommunion', la: 'Communio' },
|
||||
postcommunion: { en: 'Postcommunion', de: 'Schlussgebet', la: 'Postcommunio' }
|
||||
Introitus: { en: 'Introit', de: 'Introitus', la: 'Introitus' },
|
||||
Oratio: { en: 'Collect', de: 'Kollekte', la: 'Oratio' },
|
||||
Lectio: { en: 'Epistle', de: 'Epistel', la: 'Lectio' },
|
||||
Graduale: { en: 'Gradual', de: 'Graduale', la: 'Graduale' },
|
||||
GradualeF: { en: 'Alleluia', de: 'Alleluja', la: 'Alleluia' },
|
||||
Tractus: { en: 'Tract', de: 'Tractus', la: 'Tractus' },
|
||||
Sequentia: { en: 'Sequence', de: 'Sequenz', la: 'Sequentia' },
|
||||
Evangelium: { en: 'Gospel', de: 'Evangelium', la: 'Evangelium' },
|
||||
Offertorium: { en: 'Offertory', de: 'Offertorium', la: 'Offertorium' },
|
||||
Secreta: { en: 'Secret', de: 'Stillgebet', la: 'Secreta' },
|
||||
Communio: { en: 'Communion', de: 'Kommunion', la: 'Communio' },
|
||||
Postcommunio: { en: 'Postcommunion', de: 'Schlussgebet', la: 'Postcommunio' }
|
||||
};
|
||||
|
||||
export function properLabel(key: string, lang: CalendarLang): string {
|
||||
|
||||
Reference in New Issue
Block a user