import { LitElement, html, css } from 'lit'; import { property } from 'lit/decorators.js'; export class Link extends LitElement { @property({ type: String }) href = ''; @property({ type: String }) target = ''; @property({ type: String }) rel = ''; @property({ type: String }) color: 'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success' | 'inherit' | 'textPrimary' | 'textSecondary' = 'primary'; @property({ type: String }) variant: 'text' | 'outlined' | 'contained' = 'text'; @property({ type: String }) underline: 'none' | 'hover' | 'always' = 'hover'; @property({ type: Boolean }) disabled = false; @property({ type: String }) component = 'a'; static styles = css` :host { display: inline; font-family: "Roboto", "Helvetica", "Arial", sans-serif; } .link { display: inline-flex; align-items: center; gap: 4px; font-size: inherit; line-height: inherit; font-weight: inherit; text-decoration: none; cursor: pointer; border-radius: 4px; transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1), background-color 150ms cubic-bezier(0.4, 0, 0.2, 1), border-color 150ms cubic-bezier(0.4, 0, 0.2, 1), text-decoration 150ms cubic-bezier(0.4, 0, 0.2, 1); outline: none; border: none; background: none; padding: 0; margin: 0; } .link:focus { outline: 2px solid rgba(25, 118, 210, 0.5); outline-offset: 2px; } .link.disabled { color: rgba(0, 0, 0, 0.26); cursor: default; pointer-events: none; } /* Text variant colors */ .link.text.primary { color: #1976d2; } .link.text.primary:hover { color: #1565c0; background-color: rgba(25, 118, 210, 0.04); } .link.text.secondary { color: #9c27b0; } .link.text.secondary:hover { color: #7b1fa2; background-color: rgba(156, 39, 176, 0.04); } .link.text.error { color: #d32f2f; } .link.text.error:hover { color: #c62828; background-color: rgba(211, 47, 47, 0.04); } .link.text.warning { color: #ed6c02; } .link.text.warning:hover { color: #e65100; background-color: rgba(237, 108, 2, 0.04); } .link.text.info { color: #0288d1; } .link.text.info:hover { color: #0277bd; background-color: rgba(2, 136, 209, 0.04); } .link.text.success { color: #2e7d32; } .link.text.success:hover { color: #1b5e20; background-color: rgba(46, 125, 50, 0.04); } .link.text.inherit { color: inherit; } .link.text.inherit:hover { background-color: rgba(0, 0, 0, 0.04); } .link.text.textPrimary { color: rgba(0, 0, 0, 0.87); } .link.text.textPrimary:hover { color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.04); } .link.text.textSecondary { color: rgba(0, 0, 0, 0.6); } .link.text.textSecondary:hover { color: rgba(0, 0, 0, 0.87); background-color: rgba(0, 0, 0, 0.04); } /* Outlined variant */ .link.outlined { padding: 4px 8px; border: 1px solid currentColor; border-radius: 4px; } .link.outlined.primary { color: #1976d2; border-color: rgba(25, 118, 210, 0.5); } .link.outlined.primary:hover { background-color: rgba(25, 118, 210, 0.04); border-color: #1976d2; } .link.outlined.secondary { color: #9c27b0; border-color: rgba(156, 39, 176, 0.5); } .link.outlined.secondary:hover { background-color: rgba(156, 39, 176, 0.04); border-color: #9c27b0; } .link.outlined.error { color: #d32f2f; border-color: rgba(211, 47, 47, 0.5); } .link.outlined.error:hover { background-color: rgba(211, 47, 47, 0.04); border-color: #d32f2f; } .link.outlined.warning { color: #ed6c02; border-color: rgba(237, 108, 2, 0.5); } .link.outlined.warning:hover { background-color: rgba(237, 108, 2, 0.04); border-color: #ed6c02; } .link.outlined.info { color: #0288d1; border-color: rgba(2, 136, 209, 0.5); } .link.outlined.info:hover { background-color: rgba(2, 136, 209, 0.04); border-color: #0288d1; } .link.outlined.success { color: #2e7d32; border-color: rgba(46, 125, 50, 0.5); } .link.outlined.success:hover { background-color: rgba(46, 125, 50, 0.04); border-color: #2e7d32; } /* Contained variant */ .link.contained { padding: 6px 16px; border-radius: 4px; color: #fff; font-weight: 500; } .link.contained.primary { background-color: #1976d2; } .link.contained.primary:hover { background-color: #1565c0; } .link.contained.secondary { background-color: #9c27b0; } .link.contained.secondary:hover { background-color: #7b1fa2; } .link.contained.error { background-color: #d32f2f; } .link.contained.error:hover { background-color: #c62828; } .link.contained.warning { background-color: #ed6c02; } .link.contained.warning:hover { background-color: #e65100; } .link.contained.info { background-color: #0288d1; } .link.contained.info:hover { background-color: #0277bd; } .link.contained.success { background-color: #2e7d32; } .link.contained.success:hover { background-color: #1b5e20; } /* Underline styles */ .link.underline-none { text-decoration: none; } .link.underline-hover { text-decoration: none; } .link.underline-hover:hover { text-decoration: underline; } .link.underline-always { text-decoration: underline; } /* Focus styles for contained variant */ .link.contained:focus { outline: 2px solid rgba(255, 255, 255, 0.5); } /* Active states */ .link:active { transform: scale(0.98); } .link.contained:active { box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); } /* External link icon */ .external-icon { width: 16px; height: 16px; margin-left: 4px; opacity: 0.7; } /* Button-like styling when component is button */ .link.button-component { background: none; border: none; font-family: inherit; font-size: inherit; line-height: inherit; } `; private _handleClick(event: Event) { if (this.disabled) { event.preventDefault(); event.stopPropagation(); return; } this.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true, detail: { originalEvent: event } })); } private _isExternalLink(): boolean { if (!this.href) return false; try { const url = new URL(this.href, window.location.href); return url.hostname !== window.location.hostname; } catch { return false; } } private _renderExternalIcon() { if (!this._isExternalLink() || this.variant === 'contained') return ''; return html` `; } render() { const classes = [ 'link', this.variant, this.color, `underline-${this.underline}`, this.disabled ? 'disabled' : '', this.component === 'button' ? 'button-component' : '' ].filter(Boolean).join(' '); const commonProps = { class: classes, 'aria-disabled': this.disabled ? 'true' : 'false', '@click': this._handleClick }; if (this.component === 'button') { return html` `; } // Default to anchor element const rel = this.rel || (this._isExternalLink() ? 'noopener noreferrer' : ''); return html` ${this._renderExternalIcon()} `; } }