import { css, html } from "lit"; import { property } from "lit/decorators.js"; import { BaseElement } from "../../core/base-element.ts"; import type { ColorIntent, ComponentSize } from "../theme-context.ts"; /** * CFChip - Reusable pill/chip component * * @element cf-chip * * @attr {string} label - Chip label text to display * @attr {string} color - Color intent: "neutral" | "primary" | "accent" | "danger" (default: "neutral") * @attr {string} size - Size variant: "xs" | "sm" | "md" | "lg" | "xl" (default: "sm") * @attr {boolean} removable - Whether to show remove button (default: false) * @attr {boolean} interactive - Whether chip is clickable (default: false) * * @fires cf-remove - Fired when remove button is clicked * @fires cf-click - Fired when chip is clicked (if interactive) * * @slot icon - Optional icon before the label * @slot - Main content (overrides label) * * @example * * */ export class CFChip extends BaseElement { static override styles = [ BaseElement.baseStyles, css` :host { --cf-chip-border-radius: var( --cf-pill-border-radius, var(--cf-border-radius-full, 9999px) ); --cf-chip-min-height: var( --cf-pill-sm-min-height, var(--cf-size-sm-height) ); --cf-chip-padding-h: var( --cf-pill-sm-padding-h, var(--cf-size-sm-padding-h) ); --cf-chip-padding-v: var( --cf-pill-sm-padding-v, var(--cf-size-sm-padding-v) ); --cf-chip-gap: var(--cf-pill-sm-gap, var(--cf-size-sm-spacing)); --cf-chip-font-size: var( --cf-pill-sm-font-size, var(--cf-size-sm-font-size) ); --cf-chip-line-height: var( --cf-pill-sm-line-height, var(--cf-size-sm-line-height) ); display: inline-block; box-sizing: border-box; } *, *::before, *::after { box-sizing: inherit; } .chip { display: inline-flex; align-items: center; min-height: var(--cf-chip-min-height); gap: var(--cf-chip-gap); padding: var(--cf-chip-padding-v) var(--cf-chip-padding-h); background: var( --cf-chip-background, var( --cf-theme-color-surface, var(--cf-colors-gray-100, #f2f3f6) ) ); color: var( --cf-chip-color, var( --cf-theme-color-text, var(--cf-colors-gray-900, #16181d) ) ); border: 1px solid var( --cf-chip-border-color, var( --cf-theme-color-border, var(--cf-colors-gray-300, #d5d7dd) ) ); border-radius: var( --cf-chip-border-radius, var(--cf-border-radius-full, 9999px) ); font-size: var(--cf-chip-font-size); line-height: var(--cf-chip-line-height); user-select: none; transition: background-color var(--cf-theme-animation-duration, 200ms) ease, border-color var(--cf-theme-animation-duration, 200ms) ease; } :host([size="xs"]) .chip { min-height: var(--cf-pill-xs-min-height, var(--cf-size-xs-height)); padding: var(--cf-pill-xs-padding-v, var(--cf-size-xs-padding-v)) var(--cf-pill-xs-padding-h, var(--cf-size-xs-padding-h)); font-size: var(--cf-pill-xs-font-size, var(--cf-size-xs-font-size)); line-height: var(--cf-pill-xs-line-height, var(--cf-size-xs-line-height)); gap: var(--cf-pill-xs-gap, var(--cf-size-xs-spacing)); } /* sm is default — no override needed */ :host([size="md"]) .chip { min-height: var(--cf-pill-md-min-height, var(--cf-size-md-height)); padding: var(--cf-pill-md-padding-v, var(--cf-size-md-padding-v)) var(--cf-pill-md-padding-h, var(--cf-size-md-padding-h)); font-size: var(--cf-pill-md-font-size, var(--cf-size-md-font-size)); line-height: var(--cf-pill-md-line-height, var(--cf-size-md-line-height)); gap: var(--cf-pill-md-gap, var(--cf-size-md-spacing)); } :host([size="lg"]) .chip { min-height: var(--cf-pill-lg-min-height, var(--cf-size-lg-height)); padding: var(--cf-pill-lg-padding-v, var(--cf-size-lg-padding-v)) var(--cf-pill-lg-padding-h, var(--cf-size-lg-padding-h)); font-size: var(--cf-pill-lg-font-size, var(--cf-size-lg-font-size)); line-height: var(--cf-pill-lg-line-height, var(--cf-size-lg-line-height)); gap: var(--cf-pill-lg-gap, var(--cf-size-lg-spacing)); } :host([size="xl"]) .chip { min-height: var(--cf-pill-xl-min-height, var(--cf-size-xl-height)); padding: var(--cf-pill-xl-padding-v, var(--cf-size-xl-padding-v)) var(--cf-pill-xl-padding-h, var(--cf-size-xl-padding-h)); font-size: var(--cf-pill-xl-font-size, var(--cf-size-xl-font-size)); line-height: var(--cf-pill-xl-line-height, var(--cf-size-xl-line-height)); gap: var(--cf-pill-xl-gap, var(--cf-size-xl-spacing)); } .chip.interactive { cursor: pointer; } .chip.interactive:hover { background: var( --cf-theme-color-surface-hover, var(--cf-colors-gray-200, #eceef1) ); } /* Color: primary (blue - for mentions) */ .chip.primary { background: var( --cf-chip-primary-background, color-mix( in srgb, var( --cf-theme-color-primary, var(--cf-colors-primary-500, #4979fa) ) 12%, var(--cf-theme-color-surface, var(--cf-colors-gray-50, #ffffff)) ) ); border-color: var( --cf-chip-primary-border-color, color-mix( in srgb, var( --cf-theme-color-primary, var(--cf-colors-primary-500, #4979fa) ) 28%, var(--cf-theme-color-surface, var(--cf-colors-gray-50, #ffffff)) ) ); color: var( --cf-chip-primary-color, var(--cf-theme-color-primary, var(--cf-colors-primary-700, #376bf9)) ); } /* Color: accent (purple - for clipboard) */ .chip.accent { background: var( --cf-chip-accent-background, color-mix( in srgb, var(--cf-theme-color-accent, var(--cf-colors-purple, #8952fd)) 12%, var(--cf-theme-color-surface, var(--cf-colors-gray-50, #ffffff)) ) ); border-color: var( --cf-chip-accent-border-color, color-mix( in srgb, var(--cf-theme-color-accent, var(--cf-colors-purple, #8952fd)) 28%, var(--cf-theme-color-surface, var(--cf-colors-gray-50, #ffffff)) ) ); color: var( --cf-chip-accent-color, var(--cf-theme-color-accent, var(--cf-colors-purple, #8952fd)) ); } /* Color: danger (red) */ .chip.danger { background: var( --cf-chip-danger-background, color-mix( in srgb, var(--cf-theme-color-error, #dc2626) 12%, var(--cf-theme-color-surface, var(--cf-colors-gray-50, #ffffff)) ) ); border-color: var( --cf-chip-danger-border-color, color-mix( in srgb, var(--cf-theme-color-error, #dc2626) 28%, var(--cf-theme-color-surface, var(--cf-colors-gray-50, #ffffff)) ) ); color: var( --cf-chip-danger-color, var(--cf-theme-color-error, #dc2626) ); } .chip-icon { display: flex; align-items: center; font-size: var(--cf-font-body-compact-size, 0.8125rem); line-height: 1; } :host:not(:has([slot="icon"])) .chip-icon { display: none; } .chip-label { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chip-remove { display: flex; align-items: center; justify-content: center; width: 0.75rem; height: 0.75rem; border-radius: 50%; cursor: pointer; transition: background-color 0.1s; color: currentColor; opacity: 0.6; } .chip-remove:hover { background: rgba(0, 0, 0, 0.1); opacity: 1; } `, ]; @property({ type: String }) accessor label = ""; @property({ type: String, reflect: true }) accessor color: ColorIntent = "neutral"; @property({ type: Boolean }) accessor removable = false; @property({ type: Boolean }) accessor interactive = false; @property({ type: String, reflect: true }) accessor size: ComponentSize = "sm"; private _handleRemove(e: Event): void { e.stopPropagation(); this.emit("cf-remove"); } private _handleClick(): void { if (this.interactive) { this.emit("cf-click"); } } override render() { const classes = [ "chip", this.color, this.interactive && "interactive", ].filter(Boolean).join(" "); return html`
${this.label} ${this.removable ? html` × ` : ""}
`; } }