import 'tippy.js/animations/shift-toward-subtle.css' import 'tippy.js/dist/tippy.css' import tippy from 'tippy.js' // optional import type {ClientResponseError} from 'pocketbase' class AppStore { theme: string = $state('dark') showMobileNavigation: boolean = $state(false) seo = $state({ title: 'Title', description: 'Description', }) // meta: { // customer: CustomersResponse | undefined // site: SitesResponse | undefined // record: RecordsResponse | undefined // contact: ContactsResponse | undefined // file: FilesResponse | undefined // user: UsersResponse | undefined // } = $state({ // customer: undefined, // site: undefined, // record: undefined, // contact: undefined, // file: undefined, // user: undefined, // }) showGlobalActionMenu = $state(false) confirm: { show: boolean title: string text: string error: boolean | ClientResponseError yes: () => void no: () => void request: (props: any) => void close: () => void } = $state({ show: false, title: '', text: '', error: false, yes: () => { }, no: () => { }, request: (props: any) => { this.confirm = {...this.confirm, ...props} this.confirm.show = true }, close: () => { this.confirm.show = false this.confirm.title = '' this.confirm.text = '' }, }) /** Global Hotkey handler Ignores hotkey handling when focus of user is in generic input-tags or [contenteditable] @example */ hotkey = (event: KeyboardEvent, condition: boolean, callback: Function) => { // Sicherstellen, dass `event` und `event.target` vorhanden sind if (!event || !event.target) { return } // Liste ignorierter Tag-Namen const ignoredTags = ['INPUT', 'TEXTAREA', 'SELECT', 'OPTION'] // Standard-TAG-Prüfung mit optionaler Erweiterung const isIgnoredTag = ignoredTags.includes((event.target as HTMLElement).tagName?.toUpperCase()) // Fokusierbare oder editierbare Inhalte ignorieren const isEditable = (event.target as HTMLElement).isContentEditable || // `contenteditable=true` (event.target as HTMLElement).hasAttribute('contenteditable') // Attribut prüfen für dynamische Inhalte if (isIgnoredTag || isEditable) { // Rücksprung, keine Aktion ausführen return } // Bedingung und Callback ausführen if (condition) { event.preventDefault() // Standardaktion abbrechen callback() // Callback-Funktion ausführen } } toggleTheme = () => { this.theme = this.theme === 'dark' ? 'light' : 'dark' } } export const app = new AppStore() export const utils = { getFileSize: async (fileUrl: string): Promise => { try { const response = await fetch(fileUrl, {method: 'HEAD'}) const contentLength = response.headers.get('content-length') const bytes = contentLength ? parseInt(contentLength, 10) : 0 const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) if (bytes === 0) return '0 Bytes' return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } catch (error) { console.error('Error fetching file size:', error) return 'Unbekannt' } }, sanitize: (input: string) => { return input.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, '') }, excerpt: (input: string, length: number = 20) => { if (input.length <= 20) return input return input.substring(0, length) + '…' }, slugify: (input: string) => { if (!input) return '' // make lower case and trim var slug = input.toLowerCase().trim() // remove accents from charaters slug = slug.normalize('NFD').replace(/[\u0300-\u036f]/g, '') // replace invalid chars with spaces slug = slug.replace(/[^a-z0-9\s-]/g, ' ').trim() // replace multiple spaces or hyphens with a single hyphen slug = slug.replace(/[\s-]+/g, '-') return slug }, } /** * Tooltip für ein Element. Appweit die einzige Art, einen Hinweis an einen * Knopf zu hängen — `title` überlassen wir dem Browser nicht mehr: Es * erscheint erst nach einer Sekunde, sieht auf jedem System anders aus und * lässt sich auf Touch-Geräten gar nicht aufrufen. * * Die Action beherrscht Aktualisieren und Aufräumen: Ein Text, der sich * ändert (»Erledigt« / »Wieder öffnen«), muss auch im Tooltip wechseln, und * eine Instanz, die ein entferntes Element überlebt, hinge als leere Blase * im Dokument. */ export const tooltip = ( element: HTMLElement, props: { active?: boolean; content?: string; showOnCreate?: boolean } = {}, ) => { const options = (p: typeof props) => ({ animation: 'shift-toward-subtle', allowHTML: false, content: p.content ?? '', showOnCreate: p.showOnCreate, }) const wanted = (p: typeof props) => p.active !== false && !!p.content?.trim() let instance = wanted(props) ? tippy(element, options(props)) : null return { update(next: typeof props) { if (!wanted(next)) { instance?.destroy() instance = null return } if (instance) instance.setProps(options(next)) else instance = tippy(element, options(next)) }, destroy() { instance?.destroy() instance = null }, } }