initial
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-actionsheet.js';
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-ui/dees-mobile-button/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-button.js';
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-ui/dees-mobile-header/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-header.js';
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
189
ts_web/elements/00group-ui/dees-mobile-icon/dees-mobile-icon.ts
Normal file
189
ts_web/elements/00group-ui/dees-mobile-icon/dees-mobile-icon.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-ui/dees-mobile-icon/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-icon.js';
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-ui/dees-mobile-modal/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-modal.js';
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-ui/dees-mobile-toast/index.ts
Normal file
1
ts_web/elements/00group-ui/dees-mobile-toast/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-mobile-toast.js';
|
||||
7
ts_web/elements/00group-ui/index.ts
Normal file
7
ts_web/elements/00group-ui/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user