tgui-angular
Version:
🚀 Angular UI library for Telegram Web Apps with modern components and theming support | 💼 Author is open to work opportunities | 📧 Contact: a.blagovestnov@gmail.com | 💬 Telegram: @ablagovestnov
1 lines • 810 kB
Source Map (JSON)
{"version":3,"file":"tgui-angular.mjs","sources":["../../../projects/tgui/src/lib/services/telegram.service.ts","../../../projects/tgui/src/lib/services/theme.service.ts","../../../projects/tgui/src/lib/services/platform.service.ts","../../../projects/tgui/src/lib/services/portal.service.ts","../../../projects/tgui/src/lib/services/ripple.service.ts","../../../projects/tgui/src/lib/utils/theme-helper.ts","../../../projects/tgui/src/lib/utils/has-node.ts","../../../projects/tgui/src/lib/utils/class-names.ts","../../../projects/tgui/src/lib/utils/function.ts","../../../projects/tgui/src/lib/utils/chunk.ts","../../../projects/tgui/src/lib/directives/tgui-theme.directive.ts","../../../projects/tgui/src/lib/directives/portal-outlet.directive.ts","../../../projects/tgui/src/lib/directives/horizontal-scroll.directive.ts","../../../projects/tgui/src/lib/directives/visually-hidden.directive.ts","../../../projects/tgui/src/lib/components/utils/tappable/components/ripple/ripple.component.ts","../../../projects/tgui/src/lib/components/utils/tappable/tappable.component.ts","../../../projects/tgui/src/lib/components/utils/portal/root-portal.component.ts","../../../projects/tgui/src/lib/components/utils/tgui-root/tgui-root.component.ts","../../../projects/tgui/src/lib/components/utils/touch/touch.component.ts","../../../projects/tgui/src/lib/components/utils/root-renderer/root-renderer.component.ts","../../../projects/tgui/src/lib/components/typography/typography.component.ts","../../../projects/tgui/src/lib/components/typography/caption/caption.component.ts","../../../projects/tgui/src/lib/components/typography/subheadline/subheadline.component.ts","../../../projects/tgui/src/lib/components/blocks/badge/badge.component.ts","../../../projects/tgui/src/lib/components/feedback/spinner/components/base-spinner/base-spinner.component.ts","../../../projects/tgui/src/lib/components/feedback/spinner/components/ios-spinner/ios-spinner.component.ts","../../../projects/tgui/src/lib/components/feedback/spinner/spinner.component.ts","../../../projects/tgui/src/lib/components/feedback/spoiler/spoiler.component.ts","../../../projects/tgui/src/lib/components/feedback/skeleton/skeleton.component.ts","../../../projects/tgui/src/lib/components/feedback/progress/progress.component.ts","../../../projects/tgui/src/lib/components/feedback/circular-progress/utils/get-circle-attributes.ts","../../../projects/tgui/src/lib/components/feedback/circular-progress/circular-progress.component.ts","../../../projects/tgui/src/lib/components/feedback/snackbar/snackbar.component.ts","../../../projects/tgui/src/lib/components/feedback/snackbar/snackbar.service.ts","../../../projects/tgui/src/lib/components/feedback/snackbar/components/snackbar-button/snackbar-button.component.ts","../../../projects/tgui/src/lib/icons/icon-base.component.ts","../../../projects/tgui/src/lib/icons/directives/svg-props.directive.ts","../../../projects/tgui/src/lib/icons/icon12/tgui-icon12-quote.ts","../../../projects/tgui/src/lib/icons/icon16/tgui-icon16-chevron.ts","../../../projects/tgui/src/lib/icons/icon16/tgui-icon16-cancel.ts","../../../projects/tgui/src/lib/icons/icon20/tgui-icon20-select-ios.ts","../../../projects/tgui/src/lib/icons/icon20/tgui-icon20-select.ts","../../../projects/tgui/src/lib/icons/icon20/tgui-icon20-question-mark.ts","../../../projects/tgui/src/lib/icons/icon20/tgui-icon20-chevron-down.ts","../../../projects/tgui/src/lib/icons/icon20/tgui-icon20-copy.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-close.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-sun-low.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-qr.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-person-remove.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-notifications.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-chevron-right.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-chevron-left.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-chat.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-channel.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-cancel.ts","../../../projects/tgui/src/lib/icons/icon24/tgui-icon24-chevron-down.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-close.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-close-ambient.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-stats.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-heart.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-edit.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-devices.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-chat.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-attach.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-archive.ts","../../../projects/tgui/src/lib/icons/icon28/tgui-icon28-add-circle.ts","../../../projects/tgui/src/lib/icons/icon32/tgui-icon32-profile-colored-square.ts","../../../projects/tgui/src/lib/icons/icon36/tgui-icon36-backspace.ts","../../../projects/tgui/src/lib/icons/index.ts","../../../projects/tgui/src/lib/icons/dynamic-icon.component.ts","../../../projects/tgui/src/lib/components/typography/headline/headline.component.ts","../../../projects/tgui/src/lib/components/typography/large-title/large-title.component.ts","../../../projects/tgui/src/lib/components/typography/text/text.component.ts","../../../projects/tgui/src/lib/components/typography/title/title.component.ts","../../../projects/tgui/src/lib/components/blocks/button/button.component.ts","../../../projects/tgui/src/lib/components/blocks/avatar/components/avatar-acronym/avatar-acronym.component.ts","../../../projects/tgui/src/lib/components/blocks/avatar/avatar.component.ts","../../../projects/tgui/src/lib/components/blocks/avatar-stack/avatar-stack.component.ts","../../../projects/tgui/src/lib/components/misc/divider/divider.component.ts","../../../projects/tgui/src/lib/components/blocks/section/components/section-header/section-header.component.ts","../../../projects/tgui/src/lib/components/blocks/section/components/section-footer/section-footer.component.ts","../../../projects/tgui/src/lib/components/blocks/section/section.component.ts","../../../projects/tgui/src/lib/components/blocks/card/components/card-cell/card-cell.component.ts","../../../projects/tgui/src/lib/components/blocks/card/components/card-cell/card-cell.component.html","../../../projects/tgui/src/lib/components/blocks/card/card.component.ts","../../../projects/tgui/src/lib/components/blocks/card/card.component.html","../../../projects/tgui/src/lib/components/blocks/icon-button/icon-button.component.ts","../../../projects/tgui/src/lib/components/blocks/image/image.component.ts","../../../projects/tgui/src/lib/components/blocks/image/components/image-badge/image-badge.component.ts","../../../projects/tgui/src/lib/components/blocks/icon-container/icon-container.component.ts","../../../projects/tgui/src/lib/components/blocks/placeholder/placeholder.component.ts","../../../projects/tgui/src/lib/components/blocks/list/list.component.ts","../../../projects/tgui/src/lib/components/blocks/steps/steps.component.ts","../../../projects/tgui/src/lib/components/blocks/timeline/components/timeline-item/timeline-item.component.ts","../../../projects/tgui/src/lib/components/blocks/timeline/timeline.component.ts","../../../projects/tgui/src/lib/components/blocks/cell/services/typography-cell.service.ts","../../../projects/tgui/src/lib/components/blocks/cell/cell.component.ts","../../../projects/tgui/src/lib/components/blocks/accordion/accordion.component.ts","../../../projects/tgui/src/lib/components/blocks/accordion/components/accordion-summary/accordion-summary.component.ts","../../../projects/tgui/src/lib/components/blocks/accordion/components/accordion-content/accordion-content.component.ts","../../../projects/tgui/src/lib/components/blocks/cell/components/button-cell/button-cell.component.ts","../../../projects/tgui/src/lib/components/blocks/blockquote/blockquote.component.ts","../../../projects/tgui/src/lib/components/blocks/banner/banner.component.ts","../../../projects/tgui/src/lib/components/blocks/inline-buttons/inline-buttons.component.ts","../../../projects/tgui/src/lib/components/blocks/inline-buttons/components/inline-buttons-item.component.ts","../../../projects/tgui/src/lib/components/blocks/avatar/components/avatar-badge/avatar-badge.component.ts","../../../projects/tgui/src/lib/components/form/chip/chip.component.ts","../../../projects/tgui/src/lib/components/form/form-input/components/form-input-title.component.ts","../../../projects/tgui/src/lib/components/form/form-input/form-input.component.ts","../../../projects/tgui/src/lib/components/form/input/input.component.ts","../../../projects/tgui/src/lib/components/form/checkbox/icons/checkbox.component.ts","../../../projects/tgui/src/lib/components/form/checkbox/icons/checkbox-checked.component.ts","../../../projects/tgui/src/lib/components/form/checkbox/icons/checkbox-indeterminate.component.ts","../../../projects/tgui/src/lib/components/form/checkbox/checkbox.component.ts","../../../projects/tgui/src/lib/components/form/color-input/color-input.component.ts","../../../projects/tgui/src/lib/components/form/radio/icons/radio.component.ts","../../../projects/tgui/src/lib/components/form/radio/icons/radio-checked.component.ts","../../../projects/tgui/src/lib/components/form/radio/radio.component.ts","../../../projects/tgui/src/lib/components/form/file-input/file-input.component.ts","../../../projects/tgui/src/lib/components/form/pin-input/components/pin-input-button/pin-input-button.component.ts","../../../projects/tgui/src/lib/components/form/pin-input/components/pin-input-cell/pin-input-cell.component.ts","../../../projects/tgui/src/lib/utils/math.ts","../../../projects/tgui/src/lib/components/form/pin-input/hooks/use-pin-input.ts","../../../projects/tgui/src/lib/components/form/pin-input/pin-input.component.ts","../../../projects/tgui/src/lib/components/form/rating/rating.component.ts","../../../projects/tgui/src/lib/components/form/select/select.component.ts","../../../projects/tgui/src/lib/components/form/switch/switch.component.ts","../../../projects/tgui/src/lib/components/form/textarea/textarea.component.ts","../../../projects/tgui/src/lib/components/form/multiselectable/multiselectable.component.ts","../../../projects/tgui/src/lib/components/form/index.ts","../../../projects/tgui/src/lib/components/layout/fixed-layout/fixed-layout.component.ts","../../../projects/tgui/src/lib/components/layout/tabbar/tabbar-item.component.ts","../../../projects/tgui/src/lib/components/layout/tabbar/tabbar.component.ts","../../../projects/tgui/src/lib/components/navigation/breadcrumbs/breadcrumbs-item.component.ts","../../../projects/tgui/src/lib/components/navigation/breadcrumbs/icons/dot-icon.component.ts","../../../projects/tgui/src/lib/components/navigation/breadcrumbs/icons/slash-icon.component.ts","../../../projects/tgui/src/lib/components/navigation/breadcrumbs/breadcrumbs.component.ts","../../../projects/tgui/src/lib/components/navigation/tabs-list/tabs-item.component.ts","../../../projects/tgui/src/lib/components/navigation/tabs-list/tabs-list.component.ts","../../../projects/tgui/src/lib/components/navigation/segmented-control/segmented-control-item.component.ts","../../../projects/tgui/src/lib/components/navigation/segmented-control/segmented-control.component.ts","../../../projects/tgui/src/lib/components/navigation/pagination/hooks/pagination.enum.ts","../../../projects/tgui/src/lib/components/navigation/pagination/hooks/array-utils.ts","../../../projects/tgui/src/lib/components/navigation/pagination/hooks/use-pagination.ts","../../../projects/tgui/src/lib/components/navigation/pagination/pagination.component.ts","../../../projects/tgui/src/lib/components/navigation/compact-pagination/compact-pagination-item.component.ts","../../../projects/tgui/src/lib/components/navigation/compact-pagination/compact-pagination.component.ts","../../../projects/tgui/src/lib/components/navigation/link/link.component.ts","../../../projects/tgui/src/lib/components/overlays/popper/components/floating-arrow/floating-arrow.component.ts","../../../projects/tgui/src/lib/components/overlays/popper/hooks/helpers/alignment.ts","../../../projects/tgui/src/lib/components/overlays/popper/hooks/use-floating-middlewares.ts","../../../projects/tgui/src/lib/components/overlays/popper/helpers/auto-update-floating-element.ts","../../../projects/tgui/src/lib/components/overlays/popper/popper.component.ts","../../../projects/tgui/src/lib/components/overlays/tooltip/tooltip.component.ts","../../../projects/tgui/src/lib/components/overlays/modal/modal-header.component.ts","../../../projects/tgui/src/lib/components/overlays/modal/modal-close.component.ts","../../../projects/tgui/src/lib/components/overlays/modal/modal-overlay.component.ts","../../../projects/tgui/src/lib/components/overlays/modal/modal.component.ts","../../../projects/tgui/src/lib/tgui.service.ts","../../../projects/tgui/src/lib/tgui.component.ts","../../../projects/tgui/src/public-api.ts","../../../projects/tgui/src/tgui-angular.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n// Define Telegram WebApp interface\ninterface ThemeParams {\n bg_color?: string;\n text_color?: string;\n hint_color?: string;\n link_color?: string;\n button_color?: string;\n button_text_color?: string;\n secondary_bg_color?: string;\n header_bg_color?: string;\n accent_text_color?: string;\n section_bg_color?: string;\n section_header_text_color?: string;\n subtitle_text_color?: string;\n destructive_text_color?: string;\n}\n\ninterface WebApp {\n colorScheme: 'light' | 'dark';\n themeParams: ThemeParams;\n onEvent(eventName: string, eventHandler: () => void): void;\n offEvent(eventName: string, eventHandler: () => void): void;\n}\n\ndeclare global {\n interface Window {\n Telegram?: {\n WebApp?: WebApp;\n };\n }\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class TelegramService {\n private ready = new BehaviorSubject<boolean>(false);\n private initializationTimeout: number | null = null;\n\n constructor() {\n this.waitForTelegramWebApp();\n }\n\n // Check if DOM is available\n private get canUseDOM(): boolean {\n return !!(typeof window !== 'undefined' && window.document && window.document.createElement);\n }\n\n // Get Telegram WebApp data\n public getTelegramData(): WebApp | undefined {\n if (!this.canUseDOM) {\n return undefined;\n }\n\n const webApp = window.Telegram?.WebApp;\n return webApp;\n }\n\n // Wait for Telegram WebApp to be available\n private waitForTelegramWebApp(maxAttempts = 50): void {\n let attempts = 0;\n \n const checkWebApp = () => {\n const webApp = this.getTelegramData();\n \n if (webApp) {\n this.ready.next(true);\n if (this.initializationTimeout !== null) {\n window.clearTimeout(this.initializationTimeout);\n this.initializationTimeout = null;\n }\n } else if (attempts < maxAttempts) {\n attempts++;\n this.initializationTimeout = window.setTimeout(checkWebApp, 100);\n } else {\n console.warn('TelegramService: Failed to initialize WebApp after', maxAttempts, 'attempts');\n this.ready.next(false);\n }\n };\n\n checkWebApp();\n }\n\n // Get ready state as observable\n public get isReady$(): Observable<boolean> {\n return this.ready.asObservable();\n }\n\n // Get current ready state\n public get isReady(): boolean {\n return this.ready.value;\n }\n\n // Helper function to convert hex color to RGB\n public hexToRGB(hex: string): [number, number, number] {\n // Remove # if present\n hex = hex.replace('#', '');\n \n // Parse hex values\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n \n return [r, g, b];\n }\n} ","import { DOCUMENT } from '@angular/common';\nimport { Inject, Injectable, OnDestroy, signal, inject, Renderer2, RendererFactory2 } from '@angular/core';\nimport { TelegramService } from './telegram.service';\n\nexport type AppearanceType = 'light' | 'dark';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class ThemeService implements OnDestroy {\n // Signal for appearance that components can subscribe to\n public appearance = signal<AppearanceType>('light');\n private themeChangeListener: (() => void) | null = null;\n private mediaQueryList: MediaQueryList | null = null;\n private handleThemeChange: ((event: MediaQueryListEvent) => void) | null = null;\n private useSystemTheme = false;\n private currentTheme: AppearanceType = 'light';\n private renderer: Renderer2;\n\n private telegramService = inject(TelegramService);\n private document = inject(DOCUMENT);\n\n constructor(rendererFactory: RendererFactory2) {\n this.renderer = rendererFactory.createRenderer(null, null);\n \n // Wait for Telegram WebApp to be ready\n this.telegramService.isReady$.subscribe(isReady => {\n if (isReady) {\n this.initializeTheme();\n } else {\n console.warn('themeService: Telegram WebApp is not available, using browser theme');\n this.useSystemTheme = true;\n this.setupBrowserThemeDetection();\n }\n });\n }\n\n ngOnDestroy(): void {\n this.cleanupListeners();\n }\n\n /**\n * Change the theme manually\n * @param theme The theme to set\n * @param followSystem If true, will follow system theme changes after setting. Default false.\n */\n public setTheme(theme: AppearanceType, followSystem = false): void {\n this.cleanupListeners();\n this.appearance.set(theme);\n this.applyThemeToDOM(theme);\n \n // If instructed to follow system theme, restore the detector\n if (followSystem) {\n this.useSystemTheme = true;\n this.setupBrowserThemeDetection();\n } else {\n this.useSystemTheme = false;\n }\n }\n\n /**\n * Setup theme based on inputs and system preferences\n * @param appearance Appearance to use\n * @param followSystem Whether to follow system theme\n */\n public setupTheme(appearance?: AppearanceType, followSystem: boolean = false): void {\n this.cleanupListeners();\n if (followSystem) {\n // Follow system theme\n this.useSystemTheme = true;\n this.setupBrowserThemeDetection();\n } else if (appearance) {\n // Set specific theme\n this.setTheme(appearance, false);\n } else {\n // If nothing specified, try to detect system theme\n this.detectSystemTheme();\n }\n }\n\n /**\n * Detect and apply system theme\n */\n public detectSystemTheme(): void {\n if (typeof window === 'undefined') {\n // Use light theme by default\n this.setTheme('light', false);\n return;\n }\n \n const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\n this.setTheme(prefersDarkMode ? 'dark' : 'light', false);\n }\n\n /**\n * Load global CSS variables to make them available for all components\n */\n public loadGlobalStyles(): void {\n // Check if styles are already loaded\n if (this.document.getElementById('tgui-variables-css')) {\n return;\n }\n \n const head = this.document.head;\n const link = this.document.createElement('link');\n \n link.id = 'tgui-variables-css';\n link.rel = 'stylesheet';\n link.type = 'text/css';\n \n // Add error handling\n link.onerror = () => {\n console.error('Failed to load TGUI variables CSS file. Theme functionality may be limited.');\n };\n \n // In production builds, this will be replaced with the actual path\n // The actual file is bundled with the library during build\n link.href = 'assets/tgui/styles/variables.css';\n \n head.appendChild(link);\n }\n\n /**\n * Initialize the theme detection\n */\n private initializeTheme(): void {\n // First check Telegram API\n const telegramData = this.telegramService.getTelegramData();\n \n if (telegramData) {\n // Use Telegram theme\n this.appearance.set(telegramData.colorScheme);\n this.applyThemeToDOM(telegramData.colorScheme);\n \n // Set up listener for theme changes\n this.themeChangeListener = () => {\n const newTelegramData = this.telegramService.getTelegramData();\n if (newTelegramData) {\n this.appearance.set(newTelegramData.colorScheme);\n this.applyThemeToDOM(newTelegramData.colorScheme);\n }\n };\n \n telegramData.onEvent('themeChanged', this.themeChangeListener);\n } else {\n // Use browser preference\n this.useSystemTheme = true;\n this.setupBrowserThemeDetection();\n }\n }\n\n /**\n * Setup browser theme detection using prefers-color-scheme\n */\n private setupBrowserThemeDetection(): void {\n if (typeof window === 'undefined') return;\n \n const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)');\n this.mediaQueryList = isDarkMode;\n \n // Apply theme based on system preference only if following system theme\n if (this.useSystemTheme) {\n const theme: AppearanceType = isDarkMode.matches ? 'dark' : 'light';\n this.appearance.set(theme);\n this.applyThemeToDOM(theme);\n }\n \n // Add listener for theme changes and store reference to the handler\n this.handleThemeChange = (event: MediaQueryListEvent) => {\n if (this.useSystemTheme) {\n const newTheme: AppearanceType = event.matches ? 'dark' : 'light';\n this.appearance.set(newTheme);\n this.applyThemeToDOM(newTheme);\n }\n };\n \n isDarkMode.addEventListener('change', this.handleThemeChange);\n \n // Store reference for cleanup\n this.mediaQueryList = isDarkMode;\n }\n\n /**\n * Apply theme class to DOM\n */\n private applyThemeToDOM(theme: AppearanceType): void {\n\n if (theme === this.currentTheme) {\n if(theme === 'dark') {\n this.renderer.removeClass(this.document.documentElement, `tgui-theme-light`);\n } else {\n this.renderer.removeClass(this.document.documentElement, `tgui-theme-dark`);\n }\n return;\n };\n \n // Remove existing theme classes\n this.renderer.removeClass(this.document.documentElement, `tgui-theme-${this.currentTheme}`);\n\n // Add the appropriate theme class\n this.renderer.addClass(this.document.documentElement, `tgui-theme-${theme}`);\n \n // Update current theme\n this.currentTheme = theme;\n }\n\n /**\n * Clean up event listeners\n */\n private cleanupListeners(): void {\n // Clean up Telegram listeners\n const telegramData = this.telegramService.getTelegramData();\n if (telegramData && this.themeChangeListener) {\n telegramData.offEvent('theme_changed', this.themeChangeListener);\n this.themeChangeListener = null;\n }\n \n // Clean up media query listeners using stored reference\n if (this.mediaQueryList && this.handleThemeChange) {\n this.mediaQueryList.removeEventListener('change', this.handleThemeChange);\n this.mediaQueryList = null;\n this.handleThemeChange = null;\n }\n }\n} ","import { DOCUMENT } from '@angular/common';\nimport { Inject, Injectable, signal, inject } from '@angular/core';\nimport { TelegramService } from './telegram.service';\n\nexport type PlatformType = 'base' | 'ios';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class PlatformService {\n // Signal for platform that components can subscribe to\n public platform = signal<PlatformType>('base');\n \n // Cached value for checking iOS platform\n private _isIOSPlatform: boolean | null = null;\n\n private document = inject(DOCUMENT);\n\n constructor() {\n this.detectPlatform();\n }\n\n /**\n * Set platform manually\n */\n public setPlatform(platform: PlatformType): void {\n this.platform.set(platform);\n this.applyPlatformToDOM(platform);\n // Update cached value\n this._isIOSPlatform = platform === 'ios';\n }\n \n /**\n * Checks if current platform is iOS\n * Uses DOM class to determine platform, caches result for better performance\n */\n public isIOS(): boolean {\n // If value is already cached, return it\n if (this._isIOSPlatform !== null) {\n return this._isIOSPlatform;\n }\n \n // Otherwise check for iOS class in DOM\n if (this.document && this.document.documentElement) {\n this._isIOSPlatform = this.document.documentElement.classList.contains('tgui-platform-ios');\n return this._isIOSPlatform;\n }\n \n // If unable to determine, return false\n return false;\n }\n\n /**\n * Detect platform based on user agent\n */\n private detectPlatform(): void {\n const isIOS = this.detectIOSFromUserAgent();\n const detectedPlatform: PlatformType = isIOS ? 'ios' : 'base';\n \n this.platform.set(detectedPlatform);\n this.applyPlatformToDOM(detectedPlatform);\n // Cache result\n this._isIOSPlatform = isIOS;\n }\n\n /**\n * Apply platform class to DOM\n */\n private applyPlatformToDOM(platform: PlatformType): void {\n // Remove existing platform classes\n this.document.documentElement.classList.remove('tgui-platform-base', 'tgui-platform-ios');\n \n // Add the appropriate platform class\n this.document.documentElement.classList.add(`tgui-platform-${platform}`);\n }\n\n /**\n * Check if the device is iOS based on user agent\n */\n private detectIOSFromUserAgent(): boolean {\n if (typeof window === 'undefined' || !window.navigator) {\n return false;\n }\n\n const userAgent = window.navigator.userAgent.toLowerCase();\n \n return /iphone|ipad|ipod/.test(userAgent) || \n (userAgent.includes('mac') && 'ontouchend' in document);\n }\n} ","import { Injectable, ElementRef, signal, computed } from '@angular/core';\n\n/**\n * Service for managing portal container references\n * Provides functionality for components to render content outside of their DOM hierarchy\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class PortalService {\n // Signal to store the portal container reference\n private readonly portalContainerRef = signal<ElementRef<HTMLElement> | null>(null);\n\n // Public readonly signal for accessing the portal container\n public readonly portalContainer = this.portalContainerRef.asReadonly();\n \n // Computed signal to check if portal container is available\n public readonly hasPortalContainer = computed(() => !!this.portalContainer());\n \n constructor() {\n console.log('PortalService initialized');\n }\n \n /**\n * Set the portal container reference\n * This is typically called by the TGUIRootComponent\n */\n public setPortalContainer(elementRef: ElementRef<HTMLElement>): void {\n if (!elementRef || !elementRef.nativeElement) {\n console.error('Invalid portal container provided to PortalService');\n return;\n }\n \n console.log('Setting portal container:', elementRef.nativeElement);\n this.portalContainerRef.set(elementRef);\n }\n \n /**\n * Clear the portal container reference\n * This should be called when the container is destroyed\n */\n public clearPortalContainer(): void {\n console.log('Clearing portal container reference');\n this.portalContainerRef.set(null);\n }\n \n /**\n * Get the current portal container element\n * Returns the native DOM element or null if not set\n */\n public getPortalContainerElement(): HTMLElement | null {\n const container = this.portalContainer()?.nativeElement || null;\n if (!container) {\n console.warn('Portal container not available - make sure tgui-root component is properly set up');\n }\n return container;\n }\n \n /**\n * Check if portal container is ready for use\n */\n public isPortalReady(): boolean {\n const isReady = !!this.getPortalContainerElement();\n console.log('Portal ready status:', isReady);\n return isReady;\n }\n} ","import { Injectable, signal } from '@angular/core';\n\nexport interface RippleWave {\n x: number;\n y: number;\n date: number;\n pointerId: number;\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class RippleService {\n private RIPPLE_DELAY = 70;\n private WAVE_LIVE = 225;\n\n /**\n * Map to track pointer delay timers by pointerId\n */\n private pointerDelayTimers = new Map<number, ReturnType<typeof setTimeout>>();\n \n /**\n * Add a new ripple wave effect at the specified coordinates\n */\n addWave(x: number, y: number, pointerId: number, currentWaves: RippleWave[]): RippleWave[] {\n const dateNow = Date.now();\n // Filter out expired waves\n const filteredWaves = currentWaves.filter((wave) => wave.date + this.WAVE_LIVE > dateNow);\n\n // Add the new wave\n const newWaves = [\n ...filteredWaves,\n {\n x,\n y,\n date: dateNow,\n pointerId,\n }\n ];\n\n // Clean up the timer for this pointerId\n this.pointerDelayTimers.delete(pointerId);\n \n return newWaves;\n }\n\n /**\n * Handle pointer down event\n * @param event Pointer event\n * @param wavesSignal Signal for managing waves\n */\n handlePointerDown(event: PointerEvent, wavesSignal: ReturnType<typeof signal<RippleWave[]>>): void {\n const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n\n // Set a timeout to create the ripple effect after a short delay\n this.pointerDelayTimers.set(\n event.pointerId,\n setTimeout(() => {\n const newWaves = this.addWave(x, y, event.pointerId, wavesSignal());\n wavesSignal.set(newWaves);\n \n // Clear the waves after they've completed their animation\n setTimeout(() => {\n wavesSignal.set([]);\n }, this.WAVE_LIVE);\n }, this.RIPPLE_DELAY)\n );\n }\n\n /**\n * Handle pointer cancel/up event\n */\n handlePointerCancel(pointerId: number): void {\n const timer = this.pointerDelayTimers.get(pointerId);\n if (timer) {\n clearTimeout(timer);\n this.pointerDelayTimers.delete(pointerId);\n }\n }\n} ","/**\n * Helper functions for managing TGUI themes\n */\n\nimport { Injectable, OnDestroy, inject, signal } from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { AppearanceType } from '../services/theme.service';\n\n/**\n * Apply the specified theme by adding the appropriate CSS class to the document's root element\n * @param theme The theme to apply ('light' or 'dark')\n */\nexport function applyTheme(theme: AppearanceType): void {\n // Remove existing theme classes\n document.documentElement.classList.remove('tgui-theme-light', 'tgui-theme-dark');\n \n // Add the appropriate theme class\n document.documentElement.classList.add(`tgui-theme-${theme}`);\n}\n\n/**\n * Setup system theme detection using prefers-color-scheme\n * @param callback Optional callback function that will be called when the theme changes\n * @returns A function to cleanup the listeners\n */\nexport function setupSystemThemeDetection(callback?: (theme: AppearanceType) => void): () => void {\n if (typeof window === 'undefined') return () => {};\n \n const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)');\n \n // Apply initial theme based on system preference\n const initialTheme: AppearanceType = isDarkMode.matches ? 'dark' : 'light';\n applyTheme(initialTheme);\n callback?.(initialTheme);\n \n // Add listener for theme changes\n const handleThemeChange = (event: MediaQueryListEvent) => {\n const newTheme: AppearanceType = event.matches ? 'dark' : 'light';\n applyTheme(newTheme);\n callback?.(newTheme);\n };\n \n isDarkMode.addEventListener('change', handleThemeChange);\n \n // Return cleanup function\n return () => {\n isDarkMode.removeEventListener('change', handleThemeChange);\n };\n}\n\n/**\n * Angular service for using system themes\n * Uses signals to track theme changes\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class SystemThemeService implements OnDestroy {\n // Signal for theme with initial value\n private readonly themeSignal = signal<AppearanceType>(this.getInitialTheme());\n \n // Public readonly signal for theme access\n public readonly theme = this.themeSignal.asReadonly();\n \n private cleanup: (() => void) | null = null;\n \n constructor() {\n this.setupThemeDetection();\n }\n \n // Set theme manually\n setTheme(theme: AppearanceType): void {\n this.themeSignal.set(theme);\n applyTheme(theme);\n }\n \n // Enable system theme tracking\n enableSystemTheme(): void {\n this.cleanup && this.cleanup();\n this.setupThemeDetection();\n }\n \n // Disable system theme tracking\n disableSystemTheme(): void {\n this.cleanup && this.cleanup();\n this.cleanup = null;\n }\n \n ngOnDestroy(): void {\n this.cleanup && this.cleanup();\n }\n \n private getInitialTheme(): AppearanceType {\n if (typeof window === 'undefined') return 'light';\n \n const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)');\n return isDarkMode.matches ? 'dark' : 'light';\n }\n \n private setupThemeDetection(): void {\n if (typeof window === 'undefined') return;\n \n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n \n // Set initial theme\n const initialTheme: AppearanceType = mediaQuery.matches ? 'dark' : 'light';\n this.themeSignal.set(initialTheme);\n applyTheme(initialTheme);\n \n // Create media query change event handler\n const handleMediaQueryChange = (event: MediaQueryListEvent) => {\n const newTheme: AppearanceType = event.matches ? 'dark' : 'light';\n this.themeSignal.set(newTheme);\n applyTheme(newTheme);\n };\n \n // Add event listener\n mediaQuery.addEventListener('change', handleMediaQueryChange);\n \n // Cleanup function\n this.cleanup = () => {\n mediaQuery.removeEventListener('change', handleMediaQueryChange);\n };\n }\n} ","/**\n * Checks if a node exists\n * \n * This utility function checks if a passed value can be rendered as a React-like node.\n * It verifies if the value is not null, not undefined, and is either a primitive or a complex object.\n * \n * @param node The node to check\n * @returns True if the node exists and can be rendered\n */\nexport function hasNode(node: any): boolean {\n return (\n node !== null &&\n node !== undefined &&\n (typeof node !== 'boolean' || node === true)\n );\n} ","/**\n * Utility function to combine CSS class names with conditional logic.\n * Similar to the classnames library in React ecosystem.\n * \n * @param classes A list of class names, objects where keys are class names and values are booleans,\n * or falsy values (which will be ignored)\n * @returns A string of space-separated class names\n */\nexport function classNames(...classes: (string | Record<string, boolean> | null | undefined | false)[]): string {\n const result: string[] = [];\n\n for (const cls of classes) {\n if (!cls) continue;\n \n if (typeof cls === 'string') {\n result.push(cls);\n } else if (typeof cls === 'object') {\n for (const [key, value] of Object.entries(cls)) {\n if (value) {\n result.push(key);\n }\n }\n }\n }\n\n return result.join(' ');\n} ","/**\n * Utility function to call multiple event handlers\n * Similar to the callMultiple function in the React version\n * \n * @param handlers List of event handlers to call\n * @returns A function that calls all handlers with the same arguments\n */\nexport function callMultiple<T = any>(...handlers: Array<((e: T) => void) | undefined>): (e: T) => void {\n return (e: T) => {\n handlers.forEach((handler) => {\n if (typeof handler === 'function') {\n handler(e);\n }\n });\n };\n} ","/**\n * Creates chunks of the given array with specified size\n * @param array The array to create chunks from\n * @param chunkSize The size of each chunk\n * @returns An array of chunks\n */\nexport function createChunks<T>(array: T[], chunkSize: number): T[][] {\n const result: T[][] = [];\n \n for (let i = 0; i < array.length; i += chunkSize) {\n result.push(array.slice(i, i + chunkSize));\n }\n \n return result;\n} ","import { Directive, ElementRef, OnInit, Renderer2, inject } from '@angular/core';\n\n/**\n * Directive that automatically loads the TGUI styles\n * This should be applied once on a root element (typically body or app-root)\n */\n@Directive({\n selector: '[tguiTheme]',\n standalone: true,\n})\nexport class ThemeDirective implements OnInit {\n // CSS file path - this will be loaded from assets in the actual build\n private stylesPath = 'assets/tgui/styles/variables.css';\n \n private el = inject(ElementRef);\n private renderer = inject(Renderer2);\n\n ngOnInit(): void {\n this.loadStyles();\n }\n\n private loadStyles(): void {\n const head = document.head;\n const link = document.createElement('link');\n \n link.rel = 'stylesheet';\n link.type = 'text/css';\n link.href = this.stylesPath;\n \n head.appendChild(link);\n }\n} ","import { Directive, Input, TemplateRef, ViewContainerRef, OnInit, OnDestroy, ElementRef, EmbeddedViewRef, inject, effect } from '@angular/core';\nimport { Subject, takeUntil } from 'rxjs';\nimport { PortalService } from '../services';\n\n/**\n * Directive that renders content into a portal container\n * Use it to project content outside of its normal DOM hierarchy\n */\n@Directive({\n selector: '[tguiPortalOutlet]',\n standalone: true\n})\nexport class PortalOutletDirective implements OnInit, OnDestroy {\n @Input() tguiPortalOutlet: TemplateRef<any> | null = null;\n \n private destroy$ = new Subject<void>();\n \n private viewContainerRef = inject(ViewContainerRef);\n private portalService = inject(PortalService);\n \n constructor() {\n effect(() => {\n const container = this.portalService.portalContainer();\n \n // Skip rendering if we're not initialized yet\n if (!this.tguiPortalOutlet) return;\n \n // Clear existing content\n this.viewContainerRef.clear();\n \n // Only render if we have both a container and a template\n if (container && this.tguiPortalOutlet) {\n const embeddedViewRef = this.viewContainerRef.createEmbeddedView(this.tguiPortalOutlet) as EmbeddedViewRef<any>;\n \n // Move the generated content to the portal container\n const viewRootNodes = embeddedViewRef.rootNodes || [];\n viewRootNodes.forEach((node: Node) => {\n container.nativeElement.appendChild(node);\n });\n }\n });\n }\n \n ngOnInit(): void {\n // Initialization is now handled in the effect\n }\n \n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n} ","import { Directive, HostBinding } from '@angular/core';\n\n@Directive({\n selector: '[tguiHorizontalScroll]',\n standalone: true\n})\nexport class HorizontalScrollDirective {\n @HostBinding('style.display') display = 'flex';\n @HostBinding('style.overflow-x') overflowX = 'scroll';\n @HostBinding('style.-webkit-overflow-scrolling') webkitOverflowScrolling = 'touch';\n @HostBinding('style.scrollbar-width') scrollbarWidth = 'none'; // Firefox\n @HostBinding('style.-ms-overflow-style') msOverflowStyle = 'none'; // IE/Edge\n @HostBinding('class.tgui-hide-scrollbar') hideScrollbar = true; // Chrome/Safari\n}","import { Directive, ElementRef, OnInit } from '@angular/core';\n\n/**\n * Directive that visually hides an element while keeping it accessible for screen readers.\n * Used for improving accessibility by providing context for screen reader users\n * without affecting the visual presentation.\n */\n@Directive({\n selector: '[tguiVisuallyHidden]',\n standalone: true\n})\nexport class VisuallyHiddenDirective implements OnInit {\n constructor(private el: ElementRef<HTMLElement>) {}\n\n ngOnInit(): void {\n const element = this.el.nativeElement;\n element.style.position = 'absolute';\n element.style.blockSize = '1px';\n element.style.inlineSize = '1px';\n element.style.padding = '0';\n element.style.margin = '-1px';\n element.style.whiteSpace = 'nowrap';\n element.style.clip = 'rect(0, 0, 0, 0)';\n element.style.clipPath = 'inset(50%)';\n element.style.overflow = 'hidden';\n element.style.border = '0';\n element.style.opacity = '0';\n }\n} ","import { Component, ChangeDetectionStrategy, ViewEncapsulation, input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport type { RippleWave } from '../../../../../services';\n\n@Component({\n selector: 'tgui-ripple',\n standalone: true,\n imports: [CommonModule],\n template: `\n <span \n class=\"ripple-wrapper\" \n aria-hidden=\"true\"\n >\n <span \n *ngFor=\"let wave of waves()\" \n class=\"ripple-wave\"\n [style.top.px]=\"wave.y\"\n [style.left.px]=\"wave.x\"\n [attr.data-id]=\"wave.pointerId\"\n ></span>\n </span>\n `,\n styles: [`\n :host {\n display: block;\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n pointer-events: none;\n overflow: hidden;\n }\n \n .ripple-wrapper {\n display: block;\n overflow: hidden;\n position: absolute;\n inset: 0;\n border-radius: inherit;\n transition: background-color .15s ease-out;\n pointer-events: none;\n width: 100%;\n height: 100%;\n }\n\n .ripple-wave {\n content: '';\n position: absolute;\n height: 30px;\n width: 30px;\n margin: -15px 0;\n border-radius: 50%;\n background: var(--tgui--outline);\n animation: waveRise .3s cubic-bezier(.3, .3, .5, 1);\n opacity: 0;\n }\n\n @keyframes waveRise {\n 0% {\n transform: scale(1);\n opacity: 1;\n }\n\n 30% {\n opacity: 1;\n }\n\n 100% {\n transform: scale(20);\n opacity: 0;\n }\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class RippleComponent {\n /**\n * The collection of active ripple waves\n */\n waves = input<RippleWave[]>([]);\n} ","import {\n Component,\n Input,\n ElementRef,\n HostBinding,\n HostListener,\n OnInit,\n OnDestroy,\n OnChanges,\n SimpleChanges,\n ViewEncapsulation,\n ChangeDetectionStrategy,\n signal,\n inject,\n effect,\n Signal,\n input,\n computed\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\nimport { PlatformService, RippleService, RippleWave } from '../../../services';\nimport { RippleComponent } from './components/ripple/ripple.component';\n\n@Component({\n selector: 'tgui-tappable',\n standalone: true,\n imports: [CommonModule, RippleComponent],\n template: `\n <tgui-ripple *ngIf=\"hasRippleEffect()\" [waves]=\"rippleWaves()\"></tgui-ripple>\n <ng-content></ng-content>\n `,\n styles: [`\n :host {\n position: relative;\n isolation: isolate;\n cursor: pointer;\n display: block;\n touch-action: manipulation;\n border-radius: inherit;\n overflow: hidden;\n user-select: none;\n }\n\n :host::after {\n content: '';\n position: absolute;\n inset: 0;\n opacity: 0;\n transition: opacity .15s ease-out;\n background: var(--tgui--bg_color);\n border-radius: inherit;\n pointer-events: none;\n }\n\n :host.readonly {\n cursor: default;\n pointer-events: none;\n }\n\n :host.disabled {\n cursor: default;\n opacity: .35;\n pointer-events: none;\n }\n\n /* iOS disables ::after overlay by default */\n :host.platform-ios::after {\n content: unset;\n }\n \n /* iOS uses direct opacity for hover (like original React) */\n :host.platform-ios:hover {\n opacity: .85;\n }\n\n /* iOS uses direct opacity for active (like original React) */\n :host.platform-ios.is-active {\n opacity: .65;\n }\n\n /* Non-iOS platforms use ::after overlay on active */\n :host:not(.platform-ios).is-active::after {\n opacity: var(--tgui--button--hovered-opacity, .15);\n }\n\n @media (hover: hover) and (pointer: fine) {\n /* Hover effect uses ::after overlay for all platforms */\n :host:hover::after {\n opacity: var(--tgui--button--hovered-opacity, .07); }\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class TappableComponent implements OnInit {\n /** Animation type for clicks */\n interactiveAnimation = input<'opacity' | 'background'>('background');\n \n /** Make component read-only */\n readonly = input<boolean>(false);\n \n /** Disable component */\n disabled = input<boolean>(false);\n\n /** Ripple effect waves */\n rippleWaves = signal<RippleWave[]>([]);\n\n /** Active state signal for programmatic control */\n private isActiveState = signal<boolean>(false);\n\n /** Service injections */\n private rippleService = inject(RippleService);\n private platformService = inject(PlatformService);\n\n /** Platform signal */\n private platformSignal = this.platformService.platform;\n\n /** Computed values */\n private isIOS = computed(() => {\n return this.platformSignal() === 'ios';\n });\n \n private isReadOnlyState = computed(() => this.readonly() || this.disabled());\n\n /** Computed ripple effect state */\n hasRippleEffect = computed(() => \n !this.isIOS() &&\n this.interactiveAnimation() === 'background' &&\n !this.isReadOnlyState()\n );\n\n constructor() {\n // No need for explicit effect since we're using computed signals\n }\n\n ngOnInit(): void {\n // Initialization is handled by computed signals\n }\n\n /**\n * Pointerdown event handler\n */\n @HostListener('pointerdown', ['$event'])\n onPointerDown(event: PointerEvent): void {\n if (!this.isReadOnlyState()) {\n // Set active state programmatically\n this.isActiveState.set(true);\n \n // Handle ripple effect if needed\n if (this.hasRippleEffect()) {\n this.rippleService.handlePointerDown(event, this.rippleWaves);\n }\n }\n }\n\n /**\n * Pointercancel/pointerup/pointerleave event handler\n */\n @HostListener('pointercancel', ['$event'])\n @HostListener('pointerup', ['$event'])\n @HostListener('pointerleave', ['$event'])\n onPointerCancel(event: PointerEvent): void {\n // Always remove active state\n this.isActiveState.set(false);\n \n if (this.hasRippleEffect()) {\n this.rippleService.handlePointerCancel(event.pointerId);\n }\n }\n\n /** Host bindings */\n\n @HostBinding('class.readonly')\n get isReadonlyClass(): boolean {\n return this.readonly();\n }\n\n @HostBinding('class.disabled')\n get isDisabledClass(): boolean {\n return this.disabled();\n }\n\n @HostBinding('class.tappable--opacity')\n get isOpacityAnimation(): boolean {\n return this.interactiveAnimation() === 'opacity' && !this.isIOS();\n }\n\n @HostBinding('class.platform-ios')\n get isPlatformIOS(): boolean {\n return this.isIOS();\n }\n\n @HostBinding('class.is-active')\n get isActive(): boolean {\n return this.isActiveState();\n }\n\n @HostBinding('attr.readonly')\n get readonlyAttr() {\n return this.readonly() ? '' : null;\n }\n\n @HostBinding('attr.disabled')\n get disabledAttr() {\n return this.disabled() ? '' : null;\n }\n}","import { Component, Input, TemplateRef, ViewChild, ViewContainerRef, AfterViewInit, OnDestroy, ChangeDetectionStrategy, ViewEncapsulation, effect, inject, ChangeDetectorRef } from '@angular/core';\nimport { Subject } from 'rxjs';\nimport { PortalService } from '../../../services';\n\n/**\n * Component for rendering content in the portal container\n * Similar to RootRenderer in React version\n */\n@Component({\n selector: 'tgui-root-portal',\n template: `\n <ng-template #contentTemplate>\n <ng-content></ng-content>\n </ng-template>\n `,\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [`\n :host {\n font-family: var(--tgui--font-family);\n }\n \n :host::ng-deep * {\n font-family: var(--tgui--font-family);\n }\n `],\n})\nexport class RootPortalComponent implements AfterViewInit, OnDestroy {\n @ViewChild('contentTemplate', {static: true}) contentTemplate!: TemplateRef<any>;\n \n private destroy$ = new Subject<void>();\n private viewRef: any = null;\n private templateReady = false;\n \n private portalService = inject(PortalService);\n private viewContainerRef = inject(ViewContainerRef);\n private cdr = inject(ChangeDetectorRef);\n \n constructor() {\n // Use effect to react to portal container changes\n effect(() => {\n const container = this.portalService.portalContainer();\n \n if (!this.templateReady || !this.contentTemplate) {\n return;\n }\n \n \n // Clear previous view if it exists\n if (this.viewRef) {\n try {\n this.viewContainerRef.remove(\n this.viewContainerRef.indexOf(this.viewRef)\n );\n } catch (e) {\n console.error('Error removing portal view:', e);\n }\n this.viewRef = null;\n }\n \n if (container && container.nativeElement) {\n \n try {\n