import { LitElement, html, css, PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import type { TemplateResult } from 'lit'; import { classMap } from 'lit/directives/class-map.js'; import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import * as Icons from './icons/svg'; import { aliasMap } from './aliasmap'; interface IconPaths { [key: string]: string; } interface ButtonClassMap { [key:string]: boolean; } const iconPaths: IconPaths = Icons as IconPaths; const SMALL_ICON_SIZE = 16; const LARGE_ICON_SIZE = 24; /** * Nile icon component. * * @tag nile-icon * */ @customElement('nile-icon') export class NileIcon extends LitElement { /** * The name of the icon set * @attr icon set * @type {IconName | undefined} */ @property({ type: String, reflect: true }) public set?: string = 'local'; /** * The name of the icon * @attr name * @type {IconName | undefined} */ @property({ type: String, reflect: true }) public name?: string; /** * A description of what the icon represents * @attr description */ @property({ type: String, reflect: true }) public description = ''; @property({ type: String, reflect: true }) public method = 'fill'; /** * A path to a custom SVG file to display as the icon * @attr customSvgPath */ @property({ type: String }) public customSvgPath?: string; /** * A size of what the icon represents * @attr size */ @property({ type: String, reflect: true }) public size = SMALL_ICON_SIZE; @state() private _svg = ''; @property({ type: String }) override title = 'agents'; /** * Color */ @property({ reflect: true }) color: any ; @property({ type: Boolean }) public push = false; @property({ type: Boolean }) public noFill = false; /** * Retain Viewbox */ @property({ reflect: true }) frame: any; static override styles = css` :host { display: inline-flex; align-items: center; justify-content: center; contain: content; -webkit-font-smoothing: var(--nile-webkit-font-smoothing, var(--ng-webkit-font-smoothing)); -moz-osx-font-smoothing: var(--nile-moz-osx-font-smoothing, var(--ng-moz-osx-font-smoothing)); text-rendering: var(--nile-text-rendering, var(--ng-text-rendering)); } .nds-icon { display: flex; align-items: center; justify-content: center; } .nds-icon svg { height: var(--nile-svg-height); width: var(--nile-svg-width); stroke: var(--nile-svg-stroke); } .nds-icon svg path { fill: var(--nile-svg-fill); } .nds-icon.no-fill svg path { fill: revert-layer; } .stroke svg path { fill: none !important; stroke: var(--nile-svg-fill) !important; stroke-width: var(--nile-svg-stroke-width, var(--ng-svg-stroke-width)); } .nds-icon--push { margin-right: var(--nile-spacing-lg); } `; private _getIconSize(): number { return this.name?.endsWith('-small') ? SMALL_ICON_SIZE : LARGE_ICON_SIZE; } protected override async updated( changedProperties: PropertyValues ): Promise { if (changedProperties.has('size')) { const resolved = this.resolveCssVarExpression(`${this.size}`); if (resolved) { const numeric = parseInt(resolved.replace(/px$/, '').trim(), 10); if (!isNaN(numeric)) { this.size = numeric; } } this.style.setProperty('--nile-svg-height', `${this.size}px`); this.style.setProperty('--nile-svg-width', `${this.size}px`); } if (changedProperties.has('color')) { this.style.setProperty('--nile-svg-fill', `${this.color}`); } if (changedProperties.has('method')) { const resolved = this.resolveCssVarExpression(this.method); if (resolved) this.method = resolved.trim(); } if (changedProperties.has('name') || changedProperties.has('customSvgPath')) { const raw = this.name?.replace(/-/g, '') ?? ''; const iconName = aliasMap[raw] || raw; if (iconName && iconPaths[iconName]) { this._svg = atob(iconPaths[iconName]); } else if (this.customSvgPath) { this._svg = await this.fetchSvg(this.customSvgPath); } else { // throw new Error(`No icon named "${this.name}"`); const a = this.resolveCssVarExpression(this.name!) this.name = a!; } } this.addAttributesToSvg(); } resolveCssVarExpression(expression: string): string | null { if(!expression) { return null; } const regex = /var\((--[^,\s)]+)(?:,\s*(.+))?\)/; let match = expression?.trim(); let attempts = 0; while (match.startsWith("var(") && attempts++ < 10) { const parts = match.match(regex); if (!parts) break; const [, varName, fallback] = parts; const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); if (value) { return value; } else if (fallback) { match = fallback.trim(); } else { return null; } } return match || null; } private addAttributesToSvg(){ const svg = this.renderRoot.querySelector('#svg') const attrs = svg?.attributes if(!attrs?.getNamedItem('xmlns')){ svg?.setAttribute('xmlns',"http://www.w3.org/2000/svg"); } if(!attrs?.getNamedItem('version')){ svg?.setAttribute("version","1.1") } if(this.frame){ svg?.setAttribute('viewBox',`0 0 ${this.frame} ${this.frame}`) } else if(!attrs?.getNamedItem('viewBox')){ const viewboxLogic = this.frame ? `0 0 ${this.frame} ${this.frame}` : '0 0 16 16'; svg?.setAttribute('viewBox',viewboxLogic) } if(!attrs?.getNamedItem('height')){ svg?.setAttribute('height',String(this.size)); svg?.setAttribute('width',String(this.size)); } svg?.setAttribute('aria-hidden',`${this.description === ''}`); svg?.setAttribute('aria-label',this.description || ''); const additionalClasses = Object.keys(this.buttonClassMap).filter((key:string)=> this.buttonClassMap[key]) svg?.classList.add('nds-icon',...additionalClasses) } private async fetchSvg(path: string): Promise { const response = await fetch(path); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.text(); } private removeHyphens(iconName: string) { return iconName.replace(/-/g, ''); } private getSvgTemplate(){ const svgTagRegexPattern =/]*>/; const regexToGetSVGAttrs = this._svg.match(svgTagRegexPattern); const svgAttrs = regexToGetSVGAttrs?.[0].replace('','') const svgBody = this._svg.replace( svgTagRegexPattern, '' ) .replace('', '') return ` ${svgBody} ` } get buttonClassMap() { return { 'stroke': this.method != 'fill' , 'nds-icon--push': this.push, 'no-fill': this.noFill, } as ButtonClassMap; } protected override render(): TemplateResult { const size = this.size; const color = this.color; /** * When icons are outside frame */ const viewboxLogic = this.frame ? `0 0 ${this.frame} ${this.frame}` : '0 0 16 16'; if ( (!this._svg.includes('fill=') || this._svg.includes('fill="inherit"')) && !this.color ) { // Case 1: _svg does not have fill color or has fill="inherit" and this.color is not mentioned this.style.setProperty('--nile-svg-fill', `var(--nile-colors-dark-500,var(--ng-colors-fg-secondary-700))`); } else if (this.color) { // Case 2: If this.color is mentioned, use it as fill, regardless of _svg color this.style.setProperty('--nile-svg-fill', `${this.color}`); } return html` ${unsafeSVG(this.getSvgTemplate())} ` } } declare global { interface HTMLElementTagNameMap { 'nile-icon': NileIcon; } }