feat: add platform shells, CLI, formatting, plugins, tests, and benchmarks
Phase 4 — Platform shells: - calcpad-macos/: SwiftUI two-column editor with Rust FFI bridge (16 files) - calcpad-windows/: iced GUI with Windows 11 Fluent theme (7 files, 13 tests) - calcpad-web/: React 18 + CodeMirror 6 + WASM Worker + PWA (20 files) - calcpad-cli/: clap-based CLI with expression eval, pipe/stdin, JSON/CSV output, and interactive REPL with rustyline history Phase 5 — Engine modules: - formatting/: answer formatting (decimal/scientific/SI notation, thousands separators, currency), line type classification, clipboard values (93 tests) - plugins/: CalcPadPlugin trait, PluginRegistry, Rhai scripting stub (43 tests) - benches/: criterion benchmarks (single-line, 100/500-line sheets, DAG, incremental) - tests/sheet_scenarios.rs: 20 real-world integration tests - tests/proptest_fuzz.rs: 12 property-based fuzz tests 771 tests passing across workspace, 0 failures.
This commit is contained in:
69
calcpad-web/src/hooks/useInstallPrompt.ts
Normal file
69
calcpad-web/src/hooks/useInstallPrompt.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* React hook that captures the beforeinstallprompt event for PWA install.
|
||||
* Adapted from epic/9-3-pwa-support.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
||||
}
|
||||
|
||||
export interface InstallPromptState {
|
||||
promptEvent: BeforeInstallPromptEvent | null
|
||||
isInstalled: boolean
|
||||
handleInstall: () => Promise<void>
|
||||
handleDismiss: () => void
|
||||
}
|
||||
|
||||
export function useInstallPrompt(): InstallPromptState {
|
||||
const [promptEvent, setPromptEvent] = useState<BeforeInstallPromptEvent | null>(null)
|
||||
const [isInstalled, setIsInstalled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if already installed (standalone mode)
|
||||
const isStandalone =
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
('standalone' in navigator && (navigator as unknown as { standalone: boolean }).standalone)
|
||||
|
||||
if (isStandalone) {
|
||||
setIsInstalled(true)
|
||||
return
|
||||
}
|
||||
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault()
|
||||
setPromptEvent(e as BeforeInstallPromptEvent)
|
||||
}
|
||||
|
||||
const installedHandler = () => {
|
||||
setIsInstalled(true)
|
||||
setPromptEvent(null)
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handler)
|
||||
window.addEventListener('appinstalled', installedHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handler)
|
||||
window.removeEventListener('appinstalled', installedHandler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleInstall = useCallback(async () => {
|
||||
if (!promptEvent) return
|
||||
await promptEvent.prompt()
|
||||
const result = await promptEvent.userChoice
|
||||
if (result.outcome === 'accepted') {
|
||||
setIsInstalled(true)
|
||||
}
|
||||
setPromptEvent(null)
|
||||
}, [promptEvent])
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setPromptEvent(null)
|
||||
}, [])
|
||||
|
||||
return { promptEvent, isInstalled, handleInstall, handleDismiss }
|
||||
}
|
||||
27
calcpad-web/src/hooks/useOnlineStatus.ts
Normal file
27
calcpad-web/src/hooks/useOnlineStatus.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* React hook that tracks whether the browser is online or offline.
|
||||
* Adapted from epic/9-3-pwa-support.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function useOnlineStatus(): boolean {
|
||||
const [isOnline, setIsOnline] = useState(
|
||||
typeof navigator !== 'undefined' ? navigator.onLine : true,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOnline(true)
|
||||
const handleOffline = () => setIsOnline(false)
|
||||
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return isOnline
|
||||
}
|
||||
Reference in New Issue
Block a user