feat(components): add large set of new UI components and demos, reorganize groups, and bump a few dependencies
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Action button configuration for the action bar
|
||||
*/
|
||||
export interface IActionBarAction {
|
||||
/** Unique identifier for the action */
|
||||
id: string;
|
||||
/** Button label text */
|
||||
label: string;
|
||||
/** Primary action gets highlighted styling and receives timeout trigger */
|
||||
primary?: boolean;
|
||||
/** Lucide icon name (optional) */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for showing an action bar
|
||||
*/
|
||||
export interface IActionBarOptions {
|
||||
/** Message text to display */
|
||||
message: string;
|
||||
/** Lucide icon name for the message (optional) */
|
||||
icon?: string;
|
||||
/** Visual type affects coloring */
|
||||
type?: 'info' | 'warning' | 'error' | 'question';
|
||||
/** Action buttons to display */
|
||||
actions: IActionBarAction[];
|
||||
/** Timeout configuration (optional) */
|
||||
timeout?: {
|
||||
/** Duration in milliseconds before auto-triggering default action */
|
||||
duration: number;
|
||||
/** ID of the action to auto-trigger when timeout expires */
|
||||
defaultActionId: string;
|
||||
};
|
||||
/** Whether to show a dismiss (X) button */
|
||||
dismissible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result returned when an action bar is resolved
|
||||
*/
|
||||
export interface IActionBarResult {
|
||||
/** ID of the action that was triggered */
|
||||
actionId: string;
|
||||
/** Whether the action was triggered by timeout (true) or user click (false) */
|
||||
timedOut: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal queue item for pending action bars
|
||||
*/
|
||||
export interface IActionBarQueueItem {
|
||||
options: IActionBarOptions;
|
||||
resolve: (result: IActionBarResult) => void;
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
css,
|
||||
state,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import type {
|
||||
IActionBarOptions,
|
||||
IActionBarResult,
|
||||
IActionBarQueueItem,
|
||||
IActionBarAction,
|
||||
} from './actionbar.interfaces.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-actionbar': DeesActionbar;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-actionbar')
|
||||
export class DeesActionbar extends DeesElement {
|
||||
// STATIC
|
||||
public static demoGroups = ['Feedback', 'Overlay'];
|
||||
public static demo = () => {
|
||||
const getActionbar = (e: Event) => {
|
||||
const button = e.currentTarget as HTMLElement;
|
||||
const container = button.closest('.demo-container');
|
||||
return container?.querySelector('dees-actionbar') as DeesActionbar | null;
|
||||
};
|
||||
|
||||
const showActionBar = async (e: Event) => {
|
||||
const actionbar = getActionbar(e);
|
||||
if (!actionbar) return;
|
||||
const result = await actionbar.show({
|
||||
message: 'File changed externally. Reload?',
|
||||
type: 'warning',
|
||||
icon: 'lucide:alertTriangle',
|
||||
actions: [
|
||||
{ id: 'reload', label: 'Reload', primary: true },
|
||||
{ id: 'ignore', label: 'Ignore' },
|
||||
],
|
||||
timeout: { duration: 5000, defaultActionId: 'reload' },
|
||||
dismissible: true,
|
||||
});
|
||||
console.log('Action bar result:', result);
|
||||
};
|
||||
|
||||
const showErrorBar = async (e: Event) => {
|
||||
const actionbar = getActionbar(e);
|
||||
if (!actionbar) return;
|
||||
const result = await actionbar.show({
|
||||
message: 'Process failed with exit code 1',
|
||||
type: 'error',
|
||||
icon: 'lucide:xCircle',
|
||||
actions: [
|
||||
{ id: 'retry', label: 'Retry', primary: true },
|
||||
{ id: 'dismiss', label: 'Dismiss' },
|
||||
],
|
||||
timeout: { duration: 10000, defaultActionId: 'dismiss' },
|
||||
});
|
||||
console.log('Error bar result:', result);
|
||||
};
|
||||
|
||||
const showQuestionBar = async (e: Event) => {
|
||||
const actionbar = getActionbar(e);
|
||||
if (!actionbar) return;
|
||||
const result = await actionbar.show({
|
||||
message: 'Save changes before closing?',
|
||||
type: 'question',
|
||||
icon: 'lucide:helpCircle',
|
||||
actions: [
|
||||
{ id: 'save', label: 'Save', primary: true },
|
||||
{ id: 'discard', label: 'Discard' },
|
||||
{ id: 'cancel', label: 'Cancel' },
|
||||
],
|
||||
});
|
||||
console.log('Question bar result:', result);
|
||||
};
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 300px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.demo-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<div class="demo-content">
|
||||
<dees-button @click=${showActionBar}>Warning</dees-button>
|
||||
<dees-button @click=${showErrorBar}>Error</dees-button>
|
||||
<dees-button @click=${showQuestionBar}>Question</dees-button>
|
||||
</div>
|
||||
<dees-actionbar></dees-actionbar>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
// Queue of pending action bars
|
||||
private queue: IActionBarQueueItem[] = [];
|
||||
|
||||
// Current active bar state
|
||||
@state() accessor currentBar: IActionBarOptions | null = null;
|
||||
@state() accessor timeRemaining: number = 0;
|
||||
@state() accessor progressPercent: number = 100;
|
||||
@state() accessor isVisible: boolean = false;
|
||||
|
||||
// Timeout handling
|
||||
private timeoutInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private currentResolve: ((result: IActionBarResult) => void) | null = null;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actionbar-item {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 0.2s ease-out;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 12%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 20%)')};
|
||||
}
|
||||
|
||||
:host(.visible) .actionbar-item {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.actionbar-content {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-out;
|
||||
}
|
||||
|
||||
:host(.visible) .actionbar-content {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 3px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 18%)')};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('hsl(210 100% 50%)', 'hsl(210 100% 60%)')};
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.progress-bar-fill.warning {
|
||||
background: ${cssManager.bdTheme('hsl(38 92% 50%)', 'hsl(38 92% 55%)')};
|
||||
}
|
||||
|
||||
.progress-bar-fill.error {
|
||||
background: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 55%)')};
|
||||
}
|
||||
|
||||
.progress-bar-fill.question {
|
||||
background: ${cssManager.bdTheme('hsl(270 70% 50%)', 'hsl(270 70% 60%)')};
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
gap: 12px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.message-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||
}
|
||||
|
||||
.message-icon.info {
|
||||
color: ${cssManager.bdTheme('hsl(210 100% 45%)', 'hsl(210 100% 60%)')};
|
||||
}
|
||||
|
||||
.message-icon.warning {
|
||||
color: ${cssManager.bdTheme('hsl(38 92% 45%)', 'hsl(38 92% 55%)')};
|
||||
}
|
||||
|
||||
.message-icon.error {
|
||||
color: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 55%)')};
|
||||
}
|
||||
|
||||
.message-icon.question {
|
||||
color: ${cssManager.bdTheme('hsl(270 70% 50%)', 'hsl(270 70% 60%)')};
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(0 0% 30%)')};
|
||||
}
|
||||
|
||||
.action-button.secondary:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 18%)')};
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: ${cssManager.bdTheme('hsl(210 100% 50%)', 'hsl(210 100% 55%)')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 100% 45%)', 'hsl(210 100% 50%)')};
|
||||
}
|
||||
|
||||
.action-button.primary.warning {
|
||||
background: ${cssManager.bdTheme('hsl(38 92% 45%)', 'hsl(38 92% 50%)')};
|
||||
}
|
||||
|
||||
.action-button.primary.warning:hover {
|
||||
background: ${cssManager.bdTheme('hsl(38 92% 40%)', 'hsl(38 92% 45%)')};
|
||||
}
|
||||
|
||||
.action-button.primary.error {
|
||||
background: ${cssManager.bdTheme('hsl(0 70% 50%)', 'hsl(0 70% 55%)')};
|
||||
}
|
||||
|
||||
.action-button.primary.error:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 70% 45%)', 'hsl(0 70% 50%)')};
|
||||
}
|
||||
|
||||
.action-button.primary.question {
|
||||
background: ${cssManager.bdTheme('hsl(270 70% 50%)', 'hsl(270 70% 55%)')};
|
||||
}
|
||||
|
||||
.action-button.primary.question:hover {
|
||||
background: ${cssManager.bdTheme('hsl(270 70% 45%)', 'hsl(270 70% 50%)')};
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.dismiss-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.dismiss-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 22%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const bar = this.currentBar;
|
||||
const type = bar?.type || 'info';
|
||||
const hasTimeout = bar?.timeout && this.timeRemaining > 0;
|
||||
|
||||
// ALWAYS render wrapper - required for grid animation to work
|
||||
return html`
|
||||
<div class="actionbar-item">
|
||||
<div class="actionbar-content">
|
||||
${bar ? html`
|
||||
${hasTimeout ? html`
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-bar-fill ${type}"
|
||||
style="width: ${this.progressPercent}%"
|
||||
></div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="content">
|
||||
<div class="message-section">
|
||||
${bar.icon ? html`
|
||||
<dees-icon
|
||||
class="message-icon ${type}"
|
||||
.icon=${bar.icon}
|
||||
iconSize="16"
|
||||
></dees-icon>
|
||||
` : ''}
|
||||
<span class="message-text">${bar.message}</span>
|
||||
</div>
|
||||
<div class="actions-section">
|
||||
${bar.actions.map(action => this.renderActionButton(action, bar, hasTimeout))}
|
||||
${bar.dismissible ? html`
|
||||
<div
|
||||
class="dismiss-button"
|
||||
@click=${() => this.handleDismiss()}
|
||||
title="Dismiss"
|
||||
>
|
||||
<dees-icon .icon=${'lucide:x'} iconSize="14"></dees-icon>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActionButton(
|
||||
action: IActionBarAction,
|
||||
bar: IActionBarOptions,
|
||||
hasTimeout: boolean | undefined
|
||||
): TemplateResult {
|
||||
const isPrimary = action.primary;
|
||||
const type = bar.type || 'info';
|
||||
const isDefaultAction = bar.timeout?.defaultActionId === action.id;
|
||||
const showCountdown = hasTimeout && isDefaultAction;
|
||||
const seconds = Math.ceil(this.timeRemaining / 1000);
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="action-button ${isPrimary ? `primary ${type}` : 'secondary'}"
|
||||
@click=${() => this.handleAction(action.id, false)}
|
||||
>
|
||||
${action.icon ? html`
|
||||
<dees-icon .icon=${action.icon} iconSize="12"></dees-icon>
|
||||
` : ''}
|
||||
<span>${action.label}</span>
|
||||
${showCountdown ? html`
|
||||
<span class="countdown">(${seconds}s)</span>
|
||||
` : ''}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========== Public API ==========
|
||||
|
||||
/**
|
||||
* Show an action bar with the given options.
|
||||
* Returns a promise that resolves when an action is taken.
|
||||
*/
|
||||
public async show(options: IActionBarOptions): Promise<IActionBarResult> {
|
||||
return new Promise((resolve) => {
|
||||
// Add to queue
|
||||
this.queue.push({ options, resolve });
|
||||
|
||||
// If no current bar, process queue
|
||||
if (!this.currentBar) {
|
||||
this.processQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the current action bar without triggering any action.
|
||||
*/
|
||||
public dismiss(): void {
|
||||
this.handleDismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all pending action bars in the queue.
|
||||
*/
|
||||
public clearQueue(): void {
|
||||
// Resolve all queued items with dismiss
|
||||
for (const item of this.queue) {
|
||||
item.resolve({ actionId: 'dismissed', timedOut: false });
|
||||
}
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
// ========== Private Methods ==========
|
||||
|
||||
/**
|
||||
* Hide the current actionbar with animation.
|
||||
* Removes visible class first to trigger CSS transition, then clears content after animation.
|
||||
*/
|
||||
private async hideCurrentBar(): Promise<void> {
|
||||
// Remove visible class to start close animation
|
||||
this.classList.remove('visible');
|
||||
this.isVisible = false;
|
||||
|
||||
// Wait for animation to complete (200ms transition + buffer)
|
||||
await new Promise(resolve => setTimeout(resolve, 220));
|
||||
|
||||
// Now safe to clear content
|
||||
this.currentBar = null;
|
||||
this.currentResolve = null;
|
||||
}
|
||||
|
||||
private async processQueue(): Promise<void> {
|
||||
if (this.queue.length === 0) {
|
||||
// Hide with animation - don't await, let it run async
|
||||
this.hideCurrentBar();
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.queue.shift()!;
|
||||
this.currentBar = item.options;
|
||||
this.currentResolve = item.resolve;
|
||||
this.isVisible = true;
|
||||
|
||||
// Wait for Lit render, then add class on next frame to trigger animation
|
||||
await this.updateComplete;
|
||||
requestAnimationFrame(() => {
|
||||
this.classList.add('visible');
|
||||
});
|
||||
|
||||
// Setup timeout if configured
|
||||
if (item.options.timeout) {
|
||||
this.startTimeout(item.options.timeout.duration, item.options.timeout.defaultActionId);
|
||||
}
|
||||
}
|
||||
|
||||
private startTimeout(duration: number, defaultActionId: string): void {
|
||||
this.timeRemaining = duration;
|
||||
this.progressPercent = 100;
|
||||
|
||||
const startTime = Date.now();
|
||||
const updateInterval = 50; // Update every 50ms for smooth animation
|
||||
|
||||
this.timeoutInterval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
this.timeRemaining = Math.max(0, duration - elapsed);
|
||||
this.progressPercent = (this.timeRemaining / duration) * 100;
|
||||
|
||||
if (this.timeRemaining <= 0) {
|
||||
this.clearTimeoutInterval();
|
||||
this.handleAction(defaultActionId, true);
|
||||
}
|
||||
}, updateInterval);
|
||||
}
|
||||
|
||||
private clearTimeoutInterval(): void {
|
||||
if (this.timeoutInterval) {
|
||||
clearInterval(this.timeoutInterval);
|
||||
this.timeoutInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleAction(actionId: string, timedOut: boolean): void {
|
||||
this.clearTimeoutInterval();
|
||||
|
||||
if (this.currentResolve) {
|
||||
this.currentResolve({ actionId, timedOut });
|
||||
}
|
||||
|
||||
// Process next item in queue
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
private handleDismiss(): void {
|
||||
this.handleAction('dismissed', false);
|
||||
}
|
||||
|
||||
public async disconnectedCallback(): Promise<void> {
|
||||
await super.disconnectedCallback();
|
||||
this.clearTimeoutInterval();
|
||||
}
|
||||
}
|
||||
2
ts_web/elements/00group-feedback/dees-actionbar/index.ts
Normal file
2
ts_web/elements/00group-feedback/dees-actionbar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './dees-actionbar.js';
|
||||
export * from './actionbar.interfaces.js';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<dees-badge .text=${'Default'}></dees-badge>
|
||||
<dees-badge .type=${'primary'} .text=${'Primary'}></dees-badge>
|
||||
<dees-badge .type=${'success'} .text=${'Success'}></dees-badge>
|
||||
<dees-badge .type=${'warning'} .text=${'Warning'}></dees-badge>
|
||||
<dees-badge .type=${'error'} .text=${'Error'}></dees-badge>
|
||||
<dees-badge .type=${'primary'} .rounded=${true} .text=${'Rounded'}></dees-badge>
|
||||
</div>
|
||||
`;
|
||||
100
ts_web/elements/00group-feedback/dees-badge/dees-badge.ts
Normal file
100
ts_web/elements/00group-feedback/dees-badge/dees-badge.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
cssManager,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
type CSSResult,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-badge.demo.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-badge': DeesBadge;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-badge')
|
||||
export class DeesBadge extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Feedback'];
|
||||
|
||||
@property({ type: String })
|
||||
accessor type: 'default' | 'primary' | 'success' | 'warning' | 'error' = 'default';
|
||||
|
||||
@property({ type: String })
|
||||
accessor text: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor rounded: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge.rounded {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.badge.default {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#333')};
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
}
|
||||
|
||||
.badge.primary {
|
||||
background: #0050b9;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.badge.success {
|
||||
background: #2e7d32;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.badge.warning {
|
||||
background: #ed6c02;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.badge.error {
|
||||
background: #e4002b;
|
||||
color: #ffffff;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="badge ${this.type} ${this.rounded ? 'rounded' : ''}">
|
||||
${this.text}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-feedback/dees-badge/index.ts
Normal file
1
ts_web/elements/00group-feedback/dees-badge/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-badge.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-hint></dees-hint>
|
||||
`;
|
||||
42
ts_web/elements/00group-feedback/dees-hint/dees-hint.ts
Normal file
42
ts_web/elements/00group-feedback/dees-hint/dees-hint.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
cssManager,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
type CSSResult,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-hint.demo.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-hint': DeesHint;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-hint')
|
||||
export class DeesHint extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Feedback'];
|
||||
|
||||
@property({ type: String })
|
||||
accessor type: 'info' | 'warn' | 'error' | 'critical' = 'info';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public static styles = [themeDefaultStyles, cssManager.defaultStyles, css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
`];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html` <div class="mainbox"></div> `;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-feedback/dees-hint/index.ts
Normal file
1
ts_web/elements/00group-feedback/dees-hint/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-hint.js';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
import { DeesProgressbar } from '../dees-progressbar/dees-progressbar.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
return html`
|
||||
<dees-progressbar
|
||||
.percentage=${50}
|
||||
></dees-progressbar>
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import * as plugins from '../../00plugins.js';
|
||||
import * as colors from '../../00colors.js';
|
||||
import { demoFunc } from './dees-progressbar.demo.js';
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
DeesElement,
|
||||
property,
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
type CSSResult,
|
||||
unsafeCSS,
|
||||
unsafeHTML,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
@customElement('dees-progressbar')
|
||||
export class DeesProgressbar extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Feedback'];
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
accessor percentage = 0;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
|
||||
}
|
||||
.progressBarContainer {
|
||||
padding: 8px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#444')};
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border-top: 0.5px solid ${cssManager.bdTheme('none', '#555')};
|
||||
}
|
||||
|
||||
.progressBarFill {
|
||||
background: ${cssManager.bdTheme(colors.dark.blueActive, colors.bright.blueActive)};
|
||||
height: 8px;
|
||||
margin-top: -0.5px;
|
||||
transition: 0.2s width;
|
||||
border-radius: 4px;
|
||||
width: 0px;
|
||||
border-top: 0.5 solid ${cssManager.bdTheme('none', '#398fff')};
|
||||
}
|
||||
|
||||
.progressText {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div class="progressBarContainer">
|
||||
<div class="progressBar">
|
||||
<div class="progressBarFill"></div>
|
||||
<div class="progressText">
|
||||
${this.percentage}%
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
firstUpdated (_changedProperties: Map<string | number | symbol, unknown>): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this.updateComplete.then(() => {
|
||||
this.updatePercentage();
|
||||
});
|
||||
}
|
||||
|
||||
public async updatePercentage() {
|
||||
const progressBarFill = this.shadowRoot.querySelector('.progressBarFill') as HTMLElement;
|
||||
progressBarFill.style.width = `${this.percentage}%`;
|
||||
}
|
||||
|
||||
updated(){
|
||||
this.updatePercentage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dees-progressbar.js';
|
||||
135
ts_web/elements/00group-feedback/dees-spinner/dees-spinner.ts
Normal file
135
ts_web/elements/00group-feedback/dees-spinner/dees-spinner.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
DeesElement,
|
||||
property,
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
type CSSResult,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-spinner': DeesSpinner;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-spinner')
|
||||
export class DeesSpinner extends DeesElement {
|
||||
public static demoGroups = ['Feedback'];
|
||||
public static demo = () => html`
|
||||
<dees-spinner></dees-spinner>
|
||||
<dees-spinner status="success"></dees-spinner>
|
||||
<dees-spinner status="error"></dees-spinner>
|
||||
<dees-spinner size=${64} status="success"></dees-spinner>
|
||||
<dees-spinner .size=${64} status="error"></dees-spinner>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
accessor size = 20;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor bnw: boolean = false;
|
||||
|
||||
@property()
|
||||
accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: relative;
|
||||
transition: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
background: #8bc34a00;
|
||||
border: 3px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.3)')};
|
||||
border-radius: 50%;
|
||||
border-top-color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
-webkit-animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
#loading.success {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
animation: none;
|
||||
-webkit-animation: none;
|
||||
}
|
||||
|
||||
#loading.error {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
animation: none;
|
||||
-webkit-animation: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes spin {
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
dees-icon {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<style>
|
||||
#loading {
|
||||
width: ${this.size}px;
|
||||
height: ${this.size}px;
|
||||
}
|
||||
#loading.success {
|
||||
color: ${cssManager.bdTheme(this.bnw ? '#333': `#8bc34a`, this.bnw ? '#fff' : `#8bc34a`)};
|
||||
|
||||
}
|
||||
#loading.error {
|
||||
color: ${cssManager.bdTheme(this.bnw ? '#333': `#e64a19`, this.bnw ? '#fff' : `#e64a19`)};
|
||||
}
|
||||
dees-icon {
|
||||
font-size: ${this.size}px;
|
||||
}
|
||||
</style>
|
||||
<div class="${this.status}" id="loading">
|
||||
${(() => {
|
||||
if (this.status === 'success') {
|
||||
return html`<dees-icon style="transform: translateX(1%) translateY(3%);" .icon=${'fa:circle-check'}></dees-icon>`;
|
||||
} else if (this.status === 'error') {
|
||||
return html`<dees-icon .icon=${'fa:circle-xmark'}></dees-icon>`;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-feedback/dees-spinner/index.ts
Normal file
1
ts_web/elements/00group-feedback/dees-spinner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-spinner.js';
|
||||
260
ts_web/elements/00group-feedback/dees-toast/dees-toast.demo.ts
Normal file
260
ts_web/elements/00group-feedback/dees-toast/dees-toast.demo.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import { DeesToast } from '../dees-toast/dees-toast.js';
|
||||
import '../../00group-button/dees-button/dees-button.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 32px;
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-button class="theme-toggle" @clicked=${() => {
|
||||
document.body.classList.toggle('bright');
|
||||
}}>Toggle Theme</dees-button>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Toast Types</h2>
|
||||
<p class="demo-description">
|
||||
Different toast types for various notification scenarios. Click any button to show a toast.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.info('This is an informational message');
|
||||
}}>Info Toast</dees-button>
|
||||
|
||||
<dees-button type="highlighted" @clicked=${() => {
|
||||
DeesToast.success('Operation completed successfully!');
|
||||
}}>Success Toast</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.warning('Please review before proceeding');
|
||||
}}>Warning Toast</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.error('An error occurred while processing');
|
||||
}}>Error Toast</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Toast Positions</h2>
|
||||
<p class="demo-description">
|
||||
Toasts can appear in different positions on the screen.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Top Right Position',
|
||||
type: 'info',
|
||||
position: 'top-right'
|
||||
});
|
||||
}}>Top Right</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Top Left Position',
|
||||
type: 'info',
|
||||
position: 'top-left'
|
||||
});
|
||||
}}>Top Left</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Bottom Right Position',
|
||||
type: 'info',
|
||||
position: 'bottom-right'
|
||||
});
|
||||
}}>Bottom Right</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Bottom Left Position',
|
||||
type: 'info',
|
||||
position: 'bottom-left'
|
||||
});
|
||||
}}>Bottom Left</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Top Center Position',
|
||||
type: 'info',
|
||||
position: 'top-center'
|
||||
});
|
||||
}}>Top Center</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Bottom Center Position',
|
||||
type: 'info',
|
||||
position: 'bottom-center'
|
||||
});
|
||||
}}>Bottom Center</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Duration Options</h2>
|
||||
<p class="demo-description">
|
||||
Control how long toasts stay visible. Duration in milliseconds.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Quick toast (1 second)',
|
||||
type: 'info',
|
||||
duration: 1000
|
||||
});
|
||||
}}>1 Second</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Standard toast (3 seconds)',
|
||||
type: 'info',
|
||||
duration: 3000
|
||||
});
|
||||
}}>3 Seconds (Default)</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Long toast (5 seconds)',
|
||||
type: 'info',
|
||||
duration: 5000
|
||||
});
|
||||
}}>5 Seconds</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Manual dismiss only (click to close)',
|
||||
type: 'warning',
|
||||
duration: 0
|
||||
});
|
||||
}}>No Auto-Dismiss</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Multiple Toasts</h2>
|
||||
<p class="demo-description">
|
||||
Multiple toasts stack automatically. They maintain their order and animate smoothly.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.info('First notification');
|
||||
setTimeout(() => DeesToast.success('Second notification'), 200);
|
||||
setTimeout(() => DeesToast.warning('Third notification'), 400);
|
||||
setTimeout(() => DeesToast.error('Fourth notification'), 600);
|
||||
}}>Show Multiple</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
setTimeout(() => {
|
||||
DeesToast.show({
|
||||
message: `Notification #${i}`,
|
||||
type: i % 2 === 0 ? 'success' : 'info',
|
||||
duration: 2000 + (i * 500)
|
||||
});
|
||||
}, i * 100);
|
||||
}
|
||||
}}>Rapid Fire</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Real-World Examples</h2>
|
||||
<p class="demo-description">
|
||||
Common use cases for toast notifications in applications.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${async () => {
|
||||
const toast = await DeesToast.show({
|
||||
message: 'Saving changes...',
|
||||
type: 'info',
|
||||
duration: 0
|
||||
});
|
||||
|
||||
// Simulate save operation
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
DeesToast.success('Changes saved successfully!');
|
||||
}, 2000);
|
||||
}}>Save Operation</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.error('Failed to connect to server. Please check your internet connection.');
|
||||
}}>Network Error</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.warning('Your session will expire in 5 minutes');
|
||||
}}>Session Warning</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.success('File uploaded successfully!');
|
||||
}}>Upload Complete</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Programmatic Control</h2>
|
||||
<p class="demo-description">
|
||||
Advanced control over toast behavior.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${async () => {
|
||||
const toast = await DeesToast.show({
|
||||
message: 'This toast can be dismissed programmatically',
|
||||
type: 'info',
|
||||
duration: 0
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
DeesToast.success('Toast dismissed after 2 seconds');
|
||||
}, 2000);
|
||||
}}>Programmatic Dismiss</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
// Using the convenience methods
|
||||
DeesToast.info('Info message', 2000);
|
||||
setTimeout(() => DeesToast.success('Success message', 2000), 500);
|
||||
setTimeout(() => DeesToast.warning('Warning message', 2000), 1000);
|
||||
setTimeout(() => DeesToast.error('Error message', 2000), 1500);
|
||||
}}>Convenience Methods</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
336
ts_web/elements/00group-feedback/dees-toast/dees-toast.ts
Normal file
336
ts_web/elements/00group-feedback/dees-toast/dees-toast.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { zIndexLayers } from '../../00zindex.js';
|
||||
import { demoFunc } from './dees-toast.demo.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-toast': DeesToast;
|
||||
}
|
||||
}
|
||||
|
||||
export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
||||
export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
|
||||
|
||||
export interface IToastOptions {
|
||||
message: string;
|
||||
type?: ToastType;
|
||||
duration?: number;
|
||||
position?: ToastPosition;
|
||||
}
|
||||
|
||||
@customElement('dees-toast')
|
||||
export class DeesToast extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Feedback', 'Overlay'];
|
||||
|
||||
private static toastContainers = new Map<ToastPosition, HTMLDivElement>();
|
||||
|
||||
private static getOrCreateContainer(position: ToastPosition): HTMLDivElement {
|
||||
if (!this.toastContainers.has(position)) {
|
||||
const container = document.createElement('div');
|
||||
container.className = `toast-container toast-container-${position}`;
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: ${zIndexLayers.overlay.toast};
|
||||
pointer-events: none;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
// Position the container
|
||||
switch (position) {
|
||||
case 'top-right':
|
||||
container.style.top = '0';
|
||||
container.style.right = '0';
|
||||
break;
|
||||
case 'top-left':
|
||||
container.style.top = '0';
|
||||
container.style.left = '0';
|
||||
break;
|
||||
case 'bottom-right':
|
||||
container.style.bottom = '0';
|
||||
container.style.right = '0';
|
||||
break;
|
||||
case 'bottom-left':
|
||||
container.style.bottom = '0';
|
||||
container.style.left = '0';
|
||||
break;
|
||||
case 'top-center':
|
||||
container.style.top = '0';
|
||||
container.style.left = '50%';
|
||||
container.style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
case 'bottom-center':
|
||||
container.style.bottom = '0';
|
||||
container.style.left = '50%';
|
||||
container.style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
}
|
||||
|
||||
document.body.appendChild(container);
|
||||
this.toastContainers.set(position, container);
|
||||
}
|
||||
return this.toastContainers.get(position)!;
|
||||
}
|
||||
|
||||
public static async show(options: IToastOptions | string) {
|
||||
const opts: IToastOptions = typeof options === 'string'
|
||||
? { message: options }
|
||||
: options;
|
||||
|
||||
const toast = new DeesToast();
|
||||
toast.message = opts.message;
|
||||
toast.type = opts.type || 'info';
|
||||
toast.duration = opts.duration || 3000;
|
||||
|
||||
const container = this.getOrCreateContainer(opts.position || 'top-right');
|
||||
container.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
await toast.updateComplete;
|
||||
requestAnimationFrame(() => {
|
||||
toast.isVisible = true;
|
||||
});
|
||||
|
||||
// Auto dismiss
|
||||
if (toast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
}, toast.duration);
|
||||
}
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
// Alias for consistency with DeesModal
|
||||
public static async createAndShow(options: IToastOptions | string) {
|
||||
return this.show(options);
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
public static info(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'info', duration });
|
||||
}
|
||||
|
||||
public static success(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'success', duration });
|
||||
}
|
||||
|
||||
public static warning(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'warning', duration });
|
||||
}
|
||||
|
||||
public static error(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'error', duration });
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@property({ type: String })
|
||||
accessor message: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor type: ToastType = 'info';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor duration: number = 3000;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
accessor isVisible: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:host([isvisible]) {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.message {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.close {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.close svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Type-specific styles */
|
||||
:host([type="info"]) .icon {
|
||||
color: #0084ff;
|
||||
}
|
||||
|
||||
:host([type="success"]) .icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
:host([type="warning"]) .icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
:host([type="error"]) .icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: currentColor;
|
||||
opacity: 0.2;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
opacity: 0.8;
|
||||
transform-origin: left;
|
||||
animation: progress linear forwards;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
from {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
to {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const icons = {
|
||||
info: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"/>
|
||||
</svg>`,
|
||||
success: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>`,
|
||||
warning: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>`,
|
||||
error: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="toast" @click=${this.dismiss}>
|
||||
<div class="icon">
|
||||
${icons[this.type]}
|
||||
</div>
|
||||
<div class="message">${this.message}</div>
|
||||
<div class="close">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</div>
|
||||
${this.duration > 0 ? html`
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="animation-duration: ${this.duration}ms"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async dismiss() {
|
||||
this.isVisible = false;
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
this.remove();
|
||||
|
||||
// Clean up empty containers
|
||||
const container = this.parentElement;
|
||||
if (container && container.children.length === 0) {
|
||||
container.remove();
|
||||
for (const [position, cont] of DeesToast.toastContainers.entries()) {
|
||||
if (cont === container) {
|
||||
DeesToast.toastContainers.delete(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
// Set the type attribute for CSS
|
||||
this.setAttribute('type', this.type);
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-feedback/dees-toast/index.ts
Normal file
1
ts_web/elements/00group-feedback/dees-toast/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-toast.js';
|
||||
7
ts_web/elements/00group-feedback/index.ts
Normal file
7
ts_web/elements/00group-feedback/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Feedback Components
|
||||
export * from './dees-actionbar/index.js';
|
||||
export * from './dees-badge/index.js';
|
||||
export * from './dees-hint/index.js';
|
||||
export * from './dees-progressbar/index.js';
|
||||
export * from './dees-spinner/index.js';
|
||||
export * from './dees-toast/index.js';
|
||||
Reference in New Issue
Block a user