SkateSpots/.agents/skills/capacitor/files/lib/nativeBootstrap.ts
shakespeare.diy 06fae294b8 New project created with Shakespeare
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 19:49:07 +07:00

121 lines
4.5 KiB
TypeScript

import { Capacitor, SystemBars, SystemBarsStyle } from '@capacitor/core';
/**
* One-time Capacitor-native bootstrap: configures system chrome to match
* the active CSS theme and hides the iOS keyboard accessory bar.
*
* Call this from `src/main.tsx` **before** `createRoot(...).render(...)`.
* On web this is a no-op.
*
* What it does on native:
*
* 1. Hides the iOS keyboard accessory bar (the prev/next/done toolbar above
* the keyboard) — most apps don't need it and it steals screen space.
*
* 2. Keeps the native system bar icon style (clock, battery, etc.) in sync
* with the active CSS theme. When the app is on a dark background, icons
* are rendered light; on a light background, dark. This is re-applied
* whenever:
* - the `class` on `<html>` changes (light / dark / any custom theme
* that toggles a class, e.g. your AppProvider)
* - the contents of `<style id="theme-vars">` change (custom themes
* that set CSS variables without toggling a class)
*
* Uses `SystemBars` (exposed by the Capacitor-core plugin registry). On
* Android 16+ (API 36) setBackgroundColor on the bars no longer works — the
* bars are transparent and the web content renders behind them. Your app
* should draw its own safe-area backgrounds in CSS. Only icon style matters
* here.
*
* @example
* // src/main.tsx
* import { bootstrapNative } from '@/lib/nativeBootstrap';
* bootstrapNative();
* createRoot(document.getElementById('root')!).render(<App />);
*/
export function bootstrapNative(): void {
if (!Capacitor.isNativePlatform()) return;
// iOS-only: hide the keyboard accessory bar.
if (Capacitor.getPlatform() === 'ios') {
import('@capacitor/keyboard').then(({ Keyboard }) => {
Keyboard.setAccessoryBarVisible({ isVisible: false }).catch(() => {});
}).catch(() => {});
}
/**
* SystemBarsStyle.Dark = light/white icons (use on dark backgrounds)
* SystemBarsStyle.Light = dark/black icons (use on light backgrounds)
*/
function updateStatusBar() {
const isDark = isBackgroundDark();
SystemBars.setStyle({
style: isDark ? SystemBarsStyle.Dark : SystemBarsStyle.Light,
}).catch(() => {});
}
// Apply immediately. The theme class is usually set synchronously by the
// 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>.
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,
});
}
};
observeThemeVars();
// The style element may not exist yet — watch <head> for it to appear.
const headObserver = new MutationObserver(() => observeThemeVars());
headObserver.observe(document.head, { childList: true });
}
/**
* Detect whether the current app background is "dark" for the purposes of
* choosing status-bar icon color.
*
* Strategy (first match wins):
* 1. If `<html>` has class `dark`, return true.
* 2. If `<html>` has class `light`, return false.
* 3. Parse the computed `background-color` of `<body>` and compare its
* perceived luminance against 0.5.
*
* Steps 1 and 2 cover the default mkstack theme system. Step 3 covers
* custom themes that don't toggle the `light`/`dark` class on `<html>`.
*/
function isBackgroundDark(): boolean {
const cl = document.documentElement.classList;
if (cl.contains('dark')) return true;
if (cl.contains('light')) return false;
const bg = getComputedStyle(document.body).backgroundColor;
const rgb = parseRgb(bg);
if (!rgb) return false;
// Perceived luminance (ITU-R BT.709).
const [r, g, b] = rgb.map((c) => c / 255);
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return lum < 0.5;
}
function parseRgb(color: string): [number, number, number] | null {
const m = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}