/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { useState, useEffect, useCallback, useMemo } from 'react'; import { X, Info, Keyboard, ExternalLink, Sparkles, ChevronDown, ChevronRight, Zap, Wrench, Plus, Package, ShieldCheck } from 'lucide-react'; function GithubIcon({ className }: { className?: string }) { return ( ); } import { Button } from '@/components/ui/button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { KEYBOARD_SHORTCUTS } from '@/hooks/useKeyboardShortcuts'; import { navigateToPath } from '@/services/app-navigation'; const GITHUB_URL = 'https://github.com/LTplus-AG/ifc-lite'; interface InfoDialogProps { open: boolean; onClose: () => void; } function formatBuildDate(iso: string): string { try { return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return iso; } } const TYPE_CONFIG = { feature: { icon: Plus, className: 'text-emerald-500' }, fix: { icon: Wrench, className: 'text-amber-500' }, perf: { icon: Zap, className: 'text-blue-500' }, } as const; function PrivacyBanner() { const [expanded, setExpanded] = useState(false); return (
{expanded && (

All files are processed locally in the browser with{' '} WebAssembly (WASM) {' '}– no server upload, near-native speed.

Verify: press F12 → Network tab → no IFC data transmitted.

)}
); } function AboutTab() { const [showPackages, setShowPackages] = useState(false); const packageVersions = typeof __PACKAGE_VERSIONS__ !== 'undefined' ? __PACKAGE_VERSIONS__ : []; return (
{/* Header */}

ifc-lite

v{__APP_VERSION__} · {formatBuildDate(__BUILD_DATE__)}

{/* Links */}
ifclite.dev Docs GitHub Report issue MPL-2.0
{/* Feature chips */}
{[ 'WebGPU', 'IFC2x3', 'IFC4', 'IFC4X3', 'IFC5/IFCX', 'Federation', 'Measurements', 'Sections', 'Properties', 'Data tables', 'Lens rules', 'IDS', '2D drawings', 'BCF', 'Scripting', 'AI assistant', 'glTF export', 'CSV', 'Parquet', ].map((tag) => ( {tag} ))}
{/* Privacy & Security */} {/* Package Versions */} {packageVersions.length > 0 && (
{showPackages && (
{packageVersions.map((pkg) => (
{pkg.name.replace('@ifc-lite/', '')} {pkg.version}
))}
)}
)}
); } function formatPkgName(name: string): string { return name.replace('@ifc-lite/', ''); } type TimelineEntry = { version: string; isViewerVersion: boolean; entries: Array<{ pkg: string; highlights: typeof __RELEASE_HISTORY__[0]['releases'][0]['highlights'] }>; }; const compareSemver = (a: string, b: string) => { const pa = a.split('.').map(Number); const pb = b.split('.').map(Number); for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pb[i] || 0) - (pa[i] || 0); } return 0; }; /** Merge all per-package changelogs into a unified timeline grouped by version. */ function buildTimeline( packageChangelogs: typeof __RELEASE_HISTORY__, viewerVersion: string ): TimelineEntry[] { type Highlights = typeof __RELEASE_HISTORY__[0]['releases'][0]['highlights']; const versionMap = new Map>(); for (const pkg of packageChangelogs) { for (const release of pkg.releases) { if (!versionMap.has(release.version)) { versionMap.set(release.version, new Map()); } versionMap.get(release.version)!.set(pkg.name, release.highlights); } } return Array.from(versionMap.entries()) .sort(([a], [b]) => compareSemver(a, b)) .map(([version, pkgMap]) => ({ version, isViewerVersion: version === viewerVersion, entries: Array.from(pkgMap.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([pkg, highlights]) => ({ pkg, highlights })), })); } function WhatsNewTab() { const packageChangelogs = __RELEASE_HISTORY__; const viewerVersion = __APP_VERSION__; const [expandedVersions, setExpandedVersions] = useState>(() => new Set()); const timeline = useMemo( () => buildTimeline(packageChangelogs, viewerVersion), [packageChangelogs, viewerVersion] ); // Auto-expand the first version with actual changes useEffect(() => { if (timeline.length > 0 && expandedVersions.size === 0) { setExpandedVersions(new Set([timeline[0].version])); } }, [timeline]); const toggleVersion = useCallback((version: string) => { setExpandedVersions((prev) => { const next = new Set(prev); if (next.has(version)) next.delete(version); else next.add(version); return next; }); }, []); if (timeline.length === 0) { return (
No release history available.
); } return (
{/* Anchor the current app version. The rows below carry each package's own independent version (e.g. parser may be ahead of the viewer), so without this the highest row reads as "the version" โ€” issue #1107, item 2. */}
You’re on viewer v{viewerVersion} rows below are per-package releases
{timeline.map((release) => { const isExpanded = expandedVersions.has(release.version); const totalHighlights = release.entries.reduce((s, e) => s + e.highlights.length, 0); return (
{isExpanded && (
{release.entries.map(({ pkg, highlights }) => (
{formatPkgName(pkg)}
    {highlights.map((h) => { const { icon: Icon, className } = TYPE_CONFIG[h.type]; return (
  • {h.text}
  • ); })}
))}
)}
); })} {/* Legend */}
Feature Fix Perf
); } function ShortcutsTab() { // Group shortcuts by category const grouped = KEYBOARD_SHORTCUTS.reduce( (acc, shortcut) => { if (!acc[shortcut.category]) { acc[shortcut.category] = []; } acc[shortcut.category].push(shortcut); return acc; }, {} as Record ); return (
{/* Learn-more row: drives discovery to the marketing site and the github.io docs. Sits above the shortcut groups so it's the first thing users hunting for help see, without crowding the keyboard reference itself. */}
Learn more: ifclite.dev ยท docs
{Object.entries(grouped).map(([category, shortcuts]) => (

{category}

{shortcuts.map((shortcut) => (
{shortcut.description} {shortcut.key}
))}
))}
); } export function KeyboardShortcutsDialog({ open, onClose }: InfoDialogProps) { // Close on escape useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && open) { onClose(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [open, onClose]); if (!open) return null; return (
{/* Header โ€” the MCP CTA lives here, in line with the title, so it's a discoverable "what else can this do?" affordance without crowding the modeling toolbar. */}

Info

{/* Tabbed Content */}
About What's New Shortcuts
{/* Footer */}
Press{' '} ? {' '} to toggle this panel
); } // Hook to manage info dialog state (renamed export for backward compatibility) export function useKeyboardShortcutsDialog() { const [open, setOpen] = useState(false); const toggle = useCallback(() => setOpen((o) => !o), []); const close = useCallback(() => setOpen(false), []); // Listen for '?' key to toggle useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Ignore if typing in an input or textarea const target = e.target as HTMLElement; if ( target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable ) { return; } if (e.key === '?' || (e.key === '/' && e.shiftKey)) { e.preventDefault(); toggle(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [toggle]); return { open, toggle, close }; }