import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import { parseFrontmatter } from '../utils/frontmatter' import type { ThemeFile, VaultEntry, VaultSettings } from '../types' function tauriCall(command: string, args: Record): Promise { return isTauri() ? invoke(command, args) : mockInvoke(command, args) } /** Frontmatter keys that are metadata — not CSS custom properties. */ const NON_THEME_KEYS = new Set([ 'Is A', 'type', 'is_a', 'is a', 'Name', 'name', 'title', 'Title', 'Description', 'description', 'Archived', 'archived', 'Trashed', 'trashed', 'Trashed at', 'trashed at', 'trashed_at', 'Created at', 'created at', 'created_at', 'Created time', 'created_time', 'Owner', 'owner', 'Status', 'status', 'Cadence', 'cadence', 'aliases', 'Belongs to', 'belongs_to', 'belongs to', 'Related to', 'related_to', 'related to', ]) /** Extract CSS custom properties from a theme note's frontmatter content. */ export function extractCssVars(content: string): Record { const fm = parseFrontmatter(content) const vars: Record = {} for (const [key, value] of Object.entries(fm)) { if (NON_THEME_KEYS.has(key)) continue if (typeof value === 'string' && value) { vars[`--${key}`] = value } else if (typeof value === 'number') { vars[`--${key}`] = String(value) } } return vars } /** Extract bare colors (without -- prefix) for ThemeFile.colors from content. */ function extractColorsFromContent(content: string): Record { const fm = parseFrontmatter(content) const colors: Record = {} for (const [key, value] of Object.entries(fm)) { if (NON_THEME_KEYS.has(key)) continue if (typeof value === 'string' && value.startsWith('#')) { colors[key] = value } } return colors } /** Check if a hex color is perceptually dark (luminance < 0.5). */ export function isColorDark(hex: string): boolean { if (!hex.startsWith('#') || hex.length < 7) return false const r = parseInt(hex.slice(1, 3), 16) const g = parseInt(hex.slice(3, 5), 16) const b = parseInt(hex.slice(5, 7), 16) return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5 } /** Update color-scheme and data-theme-mode on document root based on --background. */ function updateColorScheme(vars: Record): void { const bg = vars['--background'] if (!bg) return const dark = isColorDark(bg) const root = document.documentElement root.style.setProperty('color-scheme', dark ? 'dark' : 'light') root.dataset.themeMode = dark ? 'dark' : 'light' } function clearColorScheme(): void { const root = document.documentElement root.style.removeProperty('color-scheme') delete root.dataset.themeMode } const THEME_STYLE_ID = 'laputa-theme-vars' function getOrCreateThemeStyle(): HTMLStyleElement { let el = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null if (!el) { el = document.createElement('style') el.id = THEME_STYLE_ID document.head.appendChild(el) } return el } function applyVarsToDom(vars: Record): void { const root = document.documentElement for (const [key, value] of Object.entries(vars)) { root.style.setProperty(key, value) } updateColorScheme(vars) // WKWebView doesn't invalidate ::before/::after pseudo-element styles when // CSS custom properties change via inline styles alone — `void offsetHeight` // triggers layout reflow but not style recalculation on pseudo-elements. // Replacing a