UNPKG

@stacksjs/stx

Version:

A performant UI Framework. Powered by Bun.

319 lines (286 loc) 10.4 kB
/** * Generate the Craft bridge script with custom configuration */ export declare function generateCraftBridgeScript(config?: CraftBridgeConfig): string; /** * Check if the current environment supports Craft * This is for server-side detection */ export declare function isCraftEnvironment(): boolean; /** * Craft directive for stx templates * Injects the Craft bridge script when the template is processed * * Usage in template: * @craft * or * @craft({ debug: true }) */ export declare function processCraftDirective(content: string, _context?: Record<string, unknown>): string; /** * Auto-inject Craft bridge into head if not already present */ export declare function injectCraftBridge(html: string, config?: CraftBridgeConfig): string; /** * Client-side Craft bridge script * This gets injected into stx templates when running in a Craft webview */ export declare const CRAFT_BRIDGE_SCRIPT: ` <script> (function() { 'use strict'; // Skip if already initialized or not in Craft environment if (window.__craftBridgeInitialized) return; window.__craftBridgeInitialized = true; // Message ID counter let messageId = 0; const generateId = () => 'msg_' + Date.now() + '_' + (++messageId); // Pending requests const pending = new Map(); // Event listeners const eventListeners = new Map(); // Configuration const config = { debug: false, timeout: 30000 }; // Send message to native function send(message) { const json = JSON.stringify(message); if (config.debug) { console.log('[Craft Bridge] Sending:', message.method, message.params); } // iOS WKWebView if (window.webkit?.messageHandlers?.craft) { window.webkit.messageHandlers.craft.postMessage(message); return true; } // Android WebView if (window.CraftBridge) { window.CraftBridge.postMessage(json); return true; } // Electron IPC if (window.craftIPC) { window.craftIPC.send('bridge-message', message); return true; } return false; } // Make a request and wait for response function request(method, params) { return new Promise((resolve, reject) => { const id = generateId(); const timeout = setTimeout(() => { pending.delete(id); reject(new Error('Request timeout: ' + method)); }, config.timeout); pending.set(id, { resolve, reject, timeout }); const sent = send({ id, type: 'request', method, params }); if (!sent) { clearTimeout(timeout); pending.delete(id); // Return gracefully if not in Craft environment resolve(undefined); } }); } // Handle incoming messages function handleMessage(event) { let data; try { data = event.detail || (typeof event.data === 'string' ? JSON.parse(event.data) : event.data); } catch (e) { return; } if (!data || !data.id) return; if (config.debug) { console.log('[Craft Bridge] Received:', data); } if (data.type === 'response') { const req = pending.get(data.id); if (req) { clearTimeout(req.timeout); pending.delete(data.id); if (data.error) { req.reject(new Error(data.error.message)); } else { req.resolve(data.result); } } } else if (data.type === 'event' && data.method) { const listeners = eventListeners.get(data.method) || []; listeners.forEach(fn => fn(data.params)); } } // Listen for messages window.addEventListener('message', handleMessage); window.addEventListener('craft-bridge-message', handleMessage); // Check if running in Craft function isCraft() { return !!( window.webkit?.messageHandlers?.craft || window.CraftBridge || window.craftIPC ); } // Subscribe to events function on(event, callback) { if (!eventListeners.has(event)) { eventListeners.set(event, []); } eventListeners.get(event).push(callback); return () => { const listeners = eventListeners.get(event); const idx = listeners.indexOf(callback); if (idx > -1) listeners.splice(idx, 1); }; } // Build the craft API object window.craft = { // Meta isCraft, on, config, // Window API window: { show: () => request('window.show'), hide: () => request('window.hide'), close: () => request('window.close'), minimize: () => request('window.minimize'), maximize: () => request('window.maximize'), restore: () => request('window.restore'), focus: () => request('window.focus'), blur: () => request('window.blur'), setTitle: (title) => request('window.setTitle', { title }), setSize: (width, height) => request('window.setSize', { width, height }), setPosition: (x, y) => request('window.setPosition', { x, y }), setFullscreen: (fullscreen) => request('window.setFullscreen', { fullscreen }), setAlwaysOnTop: (alwaysOnTop) => request('window.setAlwaysOnTop', { alwaysOnTop }), getSize: () => request('window.getSize'), getPosition: () => request('window.getPosition'), isFullscreen: () => request('window.isFullscreen'), isMaximized: () => request('window.isMaximized'), isMinimized: () => request('window.isMinimized'), isVisible: () => request('window.isVisible'), center: () => request('window.center'), toggleFullscreen: () => request('window.toggleFullscreen'), startDrag: () => request('window.startDrag'), }, // System Tray/Menubar API tray: { setTitle: (title) => request('tray.setTitle', { title }), setTooltip: (tooltip) => request('tray.setTooltip', { tooltip }), setIcon: (icon) => request('tray.setIcon', { icon }), setMenu: (menu) => request('tray.setMenu', { menu }), show: () => request('tray.show'), hide: () => request('tray.hide'), onClick: (callback) => on('tray.click', callback), onDoubleClick: (callback) => on('tray.doubleClick', callback), onRightClick: (callback) => on('tray.rightClick', callback), }, // App API app: { quit: () => request('app.quit'), hide: () => request('app.hide'), show: () => request('app.show'), focus: () => request('app.focus'), getInfo: () => request('app.getInfo'), getVersion: () => request('app.getVersion'), getName: () => request('app.getName'), getPath: (name) => request('app.getPath', { name }), isDarkMode: () => request('app.isDarkMode'), getLocale: () => request('app.getLocale'), setBadge: (badge) => request('app.setBadge', { badge }), hideDockIcon: () => request('app.hideDockIcon'), showDockIcon: () => request('app.showDockIcon'), notify: (options) => request('app.notify', options), registerShortcut: (accelerator, callback) => { const id = generateId(); on('shortcut.' + id, callback); return request('app.registerShortcut', { accelerator, id }); }, unregisterShortcut: (accelerator) => request('app.unregisterShortcut', { accelerator }), }, // Dialog API dialog: { openFile: (options) => request('dialog.openFile', options), openFolder: (options) => request('dialog.openFolder', options), saveFile: (options) => request('dialog.saveFile', options), showAlert: (options) => request('dialog.showAlert', options), showConfirm: (options) => request('dialog.showConfirm', options), showPrompt: (options) => request('dialog.showPrompt', options), showColorPicker: (options) => request('dialog.showColorPicker', options), }, // Clipboard API clipboard: { writeText: (text) => request('clipboard.writeText', { text }), readText: () => request('clipboard.readText'), writeHTML: (html) => request('clipboard.writeHTML', { html }), readHTML: () => request('clipboard.readHTML'), clear: () => request('clipboard.clear'), }, // File System API (limited for security) fs: { readFile: (path, encoding) => request('fs.readFile', { path, encoding }), writeFile: (path, content, encoding) => request('fs.writeFile', { path, content, encoding }), exists: (path) => request('fs.exists', { path }), stat: (path) => request('fs.stat', { path }), readDir: (path) => request('fs.readDir', { path }), createDir: (path, recursive) => request('fs.createDir', { path, recursive }), remove: (path, recursive) => request('fs.remove', { path, recursive }), }, // Process/Shell API (limited for security) process: { exec: (command, options) => request('process.exec', { command, ...options }), spawn: (command, args, options) => request('process.spawn', { command, args, ...options }), env: () => request('process.env'), cwd: () => request('process.cwd'), platform: () => request('process.platform'), }, // Native component helpers components: { createSidebar: (config) => request('component.createSidebar', config), createFileBrowser: (config) => request('component.createFileBrowser', config), createSplitView: (config) => request('component.createSplitView', config), updateComponent: (id, props) => request('component.update', { componentId: id, props }), destroyComponent: (id) => request('component.destroy', { componentId: id }), } }; // Dispatch ready event window.dispatchEvent(new CustomEvent('craft:ready', { detail: { isCraft: isCraft() } })); if (config.debug) { console.log('[Craft Bridge] Initialized, isCraft:', isCraft()); } })(); </script> `; /** * Craft Bridge Integration for stx * * Provides native desktop/mobile APIs when running in a Craft webview. * This module generates client-side JavaScript that can be injected into stx templates * to enable access to Craft's native capabilities. * * @example * ```html * <script> * // In your stx template * if (window.craft) { * await window.craft.tray.setTitle('🎙️ Listening...') * await window.craft.app.notify({ title: 'Ready', body: 'App is ready' }) * } * </script> * ``` */ export declare interface CraftBridgeConfig { debug?: boolean timeout?: number enableOfflineQueue?: boolean } export default craftBridge;