Make day-nav/language-toggle real links, group hours into rows
Deploy / deploy (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m19s
Day navigation (prev/today/next) and the language toggle were plain buttons wired to click handlers only; switched them to real <a href> elements (built via router's new exported urlFor) so right-click, middle-click, and open-in-new-tab work, with a click handler that still SPA-routes on a plain left-click and lets modified clicks fall through to normal browser navigation. Language selection is now reflected in the URL as a ?lang= query param (parsed/pushed by router.ts, omitted for the default single- English case to keep that URL clean), so a shared link preserves the viewer's chosen language(s). hour-list.ts now groups the 8 hours into three traditional rows (night office, day hours, evening) instead of one flat wrapped list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014axryJBrRswYh2niA7WCUc
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getState, setLanguages, type AppState } from '../app-state';
|
||||
import { urlFor } from '../router';
|
||||
|
||||
// Hardcoded to en/la since those are the only languages with any data so
|
||||
// far. The two-language ceiling (never three+ columns) is the real
|
||||
@@ -11,21 +12,28 @@ const OPTIONS: { label: string; languages: AppState['languages'] }[] = [
|
||||
];
|
||||
|
||||
export function renderLanguageToggle(container: HTMLElement): void {
|
||||
const { languages } = getState();
|
||||
const { date, selectedHour, languages } = getState();
|
||||
const current = languages.join('+');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="language-toggle" role="group" aria-label="Display language">
|
||||
${OPTIONS.map((opt) => {
|
||||
const isActive = opt.languages.join('+') === current;
|
||||
return `<button type="button" class="lang-btn${isActive ? ' is-active' : ''}" data-langs="${opt.languages.join(',')}">${opt.label}</button>`;
|
||||
const url = urlFor(date, selectedHour, opt.languages);
|
||||
return `<a href="${url}" class="lang-btn${isActive ? ' is-active' : ''}" data-langs="${opt.languages.join(',')}">${opt.label}</a>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.querySelectorAll<HTMLButtonElement>('[data-langs]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const langs = btn.dataset.langs!.split(',') as AppState['languages'];
|
||||
container.querySelectorAll<HTMLAnchorElement>('[data-langs]').forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
// Let modifier/middle clicks fall through to normal browser navigation
|
||||
// (open in new tab, etc.) — only plain left-clicks are SPA-routed.
|
||||
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const langs = link.dataset.langs!.split(',') as AppState['languages'];
|
||||
setLanguages(langs);
|
||||
});
|
||||
});
|
||||
|
||||
+24
-8
@@ -1,6 +1,7 @@
|
||||
import { getState, shiftDate, goToToday } from '../app-state';
|
||||
import { getState, shiftDate, goToToday, addDays, todayIso } from '../app-state';
|
||||
import { resolveDay } from '../calendar';
|
||||
import { getDayLabel } from '../calendar/day-label';
|
||||
import { urlFor } from '../router';
|
||||
import { formatDateLong, renderBilingual } from './format';
|
||||
|
||||
/** Renders navigation plus the merged "when" header: the civil date/
|
||||
@@ -11,22 +12,37 @@ import { formatDateLong, renderBilingual } from './format';
|
||||
* hour on a given date), so it's resolved directly here via `resolveDay`
|
||||
* rather than through any particular hour's `resolveOrdo`. */
|
||||
export function renderDayNav(container: HTMLElement): void {
|
||||
const { date, languages } = getState();
|
||||
const { date, selectedHour, languages } = getState();
|
||||
const day = resolveDay(date);
|
||||
const dateLabel = formatDateLong(date);
|
||||
const dayLabel = getDayLabel(day);
|
||||
const prevUrl = urlFor(addDays(date, -1), selectedHour, languages);
|
||||
const nextUrl = urlFor(addDays(date, 1), selectedHour, languages);
|
||||
const todayUrl = urlFor(todayIso(), selectedHour, languages);
|
||||
container.innerHTML = `
|
||||
<nav class="day-nav" aria-label="Day navigation">
|
||||
<button type="button" class="day-nav-btn" data-action="prev" aria-label="Previous day">←</button>
|
||||
<button type="button" class="day-nav-btn day-nav-today" data-action="today">Today</button>
|
||||
<a href="${prevUrl}" class="day-nav-btn" data-action="prev" aria-label="Previous day">←</a>
|
||||
<a href="${todayUrl}" class="day-nav-btn day-nav-today" data-action="today">Today</a>
|
||||
<div class="day-nav-header">
|
||||
${renderBilingual(dateLabel, languages, 'day-nav-date')}
|
||||
${renderBilingual(dayLabel, languages, 'day-nav-label')}
|
||||
</div>
|
||||
<button type="button" class="day-nav-btn" data-action="next" aria-label="Next day">→</button>
|
||||
<a href="${nextUrl}" class="day-nav-btn" data-action="next" aria-label="Next day">→</a>
|
||||
</nav>
|
||||
`;
|
||||
container.querySelector('[data-action="prev"]')?.addEventListener('click', () => shiftDate(-1));
|
||||
container.querySelector('[data-action="next"]')?.addEventListener('click', () => shiftDate(1));
|
||||
container.querySelector('[data-action="today"]')?.addEventListener('click', () => goToToday());
|
||||
|
||||
const bindNav = (action: string, mutate: () => void): void => {
|
||||
container.querySelector<HTMLAnchorElement>(`[data-action="${action}"]`)?.addEventListener('click', (event) => {
|
||||
// Let modifier/middle clicks fall through to normal browser navigation
|
||||
// (open in new tab, etc.) — only plain left-clicks are SPA-routed.
|
||||
if (event.defaultPrevented || (event as MouseEvent).button !== 0 || (event as MouseEvent).metaKey || (event as MouseEvent).ctrlKey || (event as MouseEvent).shiftKey || (event as MouseEvent).altKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
mutate();
|
||||
});
|
||||
};
|
||||
bindNav('prev', () => shiftDate(-1));
|
||||
bindNav('next', () => shiftDate(1));
|
||||
bindNav('today', () => goToToday());
|
||||
}
|
||||
|
||||
+15
-4
@@ -1,13 +1,21 @@
|
||||
import { getState, setSelectedHour } from '../app-state';
|
||||
import { HOUR_IDS, resolveOrdo, type HourId } from '../hours';
|
||||
import { resolveOrdo, type HourId } from '../hours';
|
||||
import { hourLabel } from './format';
|
||||
|
||||
const BASE = import.meta.env.BASE_URL;
|
||||
|
||||
// Traditional grouping into rows, each starting a "hinge" hour: night
|
||||
// office, day hours, evening — rather than one flat wrapped list.
|
||||
const HOUR_ROWS: readonly HourId[][] = [
|
||||
['matins', 'lauds'],
|
||||
['prime', 'terce', 'sext', 'none'],
|
||||
['vespers', 'compline'],
|
||||
];
|
||||
|
||||
export function renderHourList(container: HTMLElement): void {
|
||||
const { date, selectedHour } = getState();
|
||||
|
||||
const items = HOUR_IDS.map((hourId) => {
|
||||
const renderItem = (hourId: HourId): string => {
|
||||
const ordo = resolveOrdo(hourId, date);
|
||||
const isSelected = hourId === selectedHour;
|
||||
const isReady = !ordo.notImplemented;
|
||||
@@ -24,9 +32,12 @@ export function renderHourList(container: HTMLElement): void {
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
container.innerHTML = `<ul class="hour-list">${items}</ul>`;
|
||||
const separator = '<li class="hour-sep"> </li>';
|
||||
const rows = HOUR_ROWS.map((row) => `<ul class="hour-list">${row.map(renderItem).join(separator)}</ul>`).join('');
|
||||
|
||||
container.innerHTML = rows;
|
||||
|
||||
container.querySelectorAll<HTMLAnchorElement>('[data-hour]').forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
|
||||
@@ -198,9 +198,16 @@ button:focus-visible {
|
||||
padding: var(--space-3) var(--space-2);
|
||||
}
|
||||
|
||||
.hour-list-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.hour-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -239,6 +246,12 @@ button:focus-visible {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.hour-sep {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-border);
|
||||
}
|
||||
|
||||
.hour-status {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
|
||||
Reference in New Issue
Block a user