import { css, html } from "lit";
import { classMap } from "lit/directives/class-map.js";
import { BaseElement } from "../../core/base-element.ts";
/**
* CFListItem - Generic list row inspired by SwiftUI List
*
* Supports simple label rows, command items with icons/shortcuts,
* and complex expandable rows with detail content.
*
* @element cf-list-item
*
* @attr {string} label - Primary text content
* @attr {string} description - Secondary text below the label
* @attr {boolean} expandable - Whether the row can expand to show detail
* @attr {boolean} expanded - Current expand state (only when expandable)
* @attr {boolean} disabled - Prevents interaction
*
* @slot icon - Leading icon
* @slot - Primary content (overrides label attribute)
* @slot description - Secondary text (overrides description attribute)
* @slot action - Trailing action (button, badge, keyboard shortcut)
* @slot detail - Expandable detail area (shown when expanded)
*
* @fires cf-click - Fired when the item is clicked
* @fires cf-expand - Fired when expanded state changes, detail: { expanded }
*
* @example Simple row
*
*
* @example Command item with icon and shortcut
*
* 📁
* ⌘N
*
*
* @example Expandable row
*
* 3 tasks
* Detail content shown when expanded
*
*/
export class CFListItem extends BaseElement {
static override properties = {
label: { type: String, reflect: true },
description: { type: String },
expandable: { type: Boolean, reflect: true },
expanded: { type: Boolean, reflect: true },
disabled: { type: Boolean, reflect: true },
_hasIcon: { state: true },
_hasDescription: { state: true },
_hasAction: { state: true },
_hasDetail: { state: true },
};
static override styles = [
BaseElement.baseStyles,
css`
:host {
display: block;
}
.row {
display: flex;
align-items: center;
}
.item {
display: flex;
align-items: center;
gap: var(--cf-list-item-gap, 0.75rem);
flex: 1;
min-width: 0;
padding: var(--cf-list-item-padding, 0.5rem);
border: none;
border-radius: var(
--cf-list-item-radius,
var(--cf-theme-border-radius, 0.75rem)
);
background: transparent;
color: var(--cf-list-item-color, var(--cf-theme-color-text, #34373c));
font-family: inherit;
font-size: var(
--cf-list-item-font-size,
var(--cf-font-body-compact-size, 0.8125rem)
);
font-weight: var(
--cf-list-item-font-weight,
var(--cf-font-body-compact-weight, 500)
);
line-height: var(
--cf-list-item-line-height,
var(--cf-font-body-compact-line-height, 1.25rem)
);
text-align: left;
cursor: pointer;
transition: background-color var(--cf-transition-duration-fast, 150ms)
var(--cf-transition-timing-ease, ease);
}
.item:hover:not(:disabled) {
background: var(
--cf-list-item-hover-bg,
var(--cf-theme-color-surface-hover, rgba(0, 0, 0, 0.03))
);
}
.item:focus-visible {
outline: 2px solid
var(--cf-theme-color-primary, var(--cf-colors-primary-500));
outline-offset: 2px;
}
.item:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Icon */
.icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: var(--cf-list-item-icon-size, 1.5rem);
height: var(--cf-list-item-icon-size, 1.5rem);
}
.icon.empty {
display: none;
}
/* Content */
.content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.description {
font-weight: var(--cf-font-body-weight, 400);
font-size: var(
--cf-list-item-description-size,
var(--cf-font-caption-size, 0.75rem)
);
color: var(
--cf-list-item-description-color,
var(--cf-theme-color-text-muted, #71747a)
);
line-height: var(--cf-font-caption-line-height, 1rem);
}
.description.empty {
display: none;
}
/* Action */
.action {
display: flex;
align-items: center;
flex-shrink: 0;
}
.action.empty {
display: none;
}
/* Expand chevron */
.chevron {
display: flex;
align-items: center;
flex-shrink: 0;
color: var(--cf-theme-color-text-muted, #71747a);
transition: transform var(--cf-transition-duration-fast, 150ms)
var(--cf-transition-timing-ease, ease);
}
:host([expanded]) .chevron {
transform: rotate(90deg);
}
/* Detail */
.detail {
overflow: hidden;
max-height: 0;
opacity: 0;
transition:
max-height var(--cf-transition-duration-base, 200ms)
var(--cf-transition-timing-ease, ease),
opacity var(--cf-transition-duration-fast, 150ms)
var(--cf-transition-timing-ease, ease);
}
:host([expanded]) .detail {
max-height: 500px;
opacity: 1;
}
.detail.empty {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.item,
.chevron,
.detail {
transition: none;
}
}
`,
];
declare label: string;
declare description: string;
declare expandable: boolean;
declare expanded: boolean;
declare disabled: boolean;
declare _hasIcon: boolean;
declare _hasDescription: boolean;
declare _hasAction: boolean;
declare _hasDetail: boolean;
constructor() {
super();
this.label = "";
this.description = "";
this.expandable = false;
this.expanded = false;
this.disabled = false;
this._hasIcon = false;
this._hasDescription = false;
this._hasAction = false;
this._hasDetail = false;
}
override render() {
return html`
${this.expandable
? html`
`
: ""}
`;
}
private _handleClick = () => {
if (this.disabled) return;
if (this.expandable) {
this.expanded = !this.expanded;
this.emit("cf-expand", { expanded: this.expanded });
}
this.emit("cf-click", { label: this.label });
};
private _handleIconSlotChange = (e: Event) => {
const slot = e.target as HTMLSlotElement;
this._hasIcon = slot.assignedElements().length > 0;
};
private _handleDescriptionSlotChange = (e: Event) => {
const slot = e.target as HTMLSlotElement;
this._hasDescription = slot.assignedElements().length > 0;
};
private _handleActionSlotChange = (e: Event) => {
const slot = e.target as HTMLSlotElement;
this._hasAction = slot.assignedElements().length > 0;
};
private _handleDetailSlotChange = (e: Event) => {
const slot = e.target as HTMLSlotElement;
this._hasDetail = slot.assignedElements().length > 0;
};
}