97da8ae822
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.
125 lines
5.6 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}
|