import { css, html } from "lit";
import { BaseElement } from "../../core/base-element.ts";
/**
* CTTable - Semantic table component with styling
*
* @element ct-table
*
* @attr {boolean} striped - Alternate row coloring
* @attr {boolean} hover - Hover effect on rows
* @attr {boolean} bordered - Add borders to all cells
* @attr {string} size - Table size (sm, md, lg)
* @attr {boolean} sticky-header - Make header sticky
* @attr {boolean} full-width - Make table full width
*
* @slot - Table content (thead, tbody, tfoot)
*
* @fires ct-table-sort - Fired when table is sorted with detail: { columnIndex, ascending }
*
* @example
*
*
*
* | Name |
* Email |
*
*
*
*
* | John Doe |
* john@example.com |
*
*
*
*/
export class CTTable extends BaseElement {
static override styles = css`
:host {
display: block;
overflow: auto;
}
table {
border-collapse: collapse;
caption-side: bottom;
text-align: left;
font-size: 0.875rem;
width: auto;
}
:host([full-width]) table {
width: 100%;
}
/* Size variants */
:host([size="sm"]) table {
font-size: 0.75rem;
}
:host([size="sm"]) ::slotted(th),
:host([size="sm"]) ::slotted(td) {
padding: 0.25rem 0.5rem;
}
:host([size="md"]) ::slotted(th),
:host([size="md"]) ::slotted(td) {
padding: 0.5rem 0.75rem;
}
:host([size="lg"]) table {
font-size: 1rem;
}
:host([size="lg"]) ::slotted(th),
:host([size="lg"]) ::slotted(td) {
padding: 0.75rem 1rem;
}
/* Base cell styles */
::slotted(th),
::slotted(td) {
border-bottom: 1px solid var(--border, #e2e8f0);
text-align: inherit;
vertical-align: middle;
}
::slotted(th) {
font-weight: 600;
color: var(--foreground, #0f172a);
background-color: var(--muted, #f8fafc);
}
/* Sticky header */
:host([sticky-header]) ::slotted(thead) ::slotted(th) {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--muted, #f8fafc);
}
/* Striped rows */
:host([striped]) ::slotted(tbody) ::slotted(tr:nth-of-type(even)) {
background-color: var(--muted, #f8fafc);
}
/* Hover effect */
:host([hover]) ::slotted(tbody) ::slotted(tr:hover) {
background-color: var(--muted, #f1f5f9);
}
/* Bordered variant */
:host([bordered]) ::slotted(th),
:host([bordered]) ::slotted(td) {
border: 1px solid var(--border, #e2e8f0);
}
/* Remove double borders */
:host([bordered]) ::slotted(thead) ::slotted(tr:last-child) ::slotted(th),
:host([bordered]) ::slotted(thead) ::slotted(tr:last-child) ::slotted(td) {
border-bottom-width: 2px;
}
/* Caption styling */
::slotted(caption) {
padding: 0.5rem 0.75rem;
color: var(--muted-foreground, #64748b);
text-align: left;
}
/* Responsive wrapper for overflow */
.table-wrapper {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Ensure minimum width for cells */
::slotted(th),
::slotted(td) {
white-space: nowrap;
}
/* Allow wrapping for specific cells if needed */
::slotted(th.wrap),
::slotted(td.wrap) {
white-space: normal;
}
/* Alignment utilities */
::slotted(.text-left) {
text-align: left;
}
::slotted(.text-center) {
text-align: center;
}
::slotted(.text-right) {
text-align: right;
}
/* Vertical alignment */
::slotted(.align-top) {
vertical-align: top;
}
::slotted(.align-middle) {
vertical-align: middle;
}
::slotted(.align-bottom) {
vertical-align: bottom;
}
`;
static override properties = {
striped: { type: Boolean },
hover: { type: Boolean },
bordered: { type: Boolean },
size: { type: String },
stickyHeader: { type: Boolean, attribute: "sticky-header" },
fullWidth: { type: Boolean, attribute: "full-width" },
};
declare striped: boolean;
declare hover: boolean;
declare bordered: boolean;
declare size: "sm" | "md" | "lg";
declare stickyHeader: boolean;
declare fullWidth: boolean;
constructor() {
super();
this.striped = false;
this.hover = false;
this.bordered = false;
this.size = "md";
this.stickyHeader = false;
this.fullWidth = false;
}
override render() {
return html`
`;
}
/**
* Get all table rows
*/
getRows(): HTMLTableRowElement[] {
return Array.from(this.querySelectorAll("tbody tr"));
}
/**
* Get table headers
*/
getHeaders(): HTMLTableCellElement[] {
return Array.from(this.querySelectorAll("thead th"));
}
/**
* Sort table by column index
*/
sortByColumn(columnIndex: number, ascending: boolean = true) {
const tbody = this.querySelector("tbody");
if (!tbody) return;
const rows = this.getRows();
const sortedRows = rows.sort((a, b) => {
const aCell = a.cells[columnIndex]?.textContent || "";
const bCell = b.cells[columnIndex]?.textContent || "";
// Try to parse as numbers first
const aNum = parseFloat(aCell);
const bNum = parseFloat(bCell);
if (!isNaN(aNum) && !isNaN(bNum)) {
return ascending ? aNum - bNum : bNum - aNum;
}
// Otherwise sort as strings
return ascending
? aCell.localeCompare(bCell)
: bCell.localeCompare(aCell);
});
// Reorder rows in DOM
sortedRows.forEach((row) => tbody.appendChild(row));
// Emit sort event
this.emit("ct-table-sort", {
columnIndex,
ascending,
});
}
}
globalThis.customElements.define("ct-table", CTTable);