import Foundation import WebKit /// Vor-gefertigte `WKUserScript`-Helfer für ``WebShellView``. Apps /// pickern, was sie brauchen, und reichen das Ergebnis als /// `config.userScripts` durch. /// /// `WKUserScript` ist MainActor-isolated; deshalb sind die Factory- /// Methoden hier ebenfalls MainActor. Aufrufer leben sowieso auf Main /// (SwiftUI `makeUIView`/`makeNSView` sind MainActor). @MainActor public enum WebShellScripts { /// Erzwingt Dark-Color-Scheme im WebView, indem ein `` injiziert und `.dark` an /// `` gehängt wird. Sinnvoll für Web-Apps, die nur Dark- /// Styles haben (Seepuls) oder bei denen die App das Light/Dark /// hart festlegt. public static let preferDarkScheme: WKUserScript = .init( source: """ (function() { var meta = document.querySelector('meta[name="color-scheme"]'); if (!meta) { meta = document.createElement('meta'); meta.setAttribute('name', 'color-scheme'); (document.head || document.documentElement).appendChild(meta); } meta.setAttribute('content', 'dark'); var html = document.documentElement; if (html) html.classList.add('dark'); })(); """, injectionTime: .atDocumentStart, forMainFrameOnly: true ) /// Synct den System-Dark-Mode in den WebView via /// `matchMedia('(prefers-color-scheme: dark)')`. Setzt eine /// `.dark`-Klasse auf `` und optional einen `localStorage`- /// Key, an dem das Web-Theme hängt. Listener für Live-Switch /// während die Page offen ist. /// /// - Parameter localStorageKey: Key, an dem das Web seinen Theme- /// State liest. `nil` falls Web nur auf `.dark` reagiert. public static func syncDarkMode(localStorageKey: String? = nil) -> WKUserScript { let setStorage: String if let key = localStorageKey { let escaped = key.replacingOccurrences(of: "'", with: "\\'") setStorage = """ try { if (isDark) localStorage.setItem('\(escaped)', 'dark'); else localStorage.removeItem('\(escaped)'); } catch (e) {} """ } else { setStorage = "" } let source = """ (function() { function apply(isDark) { \(setStorage) var html = document.documentElement; if (!html) return; if (isDark) html.classList.add('dark'); else html.classList.remove('dark'); } var mq = window.matchMedia('(prefers-color-scheme: dark)'); apply(mq.matches); if (mq.addEventListener) { mq.addEventListener('change', function(e) { apply(e.matches); }); } })(); """ return WKUserScript( source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true ) } /// Versteckt eine Top-Nav-Komponente per CSS, damit eine native /// TabBar nicht doppelt rendert. Mehrere Selektoren werden /// gestapelt (mit `,`-Group), damit ein Markup-Refactor in /// Web-Land das Hide nicht still bricht. /// /// Konvention für Selektor-Kaskaden: /// 1. `nav[data-app-nav]` / `header[data-app-nav]` — explizites /// Attribut, falls Web es markieren will (greift sofort) /// 2. strukturell (`body header:has(a.brand)` o.ä.) — heutige /// Realität /// 3. positionell (`body > nav:first-of-type`) — Fallback /// /// - Parameter selectors: CSS-Selektoren, die `display: none /// !important` bekommen. Werden mit `,` gejoint. /// - Parameter tagName: Wert für das `data-mana-webshell`- /// Attribut auf dem Style-Tag (debugging, source inspection). public static func hideElements( selectors: [String], tagName: String = "hide" ) -> WKUserScript { let joined = selectors.joined(separator: ",\n") let escapedTag = tagName.replacingOccurrences(of: "'", with: "\\'") let source = """ (function() { var css = `\(joined) { display: none !important; }`; var style = document.createElement('style'); style.setAttribute('data-mana-webshell', '\(escapedTag)'); style.textContent = css; (document.head || document.documentElement).appendChild(style); })(); """ return WKUserScript( source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true ) } }