This commit is contained in:
2025-12-22 10:53:15 +00:00
commit 753a98c67b
63 changed files with 15976 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
import { html } from '@design.estate/dees-element';
import { injectCssVariables } from '../../00variables.js';
export const demoFunc = () => {
injectCssVariables();
return html`
<style>
.demo-section {
margin-bottom: 2rem;
}
.demo-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.demo-note {
font-size: 0.875rem;
color: var(--dees-muted-foreground);
margin-top: 0.5rem;
}
</style>
<div class="demo-section">
<h3>Action Sheet</h3>
<dees-mobile-button
@click=${(e: Event) => {
const container = (e.target as HTMLElement).parentElement;
const existing = container?.querySelector('dees-mobile-actionsheet');
if (existing) existing.remove();
const sheet = document.createElement('dees-mobile-actionsheet');
(sheet as any).title = 'Add Photo';
(sheet as any).options = [
{
id: 'camera',
icon: 'camera',
iconColor: 'var(--dees-primary)',
iconBackground: 'rgba(59, 130, 246, 0.1)',
title: 'Take Photo',
subtitle: 'Use camera to capture a new photo'
},
{
id: 'gallery',
icon: 'image',
iconColor: '#16a34a',
iconBackground: '#dcfce7',
title: 'Choose from Gallery',
subtitle: 'Select an existing photo'
}
];
sheet.addEventListener('close', () => sheet.remove());
sheet.addEventListener('select', (ev: any) => {
console.log('Selected:', ev.detail);
sheet.remove();
});
document.body.appendChild(sheet);
}}
>Show Photo Options</dees-mobile-button>
<p class="demo-note">Opens an iOS-style action sheet from the bottom of the screen.</p>
</div>
<div class="demo-section">
<h3>Share Options</h3>
<dees-mobile-button
variant="outline"
@click=${(e: Event) => {
const sheet = document.createElement('dees-mobile-actionsheet');
(sheet as any).title = 'Share';
(sheet as any).options = [
{
id: 'copy',
icon: 'copy',
title: 'Copy Link'
},
{
id: 'email',
icon: 'mail',
title: 'Send via Email'
},
{
id: 'message',
icon: 'message-circle',
title: 'Send Message'
}
];
sheet.addEventListener('close', () => sheet.remove());
sheet.addEventListener('select', (ev: any) => {
console.log('Share via:', ev.detail);
sheet.remove();
});
document.body.appendChild(sheet);
}}
>Share Options</dees-mobile-button>
</div>
`;
};

View File

@@ -0,0 +1,223 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import '../dees-mobile-icon/dees-mobile-icon.js';
import { demoFunc } from './dees-mobile-actionsheet.demo.js';
export interface IActionSheetOption {
id: string;
icon?: string;
iconColor?: string;
iconBackground?: string;
title: string;
subtitle?: string;
}
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-actionsheet': DeesMobileActionsheet;
}
}
@customElement('dees-mobile-actionsheet')
export class DeesMobileActionsheet extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor title: string = '';
@property({ type: Array })
accessor options: IActionSheetOption[] = [];
@property({ type: String })
accessor cancelText: string = 'Cancel';
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
:host {
position: fixed;
inset: 0;
z-index: var(--dees-z-modal, 500);
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
animation: fadeInBackdrop 0.2s ease-out forwards;
}
@keyframes fadeInBackdrop {
to {
background: rgba(0, 0, 0, 0.5);
}
}
.sheet {
position: relative;
background: var(--dees-card);
border-radius: var(--dees-radius-lg) var(--dees-radius-lg) 0 0;
padding: var(--dees-space-md);
padding-bottom: calc(var(--dees-space-md) + env(safe-area-inset-bottom, 0px));
transform: translateY(100%);
animation: slideUp 0.3s ease-out forwards;
}
@keyframes slideUp {
to {
transform: translateY(0);
}
}
.sheet-title {
text-align: center;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
margin-bottom: var(--dees-space-md);
padding-bottom: var(--dees-space-sm);
border-bottom: 1px solid var(--dees-border);
}
.options {
display: flex;
flex-direction: column;
gap: var(--dees-space-xs);
}
.option {
display: flex;
align-items: center;
gap: var(--dees-space-md);
padding: var(--dees-space-md);
border: none;
background: var(--dees-background);
border-radius: var(--dees-radius-md);
cursor: pointer;
color: var(--dees-foreground);
font-size: 1rem;
font-weight: 500;
text-align: left;
transition: background var(--dees-transition-fast);
}
.option:hover {
background: var(--dees-muted);
}
.option:active {
background: var(--dees-accent);
}
.option-icon {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--dees-radius-full);
flex-shrink: 0;
background: var(--dees-accent);
color: var(--dees-primary);
}
.option-text {
flex: 1;
}
.option-title {
font-weight: 500;
}
.option-subtitle {
font-size: 0.75rem;
color: var(--dees-muted-foreground);
font-weight: normal;
margin-top: 2px;
}
.cancel {
margin-top: var(--dees-space-sm);
padding: var(--dees-space-md);
border: none;
background: var(--dees-background);
border-radius: var(--dees-radius-md);
cursor: pointer;
color: var(--dees-danger);
font-size: 1rem;
font-weight: 500;
width: 100%;
transition: background var(--dees-transition-fast);
}
.cancel:hover {
background: rgba(220, 38, 38, 0.1);
}
`,
];
private handleSelect(option: IActionSheetOption) {
this.dispatchEvent(new CustomEvent('select', {
detail: option,
bubbles: true,
composed: true,
}));
}
private handleClose() {
this.dispatchEvent(new CustomEvent('close', {
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
return html`
<div class="backdrop" @click=${this.handleClose}></div>
<div class="sheet">
${this.title ? html`<div class="sheet-title">${this.title}</div>` : ''}
<div class="options">
${this.options.map(option => html`
<button class="option" @click=${() => this.handleSelect(option)}>
${option.icon ? html`
<div
class="option-icon"
style=${option.iconBackground ? `background: ${option.iconBackground}` : ''}
>
<dees-mobile-icon
icon=${option.icon}
size="24"
color=${option.iconColor || 'currentColor'}
></dees-mobile-icon>
</div>
` : ''}
<div class="option-text">
<div class="option-title">${option.title}</div>
${option.subtitle ? html`
<div class="option-subtitle">${option.subtitle}</div>
` : ''}
</div>
</button>
`)}
</div>
<button class="cancel" @click=${this.handleClose}>
${this.cancelText}
</button>
</div>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-actionsheet.js';

View File

@@ -0,0 +1,72 @@
import { html } from '@design.estate/dees-element';
import { injectCssVariables } from '../../00variables.js';
export const demoFunc = () => {
injectCssVariables();
return html`
<style>
.demo-section {
margin-bottom: 2rem;
}
.demo-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
</style>
<div class="demo-section">
<h3>Variants</h3>
<div class="demo-row">
<dees-mobile-button variant="default">Default</dees-mobile-button>
<dees-mobile-button variant="primary">Primary</dees-mobile-button>
<dees-mobile-button variant="secondary">Secondary</dees-mobile-button>
<dees-mobile-button variant="outline">Outline</dees-mobile-button>
<dees-mobile-button variant="ghost">Ghost</dees-mobile-button>
<dees-mobile-button variant="destructive">Destructive</dees-mobile-button>
<dees-mobile-button variant="link">Link</dees-mobile-button>
</div>
</div>
<div class="demo-section">
<h3>Sizes</h3>
<div class="demo-row">
<dees-mobile-button size="sm">Small</dees-mobile-button>
<dees-mobile-button size="md">Medium</dees-mobile-button>
<dees-mobile-button size="lg">Large</dees-mobile-button>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<div class="demo-row">
<dees-mobile-button>Normal</dees-mobile-button>
<dees-mobile-button disabled>Disabled</dees-mobile-button>
<dees-mobile-button loading>Loading</dees-mobile-button>
</div>
</div>
<div class="demo-section">
<h3>Icon Buttons</h3>
<div class="demo-row">
<dees-mobile-button icon size="sm">
<dees-mobile-icon icon="plus" size="16"></dees-mobile-icon>
</dees-mobile-button>
<dees-mobile-button icon>
<dees-mobile-icon icon="settings" size="18"></dees-mobile-icon>
</dees-mobile-button>
<dees-mobile-button icon size="lg">
<dees-mobile-icon icon="menu" size="20"></dees-mobile-icon>
</dees-mobile-button>
</div>
</div>
`;
};

View File

@@ -0,0 +1,224 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import { demoFunc } from './dees-mobile-button.demo.js';
export type ButtonVariant = 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link';
export type ButtonSize = 'sm' | 'md' | 'lg';
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-button': DeesMobileButton;
}
}
@customElement('dees-mobile-button')
export class DeesMobileButton extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor variant: ButtonVariant = 'default';
@property({ type: String })
accessor size: ButtonSize = 'md';
@property({ type: Boolean })
accessor disabled: boolean = false;
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: Boolean, reflect: true })
accessor icon: boolean = false;
@property({ type: String })
accessor type: 'button' | 'submit' | 'reset' = 'button';
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
:host {
display: inline-block;
}
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-family: inherit;
font-weight: 500;
border: none;
border-radius: calc(var(--dees-radius, 0.5rem) - 2px);
cursor: pointer;
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
position: relative;
white-space: nowrap;
text-decoration: none;
}
button:focus-visible {
outline: 2px solid var(--dees-primary);
outline-offset: 2px;
}
button:active:not(:disabled) {
transform: scale(0.98);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Sizes */
button.sm {
height: 2rem;
padding: 0 0.75rem;
font-size: 0.75rem;
border-radius: calc(var(--dees-radius, 0.5rem) - 4px);
}
button.md {
height: 2.25rem;
padding: 0 1rem;
font-size: 0.875rem;
}
button.lg {
height: 2.75rem;
padding: 0 2rem;
font-size: 0.875rem;
}
/* Variants - using bdTheme for bright/dark support */
button.default,
button.primary {
background: #3b82f6;
color: #ffffff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
button.default:not(:disabled):hover,
button.primary:not(:disabled):hover {
background: #2563eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
button.secondary {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
button.secondary:not(:disabled):hover {
background: ${cssManager.bdTheme('#e4e4e7', '#3f3f46')};
}
button.outline {
background: transparent;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#3f3f46')};
}
button.outline:not(:disabled):hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-color: ${cssManager.bdTheme('#d4d4d8', '#52525b')};
}
button.ghost {
background-color: transparent;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
button.ghost:not(:disabled):hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
button.destructive {
background: #dc2626;
color: #ffffff;
}
button.destructive:not(:disabled):hover {
background: #b91c1c;
}
button.link {
background: transparent;
color: #3b82f6;
text-decoration: underline;
text-underline-offset: 4px;
padding: 0;
height: auto;
}
button.link:not(:disabled):hover {
text-decoration: underline;
}
/* Loading state */
.spinner {
width: 1em;
height: 1em;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Icon-only button */
:host([icon]) button {
width: 2.25rem;
height: 2.25rem;
padding: 0;
}
:host([icon]) button.sm {
width: 2rem;
height: 2rem;
}
:host([icon]) button.lg {
width: 2.75rem;
height: 2.75rem;
}
`,
];
private handleClick(e: MouseEvent) {
if (this.disabled || this.loading) {
e.preventDefault();
e.stopPropagation();
return;
}
}
public render(): TemplateResult {
return html`
<button
type=${this.type}
class=${`${this.variant} ${this.size}`}
?disabled=${this.disabled || this.loading}
@click=${this.handleClick}
>
${this.loading ? html`<span class="spinner"></span>` : ''}
<slot></slot>
</button>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-button.js';

View File

@@ -0,0 +1,68 @@
import { html } from '@design.estate/dees-element';
import { injectCssVariables } from '../../00variables.js';
export const demoFunc = () => {
injectCssVariables();
return html`
<style>
.demo-section {
margin-bottom: 2rem;
}
.demo-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.header-container {
border: 1px solid var(--dees-border);
border-radius: var(--dees-radius);
overflow: hidden;
margin-bottom: 1rem;
}
</style>
<div class="demo-section">
<h3>Basic Header</h3>
<div class="header-container">
<dees-mobile-header title="Page Title"></dees-mobile-header>
</div>
</div>
<div class="demo-section">
<h3>Header with Subtitle</h3>
<div class="header-container">
<dees-mobile-header title="Settings" subtitle="Manage your preferences"></dees-mobile-header>
</div>
</div>
<div class="demo-section">
<h3>Header with Back Action</h3>
<div class="header-container">
<dees-mobile-header title="Item Details">
<dees-mobile-button slot="left-action" icon variant="ghost">
<dees-mobile-icon icon="arrow-left" size="20"></dees-mobile-icon>
</dees-mobile-button>
</dees-mobile-header>
</div>
</div>
<div class="demo-section">
<h3>Header with Actions</h3>
<div class="header-container">
<dees-mobile-header title="Shopping List">
<dees-mobile-button slot="left-action" icon variant="ghost">
<dees-mobile-icon icon="menu" size="20"></dees-mobile-icon>
</dees-mobile-button>
<dees-mobile-button slot="actions" icon variant="ghost">
<dees-mobile-icon icon="search" size="20"></dees-mobile-icon>
</dees-mobile-button>
<dees-mobile-button slot="actions" icon variant="ghost">
<dees-mobile-icon icon="plus" size="20"></dees-mobile-icon>
</dees-mobile-button>
</dees-mobile-header>
</div>
</div>
`;
};

View File

@@ -0,0 +1,171 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import { demoFunc } from './dees-mobile-header.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-header': DeesMobileHeader;
}
}
@customElement('dees-mobile-header')
export class DeesMobileHeader extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor title: string = '';
@property({ type: String })
accessor subtitle: string = '';
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
position: relative;
}
.header {
/* Mobile-first defaults */
height: 4rem;
padding: 0 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
max-width: 768px;
margin: 0 auto;
box-sizing: border-box;
}
/* Desktop enhancements */
@media (min-width: 641px) {
.header {
height: 5rem;
padding: 0 1.25rem;
gap: 1rem;
}
}
.left-action {
flex-shrink: 0;
margin-left: -0.5rem;
}
.left-action:empty {
display: none;
}
.left-action ::slotted(*) {
width: 2.5rem;
height: 2.5rem;
}
.content {
flex: 1;
min-width: 0;
}
.middle {
flex-shrink: 0;
display: flex;
align-items: center;
}
.middle:empty {
display: none;
}
h1 {
/* Mobile-first defaults */
font-size: 1rem;
font-weight: 600;
margin: 0;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
/* Desktop enhancements */
@media (min-width: 641px) {
h1 {
font-size: 1.125rem;
}
}
.subtitle {
/* Mobile-first defaults */
font-size: 0.8125rem;
margin: 0.125rem 0 0;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
line-height: 1.3;
}
/* Desktop enhancements */
@media (min-width: 641px) {
.subtitle {
font-size: 0.875rem;
margin: 0.25rem 0 0;
}
}
.actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
::slotted([slot="actions"]) {
width: 2.5rem;
height: 2.5rem;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
display: block;
}
::slotted([slot="actions"]:hover) {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 4px;
}
`,
];
public render(): TemplateResult {
return html`
<header class="header">
<div class="left-action">
<slot name="left-action"></slot>
</div>
<div class="content">
<slot name="content">
<h1>${this.title}</h1>
${this.subtitle ? html`<div class="subtitle">${this.subtitle}</div>` : ''}
</slot>
</div>
<div class="middle">
<slot name="middle"></slot>
</div>
<div class="actions">
<slot name="actions"></slot>
</div>
</header>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-header.js';

View File

@@ -0,0 +1,100 @@
import { html } from '@design.estate/dees-element';
import { injectCssVariables } from '../../00variables.js';
export const demoFunc = () => {
injectCssVariables();
return html`
<style>
.demo-section {
margin-bottom: 2rem;
}
.demo-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
.icon-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
border-radius: var(--dees-radius);
background: var(--dees-surface);
min-width: 80px;
}
.icon-label {
font-size: 0.75rem;
color: var(--dees-muted-foreground);
}
</style>
<div class="demo-section">
<h3>Common Icons</h3>
<div class="demo-row">
<div class="icon-item">
<dees-mobile-icon icon="home" size="24"></dees-mobile-icon>
<span class="icon-label">home</span>
</div>
<div class="icon-item">
<dees-mobile-icon icon="settings" size="24"></dees-mobile-icon>
<span class="icon-label">settings</span>
</div>
<div class="icon-item">
<dees-mobile-icon icon="user" size="24"></dees-mobile-icon>
<span class="icon-label">user</span>
</div>
<div class="icon-item">
<dees-mobile-icon icon="search" size="24"></dees-mobile-icon>
<span class="icon-label">search</span>
</div>
<div class="icon-item">
<dees-mobile-icon icon="menu" size="24"></dees-mobile-icon>
<span class="icon-label">menu</span>
</div>
<div class="icon-item">
<dees-mobile-icon icon="x" size="24"></dees-mobile-icon>
<span class="icon-label">x</span>
</div>
</div>
</div>
<div class="demo-section">
<h3>Sizes</h3>
<div class="demo-row">
<dees-mobile-icon icon="star" size="16"></dees-mobile-icon>
<dees-mobile-icon icon="star" size="20"></dees-mobile-icon>
<dees-mobile-icon icon="star" size="24"></dees-mobile-icon>
<dees-mobile-icon icon="star" size="32"></dees-mobile-icon>
<dees-mobile-icon icon="star" size="48"></dees-mobile-icon>
</div>
</div>
<div class="demo-section">
<h3>Colors</h3>
<div class="demo-row">
<dees-mobile-icon icon="heart" size="24" color="var(--dees-danger)"></dees-mobile-icon>
<dees-mobile-icon icon="check-circle" size="24" color="var(--dees-success)"></dees-mobile-icon>
<dees-mobile-icon icon="alert-triangle" size="24" color="var(--dees-warning)"></dees-mobile-icon>
<dees-mobile-icon icon="info" size="24" color="var(--dees-primary)"></dees-mobile-icon>
</div>
</div>
<div class="demo-section">
<h3>Stroke Width</h3>
<div class="demo-row">
<dees-mobile-icon icon="circle" size="32" strokeWidth="1"></dees-mobile-icon>
<dees-mobile-icon icon="circle" size="32" strokeWidth="2"></dees-mobile-icon>
<dees-mobile-icon icon="circle" size="32" strokeWidth="3"></dees-mobile-icon>
</div>
</div>
`;
};

View File

@@ -0,0 +1,189 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import * as lucideIcons from 'lucide';
import { createElement } from 'lucide';
import { demoFunc } from './dees-mobile-icon.demo.js';
// Create a type-safe icon name type
export type LucideIconName = keyof typeof lucideIcons;
// Cache for rendered icons to improve performance
const iconCache = new Map<string, string>();
const MAX_CACHE_SIZE = 500;
function limitCacheSize() {
if (iconCache.size > MAX_CACHE_SIZE) {
const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
keysToDelete.forEach(key => iconCache.delete(key));
}
}
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-icon': DeesMobileIcon;
}
}
@customElement('dees-mobile-icon')
export class DeesMobileIcon extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor icon: string = '';
@property({ type: Number })
accessor size: number = 20;
@property({ type: String })
accessor color: string = 'currentColor';
@property({ type: Number })
accessor strokeWidth: number = 2;
private lastIcon: string | null = null;
private lastSize: number | null = null;
private lastColor: string | null = null;
private lastStrokeWidth: number | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
vertical-align: middle;
}
#iconContainer {
display: flex;
align-items: center;
justify-content: center;
}
#iconContainer svg {
display: block;
height: 100%;
width: 100%;
}
`,
];
private renderLucideIcon(iconName: string): string {
const cacheKey = `${iconName}:${this.size}:${this.color}:${this.strokeWidth}`;
if (iconCache.has(cacheKey)) {
return iconCache.get(cacheKey) || '';
}
try {
// Convert kebab-case to PascalCase (e.g., "chevron-down" -> "ChevronDown")
const pascalCaseName = iconName
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
if (!(lucideIcons as any)[pascalCaseName]) {
console.warn(`Lucide icon '${pascalCaseName}' not found`);
return '';
}
const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
color: this.color,
size: this.size,
strokeWidth: this.strokeWidth
});
if (!svgElement) {
console.warn(`createElement returned empty result for ${pascalCaseName}`);
return '';
}
const result = svgElement.outerHTML;
iconCache.set(cacheKey, result);
limitCacheSize();
return result;
} catch (error) {
console.error(`Error rendering Lucide icon ${iconName}:`, error);
return '';
}
}
public render(): TemplateResult {
return html`
<style>
#iconContainer {
width: ${this.size}px;
height: ${this.size}px;
}
</style>
<div id="iconContainer"></div>
`;
}
updated() {
// Check if we actually need to update the icon
if (this.lastIcon === this.icon &&
this.lastSize === this.size &&
this.lastColor === this.color &&
this.lastStrokeWidth === this.strokeWidth) {
return;
}
this.lastIcon = this.icon || null;
this.lastSize = this.size;
this.lastColor = this.color;
this.lastStrokeWidth = this.strokeWidth;
const container = this.shadowRoot?.querySelector('#iconContainer');
if (!container || !this.icon) return;
container.innerHTML = '';
try {
const pascalCaseName = this.icon
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
if ((lucideIcons as any)[pascalCaseName]) {
const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
color: this.color,
size: this.size,
strokeWidth: this.strokeWidth
});
if (svgElement) {
container.appendChild(svgElement);
return;
}
}
// Fall back to string-based approach
const iconHtml = this.renderLucideIcon(this.icon);
if (iconHtml) {
container.innerHTML = iconHtml;
}
} catch (error) {
console.error(`Error updating icon ${this.icon}:`, error);
}
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.lastIcon = null;
this.lastSize = null;
this.lastColor = null;
this.lastStrokeWidth = null;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-icon.js';

View File

@@ -0,0 +1,74 @@
import { html } from '@design.estate/dees-element';
import { injectCssVariables } from '../../00variables.js';
export const demoFunc = () => {
injectCssVariables();
return html`
<style>
.demo-section {
margin-bottom: 2rem;
}
.demo-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.modal-content {
padding: 1rem 0;
}
</style>
<div class="demo-section">
<h3>Modal (click button to open)</h3>
<dees-mobile-button
@click=${(e: Event) => {
const modal = (e.target as HTMLElement).parentElement?.querySelector('dees-mobile-modal');
if (modal) (modal as any).open = true;
}}
>Open Modal</dees-mobile-button>
<dees-mobile-modal
title="Confirm Action"
@close=${(e: Event) => {
(e.target as any).open = false;
}}
>
<div class="modal-content">
<p>Are you sure you want to proceed with this action?</p>
</div>
<div slot="footer">
<dees-mobile-button variant="ghost">Cancel</dees-mobile-button>
<dees-mobile-button variant="primary">Confirm</dees-mobile-button>
</div>
</dees-mobile-modal>
</div>
<div class="demo-section">
<h3>Modal without Close Button</h3>
<dees-mobile-button
variant="outline"
@click=${(e: Event) => {
const modal = (e.target as HTMLElement).parentElement?.querySelector('dees-mobile-modal');
if (modal) (modal as any).open = true;
}}
>Open Required Modal</dees-mobile-button>
<dees-mobile-modal
title="Terms & Conditions"
.showCloseButton=${false}
@close=${(e: Event) => {
(e.target as any).open = false;
}}
>
<div class="modal-content">
<p>You must accept the terms to continue.</p>
</div>
<div slot="footer">
<dees-mobile-button variant="primary">I Accept</dees-mobile-button>
</div>
</dees-mobile-modal>
</div>
`;
};

View File

@@ -0,0 +1,202 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import '../dees-mobile-icon/dees-mobile-icon.js';
import { demoFunc } from './dees-mobile-modal.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-modal': DeesMobileModal;
}
}
@customElement('dees-mobile-modal')
export class DeesMobileModal extends DeesElement {
public static demo = demoFunc;
@property({ type: Boolean })
accessor open: boolean = false;
@property({ type: String })
accessor title: string = '';
@property({ type: Boolean })
accessor showCloseButton: boolean = true;
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--dees-z-modal, 500);
padding: 1rem;
animation: fadeIn 200ms ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border-radius: 0.75rem;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
/* Mobile-first defaults */
max-width: 100%;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: slideUp 200ms ease-out;
}
/* Desktop enhancements */
@media (min-width: 641px) {
.modal {
max-width: 500px;
}
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
/* Mobile-first defaults */
padding: 1rem;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
/* Desktop enhancements */
@media (min-width: 641px) {
.modal-header {
padding: 1.5rem;
}
}
.modal-title {
font-size: 1.125rem;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin: 0;
}
.close-button {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: none;
background: transparent;
border-radius: 0.25rem;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 150ms ease;
}
.close-button:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.modal-content {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 1rem;
}
@media (min-width: 641px) {
.modal-content {
padding: 1.5rem;
}
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem;
border-top: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
@media (min-width: 641px) {
.modal-footer {
padding: 1.5rem;
}
}
.modal-footer:empty {
display: none;
}
`,
];
private handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
this.handleClose();
}
}
private handleClose() {
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
}
public render(): TemplateResult {
if (!this.open) return html``;
return html`
<div class="modal-backdrop" @click=${this.handleBackdropClick}>
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">${this.title}</h2>
${this.showCloseButton ? html`
<button class="close-button" @click=${this.handleClose} aria-label="Close">
<dees-mobile-icon icon="x" size="20"></dees-mobile-icon>
</button>
` : ''}
</div>
<div class="modal-content">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer"></slot>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-modal.js';

View File

@@ -0,0 +1,103 @@
import { html } from '@design.estate/dees-element';
import { injectCssVariables } from '../../00variables.js';
export const demoFunc = () => {
injectCssVariables();
return html`
<style>
.demo-section {
margin-bottom: 2rem;
}
.demo-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: var(--dees-muted-foreground);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.demo-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
</style>
<div class="demo-section">
<h3>Toast Types</h3>
<div class="demo-row">
<dees-mobile-button
variant="outline"
@click=${() => {
const toast = document.createElement('dees-mobile-toast');
(toast as any).type = 'success';
(toast as any).message = 'Item saved successfully!';
toast.addEventListener('close', () => toast.remove());
document.body.appendChild(toast);
}}
>Success Toast</dees-mobile-button>
<dees-mobile-button
variant="outline"
@click=${() => {
const toast = document.createElement('dees-mobile-toast');
(toast as any).type = 'error';
(toast as any).message = 'Failed to save item. Please try again.';
toast.addEventListener('close', () => toast.remove());
document.body.appendChild(toast);
}}
>Error Toast</dees-mobile-button>
<dees-mobile-button
variant="outline"
@click=${() => {
const toast = document.createElement('dees-mobile-toast');
(toast as any).type = 'warning';
(toast as any).message = 'Your session will expire in 5 minutes.';
toast.addEventListener('close', () => toast.remove());
document.body.appendChild(toast);
}}
>Warning Toast</dees-mobile-button>
<dees-mobile-button
variant="outline"
@click=${() => {
const toast = document.createElement('dees-mobile-toast');
(toast as any).type = 'info';
(toast as any).message = 'New updates are available.';
toast.addEventListener('close', () => toast.remove());
document.body.appendChild(toast);
}}
>Info Toast</dees-mobile-button>
</div>
</div>
<div class="demo-section">
<h3>Custom Duration</h3>
<div class="demo-row">
<dees-mobile-button
variant="secondary"
@click=${() => {
const toast = document.createElement('dees-mobile-toast');
(toast as any).type = 'info';
(toast as any).message = 'This toast stays for 10 seconds.';
(toast as any).duration = 10000;
toast.addEventListener('close', () => toast.remove());
document.body.appendChild(toast);
}}
>Long Duration (10s)</dees-mobile-button>
<dees-mobile-button
variant="secondary"
@click=${() => {
const toast = document.createElement('dees-mobile-toast');
(toast as any).type = 'success';
(toast as any).message = 'Quick notification!';
(toast as any).duration = 1500;
toast.addEventListener('close', () => toast.remove());
document.body.appendChild(toast);
}}
>Short Duration (1.5s)</dees-mobile-button>
</div>
</div>
`;
};

View File

@@ -0,0 +1,339 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { mobileComponentStyles } from '../../00componentstyles.js';
import '../dees-mobile-icon/dees-mobile-icon.js';
import { demoFunc } from './dees-mobile-toast.demo.js';
export type ToastType = 'success' | 'error' | 'info' | 'warning';
declare global {
interface HTMLElementTagNameMap {
'dees-mobile-toast': DeesMobileToast;
}
}
@customElement('dees-mobile-toast')
export class DeesMobileToast extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor message: string = '';
@property({ type: String })
accessor type: ToastType = 'info';
@property({ type: Number })
accessor duration: number = 0; // 0 means use default
private timeoutId?: number;
public static styles = [
cssManager.defaultStyles,
mobileComponentStyles,
css`
:host {
display: block;
position: fixed;
/* Mobile-first defaults */
bottom: 1rem;
left: 1rem;
right: 1rem;
transform: none;
z-index: var(--dees-z-notification, 900);
animation: slideUp 200ms var(--dees-spring);
}
/* Desktop enhancements */
@media (min-width: 641px) {
:host {
bottom: 2rem;
left: 50%;
right: auto;
transform: translateX(-50%);
}
}
/* Mobile-first animations */
@keyframes slideUp {
from {
transform: translateY(100%) scale(0.95);
opacity: 0;
}
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translateY(0) scale(1);
opacity: 1;
}
to {
transform: translateY(100%) scale(0.95);
opacity: 0;
}
}
/* Desktop-specific animations that include X translation */
@media (min-width: 641px) {
@keyframes slideUp {
from {
transform: translate(-50%, 100%) scale(0.95);
opacity: 0;
}
to {
transform: translate(-50%, 0) scale(1);
opacity: 1;
}
}
@keyframes slideDown {
from {
transform: translate(-50%, 0) scale(1);
opacity: 1;
}
to {
transform: translate(-50%, 100%) scale(0.95);
opacity: 0;
}
}
}
:host(.closing) {
animation: slideDown 200ms var(--dees-ease-in);
}
.toast {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-radius: var(--dees-radius);
box-shadow: var(--dees-shadow-lg);
/* Mobile-first defaults */
width: 100%;
min-width: auto;
max-width: none;
position: relative;
overflow: hidden;
}
/* Desktop enhancements */
@media (min-width: 641px) {
.toast {
width: auto;
min-width: 300px;
max-width: 500px;
}
}
/* Type-specific styles */
.toast.success {
background: var(--dees-card);
color: var(--dees-foreground);
border: 1px solid var(--dees-border);
}
.toast.error {
background: var(--dees-danger);
color: white;
border: 1px solid var(--dees-danger);
}
.toast.warning {
background: var(--dees-warning);
color: white;
border: 1px solid var(--dees-warning);
}
.toast.info {
background: var(--dees-primary);
color: white;
border: 1px solid var(--dees-primary);
}
.icon {
flex-shrink: 0;
width: 1.25rem;
height: 1.25rem;
position: relative;
z-index: 1;
}
.icon.success {
color: var(--dees-success);
}
.icon.error,
.icon.warning,
.icon.info {
color: currentColor;
}
.message {
flex: 1;
font-size: 0.875rem;
font-weight: 500;
position: relative;
z-index: 1;
}
.close {
flex-shrink: 0;
width: 2rem;
height: 2rem;
padding: 0.375rem;
margin: -0.375rem;
background: none;
border: none;
cursor: pointer;
transition: all var(--dees-transition-fast);
opacity: 0.8;
position: relative;
z-index: 2;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
}
.close.success {
color: var(--dees-muted-foreground);
}
.close.error,
.close.warning,
.close.info {
color: currentColor;
}
.close:hover {
opacity: 1;
transform: scale(1.1);
}
/* Progress bar */
.progress-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background: currentColor;
opacity: 0.3;
transform-origin: left;
animation: progress linear forwards;
pointer-events: none;
transform: scaleX(1);
}
.toast.success .progress-bar {
background: var(--dees-success);
}
.toast.error .progress-bar,
.toast.warning .progress-bar,
.toast.info .progress-bar {
background: rgba(255, 255, 255, 0.5);
}
@keyframes progress {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
`,
];
private get defaultDuration(): number {
switch (this.type) {
case 'success': return 3000;
case 'error': return 5000;
case 'warning': return 4000;
case 'info': return 4000;
}
}
async connectedCallback() {
await super.connectedCallback();
// Auto-dismiss after duration
const duration = this.duration || this.defaultDuration;
this.timeoutId = window.setTimeout(() => {
this.handleClose();
}, duration);
}
async disconnectedCallback() {
await super.disconnectedCallback();
// Clear the timeout when the element is removed
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
private handleClose() {
// Cancel the auto-dismiss timer
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
// Prevent double-triggering
if (this.classList.contains('closing')) return;
// Add closing animation
this.classList.add('closing');
// Wait for closing animation to complete
setTimeout(() => {
this.dispatchEvent(new CustomEvent('close', {
bubbles: true,
composed: true,
}));
}, 200);
}
private getIcon(): string {
switch (this.type) {
case 'success': return 'check-circle';
case 'error': return 'alert-circle';
case 'warning': return 'alert-triangle';
case 'info': return 'info';
}
}
public render(): TemplateResult {
const duration = this.duration || this.defaultDuration;
return html`
<div class="toast ${this.type}">
<div class="icon ${this.type}">
<dees-mobile-icon icon=${this.getIcon()} size="20"></dees-mobile-icon>
</div>
<span class="message">${this.message}</span>
<button
class="close ${this.type}"
@click=${() => this.handleClose()}
aria-label="Close"
type="button"
>
<dees-mobile-icon icon="x" size="20"></dees-mobile-icon>
</button>
<div class="progress-bar" style="animation-duration: ${duration}ms"></div>
</div>
`;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-mobile-toast.js';

View File

@@ -0,0 +1,7 @@
// Core UI Components
export * from './dees-mobile-button/index.js';
export * from './dees-mobile-icon/index.js';
export * from './dees-mobile-header/index.js';
export * from './dees-mobile-modal/index.js';
export * from './dees-mobile-actionsheet/index.js';
export * from './dees-mobile-toast/index.js';