Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
346abfa685 | |||
f1123f319f | |||
ac15da9c82 | |||
b9432c8489 | |||
b35b1fbae7 | |||
e39590df2c | |||
fad7fda2a6 | |||
987f557c60 |
18
package.json
18
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "1.8.1",
|
"version": "1.8.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@ -16,7 +16,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.1.1",
|
"@design.estate/dees-domtools": "^2.1.1",
|
||||||
"@design.estate/dees-element": "^2.0.41",
|
"@design.estate/dees-element": "^2.0.42",
|
||||||
"@design.estate/dees-wcctools": "^1.0.90",
|
"@design.estate/dees-wcctools": "^1.0.90",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
@ -25,25 +25,25 @@
|
|||||||
"@push.rocks/smarti18n": "^1.0.4",
|
"@push.rocks/smarti18n": "^1.0.4",
|
||||||
"@push.rocks/smartpromise": "^4.2.0",
|
"@push.rocks/smartpromise": "^4.2.0",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@tsclass/tsclass": "^9.0.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"@webcontainer/api": "1.2.0",
|
"@webcontainer/api": "1.2.0",
|
||||||
"apexcharts": "^4.3.0",
|
"apexcharts": "^4.7.0",
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"ibantools": "^4.5.1",
|
"ibantools": "^4.5.1",
|
||||||
"lucide": "^0.501.0",
|
"lucide": "^0.514.0",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0"
|
"xterm-addon-fit": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.84",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsbundle": "^2.0.15",
|
"@git.zone/tsbundle": "^2.0.15",
|
||||||
"@git.zone/tstest": "^1.0.90",
|
"@git.zone/tstest": "^2.3.1",
|
||||||
"@git.zone/tswatch": "^2.0.37",
|
"@git.zone/tswatch": "^2.0.37",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/tapbundle": "^5.5.6",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^22.14.1"
|
"@types/node": "^22.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
1941
pnpm-lock.yaml
generated
1941
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,3 +2,42 @@
|
|||||||
* Give a short rundown of components and a few points abputspecific features on each.
|
* Give a short rundown of components and a few points abputspecific features on each.
|
||||||
* Try to list all components in a summary.
|
* Try to list all components in a summary.
|
||||||
* Then list all components with a short description.
|
* Then list all components with a short description.
|
||||||
|
|
||||||
|
## Chart Components
|
||||||
|
|
||||||
|
### dees-chart-area
|
||||||
|
- Fully functional area chart component using ApexCharts
|
||||||
|
- Displays time-series data with gradient fills
|
||||||
|
- Responsive with ResizeObserver (debounced to prevent flicker)
|
||||||
|
- Fixed: Chart now properly respects container boundaries on initial render
|
||||||
|
- Overflow prevention with proper CSS containment
|
||||||
|
- Enhanced demo features:
|
||||||
|
- Multiple dataset examples (System Usage, Network Traffic, Sales Analytics)
|
||||||
|
- Real-time data simulation with automatic updates
|
||||||
|
- Dynamic dataset switching
|
||||||
|
- Customizable Y-axis formatters (percentages, currency, units)
|
||||||
|
- Data randomization for testing
|
||||||
|
- Manual data point addition
|
||||||
|
- Properties:
|
||||||
|
- `label`: Chart title
|
||||||
|
- `series`: ApexAxisChartSeries data
|
||||||
|
- `yAxisFormatter`: Custom Y-axis label formatter function
|
||||||
|
- Methods:
|
||||||
|
- `updateSeries()`: Update chart data
|
||||||
|
- `appendData()`: Add new data points to existing series
|
||||||
|
|
||||||
|
### dees-chart-log
|
||||||
|
- Server log viewer component (not a chart despite the name)
|
||||||
|
- Terminal-style interface with monospace font
|
||||||
|
- Supports log levels: debug, info, warn, error, success
|
||||||
|
- Features:
|
||||||
|
- Auto-scroll toggle
|
||||||
|
- Clear logs button
|
||||||
|
- Colored log levels
|
||||||
|
- Timestamp with milliseconds
|
||||||
|
- Source labels for log entries
|
||||||
|
- Maximum 1000 entries (configurable)
|
||||||
|
- Light/dark theme support
|
||||||
|
- Demo includes realistic server log simulation
|
||||||
|
- Note: In demos, buttons use `@clicked` event (not `@click`)
|
||||||
|
- Demo uses global reference to access log element (window.__demoLogElement)
|
33
readme.md
33
readme.md
@ -104,7 +104,7 @@ Loading indicator with customizable appearance.
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `DeesToast`
|
#### `DeesToast`
|
||||||
Notification toast messages with various styles and auto-dismiss.
|
Notification toast messages with various styles, positions, and auto-dismiss functionality.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Programmatic usage
|
// Programmatic usage
|
||||||
@ -112,18 +112,43 @@ DeesToast.show({
|
|||||||
message: 'Operation successful',
|
message: 'Operation successful',
|
||||||
type: 'success', // Options: info, success, warning, error
|
type: 'success', // Options: info, success, warning, error
|
||||||
duration: 3000, // Time in milliseconds before auto-dismiss
|
duration: 3000, // Time in milliseconds before auto-dismiss
|
||||||
position: 'top-right' // Options: top-right, top-left, bottom-right, bottom-left
|
position: 'top-right' // Options: top-right, top-left, bottom-right, bottom-left, top-center, bottom-center
|
||||||
});
|
});
|
||||||
|
|
||||||
// Component usage
|
// Convenience methods
|
||||||
|
DeesToast.info('Information message');
|
||||||
|
DeesToast.success('Success message');
|
||||||
|
DeesToast.warning('Warning message');
|
||||||
|
DeesToast.error('Error message');
|
||||||
|
|
||||||
|
// Advanced control
|
||||||
|
const toast = await DeesToast.show({
|
||||||
|
message: 'Processing...',
|
||||||
|
type: 'info',
|
||||||
|
duration: 0 // No auto-dismiss
|
||||||
|
});
|
||||||
|
|
||||||
|
// Later dismiss programmatically
|
||||||
|
toast.dismiss();
|
||||||
|
|
||||||
|
// Component usage (not typically used directly)
|
||||||
<dees-toast
|
<dees-toast
|
||||||
message="Changes saved"
|
message="Changes saved"
|
||||||
type="success"
|
type="success"
|
||||||
autoClose
|
|
||||||
duration="3000"
|
duration="3000"
|
||||||
></dees-toast>
|
></dees-toast>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
- Multiple toast types with distinct icons and colors
|
||||||
|
- 6 position options for flexible placement
|
||||||
|
- Auto-dismiss with visual progress indicator
|
||||||
|
- Manual dismiss by clicking
|
||||||
|
- Smooth animations and transitions
|
||||||
|
- Automatic stacking of multiple toasts
|
||||||
|
- Theme-aware styling
|
||||||
|
- Programmatic control
|
||||||
|
|
||||||
### Form Components
|
### Form Components
|
||||||
|
|
||||||
#### `DeesForm`
|
#### `DeesForm`
|
||||||
|
@ -1,8 +1,192 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css } from '@design.estate/dees-element';
|
||||||
|
import type { DeesChartArea } from './dees-chart-area.js';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
|
let chartElement: DeesChartArea;
|
||||||
|
let intervalId: number;
|
||||||
|
let currentDataset = 'system';
|
||||||
|
|
||||||
|
// Get element reference after render
|
||||||
|
setTimeout(() => {
|
||||||
|
const charts = document.querySelectorAll('dees-chart-area');
|
||||||
|
if (charts.length > 0) {
|
||||||
|
chartElement = charts[charts.length - 1] as DeesChartArea;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Y-axis formatters for different datasets
|
||||||
|
const formatters = {
|
||||||
|
system: (val: number) => `${val}%`,
|
||||||
|
network: (val: number) => `${val} Mbps`,
|
||||||
|
sales: (val: number) => `$${val.toLocaleString()}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Different datasets to showcase
|
||||||
|
const datasets = {
|
||||||
|
system: {
|
||||||
|
label: 'System Usage (%)',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 25 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 30 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 28 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 35 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 32 },
|
||||||
|
{ x: new Date().toISOString(), y: 38 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 45 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 48 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 46 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 52 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 50 },
|
||||||
|
{ x: new Date().toISOString(), y: 55 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
label: 'Network Traffic (Mbps)',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Download',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 120 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 150 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 180 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 165 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 190 },
|
||||||
|
{ x: new Date().toISOString(), y: 175 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Upload',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 25 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 30 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 35 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 28 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 32 },
|
||||||
|
{ x: new Date().toISOString(), y: 40 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sales: {
|
||||||
|
label: 'Sales Analytics',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Revenue',
|
||||||
|
data: [
|
||||||
|
{ x: '2025-01-01', y: 45000 },
|
||||||
|
{ x: '2025-01-02', y: 52000 },
|
||||||
|
{ x: '2025-01-03', y: 48000 },
|
||||||
|
{ x: '2025-01-04', y: 61000 },
|
||||||
|
{ x: '2025-01-05', y: 58000 },
|
||||||
|
{ x: '2025-01-06', y: 65000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Profit',
|
||||||
|
data: [
|
||||||
|
{ x: '2025-01-01', y: 12000 },
|
||||||
|
{ x: '2025-01-02', y: 14000 },
|
||||||
|
{ x: '2025-01-03', y: 11000 },
|
||||||
|
{ x: '2025-01-04', y: 18000 },
|
||||||
|
{ x: '2025-01-05', y: 16000 },
|
||||||
|
{ x: '2025-01-06', y: 20000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random value within range
|
||||||
|
const getRandomValue = (min: number, max: number) => {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add real-time data
|
||||||
|
const addRealtimeData = () => {
|
||||||
|
if (!chartElement) return;
|
||||||
|
|
||||||
|
const dataset = datasets[currentDataset];
|
||||||
|
const newTimestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Generate new data points based on dataset type
|
||||||
|
let newData: any[][] = [];
|
||||||
|
|
||||||
|
if (currentDataset === 'system') {
|
||||||
|
newData = [
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(25, 45) }], // CPU
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(45, 65) }], // Memory
|
||||||
|
];
|
||||||
|
} else if (currentDataset === 'network') {
|
||||||
|
newData = [
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(100, 250) }], // Download
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(20, 50) }], // Upload
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only last 10 data points
|
||||||
|
const currentSeries = chartElement.series.map((series, index) => ({
|
||||||
|
...series,
|
||||||
|
data: [...series.data.slice(-9), ...(newData[index] || [])],
|
||||||
|
}));
|
||||||
|
|
||||||
|
chartElement.series = currentSeries;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Switch dataset
|
||||||
|
const switchDataset = (name: string) => {
|
||||||
|
currentDataset = name;
|
||||||
|
if (chartElement) {
|
||||||
|
const dataset = datasets[name];
|
||||||
|
chartElement.label = dataset.label;
|
||||||
|
chartElement.series = dataset.series;
|
||||||
|
chartElement.yAxisFormatter = formatters[name];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start/stop real-time updates
|
||||||
|
const startRealtime = () => {
|
||||||
|
if (!intervalId && (currentDataset === 'system' || currentDataset === 'network')) {
|
||||||
|
intervalId = window.setInterval(() => addRealtimeData(), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRealtime = () => {
|
||||||
|
if (intervalId) {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Randomize current data
|
||||||
|
const randomizeData = () => {
|
||||||
|
if (!chartElement) return;
|
||||||
|
|
||||||
|
const currentSeries = chartElement.series.map(series => ({
|
||||||
|
...series,
|
||||||
|
data: series.data.map(point => ({
|
||||||
|
...point,
|
||||||
|
y: typeof point.y === 'number'
|
||||||
|
? point.y * (0.8 + Math.random() * 0.4) // +/- 20% variation
|
||||||
|
: point.y,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
chartElement.series = currentSeries;
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
|
${css`
|
||||||
.demoBox {
|
.demoBox {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #000000;
|
background: #000000;
|
||||||
@ -10,12 +194,92 @@ export const demoFunc = () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: #666;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
</style>
|
</style>
|
||||||
<div class="demoBox">
|
<div class="demoBox">
|
||||||
|
<div class="controls">
|
||||||
|
<div class="control-section">
|
||||||
|
<span class="section-label">Dataset:</span>
|
||||||
|
<dees-button
|
||||||
|
@clicked=${() => switchDataset('system')}
|
||||||
|
type=${currentDataset === 'system' ? 'highlighted' : 'normal'}
|
||||||
|
>System Usage</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@clicked=${() => switchDataset('network')}
|
||||||
|
type=${currentDataset === 'network' ? 'highlighted' : 'normal'}
|
||||||
|
>Network Traffic</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@clicked=${() => switchDataset('sales')}
|
||||||
|
type=${currentDataset === 'sales' ? 'highlighted' : 'normal'}
|
||||||
|
>Sales Data</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-section">
|
||||||
|
<span class="section-label">Real-time:</span>
|
||||||
|
<dees-button @clicked=${() => startRealtime()}>Start Live</dees-button>
|
||||||
|
<dees-button @clicked=${() => stopRealtime()}>Stop Live</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-section">
|
||||||
|
<span class="section-label">Actions:</span>
|
||||||
|
<dees-button @clicked=${() => randomizeData()}>Randomize Values</dees-button>
|
||||||
|
<dees-button @clicked=${() => addRealtimeData()}>Add Point</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
<dees-chart-area
|
<dees-chart-area
|
||||||
.label=${'System Usage'}
|
.label=${datasets[currentDataset].label}
|
||||||
|
.series=${datasets[currentDataset].series}
|
||||||
|
.yAxisFormatter=${formatters[currentDataset]}
|
||||||
></dees-chart-area>
|
></dees-chart-area>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
Real-time updates work with System Usage and Network Traffic datasets •
|
||||||
|
Chart updates every 2 seconds when live mode is active •
|
||||||
|
Try switching datasets and randomizing values
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
@ -32,28 +32,47 @@ export class DeesChartArea extends DeesElement {
|
|||||||
@property()
|
@property()
|
||||||
public label: string = 'Untitled Chart';
|
public label: string = 'Untitled Chart';
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public series: ApexAxisChartSeries = [];
|
||||||
|
|
||||||
|
@property({ type: Function })
|
||||||
|
public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
|
||||||
|
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
private resizeTimeout: number;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
domtools.elementBasic.setup();
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver((entries) => {
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
// Debounce resize calls to prevent excessive updates
|
||||||
|
if (this.resizeTimeout) {
|
||||||
|
clearTimeout(this.resizeTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resizeTimeout = window.setTimeout(() => {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
if (entry.target.classList.contains('mainbox')) {
|
if (entry.target.classList.contains('mainbox') && this.chart) {
|
||||||
this.resizeChart(); // Call resizeChart when the .mainbox size changes
|
this.resizeChart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, 100); // 100ms debounce
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerStartupFunction(async () => {
|
this.registerStartupFunction(async () => {
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||||
if (mainbox) {
|
if (mainbox) {
|
||||||
this.resizeObserver.observe(mainbox); // Start observing the .mainbox element
|
this.resizeObserver.observe(mainbox);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerGarbageFunction(async () => {
|
this.registerGarbageFunction(async () => {
|
||||||
|
if (this.resizeTimeout) {
|
||||||
|
clearTimeout(this.resizeTimeout);
|
||||||
|
}
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -73,6 +92,7 @@ export class DeesChartArea extends DeesElement {
|
|||||||
height: 400px;
|
height: 400px;
|
||||||
background: #111;
|
background: #111;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartTitle {
|
.chartTitle {
|
||||||
@ -82,6 +102,7 @@ export class DeesChartArea extends DeesElement {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.chartContainer {
|
.chartContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -90,6 +111,7 @@ export class DeesChartArea extends DeesElement {
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
padding: 32px 16px 16px 0px;
|
padding: 32px 16px 16px 0px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@ -104,9 +126,32 @@ export class DeesChartArea extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const domtoolsInstance = await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
var options: ApexCharts.ApexOptions = {
|
|
||||||
series: [
|
// Wait for next animation frame to ensure layout is complete
|
||||||
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
|
||||||
|
// Get actual dimensions of the container
|
||||||
|
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||||
|
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||||
|
|
||||||
|
if (!mainbox || !chartContainer) {
|
||||||
|
console.error('Chart containers not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate initial dimensions
|
||||||
|
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||||
|
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
||||||
|
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
||||||
|
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
||||||
|
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
||||||
|
|
||||||
|
const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||||
|
const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||||
|
|
||||||
|
// Use provided series data or default demo data
|
||||||
|
const chartSeries = this.series.length > 0 ? this.series : [
|
||||||
{
|
{
|
||||||
name: 'cpu',
|
name: 'cpu',
|
||||||
data: [
|
data: [
|
||||||
@ -127,14 +172,25 @@ export class DeesChartArea extends DeesElement {
|
|||||||
{ x: '2025-01-15T19:00:00', y: 40 },
|
{ x: '2025-01-15T19:00:00', y: 40 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
|
||||||
|
var options: ApexCharts.ApexOptions = {
|
||||||
|
series: chartSeries,
|
||||||
chart: {
|
chart: {
|
||||||
width: 0, // Adjusted for responsive width
|
width: initialWidth || 100, // Use actual width or fallback
|
||||||
height: 0, // Adjusted for responsive height
|
height: initialHeight || 100, // Use actual height or fallback
|
||||||
type: 'area',
|
type: 'area',
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: false, // This line disables the toolbar
|
show: false, // This line disables the toolbar
|
||||||
},
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: true,
|
||||||
|
speed: 400,
|
||||||
|
animateGradually: {
|
||||||
|
enabled: true,
|
||||||
|
delay: 150
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -162,9 +218,7 @@ export class DeesChartArea extends DeesElement {
|
|||||||
yaxis: {
|
yaxis: {
|
||||||
min: 0,
|
min: 0,
|
||||||
labels: {
|
labels: {
|
||||||
formatter: function (val: number) {
|
formatter: this.yAxisFormatter,
|
||||||
return `${val} Mbps`; // Format Y-axis labels
|
|
||||||
},
|
|
||||||
style: {
|
style: {
|
||||||
colors: '#9e9e9e', // Label color
|
colors: '#9e9e9e', // Label color
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
@ -184,14 +238,11 @@ export class DeesChartArea extends DeesElement {
|
|||||||
x: {
|
x: {
|
||||||
format: 'dd/MM/yy HH:mm',
|
format: 'dd/MM/yy HH:mm',
|
||||||
},
|
},
|
||||||
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
|
custom: function ({ series, dataPointIndex, w }: any) {
|
||||||
// Get the x value
|
|
||||||
const xValue = w.globals.labels[dataPointIndex];
|
|
||||||
// Iterate through each series and get its value
|
// Iterate through each series and get its value
|
||||||
let tooltipContent = `<div style="padding: 10px; background: #1e1e2f; color: white; border-radius: 5px;">`;
|
let tooltipContent = `<div style="padding: 10px; background: #1e1e2f; color: white; border-radius: 5px;">`;
|
||||||
tooltipContent += ``; // `<strong>Time:</strong> ${xValue}<br/>`;
|
|
||||||
|
|
||||||
series.forEach((s, index) => {
|
series.forEach((s: number[], index: number) => {
|
||||||
const label = w.globals.seriesNames[index]; // Get series label
|
const label = w.globals.seriesNames[index]; // Get series label
|
||||||
const value = s[dataPointIndex]; // Get value at data point
|
const value = s[dataPointIndex]; // Get value at data point
|
||||||
tooltipContent += `<strong>${label}:</strong> ${value} Mbps<br/>`;
|
tooltipContent += `<strong>${label}:</strong> ${value} Mbps<br/>`;
|
||||||
@ -235,15 +286,65 @@ export class DeesChartArea extends DeesElement {
|
|||||||
};
|
};
|
||||||
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
||||||
await this.chart.render();
|
await this.chart.render();
|
||||||
|
|
||||||
|
// Give the chart a moment to fully initialize before resizing
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
await this.resizeChart();
|
await this.resizeChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updated(changedProperties: Map<string, any>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
// Update chart if series data changes
|
||||||
|
if (changedProperties.has('series') && this.chart && this.series.length > 0) {
|
||||||
|
await this.updateSeries(this.series);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update y-axis formatter if it changes
|
||||||
|
if (changedProperties.has('yAxisFormatter') && this.chart) {
|
||||||
|
await this.chart.updateOptions({
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: this.yAxisFormatter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateSeries(newSeries: ApexAxisChartSeries) {
|
||||||
|
if (!this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.chart.updateSeries(newSeries, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async appendData(seriesIndex: number, newData: any[]) {
|
||||||
|
if (!this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSeries = [...this.series];
|
||||||
|
if (currentSeries[seriesIndex]) {
|
||||||
|
currentSeries[seriesIndex].data = [...currentSeries[seriesIndex].data, ...newData];
|
||||||
|
await this.updateSeries(currentSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async resizeChart() {
|
public async resizeChart() {
|
||||||
|
if (!this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||||
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||||
|
|
||||||
|
if (!mainbox || !chartContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get computed style of the element
|
// Get computed style of the element
|
||||||
const styleMainbox = window.getComputedStyle(mainbox);
|
|
||||||
const styleChartContainer = window.getComputedStyle(chartContainer);
|
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||||
|
|
||||||
// Extract padding values
|
// Extract padding values
|
||||||
|
@ -1,6 +1,123 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
|
let intervalId: number;
|
||||||
|
|
||||||
|
const serverSources = ['Server', 'Database', 'API', 'Auth', 'Cache', 'Queue', 'WebSocket', 'Scheduler'];
|
||||||
|
|
||||||
|
const logTemplates = {
|
||||||
|
debug: [
|
||||||
|
'Loading module: {{module}}',
|
||||||
|
'Cache hit for key: {{key}}',
|
||||||
|
'SQL query executed in {{time}}ms',
|
||||||
|
'Request headers: {{headers}}',
|
||||||
|
'Environment variable loaded: {{var}}',
|
||||||
|
],
|
||||||
|
info: [
|
||||||
|
'Request received: {{method}} {{path}}',
|
||||||
|
'User {{userId}} authenticated successfully',
|
||||||
|
'Processing job {{jobId}} from queue',
|
||||||
|
'Scheduled task "{{task}}" started',
|
||||||
|
'WebSocket connection established from {{ip}}',
|
||||||
|
],
|
||||||
|
warn: [
|
||||||
|
'Slow query detected: {{query}} ({{time}}ms)',
|
||||||
|
'Memory usage at {{percent}}%',
|
||||||
|
'Rate limit approaching for IP {{ip}}',
|
||||||
|
'Deprecated API endpoint called: {{endpoint}}',
|
||||||
|
'Certificate expires in {{days}} days',
|
||||||
|
],
|
||||||
|
error: [
|
||||||
|
'Database connection lost: {{error}}',
|
||||||
|
'Failed to process request: {{error}}',
|
||||||
|
'Authentication failed for user {{user}}',
|
||||||
|
'File not found: {{path}}',
|
||||||
|
'Service unavailable: {{service}}',
|
||||||
|
],
|
||||||
|
success: [
|
||||||
|
'Server started successfully on port {{port}}',
|
||||||
|
'Database migration completed',
|
||||||
|
'Backup completed: {{size}} MB',
|
||||||
|
'SSL certificate renewed',
|
||||||
|
'Health check passed: all systems operational',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRandomLog = () => {
|
||||||
|
const logElement = (window as any).__demoLogElement;
|
||||||
|
if (!logElement) {
|
||||||
|
console.warn('Log element not ready yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const levels: Array<'debug' | 'info' | 'warn' | 'error' | 'success'> = ['debug', 'info', 'warn', 'error', 'success'];
|
||||||
|
const weights = [0.2, 0.5, 0.15, 0.1, 0.05]; // Weighted probability
|
||||||
|
|
||||||
|
const random = Math.random();
|
||||||
|
let cumulative = 0;
|
||||||
|
let level: typeof levels[0] = 'info';
|
||||||
|
|
||||||
|
for (let i = 0; i < weights.length; i++) {
|
||||||
|
cumulative += weights[i];
|
||||||
|
if (random < cumulative) {
|
||||||
|
level = levels[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = serverSources[Math.floor(Math.random() * serverSources.length)];
|
||||||
|
const templates = logTemplates[level];
|
||||||
|
const template = templates[Math.floor(Math.random() * templates.length)];
|
||||||
|
|
||||||
|
// Replace placeholders with random values
|
||||||
|
const message = template
|
||||||
|
.replace('{{module}}', ['express', 'mongoose', 'redis', 'socket.io'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{key}}', 'user:' + Math.floor(Math.random() * 1000))
|
||||||
|
.replace('{{time}}', String(Math.floor(Math.random() * 500) + 50))
|
||||||
|
.replace('{{headers}}', 'Content-Type: application/json, Authorization: Bearer ...')
|
||||||
|
.replace('{{var}}', ['NODE_ENV', 'DATABASE_URL', 'API_KEY', 'PORT'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{method}}', ['GET', 'POST', 'PUT', 'DELETE'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{path}}', ['/api/users', '/api/auth/login', '/api/products', '/health'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{userId}}', String(Math.floor(Math.random() * 10000)))
|
||||||
|
.replace('{{jobId}}', 'job_' + Math.random().toString(36).substring(2, 11))
|
||||||
|
.replace('{{task}}', ['cleanup', 'backup', 'report-generation', 'cache-refresh'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{ip}}', `192.168.1.${Math.floor(Math.random() * 255)}`)
|
||||||
|
.replace('{{query}}', 'SELECT * FROM users WHERE ...')
|
||||||
|
.replace('{{percent}}', String(Math.floor(Math.random() * 30) + 70))
|
||||||
|
.replace('{{endpoint}}', '/api/v1/legacy')
|
||||||
|
.replace('{{days}}', String(Math.floor(Math.random() * 30) + 1))
|
||||||
|
.replace('{{error}}', ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'][Math.floor(Math.random() * 3)])
|
||||||
|
.replace('{{user}}', 'user_' + Math.floor(Math.random() * 1000))
|
||||||
|
.replace('{{service}}', ['Redis', 'MongoDB', 'ElasticSearch'][Math.floor(Math.random() * 3)])
|
||||||
|
.replace('{{port}}', String(3000 + Math.floor(Math.random() * 10)))
|
||||||
|
.replace('{{size}}', String(Math.floor(Math.random() * 500) + 100));
|
||||||
|
|
||||||
|
logElement.addLog(level, message, source);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startSimulation = () => {
|
||||||
|
if (!intervalId) {
|
||||||
|
// Generate logs at random intervals between 500ms and 2500ms
|
||||||
|
const scheduleNext = () => {
|
||||||
|
generateRandomLog();
|
||||||
|
const nextDelay = Math.random() * 2000 + 500;
|
||||||
|
intervalId = window.setTimeout(() => {
|
||||||
|
if (intervalId) {
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
}, nextDelay);
|
||||||
|
};
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSimulation = () => {
|
||||||
|
if (intervalId) {
|
||||||
|
window.clearTimeout(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.demoBox {
|
.demoBox {
|
||||||
@ -9,11 +126,31 @@ export const demoFunc = () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="demoBox">
|
<div class="demoBox">
|
||||||
|
<div class="controls">
|
||||||
|
<dees-button @clicked=${() => generateRandomLog()}>Add Single Log</dees-button>
|
||||||
|
<dees-button @clicked=${() => startSimulation()}>Start Simulation</dees-button>
|
||||||
|
<dees-button @clicked=${() => stopSimulation()}>Stop Simulation</dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="info">Simulating realistic server logs with various levels and sources</div>
|
||||||
<dees-chart-log
|
<dees-chart-log
|
||||||
.label=${'Event Log'}
|
.label=${'Production Server Logs'}
|
||||||
></dees-chart-log>
|
></dees-chart-log>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -5,15 +5,12 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
property,
|
property,
|
||||||
state,
|
|
||||||
type CSSResult,
|
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { demoFunc } from './dees-chart-log.demo.js';
|
import { demoFunc } from './dees-chart-log.demo.js';
|
||||||
|
|
||||||
import ApexCharts from 'apexcharts';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -21,69 +18,308 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ILogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: 'debug' | 'info' | 'warn' | 'error' | 'success';
|
||||||
|
message: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('dees-chart-log')
|
@customElement('dees-chart-log')
|
||||||
export class DeesChartLog extends DeesElement {
|
export class DeesChartLog extends DeesElement {
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
// instance
|
|
||||||
@state()
|
|
||||||
public chart: ApexCharts;
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public label: string = 'Untitled Chart';
|
public label: string = 'Server Logs';
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public logEntries: ILogEntry[] = [];
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public autoScroll: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxEntries: number = 1000;
|
||||||
|
|
||||||
|
private logContainer: HTMLDivElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
domtools.elementBasic.setup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
font-family: 'Geist Sans', sans-serif;
|
font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.mainbox {
|
.mainbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
background: #222;
|
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 32px 16px 16px 0px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartTitle {
|
.header {
|
||||||
position: absolute;
|
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||||
top: 0;
|
padding: 8px 16px;
|
||||||
left: 0;
|
border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||||
width: 100%;
|
display: flex;
|
||||||
text-align: center;
|
justify-content: space-between;
|
||||||
padding-top: 16px;
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.chartContainer {
|
|
||||||
position: relative;
|
.title {
|
||||||
width: 100%;
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#212529', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: ${cssManager.bdTheme('#495057', '#ccc')};
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')};
|
||||||
|
border-color: ${cssManager.bdTheme('#adb5bd', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button.active {
|
||||||
|
background: ${cssManager.bdTheme('#007bff', '#4a4a4a')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEntry {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: flex;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
margin-right: 8px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.debug {
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#999')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.info {
|
||||||
|
color: ${cssManager.bdTheme('#0066cc', '#4a9eff')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.warn {
|
||||||
|
color: ${cssManager.bdTheme('#ff8800', '#ffb84a')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.error {
|
||||||
|
color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.success {
|
||||||
|
color: ${cssManager.bdTheme('#28a745', '#4aff88')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#888')};
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: ${cssManager.bdTheme('#212529', '#ddd')};
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
.logContainer::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer::-webkit-scrollbar-track {
|
||||||
|
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer::-webkit-scrollbar-thumb {
|
||||||
|
background: ${cssManager.bdTheme('#adb5bd', '#444')};
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${cssManager.bdTheme('#6c757d', '#555')};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html` <div class="mainbox">
|
return html`
|
||||||
<div class="chartTitle">${this.label}</div>
|
<div class="mainbox">
|
||||||
<div class="chartContainer"></div>
|
<div class="header">
|
||||||
</div> `;
|
<div class="title">${this.label}</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button
|
||||||
|
class="control-button ${this.autoScroll ? 'active' : ''}"
|
||||||
|
@click=${() => { this.autoScroll = !this.autoScroll; }}
|
||||||
|
>
|
||||||
|
Auto Scroll
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-button"
|
||||||
|
@click=${() => { this.clearLogs(); }}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="logContainer">
|
||||||
|
${this.logEntries.length === 0
|
||||||
|
? html`<div class="empty-state">No logs to display</div>`
|
||||||
|
: this.logEntries.map(entry => this.renderLogEntry(entry))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLogEntry(entry: ILogEntry): TemplateResult {
|
||||||
|
const timestamp = new Date(entry.timestamp).toLocaleTimeString('en-US', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
fractionalSecondDigits: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="logEntry">
|
||||||
|
<span class="timestamp">${timestamp}</span>
|
||||||
|
<span class="level ${entry.level}">${entry.level}</span>
|
||||||
|
${entry.source ? html`<span class="source">[${entry.source}]</span>` : ''}
|
||||||
|
<span class="message">${entry.message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const domtoolsInstance = await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
|
this.logContainer = this.shadowRoot.querySelector('.logContainer');
|
||||||
|
|
||||||
}
|
// Initialize with demo server logs
|
||||||
|
const demoLogs: ILogEntry[] = [
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'Server started on port 3000', source: 'Server' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'debug', message: 'Loading configuration from /etc/app/config.json', source: 'Config' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'Connected to MongoDB at mongodb://localhost:27017', source: 'Database' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'success', message: 'Database connection established successfully', source: 'Database' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'warn', message: 'No SSL certificate found, using self-signed certificate', source: 'Security' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'API routes initialized: GET /api/users, POST /api/users, DELETE /api/users/:id', source: 'Router' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'debug', message: 'Middleware stack: cors, bodyParser, authentication, errorHandler', source: 'Middleware' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'WebSocket server listening on ws://localhost:3001', source: 'WebSocket' },
|
||||||
|
];
|
||||||
|
|
||||||
public async updateLog() {
|
this.logEntries = demoLogs;
|
||||||
|
this.scrollToBottom();
|
||||||
|
|
||||||
|
// For demo purposes, store reference globally
|
||||||
|
if ((window as any).__demoLogElement === undefined) {
|
||||||
|
(window as any).__demoLogElement = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateLog(entries?: ILogEntry[]) {
|
||||||
|
if (entries) {
|
||||||
|
// Add new entries
|
||||||
|
this.logEntries = [...this.logEntries, ...entries];
|
||||||
|
|
||||||
|
// Trim if exceeds max entries
|
||||||
|
if (this.logEntries.length > this.maxEntries) {
|
||||||
|
this.logEntries = this.logEntries.slice(-this.maxEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger re-render
|
||||||
|
this.requestUpdate();
|
||||||
|
|
||||||
|
// Auto-scroll if enabled
|
||||||
|
await this.updateComplete;
|
||||||
|
if (this.autoScroll) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearLogs() {
|
||||||
|
this.logEntries = [];
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToBottom() {
|
||||||
|
if (this.logContainer) {
|
||||||
|
this.logContainer.scrollTop = this.logContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addLog(level: ILogEntry['level'], message: string, source?: string) {
|
||||||
|
const newEntry: ILogEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
source
|
||||||
|
};
|
||||||
|
this.updateLog([newEntry]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,262 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import { DeesToast } from './dees-toast.js';
|
||||||
|
import './dees-button.js';
|
||||||
|
|
||||||
export const demoFunc = async () => {
|
export const demoFunc = async () => {
|
||||||
return html`<dees-toast></dees-toast>`;
|
return 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>
|
||||||
|
`;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { customElement, DeesElement, type TemplateResult, html, type CSSResult, } from '@design.estate/dees-element';
|
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { demoFunc } from './dees-toast.demo.js';
|
import { demoFunc } from './dees-toast.demo.js';
|
||||||
@ -9,20 +9,317 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')
|
@customElement('dees-toast')
|
||||||
export class DeesToast extends DeesElement {
|
export class DeesToast extends DeesElement {
|
||||||
|
// STATIC
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
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: 10000;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 })
|
||||||
|
public message: string = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public type: ToastType = 'info';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public duration: number = 3000;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public isVisible: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
domtools.elementBasic.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public static styles = [
|
||||||
return html`
|
cssManager.defaultStyles,
|
||||||
${domtools.elementBasic.styles}
|
css`
|
||||||
<style></style>
|
:host {
|
||||||
|
display: block;
|
||||||
|
pointer-events: auto;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user