import { css, html } from "lit"; import { BaseElement } from "../../core/base-element.ts"; const AUTO_HIDE_MS = 30_000; /** * CTSecretViewer - Trusted UI component for revealing secret strings * * Displays a greeked/masked value (e.g., `••••••••••hJ9k`) with click-to-reveal * and a copy button. Used by patterns to show webhook URLs, API keys, or other * confidential strings without the pattern code itself needing to read the value. * * @element ct-secret-viewer * * @attr {string} value - The secret string (bound from a cell) * @attr {string} label - Optional label displayed above the value * @attr {number} trailing-chars - How many non-greeked chars to show at end (default: 4) * * @example * */ export class CTSecretViewer extends BaseElement { static override styles = [ BaseElement.baseStyles, css` :host { display: block; } .secret-viewer { display: flex; flex-direction: column; gap: var(--spacing-1, 0.25rem); } .label { font-size: var(--font-size-sm, 0.875rem); color: var(--color-text-secondary, #6b7280); font-weight: 500; } .value-row { display: flex; align-items: center; gap: var(--spacing-2, 0.5rem); background: var(--color-bg-subtle, #f9fafb); border: 1px solid var(--color-border, #e5e7eb); border-radius: var(--radius-md, 0.375rem); padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem); } .value { flex: 1; font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--font-size-sm, 0.875rem); word-break: break-all; user-select: none; color: var(--color-text-primary, #111827); } .value.revealed { user-select: text; } .actions { display: flex; align-items: center; gap: var(--spacing-1, 0.25rem); flex-shrink: 0; } `, ]; static override properties = { value: { type: String }, label: { type: String }, trailingChars: { type: Number, attribute: "trailing-chars" }, }; declare value: string; declare label: string; declare trailingChars: number; private _revealed = false; private _autoHideTimeout?: number; constructor() { super(); this.value = ""; this.label = ""; this.trailingChars = 4; } private _getMasked(): string { if (!this.value) return ""; const trailing = Math.max( 0, Math.min(this.trailingChars || 0, this.value.length), ); return trailing > 0 ? "•".repeat(12) + this.value.slice(-trailing) : "•".repeat(12); } private _toggleReveal() { this._revealed = !this._revealed; this.requestUpdate(); if (this._autoHideTimeout) { clearTimeout(this._autoHideTimeout); this._autoHideTimeout = undefined; } if (this._revealed) { this._autoHideTimeout = setTimeout(() => { this._revealed = false; this._autoHideTimeout = undefined; this.requestUpdate(); }, AUTO_HIDE_MS); } } override disconnectedCallback() { super.disconnectedCallback(); if (this._autoHideTimeout) { clearTimeout(this._autoHideTimeout); } } override render() { const displayValue = this._revealed ? this.value : this._getMasked(); return html`
${this.label ? html`
${this.label}
` : ""}
${displayValue}
${this._revealed ? "Hide" : "Reveal"}
`; } }