Files
catalog/ts_web/elements/sio-button.ts

291 lines
7.7 KiB
TypeScript
Raw Normal View History

2025-07-14 14:54:54 +00:00
import {
DeesElement,
html,
property,
customElement,
cssManager,
css,
unsafeCSS,
type TemplateResult,
} from '@design.estate/dees-element';
// Import design tokens
import { colors, bdTheme } from './00colors.js';
import { spacing, radius, shadows, transitions } from './00tokens.js';
import { fontFamilies, typography } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
'sio-button': SioButton;
}
}
@customElement('sio-button')
export class SioButton extends DeesElement {
public static demo = () => html`
<div style="display: flex; gap: 16px; flex-wrap: wrap; align-items: center;">
<sio-button>Default</sio-button>
<sio-button type="primary">Primary</sio-button>
<sio-button type="destructive">Delete</sio-button>
<sio-button type="outline">Outline</sio-button>
<sio-button type="ghost">Ghost</sio-button>
<sio-button size="sm">Small</sio-button>
<sio-button size="lg">Large</sio-button>
<sio-button disabled>Disabled</sio-button>
</div>
`;
@property({ type: String })
public accessor text: string = '';
2025-07-14 14:54:54 +00:00
@property({ type: String })
public accessor type: 'default' | 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost' = 'default';
2025-07-14 14:54:54 +00:00
@property({ type: String })
public accessor size: 'sm' | 'default' | 'lg' = 'default';
2025-07-14 14:54:54 +00:00
@property({ type: Boolean, reflect: true })
public accessor disabled: boolean = false;
2025-07-14 14:54:54 +00:00
@property({ type: String })
public accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
2025-07-14 14:54:54 +00:00
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-block;
font-family: ${unsafeCSS(fontFamilies.sans)};
}
:host([disabled]) {
pointer-events: none;
}
.button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
border-radius: 6px;
2025-07-14 14:54:54 +00:00
font-weight: 500;
font-size: 14px;
line-height: 1;
letter-spacing: -0.01em;
transition: all 120ms ease;
2025-07-14 14:54:54 +00:00
cursor: pointer;
user-select: none;
outline: none;
border: none;
gap: 6px;
2025-07-14 14:54:54 +00:00
}
/* Size variants */
.button.size-sm {
height: 32px;
padding: 0 12px;
2025-07-14 14:54:54 +00:00
font-size: 13px;
}
.button.size-default {
height: 36px;
padding: 0 16px;
2025-07-14 14:54:54 +00:00
font-size: 14px;
}
.button.size-lg {
height: 42px;
padding: 0 24px;
font-size: 15px;
2025-07-14 14:54:54 +00:00
}
/* Type variants */
.button.default {
background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
2025-07-14 14:54:54 +00:00
}
.button.default:hover:not(.disabled) {
background: ${bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 20%)')};
2025-07-14 14:54:54 +00:00
}
.button.default:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 87%)', 'hsl(0 0% 18%)')};
2025-07-14 14:54:54 +00:00
}
.button.primary {
background: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
color: ${bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
2025-07-14 14:54:54 +00:00
}
.button.primary:hover:not(.disabled) {
background: ${bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 100%)')};
2025-07-14 14:54:54 +00:00
}
.button.primary:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
}
/* Secondary variant */
.button.secondary {
background: transparent;
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
box-shadow: inset 0 0 0 1px ${bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 25%)')};
2025-07-14 14:54:54 +00:00
}
.button.secondary:hover:not(.disabled) {
background: ${bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 15%)')};
}
.button.secondary:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 12%)')};
}
/* Destructive variant */
2025-07-14 14:54:54 +00:00
.button.destructive {
background: ${bdTheme('hsl(0 100% 95%)', 'hsl(0 50% 20%)')};
color: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 75%)')};
2025-07-14 14:54:54 +00:00
}
.button.destructive:hover:not(.disabled) {
background: ${bdTheme('hsl(0 100% 45%)', 'hsl(0 100% 50%)')};
color: white;
2025-07-14 14:54:54 +00:00
}
.button.destructive:active:not(.disabled) {
background: ${bdTheme('hsl(0 100% 40%)', 'hsl(0 100% 45%)')};
color: white;
2025-07-14 14:54:54 +00:00
}
.button.outline {
background: transparent;
color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 30%)')};
2025-07-14 14:54:54 +00:00
}
.button.outline:hover:not(.disabled) {
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
box-shadow: inset 0 0 0 1.5px ${bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 50%)')};
2025-07-14 14:54:54 +00:00
}
.button.outline:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
2025-07-14 14:54:54 +00:00
}
.button.ghost {
background: transparent;
color: ${bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
2025-07-14 14:54:54 +00:00
}
.button.ghost:hover:not(.disabled) {
color: ${bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 95%)')};
background: ${bdTheme('hsl(0 0% 0% / 0.05)', 'hsl(0 0% 100% / 0.05)')};
2025-07-14 14:54:54 +00:00
}
.button.ghost:active:not(.disabled) {
background: ${bdTheme('hsl(0 0% 0% / 0.1)', 'hsl(0 0% 100% / 0.1)')};
2025-07-14 14:54:54 +00:00
}
/* Status states */
.button.pending {
pointer-events: none;
opacity: 0.7;
}
.spinner {
position: absolute;
2025-07-14 15:07:39 +00:00
left: ${unsafeCSS(spacing["3"])};
2025-07-14 14:54:54 +00:00
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.button.success {
background: ${bdTheme('success')};
color: ${bdTheme('successForeground')};
pointer-events: none;
}
.button.error {
background: ${bdTheme('destructive')};
color: ${bdTheme('destructiveForeground')};
pointer-events: none;
}
/* Disabled state */
.button.disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
2025-07-14 14:54:54 +00:00
}
/* Focus state */
.button:focus-visible {
outline: 2px solid ${bdTheme('hsl(0 0% 15% / 0.2)', 'hsl(0 0% 95% / 0.2)')};
2025-07-14 14:54:54 +00:00
outline-offset: 2px;
}
/* Icon sizing within buttons */
.button sio-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.button.size-sm sio-icon {
width: 14px;
height: 14px;
}
.button.size-lg sio-icon {
width: 18px;
height: 18px;
}
2025-07-14 14:54:54 +00:00
`,
];
public render(): TemplateResult {
const buttonClasses = [
'button',
this.type === 'primary' ? 'primary' : this.type,
`size-${this.size}`,
this.disabled ? 'disabled' : '',
this.status,
].filter(Boolean).join(' ');
return html`
<button
class="${buttonClasses}"
?disabled=${this.disabled}
@click=${this.handleClick}
>
${this.status === 'pending' ? html`
<sio-icon class="spinner" icon="loader" size="16"></sio-icon>
` : ''}
${this.status === 'success' ? html`
<sio-icon icon="check" size="16"></sio-icon>
` : ''}
${this.status === 'error' ? html`
<sio-icon icon="x" size="16"></sio-icon>
` : ''}
<slot>${this.text}</slot>
</button>
`;
}
private handleClick(event: MouseEvent) {
if (this.disabled || this.status !== 'normal') {
event.preventDefault();
event.stopPropagation();
return;
}
2025-07-14 17:26:57 +00:00
// Let the native click bubble normally
// Don't dispatch a custom event to avoid double-triggering
2025-07-14 14:54:54 +00:00
}
}