import { css, html } from "lit";
import { property } from "lit/decorators.js";
import { consume } from "@lit/context";
import { BaseElement } from "../../core/base-element.ts";
import {
applyThemeToElement,
type CTTheme,
defaultTheme,
themeContext,
} from "../theme-context.ts";
/**
* ct-list-item — Row primitive for lists
*
* Slots:
* - leading: Icon/avatar at the start of the row
* - default: Primary content (title or custom node)
* - subtitle: Secondary line below primary content
* - meta: Right-aligned metadata (time, count)
* - actions: Inline action controls; do not trigger activation
*
* States:
* - selected: Highlighted selection state
* - active: Emphasized active state (e.g., current route)
* - disabled: Non-interactive
*
* Events:
* - ct-activate: Fired when the row is activated (click/Enter/Space)
*
* @element ct-list-item
*/
export class CTListItem extends BaseElement {
/** Selected (highlight) state */
@property({ type: Boolean, reflect: true })
selected = false;
/** Active (current) state */
@property({ type: Boolean, reflect: true })
active = false;
/** Disabled (non-interactive) state */
@property({ type: Boolean, reflect: true })
disabled = false;
@consume({ context: themeContext, subscribe: true })
@property({ attribute: false })
declare theme?: CTTheme;
static override styles = [
BaseElement.baseStyles,
css`
:host {
display: block;
font-family: var(--ct-theme-font-family, inherit);
color: var(--ct-theme-color-text, var(--ct-color-gray-900, #111827));
}
.row {
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "leading main meta actions";
align-items: center;
gap: var(--ct-spacing-3);
padding: var(--ct-spacing-2) var(--ct-spacing-3);
border-radius: var(
--ct-theme-border-radius,
var(--ct-border-radius-md, 0.375rem)
);
transition: background-color var(--ct-theme-animation-duration, 150ms)
var(--ct-transition-timing-ease);
cursor: pointer;
user-select: none;
}
:host([disabled]) .row {
opacity: 0.6;
cursor: not-allowed;
}
.row:hover {
background: var(
--ct-theme-color-surface-hover,
var(--ct-color-gray-100, #f3f4f6)
);
box-shadow: inset 0 0 0 1px
var(--ct-theme-color-border, var(--ct-color-gray-200, #e5e7eb));
}
:host([selected]) .row {
background: var(
--ct-theme-color-surface,
var(--ct-color-gray-50, #f9fafb)
);
box-shadow: inset 0 0 0 1px
var(--ct-theme-color-primary, var(--ct-color-primary, #3b82f6));
}
:host([active]) .row {
background: var(
--ct-theme-color-surface-hover,
var(--ct-color-gray-100, #f3f4f6)
);
box-shadow: inset 0 0 0 1px
var(--ct-theme-color-primary, var(--ct-color-primary, #3b82f6));
}
.leading {
grid-area: leading;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
color: var(--ct-theme-color-text-muted, var(--ct-color-gray-600, #4b5563));
}
.main {
grid-area: main;
min-width: 0; /* allow text truncation */
display: grid;
grid-template-rows: auto auto;
align-items: center;
}
.title {
font-size: var(--ct-font-size-sm);
font-weight: var(--ct-font-weight-medium);
line-height: var(--ct-line-height-snug);
color: var(--ct-theme-color-text, var(--ct-color-gray-900, #111827));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.subtitle {
font-size: var(--ct-font-size-xs);
color: var(--ct-theme-color-text-muted, var(--ct-color-gray-600, #4b5563));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.meta {
grid-area: meta;
margin-left: var(--ct-spacing-2);
color: var(--ct-theme-color-text-muted, var(--ct-color-gray-600, #4b5563));
font-size: var(--ct-font-size-xs);
}
.actions {
grid-area: actions;
display: inline-flex;
align-items: center;
gap: var(--ct-spacing-1);
opacity: 0;
transition: opacity var(--ct-transition-duration-fast)
var(--ct-transition-timing-ease);
}
.row:hover .actions,
:host([active]) .actions,
:host([selected]) .actions {
opacity: 1;
}
::slotted([slot="actions"]) {
pointer-events: auto;
}
`,
];
override firstUpdated(
changed: Map,
) {
super.firstUpdated(changed);
this.#applyTheme();
}
override updated(changed: Map) {
super.updated(changed);
if (changed.has("theme")) this.#applyTheme();
}
#applyTheme() {
applyThemeToElement(this, this.theme ?? defaultTheme);
}
override connectedCallback(): void {
super.connectedCallback();
this.setAttribute("role", "listitem");
this.tabIndex = this.disabled ? -1 : 0;
this.addEventListener("click", this.#onActivate);
this.addEventListener("keydown", this.#onKeyDown);
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener("click", this.#onActivate);
this.removeEventListener("keydown", this.#onKeyDown);
}
#inActionsPath(e: Event): boolean {
const path = e.composedPath();
for (const el of path) {
if (!(el instanceof HTMLElement)) continue;
// If element is slotted into actions or is an interactive control
if (
el.getAttribute && el.getAttribute("slot") === "actions" ||
el.tagName === "BUTTON" ||
el.tagName === "A" ||
el.getAttribute("data-ct-action") !== null
) {
return true;
}
}
return false;
}
#onActivate = (e: Event) => {
if (this.disabled) return;
if (this.#inActionsPath(e)) return;
this.emit("ct-activate");
};
#onKeyDown = (e: KeyboardEvent) => {
if (this.disabled) return;
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.emit("ct-activate");
}
};
override render() {
return html`
`;
}
}
globalThis.customElements.define("ct-list-item", CTListItem);