Files
eranos/src/main.tsx
T
mkfain 97da8ae822 Add Han display font for Chinese hero text
Bebas Neue (the .font-display family) ships only Latin glyphs, so
Chinese hero headlines fall back to system fonts and lose their
industrial display character. Add @fontsource/noto-sans-tc weight 900
and a :lang() rule that swaps it in for any .font-display element
while the page language is a Chinese variant (zh, zh-Hant, zh-TW,
zh-HK).

The fontsource CSS uses unicode-range descriptors, so non-Chinese
users do not download the Han glyph slices (effectively zero cost
for Latin-only locales).

The rule reverses Tailwind's italic, uppercase, tracking, and the
hero's 0.022em -webkit-text-stroke fatten trick — none of those are
meaningful for CJK text and the stroke trick muddies strokes at
weight 900.
2026-05-24 15:57:52 -05:00

125 lines
5.6 KiB
TypeScript

import { createRoot } from 'react-dom/client';
// Polyfills for AbortSignal.any() / AbortSignal.timeout() on older WebViews.
import './lib/polyfills.ts';
// Kick off cache hydration early so data is ready before components render.
import { hydrateNip05Cache } from '@/lib/nip05Cache';
import { hydrateProfileCache } from '@/lib/profileCache';
hydrateNip05Cache();
hydrateProfileCache();
import { ErrorBoundary } from '@/components/ErrorBoundary';
import App from './App.tsx';
import './index.css';
// i18next initialization (used by the Spark wallet stack ported from Pathos).
import './i18n';
import '@fontsource-variable/inter';
import '@fontsource/bebas-neue/400.css';
// Han display font for Chinese (zh, zh-Hant, zh-TW, zh-HK). Bebas Neue
// only ships Latin glyphs, so Chinese hero text would otherwise fall back
// to system fonts and lose its display-weight character. The fontsource
// CSS uses unicode-range descriptors so non-Chinese users only download
// the slices for glyphs actually rendered on their page (effectively
// zero for Latin-only locales).
import '@fontsource/noto-sans-tc/900.css';
// ─── Native status bar theming (Android APK / iOS) ───────────────────────────
// Keeps the OS top chrome in sync with the active app theme.
// Runs before React so the very first paint matches the persisted theme.
// Uses a MutationObserver so it reacts to all subsequent theme changes
// (class changes for builtin themes, style-content changes for custom themes).
import { Capacitor, SystemBars, SystemBarsStyle } from '@capacitor/core';
import { getBackgroundThemeMode } from '@/lib/colorUtils';
if (Capacitor.isNativePlatform()) {
// Hide the iOS keyboard accessory bar (prev/next/done toolbar above the keyboard).
// Only runs on iOS — setAccessoryBarVisible is unimplemented on Android.
if (Capacitor.getPlatform() === 'ios') {
import('@capacitor/keyboard').then(({ Keyboard }) => {
Keyboard.setAccessoryBarVisible({ isVisible: false }).catch(() => {});
}).catch(() => {});
}
/**
* Sync the native system bar icon style with the active CSS theme.
*
* SystemBarsStyle.Dark = light/white icons (use on dark backgrounds)
* SystemBarsStyle.Light = dark/black icons (use on light backgrounds)
*
* On Android 16+ (API 36) setBackgroundColor no longer works — the bars
* are transparent and the web content renders behind them. The app already
* draws its own safe-area backgrounds in CSS, so only icon style matters.
*/
function updateStatusBar() {
const isDark = getBackgroundThemeMode() === 'dark';
SystemBars.setStyle({ style: isDark ? SystemBarsStyle.Dark : SystemBarsStyle.Light }).catch(() => {});
}
// Apply immediately (theme class is set synchronously by AppProvider useLayoutEffect
// before the first React paint, but we still try early in case it's already set).
updateStatusBar();
// Re-apply whenever the theme class changes on <html> (light / dark / custom)
const classObserver = new MutationObserver(() => updateStatusBar());
classObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
// Re-apply whenever the injected <style id="theme-vars"> content changes
// (covers custom themes that change CSS variables without changing the class).
const styleObserver = new MutationObserver(() => updateStatusBar());
const observeThemeVars = () => {
const el = document.getElementById('theme-vars');
if (el) {
styleObserver.observe(el, { characterData: true, childList: true, subtree: true });
}
};
// The style element may not exist yet — watch <head> for it to appear.
observeThemeVars();
const headObserver = new MutationObserver(() => observeThemeVars());
headObserver.observe(document.head, { childList: true });
}
// ─────────────────────────────────────────────────────────────────────────────
createRoot(document.getElementById("root")!).render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
// Remove the HTML preloader after React has painted.
requestAnimationFrame(() => {
document.getElementById('preloader')?.remove();
});
// ─── Service worker registration (web only) ─────────────────────────────────
//
// Register /sw.js unconditionally on web. The SW itself (public/sw.js) has
// no fetch handler and wipes caches on activate — see that file for the
// stale-SW eviction story.
//
// This registration does NOT fix the stale-SW problem on its own: returning
// users with the old precache SW never run any of our new JS, because the
// old SW serves the old bundle from cache. The browser evicts the old SW
// out-of-band by re-fetching /sw.js on its own update schedule, and the
// new SW's activate handler does the actual cache wipe + tab reload.
//
// What this registration buys us is forward-looking insurance: it ensures
// every web visitor has a SW in place, so the next time we need to ship an
// emergency cache bust via /sw.js, there's something for the browser to
// update. Without it, only push-enabled users (who hit
// usePushNotifications) would ever have a SW registered.
//
// Native (Capacitor) skips this — assets are served from the local
// filesystem, no SW involved.
if (!Capacitor.isNativePlatform() && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch((err) => {
console.warn('[sw] registration failed:', err);
});
});
}