import { css, html } from "lit";
import { property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { consume } from "@lit/context";
import { BaseElement } from "../../core/base-element.ts";
import {
applyThemeToElement,
type CTTheme,
defaultTheme,
themeContext,
} from "../theme-context.ts";
/**
* CTButton - Interactive button element with multiple variants and sizes
*
* @element ct-button
*
* @attr {string} variant - Visual style variant: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
* @attr {string} size - Button size: "default" | "sm" | "lg" | "icon"
* @attr {boolean} disabled - Whether the button is disabled
* @attr {string} type - Button type: "button" | "submit" | "reset"
*
* @slot - Default slot for button content
*
* @example
* console.log('Button clicked')}>Click Me
*/
export type ButtonVariant =
| "primary"
| "secondary"
| "destructive"
| "outline"
| "ghost"
| "link"
| "pill";
export type ButtonSize = "default" | "sm" | "lg" | "icon";
export class CTButton extends BaseElement {
static override styles = [
BaseElement.baseStyles,
css`
:host {
display: inline-block;
outline: none;
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
border-radius: var(
--ct-theme-border-radius,
var(--ct-border-radius-md, 0.375rem)
);
font-size: 0.875rem;
font-weight: 500;
font-family: var(--ct-theme-font-family, inherit);
line-height: 1.25rem;
transition: all var(--ct-theme-animation-duration, 0.2s) ease;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
outline: 2px solid transparent;
outline-offset: 2px;
background-color: transparent;
background-image: none;
text-transform: none;
-webkit-appearance: button;
text-decoration: none;
}
.button:focus-visible {
outline: 2px solid
var(--ct-theme-color-primary, var(--ct-color-primary, #3b82f6));
outline-offset: 2px;
}
.button:disabled {
pointer-events: none;
opacity: 0.5;
cursor: not-allowed;
}
/* Size variants */
.button.default {
height: 2.5rem;
padding: var(--ct-theme-spacing-normal, 0.5rem)
var(--ct-theme-spacing-loose, 1rem);
}
.button.sm {
height: 2.25rem;
padding: var(--ct-theme-spacing-tight, 0.25rem)
var(--ct-theme-spacing-normal, 0.75rem);
font-size: 0.75rem;
}
.button.lg {
height: 2.75rem;
padding: var(--ct-theme-spacing-normal, 0.5rem)
var(--ct-theme-spacing-loose, 2rem);
font-size: 1rem;
line-height: 1.5rem;
}
.button.icon {
height: 2.5rem;
width: 2.5rem;
padding: 0;
}
.button.md {
height: 2rem;
padding: var(--ct-theme-spacing-tight, 0.25rem)
var(--ct-theme-spacing-normal, 0.75rem);
font-size: 0.75rem;
}
/* Variant styles */
.button.primary {
background-color: var(
--ct-theme-color-primary,
var(--ct-color-primary, #3b82f6)
);
color: var(
--ct-theme-color-primary-foreground,
var(--ct-color-white, #ffffff)
);
border-color: var(
--ct-theme-color-primary,
var(--ct-color-primary, #3b82f6)
);
}
.button.primary:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}
.button.primary:active:not(:disabled) {
transform: translateY(0);
}
.button.destructive {
background-color: var(
--ct-theme-color-error,
var(--ct-color-red-600, #dc2626)
);
color: var(
--ct-theme-color-error-foreground,
var(--ct-color-white, #ffffff)
);
border-color: var(--ct-theme-color-error, var(--ct-color-red-600, #dc2626));
}
.button.destructive:hover:not(:disabled) {
opacity: 0.9;
}
.button.outline {
border-color: var(
--ct-theme-color-border,
var(--ct-color-gray-300, #d1d5db)
);
background-color: transparent;
color: var(--ct-theme-color-text, var(--ct-color-gray-900, #111827));
}
.button.outline:hover:not(:disabled) {
background-color: var(
--ct-theme-color-surface,
var(--ct-color-gray-50, #f9fafb)
);
}
.button.secondary {
background-color: var(
--ct-theme-color-secondary,
var(--ct-color-gray-100, #f3f4f6)
);
color: var(
--ct-theme-color-secondary-foreground,
var(--ct-color-gray-900, #111827)
);
border-color: var(
--ct-theme-color-secondary,
var(--ct-color-gray-100, #f3f4f6)
);
}
.button.secondary:hover:not(:disabled) {
background-color: var(
--ct-theme-color-surface-hover,
var(--ct-color-gray-200, #e5e7eb)
);
border-color: var(
--ct-theme-color-surface-hover,
var(--ct-color-gray-200, #e5e7eb)
);
}
.button.ghost {
color: var(
--ct-theme-color-text-muted,
var(--ct-color-gray-500, #6b7280)
);
background-color: transparent;
border: none;
padding: 0;
}
.button.ghost:hover:not(:disabled) {
color: var(--ct-theme-color-text, var(--ct-color-gray-700, #374151));
background-color: var(
--ct-theme-color-surface-hover,
var(--ct-color-gray-100, #f3f4f6)
);
}
.button.ghost.icon {
width: 1.5rem;
height: 1.5rem;
border-radius: var(
--ct-theme-border-radius,
var(--ct-border-radius-sm, 0.25rem)
);
}
.button.link {
color: var(--ct-theme-color-primary, var(--ct-color-primary, #3b82f6));
text-underline-offset: 4px;
}
.button.link:hover:not(:disabled) {
text-decoration: underline;
}
.button.pill {
background: var(
--ct-theme-color-surface,
var(--ct-color-gray-100, #f3f4f6)
);
color: var(--ct-theme-color-text, var(--ct-color-gray-900, #111827));
border: 1px solid
var(--ct-theme-color-border, var(--ct-color-gray-300, #d1d5db));
border-radius: var(
--ct-theme-border-radius-full,
var(--ct-radius-full, 9999px)
);
height: auto;
padding: 0.25rem 0.625rem;
font-size: 0.8125rem;
line-height: 1;
}
.button.pill:hover:not(:disabled) {
background: var(
--ct-theme-color-surface-hover,
var(--ct-color-gray-200, #e5e7eb)
);
}
`,
];
static override properties = {
variant: { type: String },
size: { type: String },
disabled: { type: Boolean, reflect: true },
type: { type: String },
theme: { type: Object, attribute: false },
};
declare variant: ButtonVariant;
declare size: ButtonSize;
declare disabled: boolean;
declare type: "button" | "submit" | "reset";
@consume({ context: themeContext, subscribe: true })
@property({ attribute: false })
declare theme?: CTTheme;
constructor() {
super();
this.variant = "primary";
this.size = "default";
this.disabled = false;
this.type = "button";
// Suppress click events on the host element when disabled.
// JSX attaches onClick handlers to the host element, but click events
// cross the shadow boundary and would fire even when disabled.
this.addEventListener(
"click",
(e) => {
if (this.disabled) {
e.stopImmediatePropagation();
}
},
{ capture: true },
);
}
override firstUpdated(
changedProperties: Map,
) {
super.firstUpdated(changedProperties);
this._updateThemeProperties();
}
override updated(
changedProperties: Map,
) {
super.updated(changedProperties);
if (changedProperties.has("theme")) {
this._updateThemeProperties();
}
}
private _updateThemeProperties() {
const currentTheme = this.theme || defaultTheme;
applyThemeToElement(this, currentTheme);
}
override render() {
const classes = {
button: true,
[this.variant]: true,
[this.size]: true,
};
return html`
`;
}
private _handleClick(e: Event) {
if (this.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}
// For non-button types, let the browser handle it
if (this.type !== "button") {
return;
}
}
}
globalThis.customElements.define("ct-button", CTButton);