openiis-ui
Version:
Una librería moderna de componentes UI para Angular con temas personalizables
1 lines • 399 kB
Source Map (JSON)
{"version":3,"file":"openiis-ui.mjs","sources":["../../../projects/openiis/src/lib/spinner/spinner.component.ts","../../../projects/openiis/src/lib/tooltip/tooltip.component.ts","../../../projects/openiis/src/lib/services/svg/svg-icon.service.ts","../../../projects/openiis/src/lib/services/svg/svg-icon.directive.ts","../../../projects/openiis/src/lib/buttons/button.component.ts","../../../projects/openiis/src/lib/buttons/button.component.html","../../../projects/openiis/src/lib/button-group/button-group.component.ts","../../../projects/openiis/src/lib/radio-button/radio-button.component.ts","../../../projects/openiis/src/lib/switch/switch.component.ts","../../../projects/openiis/src/lib/fab/fab.component.ts","../../../projects/openiis/src/lib/dropdowns/dropdown.component.ts","../../../projects/openiis/src/lib/dropdowns/searchable-dropdown.component.ts","../../../projects/openiis/src/lib/input/input.component.ts","../../../projects/openiis/src/lib/input/input.component.html","../../../projects/openiis/src/lib/input-date/date-input.component.ts","../../../projects/openiis/src/lib/search-input/search-input.component.ts","../../../projects/openiis/src/lib/checkboxes/checkbox.component.ts","../../../projects/openiis/src/lib/modal/modal.component.ts","../../../projects/openiis/src/lib/alert-modal/alert-modal.component.ts","../../../projects/openiis/src/lib/confirm-modal/confirm-modal.component.ts","../../../projects/openiis/src/lib/toast/toast.component.ts","../../../projects/openiis/src/lib/empty-state/empty-state.component.ts","../../../projects/openiis/src/lib/avatar/avatar.component.ts","../../../projects/openiis/src/lib/badge/badge.component.ts","../../../projects/openiis/src/lib/breadcrumb/breadcrumb.component.ts","../../../projects/openiis/src/lib/card/card.component.ts","../../../projects/openiis/src/lib/tabs/tabs.component.ts","../../../projects/openiis/src/lib/services/upload/upload.service.ts","../../../projects/openiis/src/lib/uploader/uploader.component.ts","../../../projects/openiis/src/lib/services/scroll/scroll.service.ts","../../../projects/openiis/src/lib/sidebar/sidebar.component.ts","../../../projects/openiis/src/lib/services/theme/theme.service.ts","../../../projects/openiis/src/lib/services/mode/mode.service.ts","../../../projects/openiis/src/public-api.ts","../../../projects/openiis/src/openiis-ui.ts"],"sourcesContent":["import { Component, Input, OnInit } from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\n\r\nexport type SpinnerVariant =\r\n | 'circle'\r\n | 'dots'\r\n | 'bars'\r\n | 'pulse'\r\n | 'wave'\r\n | 'ring'\r\n | 'bounce'\r\n | 'fade';\r\nexport type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\r\nexport type SpinnerColor =\r\n | 'primary'\r\n | 'secondary'\r\n | 'success'\r\n | 'warning'\r\n | 'danger'\r\n | 'info'\r\n | 'light'\r\n | 'dark';\r\nexport type SpinnerSpeed = 'slow' | 'normal' | 'fast';\r\n\r\n@Component({\r\n selector: 'openiis-spinner',\r\n standalone: true,\r\n imports: [CommonModule],\r\n template: `\r\n <div\r\n [class]=\"getClasses()\"\r\n [attr.data-variant]=\"variant\"\r\n [attr.data-size]=\"size\"\r\n [attr.data-color]=\"color\"\r\n [attr.data-speed]=\"speed\"\r\n [attr.data-overlay]=\"overlay\"\r\n [attr.role]=\"role\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n [attr.aria-live]=\"ariaLive\"\r\n [attr.aria-busy]=\"true\"\r\n [style.--spinner-size]=\"customSize\"\r\n [style.--spinner-color]=\"customColor\"\r\n [style.--spinner-speed]=\"customSpeed\"\r\n >\r\n <!-- Overlay background -->\r\n @if (overlay) {\r\n <div\r\n class=\"spinner-overlay-bg\"\r\n [style.background-color]=\"overlayColor\"\r\n ></div>\r\n }\r\n\r\n <!-- Spinner container -->\r\n <div class=\"spinner-container\">\r\n <!-- Circle spinner -->\r\n @if (variant === 'circle') {\r\n <div class=\"spinner-circle\">\r\n <div class=\"spinner-circle-inner\"></div>\r\n </div>\r\n }\r\n\r\n <!-- Dots spinner -->\r\n @if (variant === 'dots') {\r\n <div class=\"spinner-dots\">\r\n @for (dot of getDots(); track $index) {\r\n <div class=\"spinner-dot\"></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Bars spinner -->\r\n @if (variant === 'bars') {\r\n <div class=\"spinner-bars\">\r\n @for (bar of getBars(); track $index) {\r\n <div class=\"spinner-bar\"></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Pulse spinner -->\r\n @if (variant === 'pulse') {\r\n <div class=\"spinner-pulse\">\r\n <div class=\"spinner-pulse-inner\"></div>\r\n </div>\r\n }\r\n\r\n <!-- Wave spinner -->\r\n @if (variant === 'wave') {\r\n <div class=\"spinner-wave\">\r\n @for (wave of getWaves(); track $index) {\r\n <div class=\"spinner-wave-bar\"></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Ring spinner -->\r\n @if (variant === 'ring') {\r\n <div class=\"spinner-ring\">\r\n <div class=\"spinner-ring-inner\"></div>\r\n </div>\r\n }\r\n\r\n <!-- Bounce spinner -->\r\n @if (variant === 'bounce') {\r\n <div class=\"spinner-bounce\">\r\n @for (bounce of getBounces(); track $index) {\r\n <div class=\"spinner-bounce-ball\"></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Fade spinner -->\r\n @if (variant === 'fade') {\r\n <div class=\"spinner-fade\">\r\n @for (fade of getFades(); track $index) {\r\n <div class=\"spinner-fade-blade\"></div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Loading text -->\r\n @if (text) {\r\n <div class=\"spinner-text\">{{ text }}</div>\r\n }\r\n </div>\r\n </div>\r\n `,\r\n styleUrls: ['./spinner.component.css'],\r\n})\r\nexport class OpeniisSpinnerComponent implements OnInit {\r\n @Input() variant: SpinnerVariant = 'circle';\r\n @Input() size: SpinnerSize = 'md';\r\n @Input() color: SpinnerColor = 'primary';\r\n @Input() speed: SpinnerSpeed = 'normal';\r\n @Input() text?: string;\r\n @Input() overlay = false;\r\n @Input() overlayColor = 'rgba(255, 255, 255, 0.8)';\r\n @Input() customSize?: string;\r\n @Input() customColor?: string;\r\n @Input() customSpeed?: string;\r\n @Input() ariaLabel = 'Cargando...';\r\n @Input() ariaLive: 'polite' | 'assertive' | 'off' = 'polite';\r\n @Input() role = 'status';\r\n @Input() centered = false;\r\n @Input() inline = false;\r\n @Input() visible = true;\r\n\r\n ngOnInit() {\r\n // Initialize component\r\n }\r\n\r\n getClasses(): string {\r\n const classes = ['spinner'];\r\n\r\n classes.push(`spinner-${this.variant}`);\r\n classes.push(`spinner-${this.size}`);\r\n classes.push(`spinner-${this.color}`);\r\n classes.push(`spinner-${this.speed}`);\r\n\r\n if (this.overlay) classes.push('spinner-overlay');\r\n if (this.centered) classes.push('spinner-centered');\r\n if (this.inline) classes.push('spinner-inline');\r\n if (!this.visible) classes.push('spinner-hidden');\r\n if (this.text) classes.push('spinner-with-text');\r\n\r\n return classes.join(' ');\r\n }\r\n\r\n getDots(): number[] {\r\n return Array(3)\r\n .fill(0)\r\n .map((_, i) => i);\r\n }\r\n\r\n getBars(): number[] {\r\n return Array(5)\r\n .fill(0)\r\n .map((_, i) => i);\r\n }\r\n\r\n getWaves(): number[] {\r\n return Array(5)\r\n .fill(0)\r\n .map((_, i) => i);\r\n }\r\n\r\n getBounces(): number[] {\r\n return Array(3)\r\n .fill(0)\r\n .map((_, i) => i);\r\n }\r\n\r\n getFades(): number[] {\r\n return Array(8)\r\n .fill(0)\r\n .map((_, i) => i);\r\n }\r\n}\r\n","import { Component, Input, HostBinding } from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\n\r\nexport type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';\r\nexport type TooltipVariant = 'default' | 'danger';\r\n\r\n/**\r\n * Componente Tooltip reutilizable que puede posicionarse en diferentes ubicaciones\r\n * y soporta diferentes variantes de color\r\n */\r\n@Component({\r\n selector: 'openiis-tooltip',\r\n standalone: true,\r\n imports: [CommonModule],\r\n template: `\r\n <div\r\n class=\"tooltip-content\"\r\n [class]=\"tooltipClasses\"\r\n role=\"tooltip\"\r\n [attr.aria-hidden]=\"!visible\"\r\n >\r\n {{ text }}\r\n </div>\r\n `,\r\n styleUrls: ['./tooltip.component.css'],\r\n})\r\nexport class OpeniisTooltipComponent {\r\n /** Texto a mostrar en el tooltip */\r\n @Input() text: string = '';\r\n\r\n /** Posición del tooltip relativa al elemento padre */\r\n @Input() position: TooltipPosition = 'top';\r\n\r\n /** Variante de color del tooltip */\r\n @Input() variant: TooltipVariant = 'default';\r\n\r\n /** Si el tooltip está visible */\r\n @Input() visible: boolean = false;\r\n\r\n /** Genera las clases CSS del tooltip */\r\n get tooltipClasses(): string {\r\n const classes = [\r\n 'tooltip',\r\n `tooltip-${this.position}`,\r\n `tooltip-${this.variant}`,\r\n ];\r\n\r\n if (this.visible) {\r\n classes.push('tooltip-visible');\r\n }\r\n\r\n return classes.join(' ');\r\n }\r\n\r\n @HostBinding('class') hostClasses = 'tooltip-wrapper';\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { HttpClient } from '@angular/common/http';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { Observable, of, BehaviorSubject } from 'rxjs';\r\nimport { map, catchError, tap } from 'rxjs/operators';\r\n\r\nexport interface SvgIconOptions {\r\n color?: string;\r\n backgroundColor?: string;\r\n width?: string | number;\r\n height?: string | number;\r\n className?: string;\r\n strokeColor?: string;\r\n strokeWidth?: string | number;\r\n opacity?: number;\r\n fontSize?: string | number;\r\n rotate?: number;\r\n flipHorizontal?: boolean;\r\n flipVertical?: boolean;\r\n}\r\n\r\nexport interface SvgIconResult {\r\n html: SafeHtml;\r\n element: HTMLElement;\r\n destroy: () => void;\r\n}\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class SvgIconService {\r\n private cache = new Map<string, string>();\r\n private instances = new Map<string, HTMLElement[]>();\r\n private globalStyles = new BehaviorSubject<string>('');\r\n\r\n constructor(\r\n private http: HttpClient,\r\n private sanitizer: DomSanitizer,\r\n ) {\r\n this.injectGlobalStyles();\r\n }\r\n\r\n /**\r\n * Método principal: súper fácil de usar\r\n * Ejemplo: svgService.icon('assets/heart.svg', { color: 'red' })\r\n */\r\n icon(\r\n iconPath: string,\r\n options: SvgIconOptions = {},\r\n ): Observable<SvgIconResult> {\r\n return this.loadSvg(iconPath).pipe(\r\n map((svgContent) => {\r\n const processedSvg = this.processSvg(svgContent, options);\r\n const element = this.createSvgElement(processedSvg, options);\r\n const safeHtml = this.sanitizer.bypassSecurityTrustHtml(processedSvg);\r\n\r\n // Registrar instancia para cleanup global\r\n this.registerInstance(iconPath, element);\r\n\r\n return {\r\n html: safeHtml,\r\n element: element,\r\n destroy: () => this.destroyInstance(iconPath, element),\r\n };\r\n }),\r\n catchError((error) => {\r\n console.error(`Error loading SVG: ${iconPath}`, error);\r\n return of({\r\n html: this.sanitizer.bypassSecurityTrustHtml(this.getErrorSvg()),\r\n element: this.createErrorElement(),\r\n destroy: () => {},\r\n });\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Método para iconos inline - inyecta directamente en el DOM\r\n * Ejemplo: svgService.inlineIcon(elementRef, 'assets/star.svg', { color: 'gold' })\r\n */\r\n inlineIcon(\r\n targetElement: HTMLElement,\r\n iconPath: string,\r\n options: SvgIconOptions = {},\r\n ): Observable<() => void> {\r\n return this.icon(iconPath, options).pipe(\r\n tap((result) => {\r\n targetElement.innerHTML = '';\r\n targetElement.appendChild(result.element);\r\n }),\r\n map((result) => result.destroy),\r\n );\r\n }\r\n\r\n /**\r\n * Método para cambiar colores de iconos existentes\r\n */\r\n updateIconColor(iconPath: string, newOptions: SvgIconOptions): void {\r\n const instances = this.instances.get(iconPath) || [];\r\n\r\n instances.forEach((element) => {\r\n this.applyStylesToElement(element, newOptions);\r\n });\r\n }\r\n\r\n /**\r\n * Método para iconos de uso frecuente - pre-carga y cachea\r\n */\r\n preloadIcons(iconPaths: string[]): Observable<boolean> {\r\n const loadPromises = iconPaths.map((path) =>\r\n this.loadSvg(path).toPromise(),\r\n );\r\n\r\n return new Observable((observer) => {\r\n Promise.all(loadPromises)\r\n .then(() => {\r\n observer.next(true);\r\n observer.complete();\r\n })\r\n .catch((error) => {\r\n observer.error(error);\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Configuración global de estilos\r\n */\r\n setGlobalIconStyles(styles: Partial<SvgIconOptions>): void {\r\n const cssRules = this.generateGlobalCss(styles);\r\n this.globalStyles.next(cssRules);\r\n this.updateGlobalStyles();\r\n }\r\n\r\n /**\r\n * Limpiar cache y instancias\r\n */\r\n clearCache(): void {\r\n this.cache.clear();\r\n this.instances.clear();\r\n }\r\n\r\n // ========================\r\n // MÉTODOS PRIVADOS\r\n // ========================\r\n\r\n private loadSvg(iconPath: string): Observable<string> {\r\n // Verificar cache primero\r\n if (this.cache.has(iconPath)) {\r\n return of(this.cache.get(iconPath)!);\r\n }\r\n\r\n return this.http.get(iconPath, { responseType: 'text' }).pipe(\r\n tap((svg) => this.cache.set(iconPath, svg)),\r\n catchError(() => of(this.getErrorSvg())),\r\n );\r\n }\r\n\r\n private processSvg(svgContent: string, options: SvgIconOptions): string {\r\n let processedSvg = svgContent;\r\n\r\n // Remover atributos de tamaño fijos\r\n processedSvg = processedSvg.replace(/width=\"[^\"]*\"/g, '');\r\n processedSvg = processedSvg.replace(/height=\"[^\"]*\"/g, '');\r\n\r\n // Agregar viewBox si no existe\r\n if (!processedSvg.includes('viewBox')) {\r\n processedSvg = processedSvg.replace('<svg', '<svg viewBox=\"0 0 24 24\"');\r\n }\r\n\r\n // Procesar colores\r\n if (options.color) {\r\n // Cambiar fill\r\n processedSvg = processedSvg.replace(\r\n /fill=\"[^\"]*\"/g,\r\n `fill=\"${options.color}\"`,\r\n );\r\n processedSvg = processedSvg.replace(\r\n /fill:[^;\"]*/g,\r\n `fill:${options.color}`,\r\n );\r\n\r\n // Si no tiene fill, agregarlo\r\n if (!processedSvg.includes('fill=') && !processedSvg.includes('fill:')) {\r\n processedSvg = processedSvg.replace(\r\n '<path',\r\n '<path fill=\"currentColor\"',\r\n );\r\n processedSvg = processedSvg.replace(\r\n '<svg',\r\n `<svg style=\"color: ${options.color}\"`,\r\n );\r\n }\r\n }\r\n\r\n // Procesar stroke\r\n if (options.strokeColor) {\r\n processedSvg = processedSvg.replace(\r\n /stroke=\"[^\"]*\"/g,\r\n `stroke=\"${options.strokeColor}\"`,\r\n );\r\n processedSvg = processedSvg.replace(\r\n /stroke:[^;\"]*/g,\r\n `stroke:${options.strokeColor}`,\r\n );\r\n }\r\n\r\n if (options.strokeWidth) {\r\n processedSvg = processedSvg.replace(\r\n /stroke-width=\"[^\"]*\"/g,\r\n `stroke-width=\"${options.strokeWidth}\"`,\r\n );\r\n }\r\n\r\n // Agregar clase personalizada\r\n if (options.className) {\r\n processedSvg = processedSvg.replace(\r\n '<svg',\r\n `<svg class=\"svg-icon ${options.className}\"`,\r\n );\r\n } else {\r\n processedSvg = processedSvg.replace('<svg', '<svg class=\"svg-icon\"');\r\n }\r\n\r\n // Aplicar transformaciones\r\n const transforms = this.buildTransforms(options);\r\n if (transforms) {\r\n processedSvg = processedSvg.replace(\r\n '<svg',\r\n `<svg style=\"transform: ${transforms}\"`,\r\n );\r\n }\r\n\r\n return processedSvg;\r\n }\r\n\r\n private createSvgElement(\r\n svgContent: string,\r\n options: SvgIconOptions,\r\n ): HTMLElement {\r\n const wrapper = document.createElement('div');\r\n wrapper.innerHTML = svgContent;\r\n const svgElement = wrapper.firstElementChild as HTMLElement;\r\n\r\n if (svgElement) {\r\n this.applyStylesToElement(svgElement, options);\r\n }\r\n\r\n return svgElement || wrapper;\r\n }\r\n\r\n private applyStylesToElement(\r\n element: HTMLElement,\r\n options: SvgIconOptions,\r\n ): void {\r\n // Aplicar dimensiones\r\n if (options.width) {\r\n const width =\r\n typeof options.width === 'number'\r\n ? `${options.width}px`\r\n : options.width;\r\n element.style.width = width;\r\n }\r\n\r\n if (options.height) {\r\n const height =\r\n typeof options.height === 'number'\r\n ? `${options.height}px`\r\n : options.height;\r\n element.style.height = height;\r\n } else {\r\n element.style.height = 'auto'; // Altura automática si no se especifica\r\n }\r\n\r\n // Aplicar color\r\n if (options.color) {\r\n element.style.color = options.color;\r\n }\r\n\r\n // Aplicar background\r\n if (options.backgroundColor) {\r\n element.style.backgroundColor = options.backgroundColor;\r\n element.style.borderRadius = '4px';\r\n element.style.padding = '4px';\r\n }\r\n\r\n // Aplicar opacidad\r\n if (options.opacity !== undefined) {\r\n element.style.opacity = options.opacity.toString();\r\n }\r\n\r\n // Aplicar fontSize\r\n if (options.fontSize) {\r\n const fontSize =\r\n typeof options.fontSize === 'number'\r\n ? `${options.fontSize}px`\r\n : options.fontSize;\r\n element.style.fontSize = fontSize;\r\n }\r\n\r\n // Aplicar stroke\r\n if (options.strokeColor) {\r\n const svgElements = element.querySelectorAll(\r\n 'svg, path, rect, circle, ellipse, polygon, polyline, line',\r\n );\r\n svgElements.forEach((svgEl) => {\r\n (svgEl as HTMLElement).style.stroke = options.strokeColor!;\r\n });\r\n }\r\n\r\n if (options.strokeWidth) {\r\n const strokeWidth =\r\n typeof options.strokeWidth === 'number'\r\n ? `${options.strokeWidth}px`\r\n : options.strokeWidth;\r\n const svgElements = element.querySelectorAll(\r\n 'svg, path, rect, circle, ellipse, polygon, polyline, line',\r\n );\r\n svgElements.forEach((svgEl) => {\r\n (svgEl as HTMLElement).style.strokeWidth = strokeWidth;\r\n });\r\n }\r\n\r\n // Aplicar transformaciones\r\n const transforms = this.buildTransforms(options);\r\n if (transforms) {\r\n element.style.transform = transforms;\r\n }\r\n\r\n // Hacer que sea responsive por defecto\r\n element.style.display = 'inline-block';\r\n element.style.verticalAlign = 'middle';\r\n }\r\n\r\n private buildTransforms(options: SvgIconOptions): string {\r\n const transforms: string[] = [];\r\n\r\n if (options.rotate) {\r\n transforms.push(`rotate(${options.rotate}deg)`);\r\n }\r\n\r\n if (options.flipHorizontal) {\r\n transforms.push('scaleX(-1)');\r\n }\r\n\r\n if (options.flipVertical) {\r\n transforms.push('scaleY(-1)');\r\n }\r\n\r\n return transforms.join(' ');\r\n }\r\n\r\n private registerInstance(iconPath: string, element: HTMLElement): void {\r\n if (!this.instances.has(iconPath)) {\r\n this.instances.set(iconPath, []);\r\n }\r\n this.instances.get(iconPath)!.push(element);\r\n }\r\n\r\n private destroyInstance(iconPath: string, element: HTMLElement): void {\r\n const instances = this.instances.get(iconPath);\r\n if (instances) {\r\n const index = instances.indexOf(element);\r\n if (index > -1) {\r\n instances.splice(index, 1);\r\n }\r\n }\r\n }\r\n\r\n private getErrorSvg(): string {\r\n return `\r\n <svg viewBox=\"0 0 24 24\" class=\"svg-icon svg-icon--error\">\r\n <path fill=\"currentColor\" d=\"M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17M12,14A1,1 0 0,1 11,13V7A1,1 0 0,1 12,6A1,1 0 0,1 13,7V13A1,1 0 0,1 12,14Z\" />\r\n </svg>\r\n `;\r\n }\r\n\r\n private createErrorElement(): HTMLElement {\r\n const div = document.createElement('div');\r\n div.innerHTML = this.getErrorSvg();\r\n return div.firstElementChild as HTMLElement;\r\n }\r\n\r\n private generateGlobalCss(styles: Partial<SvgIconOptions>): string {\r\n const rules: string[] = [];\r\n\r\n if (styles.color) {\r\n rules.push(`color: ${styles.color}`);\r\n }\r\n\r\n if (styles.width) {\r\n const width =\r\n typeof styles.width === 'number' ? `${styles.width}px` : styles.width;\r\n rules.push(`width: ${width}`);\r\n }\r\n\r\n if (styles.height) {\r\n const height =\r\n typeof styles.height === 'number'\r\n ? `${styles.height}px`\r\n : styles.height;\r\n rules.push(`height: ${height}`);\r\n } else {\r\n rules.push('height: auto');\r\n }\r\n\r\n return `.svg-icon { ${rules.join('; ')} }`;\r\n }\r\n\r\n private injectGlobalStyles(): void {\r\n const style = document.createElement('style');\r\n style.id = 'svg-icon-global-styles';\r\n style.textContent = `\r\n .svg-icon {\r\n display: inline-block;\r\n vertical-align: middle;\r\n width: 1em;\r\n height: auto;\r\n fill: currentColor;\r\n transition: all 0.2s ease;\r\n }\r\n \r\n .svg-icon--error {\r\n color: #ef4444;\r\n }\r\n `;\r\n document.head.appendChild(style);\r\n }\r\n\r\n private updateGlobalStyles(): void {\r\n const existingStyle = document.getElementById('svg-icon-global-styles');\r\n if (existingStyle && this.globalStyles.value) {\r\n existingStyle.textContent += '\\n' + this.globalStyles.value;\r\n }\r\n }\r\n}\r\n\r\n// Función helper para uso aún más fácil\r\nexport function createSvgIcon(\r\n iconPath: string,\r\n options?: SvgIconOptions,\r\n): Observable<SvgIconResult> {\r\n const service = new SvgIconService(\r\n // Estos se inyectarán automáticamente en el contexto de Angular\r\n {} as any,\r\n {} as any,\r\n );\r\n return service.icon(iconPath, options);\r\n}\r\n","import {\r\n Directive,\r\n Input,\r\n ElementRef,\r\n OnInit,\r\n OnDestroy,\r\n OnChanges,\r\n SimpleChanges,\r\n Renderer2,\r\n} from '@angular/core';\r\nimport { SvgIconService, SvgIconOptions } from './svg-icon.service';\r\nimport { Subscription } from 'rxjs';\r\n\r\n@Directive({\r\n selector: '[svgIcon]',\r\n standalone: true,\r\n})\r\nexport class SvgIconDirective implements OnInit, OnDestroy, OnChanges {\r\n // Propiedades principales\r\n @Input('svgIcon') iconPath: string = '';\r\n @Input() svgColor: string = '';\r\n @Input() svgBackground: string = '';\r\n @Input() svgWidth: string | number = '';\r\n @Input() svgHeight: string | number = '';\r\n @Input() svgSize: string | number = '';\r\n @Input() svgStroke: string = '';\r\n @Input() svgStrokeWidth: string | number = '';\r\n @Input() svgOpacity: number = 1;\r\n @Input() svgRotate: number = 0;\r\n @Input() svgFlipH: boolean = false;\r\n @Input() svgFlipV: boolean = false;\r\n @Input() svgClass: string = '';\r\n\r\n private subscription?: Subscription;\r\n private destroyFn?: () => void;\r\n\r\n constructor(\r\n private elementRef: ElementRef<HTMLElement>,\r\n private svgService: SvgIconService,\r\n private renderer: Renderer2,\r\n ) {}\r\n\r\n ngOnInit(): void {\r\n this.loadIcon();\r\n }\r\n\r\n ngOnChanges(changes: SimpleChanges): void {\r\n // Si cambia el icono o alguna propiedad, recargar\r\n if (this.iconPath) {\r\n this.loadIcon();\r\n }\r\n }\r\n\r\n ngOnDestroy(): void {\r\n this.cleanup();\r\n }\r\n\r\n private loadIcon(): void {\r\n if (!this.iconPath) return;\r\n\r\n this.cleanup();\r\n\r\n // Determinar width y height, priorizando svgSize si está definido\r\n const width = this.svgSize || this.svgWidth;\r\n const height = this.svgSize || this.svgHeight;\r\n\r\n const options: SvgIconOptions = {\r\n color: this.svgColor,\r\n backgroundColor: this.svgBackground,\r\n width: width,\r\n height: height,\r\n strokeColor: this.svgStroke,\r\n strokeWidth: this.svgStrokeWidth,\r\n opacity: this.svgOpacity,\r\n rotate: this.svgRotate,\r\n flipHorizontal: this.svgFlipH,\r\n flipVertical: this.svgFlipV,\r\n className: this.svgClass,\r\n };\r\n\r\n this.subscription = this.svgService\r\n .icon(this.iconPath, options)\r\n .subscribe((result) => {\r\n // Limpiar contenido anterior\r\n this.elementRef.nativeElement.innerHTML = '';\r\n\r\n // Insertar el nuevo SVG\r\n this.elementRef.nativeElement.appendChild(result.element);\r\n\r\n // Guardar función de destrucción\r\n this.destroyFn = result.destroy;\r\n });\r\n }\r\n\r\n private cleanup(): void {\r\n if (this.subscription) {\r\n this.subscription.unsubscribe();\r\n this.subscription = undefined;\r\n }\r\n\r\n if (this.destroyFn) {\r\n this.destroyFn();\r\n this.destroyFn = undefined;\r\n }\r\n }\r\n}\r\n\r\n// Directiva aún más simple para casos básicos\r\n@Directive({\r\n selector: '[easyIcon]',\r\n standalone: true,\r\n})\r\nexport class EasyIconDirective implements OnInit, OnDestroy {\r\n @Input('easyIcon') iconConfig: string = ''; // Formato: \"path,color,width,height\"\r\n\r\n private svgDirective?: SvgIconDirective;\r\n\r\n constructor(\r\n private elementRef: ElementRef,\r\n private svgService: SvgIconService,\r\n private renderer: Renderer2,\r\n ) {}\r\n\r\n ngOnInit(): void {\r\n this.parseAndApply();\r\n }\r\n\r\n ngOnDestroy(): void {\r\n if (this.svgDirective) {\r\n this.svgDirective.ngOnDestroy();\r\n }\r\n }\r\n\r\n private parseAndApply(): void {\r\n if (!this.iconConfig) return;\r\n\r\n const parts = this.iconConfig.split(',').map((p) => p.trim());\r\n const [path, color, width, height] = parts;\r\n\r\n // Crear instancia de la directiva principal\r\n this.svgDirective = new SvgIconDirective(\r\n this.elementRef,\r\n this.svgService,\r\n this.renderer,\r\n );\r\n\r\n // Configurar propiedades\r\n this.svgDirective.iconPath = path || '';\r\n this.svgDirective.svgColor = color || '';\r\n this.svgDirective.svgWidth = width || '';\r\n this.svgDirective.svgHeight = height || '';\r\n\r\n // Inicializar\r\n this.svgDirective.ngOnInit();\r\n }\r\n}\r\n","import {\r\n Component,\r\n Input,\r\n Output,\r\n EventEmitter,\r\n ViewChild,\r\n ElementRef,\r\n ChangeDetectionStrategy,\r\n} from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\nimport { OpeniisSpinnerComponent } from '../spinner/spinner.component';\r\nimport { OpeniisTooltipComponent } from '../tooltip/tooltip.component';\r\nimport { SvgIconDirective } from '../services/svg/svg-icon.directive';\r\n\r\nexport type ButtonVariant =\r\n | undefined\r\n | 'primary'\r\n | 'secondary'\r\n | 'success'\r\n | 'warning'\r\n | 'danger'\r\n | 'info'\r\n | 'outline-primary'\r\n | 'outline-secondary'\r\n | 'outline-success'\r\n | 'outline-warning'\r\n | 'outline-danger'\r\n | 'outline-info'\r\n | 'ghost'\r\n | 'text'\r\n | 'link'\r\n | 'subtle'\r\n | 'icon';\r\n\r\nexport type ButtonType = 'button' | 'submit' | 'reset';\r\nexport type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\r\n\r\n@Component({\r\n selector: 'openiis-button',\r\n standalone: true,\r\n imports: [\r\n CommonModule,\r\n OpeniisSpinnerComponent,\r\n OpeniisTooltipComponent,\r\n SvgIconDirective,\r\n ],\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n templateUrl: './button.component.html',\r\n styleUrls: ['./button.component.css'],\r\n})\r\nexport class OpeniisButtonComponent {\r\n @ViewChild('buttonElement', { static: true })\r\n buttonElement!: ElementRef<HTMLButtonElement>;\r\n\r\n /** Texto del botón */\r\n @Input() text: string = '';\r\n\r\n /** Tipo de botón para compatibilidad (primary, secondary, etc.) */\r\n @Input() type: ButtonVariant = 'primary';\r\n\r\n /** Tamaño del botón */\r\n @Input() size: ButtonSize = 'md';\r\n\r\n /** Tipo HTML del botón */\r\n @Input() htmlType: ButtonType = 'button';\r\n\r\n /** Estado deshabilitado */\r\n @Input() disabled: boolean = false;\r\n\r\n /** Estado de carga */\r\n @Input() loading: boolean = false;\r\n\r\n /** Icono izquierdo (material icon name) */\r\n @Input() iconLeft: string = '';\r\n\r\n /** Icono derecho (material icon name) */\r\n @Input() iconRight: string = '';\r\n\r\n /** Solo mostrar icono (ocultar texto) */\r\n @Input() iconOnly: boolean = false;\r\n\r\n /** Botón de ancho completo */\r\n @Input() fullWidth: boolean = false;\r\n\r\n /** Texto del tooltip */\r\n @Input() tooltipText: string = '';\r\n\r\n /** Posición del tooltip */\r\n @Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';\r\n\r\n /** Variante del tooltip */\r\n @Input() tooltipVariant: 'default' | 'danger' = 'default';\r\n\r\n /** Título del botón */\r\n @Input() title: string = '';\r\n\r\n /** Botón con dropdown */\r\n @Input() hasDropdown: boolean = false;\r\n\r\n /** Estado del dropdown */\r\n @Input() dropdownOpen: boolean = false;\r\n\r\n /** Etiqueta aria */\r\n @Input() ariaLabel: string = '';\r\n\r\n /** Referencia aria describedby */\r\n @Input() ariaDescribedBy: string = '';\r\n\r\n /** Clases CSS adicionales */\r\n @Input() extraClasses: string = '';\r\n\r\n /** Botón responsivo */\r\n @Input() responsive: boolean = false;\r\n\r\n /** Evento de clic */\r\n @Output() clickEvent = new EventEmitter<MouseEvent>();\r\n\r\n /** Evento de toggle del dropdown */\r\n @Output() dropdownToggle = new EventEmitter<boolean>();\r\n\r\n // Estado interno del tooltip\r\n showTooltip: boolean = false;\r\n\r\n // Icono SVG\r\n @Input() iconAsset: string = '';\r\n @Input() colorSvg: string = 'inherit';\r\n @Input() backgroundSvg: string = 'transparent';\r\n @Input() widthSvg: string = '24px';\r\n @Input() heightSvg: string = '24px';\r\n @Input() sizeSvg: string = '24px';\r\n\r\n /**\r\n * Clases CSS del botón\r\n */\r\n get buttonClasses(): string {\r\n const variantToUse = this.type;\r\n const isIconOnly =\r\n this.iconOnly ||\r\n this.type === 'icon' ||\r\n (!this.text && !!(this.iconLeft || this.iconRight));\r\n\r\n return [\r\n 'btn',\r\n `btn-${variantToUse}`,\r\n `btn-${this.size}`,\r\n this.fullWidth ? 'btn-full-width' : '',\r\n isIconOnly ? 'btn-icon-only' : '',\r\n this.loading ? 'btn-loading' : '',\r\n this.disabled ? 'btn-disabled' : '',\r\n this.responsive ? 'btn-responsive' : '',\r\n this.extraClasses,\r\n ]\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n /**\r\n * Determina si se debe mostrar el tooltip\r\n */\r\n get shouldShowTooltip(): boolean {\r\n return !!this.tooltipText;\r\n }\r\n\r\n /**\r\n * Texto efectivo del tooltip\r\n */\r\n get effectiveTooltipText(): string {\r\n return this.tooltipText;\r\n }\r\n\r\n /**\r\n * Verifica si el botón es de solo icono\r\n */\r\n get isIconOnlyButton(): boolean {\r\n return (\r\n this.iconOnly ||\r\n this.type === 'icon' ||\r\n (!this.text && !!(this.iconLeft || this.iconRight))\r\n );\r\n }\r\n\r\n /**\r\n * Maneja el clic del botón\r\n */\r\n onClick(event: MouseEvent): void {\r\n if (this.disabled || this.loading) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n return;\r\n }\r\n\r\n if (this.hasDropdown) {\r\n this.dropdownOpen = !this.dropdownOpen;\r\n this.dropdownToggle.emit(this.dropdownOpen);\r\n }\r\n\r\n this.clickEvent.emit(event);\r\n }\r\n\r\n /**\r\n * Muestra el tooltip en hover\r\n */\r\n showTooltipOnHover(): void {\r\n if (this.shouldShowTooltip) {\r\n this.showTooltip = true;\r\n }\r\n }\r\n\r\n /**\r\n * Oculta el tooltip al salir del hover\r\n */\r\n hideTooltipOnLeave(): void {\r\n this.showTooltip = false;\r\n }\r\n\r\n /**\r\n * Enfoca el botón\r\n */\r\n focus(): void {\r\n this.buttonElement.nativeElement.focus();\r\n }\r\n\r\n /**\r\n * Desenfoca el botón\r\n */\r\n blur(): void {\r\n this.buttonElement.nativeElement.blur();\r\n }\r\n}\r\n","<div class=\"button-wrapper\" [class.has-tooltip]=\"shouldShowTooltip\">\r\n <button\r\n [type]=\"htmlType\"\r\n [class]=\"buttonClasses\"\r\n [disabled]=\"disabled || loading\"\r\n (click)=\"onClick($event)\"\r\n (mouseenter)=\"showTooltipOnHover()\"\r\n (mouseleave)=\"hideTooltipOnLeave()\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n [attr.title]=\"type === 'icon' ? '' : title\"\r\n >\r\n @if (loading) {\r\n <openiis-spinner\r\n variant=\"circle\"\r\n size=\"xs\"\r\n color=\"primary\"\r\n [inline]=\"true\"\r\n ariaLabel=\"Cargando botón\"\r\n ></openiis-spinner>\r\n } @else if (iconLeft) {\r\n <span class=\"material-icons-outlined icon-left\">{{ iconLeft }}</span>\r\n } @if (text) {\r\n <span class=\"button-text\">{{ text }}</span>\r\n } @if (iconRight && !loading) {\r\n <span class=\"material-icons-outlined icon-right\">{{ iconRight }}</span>\r\n } @if (iconAsset) { @if (iconAsset.endsWith('.svg')) {\r\n <span\r\n [svgIcon]=\"iconAsset\"\r\n [svgColor]=\"colorSvg\"\r\n [svgBackground]=\"backgroundSvg\"\r\n [svgWidth]=\"widthSvg\"\r\n [svgHeight]=\"heightSvg\"\r\n >\r\n </span>\r\n } @else {\r\n <img class=\"icon-asset\" [src]=\"iconAsset\" />\r\n } }\r\n </button>\r\n\r\n @if (shouldShowTooltip) {\r\n <openiis-tooltip\r\n [text]=\"effectiveTooltipText\"\r\n [position]=\"tooltipPosition\"\r\n [variant]=\"tooltipVariant\"\r\n [visible]=\"showTooltip\"\r\n >\r\n </openiis-tooltip>\r\n }\r\n</div>\r\n","import {\r\n Component,\r\n Input,\r\n Output,\r\n EventEmitter,\r\n ContentChildren,\r\n QueryList,\r\n AfterContentInit,\r\n OnDestroy,\r\n ElementRef,\r\n} from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\nimport { Subject, takeUntil } from 'rxjs';\r\n\r\nexport type ButtonGroupSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\r\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical';\r\nexport type ButtonGroupType =\r\n | 'default'\r\n | 'primary'\r\n | 'success'\r\n | 'warning'\r\n | 'danger'\r\n | 'subtle';\r\n\r\n@Component({\r\n selector: 'openiis-button-group',\r\n standalone: true,\r\n imports: [CommonModule],\r\n template: `\r\n <div\r\n class=\"button-group\"\r\n [class]=\"containerClasses\"\r\n [attr.data-size]=\"size\"\r\n [attr.data-orientation]=\"orientation\"\r\n [attr.data-type]=\"type\"\r\n [attr.role]=\"role\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n [attr.aria-describedby]=\"ariaDescribedBy\"\r\n >\r\n <ng-content></ng-content>\r\n </div>\r\n `,\r\n styleUrls: ['./button-group.component.css'],\r\n})\r\nexport class OpeniisButtonGroupComponent\r\n implements AfterContentInit, OnDestroy\r\n{\r\n constructor(private elementRef: ElementRef) {}\r\n /**\r\n * Tamaño de los botones en el grupo\r\n */\r\n @Input() size: ButtonGroupSize = 'md';\r\n\r\n /**\r\n * Orientación del grupo de botones\r\n */\r\n @Input() orientation: ButtonGroupOrientation = 'horizontal';\r\n\r\n /**\r\n * Tipo visual del grupo de botones\r\n */\r\n @Input() type: ButtonGroupType = 'default';\r\n\r\n /**\r\n * Permitir seleccionar múltiples botones\r\n */\r\n @Input() multiple: boolean = false;\r\n\r\n /**\r\n * Hacer que los botones ocupen el ancho completo\r\n */\r\n @Input() fullWidth: boolean = false;\r\n\r\n /**\r\n * Separar los botones con un espacio\r\n */\r\n @Input() separated: boolean = false;\r\n\r\n /**\r\n * Deshabilitar todos los botones del grupo\r\n */\r\n @Input() disabled: boolean = false;\r\n\r\n /**\r\n * Índice del botón seleccionado (para selección única)\r\n */\r\n @Input() selectedIndex: number = -1;\r\n\r\n /**\r\n * Índices de los botones seleccionados (para selección múltiple)\r\n */\r\n @Input() selectedIndices: number[] = [];\r\n\r\n /**\r\n * Rol ARIA para accesibilidad\r\n */\r\n @Input() role: string = 'group';\r\n\r\n /**\r\n * ARIA label para accesibilidad\r\n */\r\n @Input() ariaLabel: string = '';\r\n\r\n /**\r\n * ARIA describedby para accesibilidad\r\n */\r\n @Input() ariaDescribedBy: string = '';\r\n\r\n /**\r\n * Clases CSS adicionales\r\n */\r\n @Input() extraClasses: string = '';\r\n\r\n /**\r\n * Evento emitido cuando se selecciona un botón\r\n */\r\n @Output() buttonClick = new EventEmitter<{\r\n index: number;\r\n button: HTMLButtonElement;\r\n selected: boolean;\r\n }>();\r\n\r\n /**\r\n * Evento emitido cuando cambia la selección\r\n */\r\n @Output() selectionChange = new EventEmitter<number | number[]>();\r\n\r\n /**\r\n * Subject para manejar la destrucción del componente\r\n */\r\n private destroy$ = new Subject<void>();\r\n\r\n /**\r\n * Botones del grupo\r\n */\r\n private buttons: HTMLButtonElement[] = [];\r\n\r\n /**\r\n * Genera las clases CSS del contenedor\r\n */\r\n get containerClasses(): string {\r\n const classes = ['button-group'];\r\n\r\n if (this.fullWidth) classes.push('full-width');\r\n if (this.separated) classes.push('separated');\r\n if (this.disabled) classes.push('disabled');\r\n if (this.extraClasses) classes.push(this.extraClasses);\r\n\r\n return classes.join(' ');\r\n }\r\n\r\n ngAfterContentInit(): void {\r\n // Componente simplificado: solo aplicamos clases CSS\r\n // La lógica de interacción se maneja directamente en el template\r\n }\r\n\r\n ngOnDestroy(): void {\r\n this.destroy$.next();\r\n this.destroy$.complete();\r\n }\r\n\r\n /**\r\n * Maneja el clic en un botón\r\n */\r\n private handleButtonClick(\r\n index: number,\r\n button: HTMLButtonElement,\r\n event: Event\r\n ): void {\r\n if (this.disabled || button.disabled) return;\r\n\r\n event.preventDefault();\r\n event.stopPropagation();\r\n\r\n if (this.multiple) {\r\n // Selección múltiple\r\n const isSelected = this.selectedIndices.includes(index);\r\n\r\n if (isSelected) {\r\n // Deseleccionar\r\n this.selectedIndices = this.selectedIndices.filter((i) => i !== index);\r\n button.classList.remove('selected');\r\n button.setAttribute('aria-pressed', 'false');\r\n } else {\r\n // Seleccionar\r\n this.selectedIndices = [...this.selectedIndices, index];\r\n button.classList.add('selected');\r\n button.setAttribute('aria-pressed', 'true');\r\n }\r\n\r\n this.selectionChange.emit(this.selectedIndices);\r\n this.buttonClick.emit({\r\n index,\r\n button,\r\n selected: !isSelected,\r\n });\r\n } else {\r\n // Selección única\r\n // Deseleccionar todos\r\n this.buttons.forEach((btn, i) => {\r\n btn.classList.remove('selected');\r\n btn.setAttribute('aria-pressed', 'false');\r\n });\r\n\r\n // Seleccionar el actual\r\n button.classList.add('selected');\r\n button.setAttribute('aria-pressed', 'true');\r\n this.selectedIndex = index;\r\n\r\n this.selectionChange.emit(index);\r\n this.buttonClick.emit({\r\n index,\r\n button,\r\n selected: true,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Maneja la navegación por teclado\r\n */\r\n private handleKeyDown(currentIndex: number, event: KeyboardEvent): void {\r\n let targetIndex = currentIndex;\r\n\r\n if (this.orientation === 'horizontal') {\r\n if (event.key === 'ArrowLeft') {\r\n targetIndex =\r\n currentIndex > 0 ? currentIndex - 1 : this.buttons.length - 1;\r\n } else if (event.key === 'ArrowRight') {\r\n targetIndex =\r\n currentIndex < this.buttons.length - 1 ? currentIndex + 1 : 0;\r\n }\r\n } else {\r\n if (event.key === 'ArrowUp') {\r\n targetIndex =\r\n currentIndex > 0 ? currentIndex - 1 : this.buttons.length - 1;\r\n } else if (event.key === 'ArrowDown') {\r\n targetIndex =\r\n currentIndex < this.buttons.length - 1 ? currentIndex + 1 : 0;\r\n }\r\n }\r\n\r\n if (targetIndex !== currentIndex) {\r\n event.preventDefault();\r\n this.buttons[targetIndex]?.focus();\r\n }\r\n\r\n // Seleccionar con Enter o Space\r\n if (event.key === 'Enter' || event.key === ' ') {\r\n event.preventDefault();\r\n this.buttons[currentIndex]?.click();\r\n }\r\n }\r\n\r\n /**\r\n * Actualiza el estado de selección programáticamente\r\n */\r\n updateSelection(selection: number | number[]): void {\r\n if (this.multiple && Array.isArray(selection)) {\r\n this.selectedIndices = selection;\r\n } else if (!this.multiple && typeof selection === 'number') {\r\n this.selectedIndex = selection;\r\n }\r\n\r\n // Actualizar el DOM\r\n this.buttons.forEach((button, index) => {\r\n const isSelected = this.multiple\r\n ? this.selectedIndices.includes(index)\r\n : this.selectedIndex === index;\r\n\r\n if (isSelected) {\r\n button.classList.add('selected');\r\n button.setAttribute('aria-pressed', 'true');\r\n } else {\r\n button.classList.remove('selected');\r\n button.setAttribute('aria-pressed', 'false');\r\n }\r\n });\r\n }\r\n}\r\n","import {\r\n Component,\r\n Input,\r\n Output,\r\n EventEmitter,\r\n forwardRef,\r\n} from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\r\n\r\nexport type RadioButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\r\nexport type RadioButtonType = 'success' | 'warning' | 'danger' | 'subtle';\r\n\r\n@Component({\r\n selector: 'openiis-radio-button',\r\n standalone: true,\r\n imports: [CommonModule],\r\n providers: [\r\n {\r\n provide: NG_VALUE_ACCESSOR,\r\n useExisting: forwardRef(() => OpeniisRadioButtonComponent),\r\n multi: true,\r\n },\r\n ],\r\n template: `\r\n <div\r\n class=\"radio-container\"\r\n [class]=\"containerClasses\"\r\n [attr.data-size]=\"size\"\r\n [attr.data-type]=\"type\"\r\n >\r\n <label [for]=\"radioId\" class=\"radio-label\">\r\n <input\r\n [id]=\"radioId\"\r\n type=\"radio\"\r\n class=\"radio-input\"\r\n [name]=\"name\"\r\n [value]=\"value\"\r\n [checked]=\"isChecked\"\r\n [disabled]=\"disabled\"\r\n [readonly]=\"readonly\"\r\n [attr.aria-label]=\"ariaLabel\"\r\n [attr.aria-describedby]=\"ariaDescribedBy\"\r\n [attr.title]=\"title || null\"\r\n (change)=\"onRadioChange($event)\"\r\n (focus)=\"onFocus($event)\"\r\n (blur)=\"onBlur($event)\"\r\n />\r\n <div class=\"radio-indicator\">\r\n <div class=\"radio-dot\"></div>\r\n </div>\r\n @if (label) {\r\n <span class=\"radio-text\">{{ label }}</span>\r\n }\r\n </label>\r\n\r\n @if (helpText && !errorText) {\r\n <div class=\"radio-help\">{{ helpText }}</div>\r\n } @if (errorText) {\r\n <div class=\"radio-error\">{{ errorText }}</div>\r\n }\r\n </div>\r\n `,\r\n styleUrls: ['./radio-button.component.css'],\r\n})\r\nexport class OpeniisRadioButtonComponent implements ControlValueAccessor {\r\n /**\r\n * Tamaño del radio button\r\n */\r\n @Input() size: RadioButtonSize = 'md';\r\n\r\n /**\r\n * Tipo visual del radio button\r\n */\r\n @Input() type: RadioButtonType = 'success';\r\n\r\n /**\r\n * Texto de la etiqueta\r\n */\r\n @Input() label: string = '';\r\n\r\n /**\r\n * Texto de ayuda\r\n */\r\n @Input() helpText: string = '';\r\n\r\n /**\r\n * Texto de error\r\n */\r\n @Input() errorText: string = '';\r\n\r\n /**\r\n * Valor del radio button\r\n */\r\n @Input() value: any = '';\r\n\r\n /**\r\n * Nombre del grupo de radio buttons\r\n */\r\n @Input() name: string = '';\r\n\r\n /**\r\n * Estado deshabilitado\r\n */\r\n @Input() disabled: boolean = false;\r\n\r\n /**\r\n * Estado solo lectura\r\n */\r\n @Input() readonly: boolean = false;\r\n\r\n /**\r\n * ID único del radio button\r\n */\r\n @Input() radioId: string = `radio-${Math.random().toString(36).substr(2, 9)}`;\r\n\r\n /**\r\n * ARIA label para accesibilidad\r\n */\r\n @Input() ariaLabel: string = '';\r\n\r\n /**\r\n * ARIA describedby para accesibilidad\r\n */\r\n @Input() ariaDescribedBy: string = '';\r\n\r\n /**\r\n * Título para tooltip\r\n */\r\n @Input() title: string = '';\r\n\r\n /**\r\n * Clases CSS adicionales\r\n */\r\n @Input() extraClasses: string = '';\r\n\r\n /**\r\n * Evento emitido cuando el valor cambia\r\n */\r\n @Output() checkedChange = new EventEmitter<any>();\r\n\r\n /**\r\n * Evento emitido al recibir foco\r\n */\r\n @Output() focusEvent = new EventEmitter<FocusEvent>();\r\n\r\n /**\r\n * Evento emitido al perder foco\r\n */\r\n @Output() blurEvent = new EventEmitter<FocusEvent>();\r\n\r\n /**\r\n * Valor seleccionado actual\r\n */\r\n selectedValue: any = null;\r\n\r\n /**\r\n * Función llamada cuando el valor cambia (ControlValueAccessor)\r\n */\r\n private onChange = (value: any) => {};\r\n\r\n /**\r\n * Función llamada cuando el componente es tocado (ControlValueAccessor)\r\n */\r\n private onTouched = () => {};\r\n\r\n /**\r\n * Obtiene si este radio button está seleccionado\r\n */\r\n get isChecked(): boolean {\r\n return this.selectedValue === this.value;\r\n }\r\n\r\n /**\r\n * Genera las clases CSS del contenedor\r\n */\r\n get containerClasses(): string {\r\n const classes = ['radio-container'];\r\n\r\n if (this.disabled) classes.push('disabled');\r\n if (this.readonly) classes.push('readonly');\r\n if (this.errorText) classes.push('error');\r\n if (this.isChecked) classes.push('checked');\r\n if (this.extraClasses) classes.push(this.extraClasses);\r\n\r\n return classes.join(' ');\r\n }\r\n\r\n /**\r\n * Maneja el cambio del radio button\r\n */\r\n onRadioChange(event: Event): void {\r\n if (this.disabled || this.readonly) return;\r\n\r\n const target = event.target as HTMLInputElement;\r\n if (target.checked) {\r\n this.selectedValue = this.value;\r\n this.onChange(this.value);\r\n this.checkedChange.emit(this.value);\r\n }\r\n }\r\n\r\n /**\r\n * Maneja el evento de foco\r\n */\r\n onFocus(event: FocusEvent): void {\r\n this.focusEvent.emit(event);\r\n }\r\n\r\n /**\r\n * Maneja el evento de pérdida de foco\r\n */\r\n onBlur(event: FocusEvent): void {\r\n this.onTouched();\r\n this.blurEvent.emit(event);\r\n }\r\n\r\n // Implementación de ControlValueAccessor\r\n\r\n /**\r\n * Escribe un valor desde el formulario hacia el componente\r\n */\r\n writeValue(value: any): void {\r\n this.selectedValue = value;\r\n }\r\n\r\n /**\r\n * Registra la función a llamar cuando el valor cambia\r\n */\r\n registerOnChange(fn: (value: any) => void): void {\r\n this.onChange = fn;\r\n }\r\n\r\n /**\r\n * Registra la función a llamar cuando el componente es tocado\r\n */\r\n registerOnTouched(fn: () => void): void {\r\n this.onTouched = fn;\r\n }\r\n\r\n /**\r\n * Establece el estado deshabilitado\r\n */\r\n setDisabledState(isDisabled: boolean): void {\r\n this.disabled = isDisabled;\r\n }\r\n}\r\n","import {\r\n Component,\r\n Input,\r\n Output,\r\n EventEmitter,\r\n forwardRef,\r\n} from '@angular/core';\r\nimport { CommonModule } from '@angular/common';\r\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\r\n\r\nexport type SwitchSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\r\nexport type SwitchVariant =\r\n | 'default'\r\n | 'primary'\r\n | 'success'\r\n | 'warning'\r\n | 'danger'\r\n | 'subtle';\r\n\r\n@Component({\r\n selector: 'openiis-switch',\r\n standalone: true,\r\n imports: [CommonModule],\r\n providers: [\r\n {\r\n provide: NG_VALUE_ACCESSOR,\r\n useExisting: forwardRef(() => OpeniisSwitchComponent),\r\n multi: true,\r\n },\r\n ],\r\n template: `\r\n <div\r\n class=\"switch-container\"\r\n [class]=\"containerClasses\"\r\n [attr.data-size]=\"size\"\r\n [attr.data-variant]=\"variant\"\r\n >\r\n <label [for]=\"switchId\" class=\"switch-label\">\r\n @if (label) {\r\n <span class=\"switch-text\">{{ label }}</span>\r\n }\r\n <div class=\"switch-wrapper\">\r\n <input\r\n [id]=\"switchId\"