Compare commits
2 Commits
35a98f6a0a
...
be7880304c
| Author | SHA1 | Date | |
|---|---|---|---|
|
be7880304c
|
|||
|
8023a907de
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.26.1",
|
||||
"version": "1.27.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -59,7 +59,8 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"mongoose": "^9.4.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"sharp": "^0.34.5"
|
||||
"sharp": "^0.34.5",
|
||||
"web-haptics": "^0.0.6"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -53,6 +53,9 @@ importers:
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
web-haptics:
|
||||
specifier: ^0.0.6
|
||||
version: 0.0.6(svelte@5.55.1)
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: 1.56.1
|
||||
@@ -2033,6 +2036,23 @@ packages:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
web-haptics@0.0.6:
|
||||
resolution: {integrity: sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==}
|
||||
peerDependencies:
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
svelte: '>=4'
|
||||
vue: '>=3'
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3765,6 +3785,10 @@ snapshots:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
web-haptics@0.0.6(svelte@5.55.1):
|
||||
optionalDependencies:
|
||||
svelte: 5.55.1
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webidl-conversions@8.0.0: {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bocken"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
@@ -6,6 +6,10 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.VibrationAttributes
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.webkit.JavascriptInterface
|
||||
import androidx.core.app.ActivityCompat
|
||||
@@ -100,6 +104,36 @@ class AndroidBridge(private val context: Context) {
|
||||
return LocationForegroundService.getIntervalState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-vibrate bypassing silent/DND by using USAGE_ACCESSIBILITY attributes.
|
||||
* Why: default web Vibration API uses USAGE_TOUCH which Android silences.
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun forceVibrate(durationMs: Long, intensityPct: Int) {
|
||||
val vibrator: Vibrator? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
(context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
}
|
||||
if (vibrator?.hasVibrator() != true) return
|
||||
|
||||
val amplitude = (intensityPct.coerceIn(1, 100) * 255 / 100).coerceAtLeast(1)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val effect = VibrationEffect.createOneShot(durationMs, amplitude)
|
||||
val attrs = VibrationAttributes.Builder()
|
||||
.setUsage(VibrationAttributes.USAGE_ACCESSIBILITY)
|
||||
.build()
|
||||
vibrator.vibrate(effect, attrs)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(durationMs, amplitude))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(durationMs)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if at least one TTS engine is installed on the device. */
|
||||
@JavascriptInterface
|
||||
fun hasTtsEngine(): Boolean {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"productName": "Bocken",
|
||||
"identifier": "org.bocken.app",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"build": {
|
||||
"devUrl": "http://192.168.1.4:5173",
|
||||
"frontendDist": "https://bocken.org"
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<script lang="ts">
|
||||
let { onclick } = $props<{ onclick?: () => void }>();
|
||||
</script>
|
||||
|
||||
<button class="counter-button" {onclick} aria-label="Nächstes Ave Maria">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 4V2.21c0-.45-.54-.67-.85-.35l-2.8 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.32.31.86.09.86-.36V6c3.31 0 6 2.69 6 6 0 .79-.15 1.56-.44 2.25-.15.36-.04.77.23 1.04.51.51 1.37.33 1.64-.34.37-.91.57-1.91.57-2.95 0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-.79.15-1.56.44-2.25.15-.36.04-.77-.23-1.04-.51-.51-1.37-.33-1.64.34C4.2 9.96 4 10.96 4 12c0 4.42 3.58 8 8 8v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V18z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.counter-button {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: var(--nord1);
|
||||
border: 2px solid var(--nord9);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
:global(:root:not([data-theme="dark"])) .counter-button {
|
||||
background: var(--nord5);
|
||||
border-color: var(--nord10);
|
||||
}
|
||||
}
|
||||
:global(:root[data-theme="light"]) .counter-button {
|
||||
background: var(--nord5);
|
||||
border-color: var(--nord10);
|
||||
}
|
||||
|
||||
.counter-button:hover {
|
||||
background: var(--nord2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
:global(:root:not([data-theme="dark"])) .counter-button:hover {
|
||||
background: var(--nord4);
|
||||
}
|
||||
}
|
||||
:global(:root[data-theme="light"]) .counter-button:hover {
|
||||
background: var(--nord4);
|
||||
}
|
||||
|
||||
.counter-button svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
fill: var(--nord9);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
:global(:root:not([data-theme="dark"])) .counter-button svg {
|
||||
fill: var(--nord10);
|
||||
}
|
||||
}
|
||||
:global(:root[data-theme="light"]) .counter-button svg {
|
||||
fill: var(--nord10);
|
||||
}
|
||||
|
||||
.counter-button:hover svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
@@ -17,10 +17,10 @@
|
||||
:global(.links_grid a:nth-child(3n+1) svg:not(.lock-icon)) {
|
||||
fill: var(--grid-fill-pop-a);
|
||||
}
|
||||
:global(.links_grid a:nth-child(5n+3) svg:not(.lock-icon)) {
|
||||
:global(.links_grid a:nth-child(5n+2) svg:not(.lock-icon)) {
|
||||
fill: var(--grid-fill-pop-b);
|
||||
}
|
||||
:global(.links_grid a:nth-child(7n) svg:not(.lock-icon)) {
|
||||
:global(.links_grid a:nth-child(5n+4) svg:not(.lock-icon)) {
|
||||
fill: var(--grid-fill-pop-c);
|
||||
}
|
||||
|
||||
|
||||
33
src/lib/js/haptics.ts
Normal file
33
src/lib/js/haptics.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createWebHaptics } from 'web-haptics/svelte';
|
||||
|
||||
export type HapticPulse = { duration: number; intensity?: number };
|
||||
|
||||
type AndroidBridgeShape = {
|
||||
forceVibrate?: (durationMs: number, intensityPct: number) => void;
|
||||
};
|
||||
|
||||
function getBridge(): AndroidBridgeShape | undefined {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
return (window as unknown as { AndroidBridge?: AndroidBridgeShape }).AndroidBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progressively-enhanced haptics: uses the Tauri Android bridge (bypasses silent
|
||||
* mode via VibrationAttributes.USAGE_ACCESSIBILITY) when running inside the
|
||||
* installed app; falls back to web-haptics in the browser.
|
||||
*/
|
||||
export function createHaptics() {
|
||||
const web = createWebHaptics();
|
||||
const nativeAvailable = typeof getBridge()?.forceVibrate === 'function';
|
||||
|
||||
function trigger(pulse: HapticPulse = { duration: 30, intensity: 0.7 }): void {
|
||||
const bridge = getBridge();
|
||||
if (bridge?.forceVibrate) {
|
||||
bridge.forceVibrate(pulse.duration, Math.round((pulse.intensity ?? 0.7) * 100));
|
||||
return;
|
||||
}
|
||||
web.trigger([pulse]);
|
||||
}
|
||||
|
||||
return { trigger, destroy: web.destroy, nativeAvailable };
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { onMount, tick } from "svelte";
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
import { createHaptics } from "$lib/js/haptics";
|
||||
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
||||
import { createPip } from "$lib/js/pip.svelte";
|
||||
import PipImage from "$lib/components/faith/PipImage.svelte";
|
||||
@@ -14,7 +15,6 @@ import SalveRegina from "$lib/components/faith/prayers/SalveRegina.svelte";
|
||||
import ReginaCaeli from "$lib/components/faith/prayers/ReginaCaeli.svelte";
|
||||
import RosaryFinalPrayer from "$lib/components/faith/prayers/RosaryFinalPrayer.svelte";
|
||||
import MichaelGebet from "$lib/components/faith/prayers/MichaelGebet.svelte";
|
||||
import CounterButton from "$lib/components/CounterButton.svelte";
|
||||
import BibleModal from "$lib/components/faith/BibleModal.svelte";
|
||||
import { theologicalVirtueVerseDataDe, theologicalVirtueVerseDataEn } from "$lib/data/mysteryVerseData";
|
||||
import Toggle from "$lib/components/Toggle.svelte";
|
||||
@@ -27,6 +27,7 @@ import MysteryImageColumn from "./MysteryImageColumn.svelte";
|
||||
import { mysteries, mysteriesLatin, mysteriesEnglish, mysteryTitles, mysteryTitlesEnglish, mysteryTitlesLatin, allMysteryImages, getLabels, getLabelsLatin, getMysteryForWeekday, BEAD_SPACING, DECADE_OFFSET, sectionPositions } from "./rosaryData.js";
|
||||
import { isEastertide, getLiturgicalSeason } from "$lib/js/easter.svelte";
|
||||
import { setupScrollSync } from "./rosaryScrollSync.js";
|
||||
import { BookOpen } from "@lucide/svelte";
|
||||
let { data } = $props();
|
||||
|
||||
// Toggle for including Luminous mysteries (initialized from URL param or default)
|
||||
@@ -252,6 +253,15 @@ $effect(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Haptic feedback. Uses native Android bridge (bypasses silent mode) inside
|
||||
// Tauri app, else web-haptics in browser. Two profiles:
|
||||
// - BEAD: soft+crisp for each Ave Maria inside a decade
|
||||
// - CARD: heavier thump for advancing to the next prayer card
|
||||
const { trigger: triggerHaptic, destroy: destroyHaptics } = createHaptics();
|
||||
onDestroy(destroyHaptics);
|
||||
const HAPTIC_BEAD = { duration: 25, intensity: 0.6 };
|
||||
const HAPTIC_CARD = { duration: 80, intensity: 1.0 };
|
||||
|
||||
// Counter for tracking Ave Maria progress in each decade (0-10 for each)
|
||||
let decadeCounters = $state({
|
||||
secret1: 0,
|
||||
@@ -268,9 +278,79 @@ let selectedTitle = $state('');
|
||||
/** @type {any} */
|
||||
let selectedVerseData = $state(null);
|
||||
|
||||
// Function to advance the counter for a specific decade
|
||||
// Ordered list of tappable prayer-section cards (matches DOM render order).
|
||||
// Used for card-tap delegation: tap any non-decade card → advance to next.
|
||||
const orderedSections = [
|
||||
'cross', 'lbead1', 'start1', 'start2', 'start3', 'lbead2',
|
||||
'secret1_pater', 'secret1', 'secret1_transition',
|
||||
'secret2_pater', 'secret2', 'secret2_transition',
|
||||
'secret3_pater', 'secret3', 'secret3_transition',
|
||||
'secret4_pater', 'secret4', 'secret4_transition',
|
||||
'secret5_pater', 'secret5',
|
||||
'final_transition', 'final_salve', 'final_schlussgebet',
|
||||
'final_michael', 'final_paternoster', 'final_cross'
|
||||
];
|
||||
|
||||
/** @param {string} name */
|
||||
function scrollToSection(name) {
|
||||
const el = sectionElements[name];
|
||||
if (!el) return;
|
||||
const top = el.getBoundingClientRect().top + window.scrollY;
|
||||
const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
|
||||
window.scrollTo({ top: top - offset, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
/** Resolve the target prayer-section for a tap event, applying shared filters. */
|
||||
/** @param {Event} e */
|
||||
function resolvePrayerTarget(e) {
|
||||
const target = /** @type {HTMLElement | null} */ (e.target);
|
||||
if (!target) return null;
|
||||
if (target.closest('button, a, input, select, textarea, [role="button"]')) return null;
|
||||
const section = /** @type {HTMLElement | null} */ (target.closest('.prayer-section'));
|
||||
if (!section) return null;
|
||||
const name = section.getAttribute('data-section');
|
||||
if (!name) return null;
|
||||
return { section, name };
|
||||
}
|
||||
|
||||
/** Fire haptic immediately on finger-down — don't wait for click's release+no-movement check.
|
||||
* Tradeoff: a scroll gesture starting on a card emits one brief haptic. Worth it for snappy tap feel. */
|
||||
/** @param {PointerEvent} e */
|
||||
function handlePrayerCardPointerDown(e) {
|
||||
if (!e.isPrimary) return;
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||
const hit = resolvePrayerTarget(e);
|
||||
if (!hit) return;
|
||||
if (hit.section.classList.contains('decade')) {
|
||||
const key = /** @type {'secret1'|'secret2'|'secret3'|'secret4'|'secret5'} */ (`secret${parseInt(hit.name.replace('secret', ''), 10)}`);
|
||||
// Predict post-increment count: the 10th bead fires the heavier card haptic.
|
||||
triggerHaptic(decadeCounters[key] === 9 ? HAPTIC_CARD : HAPTIC_BEAD);
|
||||
} else {
|
||||
triggerHaptic(HAPTIC_CARD);
|
||||
}
|
||||
}
|
||||
|
||||
/** Click handler performs the state change only (scroll/increment). Haptic already
|
||||
* fired on pointerdown; click's built-in movement filter prevents scroll-gesture advances. */
|
||||
/** @param {MouseEvent} e */
|
||||
function handlePrayerCardClick(e) {
|
||||
const hit = resolvePrayerTarget(e);
|
||||
if (!hit) return;
|
||||
if (hit.section.classList.contains('decade')) {
|
||||
const decadeNum = parseInt(hit.name.replace('secret', ''), 10);
|
||||
incrementDecadeCounter(decadeNum);
|
||||
} else {
|
||||
const idx = orderedSections.indexOf(hit.name);
|
||||
if (idx >= 0 && idx < orderedSections.length - 1) {
|
||||
scrollToSection(orderedSections[idx + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Increment decade counter; auto-scroll to next section when reaching 10.
|
||||
* Haptic is fired upstream on pointerdown for lower latency — not here. */
|
||||
/** @param {number} decadeNum */
|
||||
function advanceDecade(decadeNum) {
|
||||
function incrementDecadeCounter(decadeNum) {
|
||||
const key = /** @type {'secret1'|'secret2'|'secret3'|'secret4'|'secret5'} */ (`secret${decadeNum}`);
|
||||
if (decadeCounters[key] < 10) {
|
||||
decadeCounters[key] += 1;
|
||||
@@ -279,24 +359,9 @@ function advanceDecade(decadeNum) {
|
||||
// and reset the counter
|
||||
if (decadeCounters[key] === 10) {
|
||||
setTimeout(() => {
|
||||
// Reset counter to clear highlighting
|
||||
decadeCounters[key] = 0;
|
||||
|
||||
// Determine next section
|
||||
let nextSection;
|
||||
if (decadeNum < 5) {
|
||||
nextSection = `secret${decadeNum}_transition`;
|
||||
} else {
|
||||
nextSection = 'final_transition';
|
||||
}
|
||||
|
||||
// Scroll to next section
|
||||
const nextElement = sectionElements[nextSection];
|
||||
if (nextElement) {
|
||||
const elementTop = nextElement.getBoundingClientRect().top + window.scrollY;
|
||||
const offset = parseFloat(getComputedStyle(document.documentElement).fontSize) * 3;
|
||||
window.scrollTo({ top: elementTop - offset, behavior: 'smooth' });
|
||||
}
|
||||
const nextSection = decadeNum < 5 ? `secret${decadeNum}_transition` : 'final_transition';
|
||||
scrollToSection(nextSection);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -483,6 +548,11 @@ onMount(() => {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
user-select: none;
|
||||
transition: transform 0.08s ease;
|
||||
}
|
||||
|
||||
/* No-JS: highlight SVG beads when a prayer section is :target */
|
||||
@@ -535,24 +605,13 @@ onMount(() => {
|
||||
|
||||
.prayer-section.decade {
|
||||
scroll-snap-align: start;
|
||||
min-height: 50vh; /* Only decades need minimum height for scroll-snap */
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Reduce min-height in monolingual mode since content is shorter */
|
||||
.prayer-section.decade:has(:global(.prayer-wrapper.monolingual)),
|
||||
.prayer-section.decade:has(:global(.prayer-wrapper.lang-la)) {
|
||||
min-height: 30vh;
|
||||
.prayer-section:active {
|
||||
transform: scale(0.995);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.prayer-section.decade {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
.prayer-section.decade:has(:global(.prayer-wrapper.monolingual)),
|
||||
.prayer-section.decade:has(:global(.prayer-wrapper.lang-la)) {
|
||||
min-height: 20vh;
|
||||
}
|
||||
.prayer-section {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -638,70 +697,48 @@ h1 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.bible-reference-text {
|
||||
color: var(--nord8);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
:global(:root:not([data-theme="dark"])) .bible-reference-text {
|
||||
color: var(--nord10);
|
||||
}
|
||||
}
|
||||
:global(:root[data-theme="light"]) .bible-reference-text {
|
||||
color: var(--nord10);
|
||||
}
|
||||
|
||||
.bible-reference-button {
|
||||
background: var(--nord3);
|
||||
border: 2px solid var(--nord2);
|
||||
color: var(--nord6);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
/* Understated citation: icon + reference as an inline typographic link. */
|
||||
.bible-reference-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
gap: 0.45rem;
|
||||
padding: 0.25rem 0.35rem;
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-variant-numeric: oldstyle-nums;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
opacity: 0.78;
|
||||
transition: color 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.bible-reference-button:hover {
|
||||
background: var(--nord8);
|
||||
border-color: var(--nord9);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
.bible-reference-link :global(svg) {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.15s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bible-reference-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
.bible-reference-link:hover {
|
||||
color: var(--color-primary-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: light) {
|
||||
:global(:root:not([data-theme="dark"])) .bible-reference-button {
|
||||
background: var(--nord5);
|
||||
border-color: var(--nord4);
|
||||
color: var(--nord0);
|
||||
}
|
||||
|
||||
:global(:root:not([data-theme="dark"])) .bible-reference-button:hover {
|
||||
background: var(--nord4);
|
||||
border-color: var(--nord3);
|
||||
}
|
||||
}
|
||||
:global(:root[data-theme="light"]) .bible-reference-button {
|
||||
background: var(--nord5);
|
||||
border-color: var(--nord4);
|
||||
color: var(--nord0);
|
||||
.bible-reference-link:hover :global(svg) {
|
||||
opacity: 1;
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
:global(:root[data-theme="light"]) .bible-reference-button:hover {
|
||||
background: var(--nord4);
|
||||
border-color: var(--nord3);
|
||||
|
||||
.bible-reference-link:focus-visible {
|
||||
outline: 1px dotted currentColor;
|
||||
outline-offset: 3px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Footnote styles */
|
||||
@@ -834,7 +871,9 @@ h1 {
|
||||
</div>
|
||||
|
||||
<!-- Main Content: Prayer Sections -->
|
||||
<div class="prayers-content">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="prayers-content" onpointerdown={handlePrayerCardPointerDown} onclick={handlePrayerCardClick}>
|
||||
<!-- Cross & Credo -->
|
||||
<div
|
||||
class="prayer-section"
|
||||
@@ -878,12 +917,14 @@ h1 {
|
||||
mysteryEnglish="Jesus, who may increase our faith"
|
||||
/>
|
||||
<div class="decade-buttons">
|
||||
<span class="bible-reference-text">{theologicalVirtueData.reference}</span>
|
||||
<button
|
||||
class="bible-reference-button"
|
||||
class="bible-reference-link"
|
||||
onclick={() => handleCitationClick(theologicalVirtueData.reference, theologicalVirtueData.title, theologicalVirtueData.verseData)}
|
||||
aria-label={labels.showBibleVerse}
|
||||
>📖</button>
|
||||
>
|
||||
<BookOpen size={15} strokeWidth={1.75} aria-hidden="true" />
|
||||
<span>{theologicalVirtueData.reference}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -901,12 +942,14 @@ h1 {
|
||||
mysteryEnglish="Jesus, who may strengthen our hope"
|
||||
/>
|
||||
<div class="decade-buttons">
|
||||
<span class="bible-reference-text">{theologicalVirtueData.reference}</span>
|
||||
<button
|
||||
class="bible-reference-button"
|
||||
class="bible-reference-link"
|
||||
onclick={() => handleCitationClick(theologicalVirtueData.reference, theologicalVirtueData.title, theologicalVirtueData.verseData)}
|
||||
aria-label={labels.showBibleVerse}
|
||||
>📖</button>
|
||||
>
|
||||
<BookOpen size={15} strokeWidth={1.75} aria-hidden="true" />
|
||||
<span>{theologicalVirtueData.reference}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -924,12 +967,14 @@ h1 {
|
||||
mysteryEnglish="Jesus, who may kindle our love"
|
||||
/>
|
||||
<div class="decade-buttons">
|
||||
<span class="bible-reference-text">{theologicalVirtueData.reference}</span>
|
||||
<button
|
||||
class="bible-reference-button"
|
||||
class="bible-reference-link"
|
||||
onclick={() => handleCitationClick(theologicalVirtueData.reference, theologicalVirtueData.title, theologicalVirtueData.verseData)}
|
||||
aria-label={labels.showBibleVerse}
|
||||
>📖</button>
|
||||
>
|
||||
<BookOpen size={15} strokeWidth={1.75} aria-hidden="true" />
|
||||
<span>{theologicalVirtueData.reference}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -958,7 +1003,7 @@ h1 {
|
||||
<Paternoster />
|
||||
</div>
|
||||
|
||||
<!-- Ave Maria decade (Gesätz) -->
|
||||
<!-- Ave Maria decade (Gesätz) - whole card tappable via parent delegation -->
|
||||
<div
|
||||
class="prayer-section decade"
|
||||
id={`secret${decadeNum}`}
|
||||
@@ -983,16 +1028,15 @@ h1 {
|
||||
<div class="decade-buttons">
|
||||
{#if currentMysteryDescriptions[decadeNum - 1]}
|
||||
{@const description = currentMysteryDescriptions[decadeNum - 1]}
|
||||
<span class="bible-reference-text">{description.reference}</span>
|
||||
<button
|
||||
class="bible-reference-button"
|
||||
class="bible-reference-link"
|
||||
onclick={() => handleCitationClick(description.reference, description.title, description.verseData)}
|
||||
aria-label={labels.showBibleVerse}
|
||||
>
|
||||
📖
|
||||
<BookOpen size={15} strokeWidth={1.75} aria-hidden="true" />
|
||||
<span>{description.reference}</span>
|
||||
</button>
|
||||
{/if}
|
||||
<CounterButton onclick={() => advanceDecade(decadeNum)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user