Compare commits

...

11 Commits

Author SHA1 Message Date
346abfa685 1.8.4
Some checks failed
Default (tags) / security (push) Failing after 32s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:11:36 +00:00
f1123f319f fix(dees-catalog): downgrade @webcontainer/api to version 1.2.0 for compatibility 2025-06-12 11:11:21 +00:00
ac15da9c82 1.8.3
Some checks failed
Default (tags) / security (push) Failing after 34s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:09:27 +00:00
b9432c8489 feat(dees-chart-area): Enhance chart component with dynamic datasets, real-time updates, and improved demo features 2025-06-12 11:09:14 +00:00
b35b1fbae7 1.8.2
Some checks failed
Default (tags) / security (push) Failing after 36s
Default (tags) / test (push) Failing after 21s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:01:20 +00:00
e39590df2c fix(dees-chart-area): Improve resize handling and initial rendering for better responsiveness
fix(dees-chart-log): Simplify firstUpdated method by removing unnecessary variable
2025-06-12 11:00:33 +00:00
fad7fda2a6 feat(dees-chart-log): Enhance log component with realistic log simulation and improved UI controls 2025-06-12 10:44:21 +00:00
987f557c60 Enhance DeesToast component with new features and improved demo
- Updated README to reflect new toast positions and convenience methods.
- Expanded demo functionality to showcase various toast types, positions, and durations.
- Added programmatic control for toast dismissal and multiple toast notifications.
- Introduced new toast positions: top-center and bottom-center.
- Implemented a progress bar for auto-dismiss functionality.
- Improved styling and animations for better user experience.
2025-06-12 09:33:46 +00:00
4eef9fc731 1.8.1
Some checks failed
Default (tags) / security (push) Failing after 25s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-10 18:58:05 +00:00
cd86001713 fix(dees-statsgrid): Adjust stats grid styling for better alignment and improved visualizations in gauge and trend tiles. 2025-06-10 18:58:05 +00:00
f7e4582fde feat(dees-statsgrid): Add dees-statsgrid component with demo and integration in the main export 2025-06-10 18:29:37 +00:00
15 changed files with 3655 additions and 976 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 2025-06-10 - 1.8.1 - fix(dees-statsgrid)
Adjust stats grid styling for better alignment and improved visualizations in gauge and trend tiles.
- Center-align tile header elements by setting align-items to center and ensuring full width.
- Increase tile content height to 90px and center its content.
- Update gauge visualization: reduce circle radius from 40 to 30, adjust stroke dasharray (from 251.2 to 188.5), and decrease gauge text font size.
- Refine trend chart layout: set trend-svg height to 40px, center trend value and adjust typography to larger, bolder text.
- Ensure overall grid responsiveness with adjusted gap and column sizing.
## 2025-04-25 - 1.8.0 - feat(dees-pagination) ## 2025-04-25 - 1.8.0 - feat(dees-pagination)
Add new pagination component to the library along with its demo and integration in the main export. Add new pagination component to the library along with its demo and integration in the main export.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.8.0", "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

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,43 @@
!!! Please pay attention to the following points when writing the readme: !!! !!! Please pay attention to the following points when writing the readme: !!!
* 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)

242
readme.md
View File

@@ -15,7 +15,7 @@ npm install @design.estate/dees-catalog
| Core UI | `DeesButton`, `DeesBadge`, `DeesChips`, `DeesIcon`, `DeesLabel`, `DeesSpinner`, `DeesToast` | | Core UI | `DeesButton`, `DeesBadge`, `DeesChips`, `DeesIcon`, `DeesLabel`, `DeesSpinner`, `DeesToast` |
| Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadio`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesFormSubmit` | | Forms | `DeesForm`, `DeesInputText`, `DeesInputCheckbox`, `DeesInputDropdown`, `DeesInputRadio`, `DeesInputFileupload`, `DeesInputIban`, `DeesInputPhone`, `DeesInputQuantitySelector`, `DeesInputMultitoggle`, `DeesFormSubmit` |
| Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesMobileNavigation` | | Layout | `DeesAppuiBase`, `DeesAppuiMainmenu`, `DeesAppuiMainselector`, `DeesAppuiMaincontent`, `DeesAppuiAppbar`, `DeesMobileNavigation` |
| Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf` | | Data Display | `DeesTable`, `DeesDataviewCodebox`, `DeesDataviewStatusobject`, `DeesPdf`, `DeesStatsGrid` |
| Visualization | `DeesChartArea`, `DeesChartLog` | | Visualization | `DeesChartArea`, `DeesChartLog` |
| Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` | | Dialogs & Overlays | `DeesModal`, `DeesContextmenu`, `DeesSpeechbubble`, `DeesWindowlayer` |
| Navigation | `DeesStepper`, `DeesProgressbar` | | Navigation | `DeesStepper`, `DeesProgressbar` |
@@ -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`
@@ -528,6 +553,213 @@ Key Features:
- Responsive layout - Responsive layout
- Loading states - Loading states
#### `DeesStatsGrid`
A responsive grid component for displaying statistical data with various visualization types including numbers, gauges, percentages, and trends.
```typescript
<dees-statsgrid
.tiles=${[
{
id: 'revenue',
title: 'Total Revenue',
value: 125420,
unit: '$',
type: 'number',
icon: 'faDollarSign',
description: '+12.5% from last month',
color: '#22c55e',
actions: [
{
name: 'View Details',
iconName: 'faChartLine',
action: async () => {
console.log('Viewing revenue details');
}
},
{
name: 'Export Data',
iconName: 'faFileExport',
action: async () => {
console.log('Exporting revenue data');
}
}
]
},
{
id: 'cpu',
title: 'CPU Usage',
value: 73,
type: 'gauge',
icon: 'faMicrochip',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#22c55e' },
{ value: 60, color: '#f59e0b' },
{ value: 80, color: '#ef4444' }
]
}
},
{
id: 'storage',
title: 'Storage Used',
value: 65,
type: 'percentage',
icon: 'faHardDrive',
description: '650 GB of 1 TB',
color: '#3b82f6'
},
{
id: 'requests',
title: 'API Requests',
value: '1.2k',
unit: '/min',
type: 'trend',
icon: 'faServer',
trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92]
},
{
id: 'uptime',
title: 'System Uptime',
value: '99.95%',
type: 'text',
icon: 'faCheckCircle',
color: '#22c55e',
description: 'Last 30 days'
}
]}
.gridActions=${[
{
name: 'Refresh',
iconName: 'faSync',
action: async () => {
console.log('Refreshing stats...');
}
},
{
name: 'Export Report',
iconName: 'faFileExport',
action: async () => {
console.log('Exporting stats report...');
}
}
]}
.minTileWidth=${250} // Minimum tile width in pixels
.gap=${16} // Gap between tiles in pixels
></dees-statsgrid>
```
Key Features:
- Auto-responsive grid layout with configurable minimum tile width
- Multiple tile types for different data visualizations
- Full theme support (light/dark mode)
- Interactive tiles with action support
- Grid-level and tile-level actions
- Smooth animations and transitions
- Icon support for visual hierarchy
Tile Types:
1. **`number`** - Display numeric values with optional units
- Large, prominent value display
- Optional unit display
- Custom color support
- Description text
2. **`gauge`** - Circular gauge visualization
- Min/max value configuration
- Color thresholds for visual alerts
- Animated value transitions
- Compact circular design
3. **`percentage`** - Progress bar visualization
- Horizontal progress bar
- Percentage display overlay
- Custom color support
- Ideal for capacity metrics
4. **`trend`** - Mini sparkline chart
- Array of numeric values for trend data
- Area chart visualization
- Current value display
- Responsive SVG rendering
5. **`text`** - Simple text display
- Flexible text content
- Custom color support
- Ideal for status messages
Action System:
- **Grid Actions**: Displayed as buttons in the grid header
- Apply to the entire stats grid
- Use standard `dees-button` components
- Support icons and text
- **Tile Actions**: Context-specific actions per tile
- Single action: Direct click on tile
- Multiple actions: Right-click context menu
- Actions access tile data through closures
- Consistent with other library components
Configuration Options:
- `tiles`: Array of `IStatsTile` objects defining the grid content
- `gridActions`: Array of actions for the entire grid
- `minTileWidth`: Minimum width for tiles (default: 250px)
- `gap`: Space between tiles (default: 16px)
Best Practices:
1. **Data Organization**
- Group related metrics together
- Use consistent units and scales
- Provide meaningful descriptions
- Choose appropriate tile types for data
2. **Visual Hierarchy**
- Use colors strategically for alerts
- Include relevant icons
- Keep titles concise
- Balance tile types for visual interest
3. **Interactivity**
- Provide relevant actions for detailed views
- Use tile actions for item-specific operations
- Use grid actions for global operations
- Keep action names clear and concise
4. **Performance**
- Update only changed tiles
- Use reasonable update intervals
- Batch updates when possible
- Consider data volume for trends
Common Use Cases:
- System monitoring dashboards
- Business intelligence displays
- Performance metrics
- Resource utilization
- Real-time statistics
- KPI tracking
Integration Example:
```typescript
// Real-time updates
setInterval(() => {
const grid = document.querySelector('dees-statsgrid');
const updatedTiles = [...grid.tiles];
// Update specific tile
const cpuTile = updatedTiles.find(t => t.id === 'cpu');
cpuTile.value = Math.round(Math.random() * 100);
// Update trend data
const trendTile = updatedTiles.find(t => t.id === 'requests');
trendTile.trendData = [...trendTile.trendData.slice(1),
Math.round(Math.random() * 100)];
grid.tiles = updatedTiles;
}, 3000);
```
### Visualization Components ### Visualization Components
#### `DeesChartArea` #### `DeesChartArea`

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '1.8.0', version: '1.8.1',
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.'
} }

View File

@@ -1,21 +1,285 @@
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>
.demoBox { ${css`
position: relative; .demoBox {
background: #000000; position: relative;
height: 100%; background: #000000;
width: 100%; height: 100%;
padding: 40px; width: 100%;
box-sizing: border-box; padding: 40px;
} 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">
<dees-chart-area <div class="controls">
.label=${'System Usage'} <div class="control-section">
></dees-chart-area> <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
.label=${datasets[currentDataset].label}
.series=${datasets[currentDataset].series}
.yAxisFormatter=${formatters[currentDataset]}
></dees-chart-area>
</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> </div>
`; `;
}; };

View File

@@ -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) => {
for (let entry of entries) { // Debounce resize calls to prevent excessive updates
if (entry.target.classList.contains('mainbox')) { if (this.resizeTimeout) {
this.resizeChart(); // Call resizeChart when the .mainbox size changes clearTimeout(this.resizeTimeout);
}
} }
this.resizeTimeout = window.setTimeout(() => {
for (let entry of entries) {
if (entry.target.classList.contains('mainbox') && this.chart) {
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,37 +126,71 @@ export class DeesChartArea extends DeesElement {
} }
public async firstUpdated() { public async firstUpdated() {
const domtoolsInstance = await this.domtoolsPromise; await this.domtoolsPromise;
// 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',
data: [
{ x: '2025-01-15T03:00:00', y: 25 },
{ x: '2025-01-15T07:00:00', y: 30 },
{ x: '2025-01-15T11:00:00', y: 20 },
{ x: '2025-01-15T15:00:00', y: 35 },
{ x: '2025-01-15T19:00:00', y: 25 },
],
},
{
name: 'memory',
data: [
{ x: '2025-01-15T03:00:00', y: 10 },
{ x: '2025-01-15T07:00:00', y: 12 },
{ x: '2025-01-15T11:00:00', y: 10 },
{ x: '2025-01-15T15:00:00', y: 30 },
{ x: '2025-01-15T19:00:00', y: 40 },
],
},
];
var options: ApexCharts.ApexOptions = { var options: ApexCharts.ApexOptions = {
series: [ series: chartSeries,
{
name: 'cpu',
data: [
{ x: '2025-01-15T03:00:00', y: 25 },
{ x: '2025-01-15T07:00:00', y: 30 },
{ x: '2025-01-15T11:00:00', y: 20 },
{ x: '2025-01-15T15:00:00', y: 35 },
{ x: '2025-01-15T19:00:00', y: 25 },
],
},
{
name: 'memory',
data: [
{ x: '2025-01-15T03:00:00', y: 10 },
{ x: '2025-01-15T07:00:00', y: 12 },
{ x: '2025-01-15T11:00:00', y: 10 },
{ x: '2025-01-15T15:00:00', y: 30 },
{ x: '2025-01-15T19:00:00', y: 40 },
],
},
],
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

View File

@@ -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>
`; `;

View File

@@ -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' },
];
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() { 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]);
} }
} }

View File

@@ -0,0 +1,389 @@
import { html, cssManager } from '@design.estate/dees-element';
import type { IStatsTile } from './dees-statsgrid.js';
export const demoFunc = () => {
// Demo data with different tile types
const demoTiles: IStatsTile[] = [
{
id: 'revenue',
title: 'Total Revenue',
value: 125420,
unit: '$',
type: 'number',
icon: 'faDollarSign',
description: '+12.5% from last month',
color: '#22c55e',
actions: [
{
name: 'View Details',
iconName: 'faChartLine',
action: async () => {
console.log('Viewing revenue details for tile:', 'revenue');
console.log('Current value:', 125420);
alert(`Revenue Details: $125,420 (+12.5%)`);
}
},
{
name: 'Export Data',
iconName: 'faFileExport',
action: async () => {
console.log('Exporting revenue data');
alert('Revenue data exported to CSV');
}
}
]
},
{
id: 'users',
title: 'Active Users',
value: 3847,
type: 'number',
icon: 'faUsers',
description: '324 new this week',
actions: [
{
name: 'View User List',
iconName: 'faList',
action: async () => {
console.log('Viewing user list');
}
}
]
},
{
id: 'cpu',
title: 'CPU Usage',
value: 73,
type: 'gauge',
icon: 'faMicrochip',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#22c55e' },
{ value: 60, color: '#f59e0b' },
{ value: 80, color: '#ef4444' }
]
}
},
{
id: 'storage',
title: 'Storage Used',
value: 65,
type: 'percentage',
icon: 'faHardDrive',
description: '650 GB of 1 TB',
color: '#3b82f6'
},
{
id: 'memory',
title: 'Memory Usage',
value: 45,
type: 'gauge',
icon: 'faMemory',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#22c55e' },
{ value: 70, color: '#f59e0b' },
{ value: 90, color: '#ef4444' }
]
}
},
{
id: 'requests',
title: 'API Requests',
value: '1.2k',
unit: '/min',
type: 'trend',
icon: 'faServer',
trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92]
},
{
id: 'uptime',
title: 'System Uptime',
value: '99.95%',
type: 'text',
icon: 'faCheckCircle',
color: '#22c55e',
description: 'Last 30 days'
},
{
id: 'latency',
title: 'Response Time',
value: 142,
unit: 'ms',
type: 'trend',
icon: 'faClock',
trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142],
description: 'P95 latency'
},
{
id: 'errors',
title: 'Error Rate',
value: 0.03,
unit: '%',
type: 'number',
icon: 'faExclamationTriangle',
color: '#ef4444',
actions: [
{
name: 'View Error Logs',
iconName: 'faFileAlt',
action: async () => {
console.log('Viewing error logs');
}
}
]
}
];
// Grid actions for the demo
const gridActions = [
{
name: 'Refresh',
iconName: 'faSync',
action: async () => {
console.log('Refreshing stats...');
// Simulate refresh animation
const grid = document.querySelector('dees-statsgrid');
if (grid) {
grid.style.opacity = '0.5';
setTimeout(() => {
grid.style.opacity = '1';
}, 500);
}
}
},
{
name: 'Export Report',
iconName: 'faFileExport',
action: async () => {
console.log('Exporting stats report...');
}
},
{
name: 'Settings',
iconName: 'faCog',
action: async () => {
console.log('Opening settings...');
}
}
];
return html`
<style>
.demo-container {
padding: 32px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
min-height: 100vh;
}
.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;
}
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
padding: 8px 16px;
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
border-radius: 8px;
cursor: pointer;
z-index: 100;
}
</style>
<div class="demo-container">
<button class="theme-toggle" @click=${() => {
document.body.classList.toggle('bright');
}}>Toggle Theme</button>
<div class="demo-section">
<h2 class="demo-title">Full Featured Stats Grid</h2>
<p class="demo-description">
A comprehensive dashboard with various tile types, actions, and real-time updates.
</p>
<dees-statsgrid
.tiles=${demoTiles}
.gridActions=${gridActions}
.minTileWidth=${250}
.gap=${16}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Compact Grid (Smaller Tiles)</h2>
<p class="demo-description">
Same data displayed with smaller minimum tile width for more compact layouts.
</p>
<dees-statsgrid
.tiles=${demoTiles.slice(0, 6)}
.minTileWidth=${180}
.gap=${12}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Simple Metrics (No Actions)</h2>
<p class="demo-description">
Clean display without interactive elements for pure visualization.
</p>
<dees-statsgrid
.tiles=${[
{
id: 'metric1',
title: 'Total Sales',
value: 48293,
type: 'number',
icon: 'faShoppingCart'
},
{
id: 'metric2',
title: 'Conversion Rate',
value: 3.4,
unit: '%',
type: 'number',
icon: 'faChartLine'
},
{
id: 'metric3',
title: 'Avg Order Value',
value: 127.50,
unit: '$',
type: 'number',
icon: 'faReceipt'
},
{
id: 'metric4',
title: 'Customer Satisfaction',
value: 92,
type: 'percentage',
icon: 'faSmile',
color: '#22c55e'
}
]}
.minTileWidth=${220}
.gap=${16}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Performance Monitoring</h2>
<p class="demo-description">
Real-time performance metrics with gauge visualizations and thresholds.
</p>
<dees-statsgrid
.tiles=${[
{
id: 'perf1',
title: 'Database Load',
value: 42,
type: 'gauge',
icon: 'faDatabase',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#10b981' },
{ value: 50, color: '#f59e0b' },
{ value: 75, color: '#ef4444' }
]
}
},
{
id: 'perf2',
title: 'Network I/O',
value: 856,
unit: 'MB/s',
type: 'trend',
icon: 'faNetworkWired',
trendData: [720, 780, 823, 845, 812, 876, 856]
},
{
id: 'perf3',
title: 'Cache Hit Rate',
value: 94.2,
type: 'percentage',
icon: 'faBolt',
color: '#3b82f6'
},
{
id: 'perf4',
title: 'Active Connections',
value: 1428,
type: 'number',
icon: 'faLink',
description: 'Peak: 2,100'
}
]}
.gridActions=${[
{
name: 'Auto Refresh',
iconName: 'faPlay',
action: async () => {
console.log('Starting auto refresh...');
}
}
]}
.minTileWidth=${280}
.gap=${20}
></dees-statsgrid>
</div>
<script>
// Simulate real-time updates
setInterval(() => {
const grids = document.querySelectorAll('dees-statsgrid');
grids.forEach(grid => {
if (grid.tiles && grid.tiles.length > 0) {
// Update some random values
const updatedTiles = [...grid.tiles];
// Update trends with new data point
updatedTiles.forEach(tile => {
if (tile.type === 'trend' && tile.trendData) {
tile.trendData = [...tile.trendData.slice(1),
tile.trendData[tile.trendData.length - 1] + Math.random() * 10 - 5
];
}
// Randomly update some numeric values
if (tile.type === 'number' && Math.random() > 0.7) {
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
tile.value = Math.round(currentValue + (Math.random() * 10 - 5));
}
// Update gauge values
if (tile.type === 'gauge' && Math.random() > 0.5) {
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const newValue = currentValue + (Math.random() * 10 - 5);
tile.value = Math.max(tile.gaugeOptions?.min || 0,
Math.min(tile.gaugeOptions?.max || 100, Math.round(newValue)));
}
});
grid.tiles = updatedTiles;
}
});
}, 3000);
</script>
</div>
`;
};

View File

@@ -0,0 +1,518 @@
import { demoFunc } from './dees-statsgrid.demo.js';
import * as plugins from './00plugins.js';
import {
customElement,
html,
DeesElement,
property,
state,
css,
unsafeCSS,
cssManager,
} from '@design.estate/dees-element';
import type { TemplateResult } from '@design.estate/dees-element';
import './dees-icon.js';
import './dees-contextmenu.js';
import './dees-button.js';
declare global {
interface HTMLElementTagNameMap {
'dees-statsgrid': DeesStatsGrid;
}
}
export interface IStatsTile {
id: string;
title: string;
value: number | string;
unit?: string;
type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text';
// For gauge type
gaugeOptions?: {
min: number;
max: number;
thresholds?: Array<{value: number; color: string}>;
};
// For trend type
trendData?: number[];
// Visual customization
color?: string;
icon?: string;
description?: string;
// Tile-specific actions
actions?: plugins.tsclass.website.IMenuItem[];
}
@customElement('dees-statsgrid')
export class DeesStatsGrid extends DeesElement {
public static demo = demoFunc;
@property({ type: Array })
public tiles: IStatsTile[] = [];
@property({ type: Number })
public minTileWidth: number = 250;
@property({ type: Number })
public gap: number = 16;
@property({ type: Array })
public gridActions: plugins.tsclass.website.IMenuItem[] = [];
@state()
private contextMenuVisible = false;
@state()
private contextMenuPosition = { x: 0, y: 0 };
@state()
private contextMenuActions: plugins.tsclass.website.IMenuItem[] = [];
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
width: 100%;
}
.grid-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: ${unsafeCSS(16)}px;
min-height: 32px;
}
.grid-title {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.grid-actions {
display: flex;
gap: 8px;
}
.grid-actions dees-button {
font-size: 14px;
min-width: auto;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(${unsafeCSS(250)}px, 1fr));
gap: ${unsafeCSS(16)}px;
width: 100%;
}
.stats-tile {
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
.stats-tile:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')};
}
.stats-tile.clickable {
cursor: pointer;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
width: 100%;
}
.tile-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#666', '#aaa')};
margin: 0;
}
.tile-icon {
opacity: 0.6;
}
.tile-content {
height: 90px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.tile-value {
font-size: 32px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
line-height: 1.2;
display: flex;
align-items: baseline;
justify-content: center;
gap: 6px;
width: 100%;
}
.tile-unit {
font-size: 18px;
font-weight: 400;
color: ${cssManager.bdTheme('#666', '#aaa')};
}
.tile-description {
font-size: 12px;
color: ${cssManager.bdTheme('#888', '#777')};
margin-top: 8px;
}
.gauge-container {
width: 100%;
height: 80px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.gauge-svg {
width: 100%;
height: 100%;
}
.gauge-background {
fill: none;
stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
stroke-width: 6;
}
.gauge-fill {
fill: none;
stroke-width: 6;
stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease;
}
.gauge-text {
fill: ${cssManager.bdTheme('#333', '#fff')};
font-size: 18px;
font-weight: 600;
text-anchor: middle;
}
.percentage-container {
width: 100%;
height: 24px;
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
border-radius: 12px;
overflow: hidden;
position: relative;
}
.percentage-fill {
height: 100%;
background: ${cssManager.bdTheme('#0084ff', '#0066cc')};
transition: width 0.5s ease;
border-radius: 12px;
}
.percentage-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.trend-container {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
}
.trend-svg {
width: 100%;
height: 40px;
flex-shrink: 0;
}
.trend-line {
fill: none;
stroke: ${cssManager.bdTheme('#0084ff', '#0066cc')};
stroke-width: 2;
}
.trend-area {
fill: ${cssManager.bdTheme('rgba(0, 132, 255, 0.1)', 'rgba(0, 102, 204, 0.2)')};
}
.text-value {
font-size: 32px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.trend-value {
font-size: 32px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
display: flex;
align-items: baseline;
gap: 6px;
}
.trend-value .tile-unit {
font-size: 18px;
}
dees-contextmenu {
position: fixed;
z-index: 1000;
}
`,
];
constructor() {
super();
}
public render(): TemplateResult {
return html`
${this.gridActions.length > 0 ? html`
<div class="grid-header">
<div class="grid-title">Statistics</div>
<div class="grid-actions">
${this.gridActions.map(action => html`
<dees-button @clicked=${() => this.handleGridAction(action)}>
${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''}
${action.name}
</dees-button>
`)}
</div>
</div>
` : ''}
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(${this.minTileWidth}px, 1fr)); gap: ${this.gap}px;">
${this.tiles.map(tile => this.renderTile(tile))}
</div>
${this.contextMenuVisible ? html`
<dees-contextmenu
.x=${this.contextMenuPosition.x}
.y=${this.contextMenuPosition.y}
.menuItems=${this.contextMenuActions}
@clicked=${() => this.contextMenuVisible = false}
></dees-contextmenu>
` : ''}
`;
}
private renderTile(tile: IStatsTile): TemplateResult {
const hasActions = tile.actions && tile.actions.length > 0;
const clickable = hasActions && tile.actions.length === 1;
return html`
<div
class="stats-tile ${clickable ? 'clickable' : ''}"
@click=${clickable ? () => this.handleTileAction(tile.actions![0], tile) : undefined}
@contextmenu=${hasActions ? (e: MouseEvent) => this.showContextMenu(e, tile) : undefined}
>
<div class="tile-header">
<h3 class="tile-title">${tile.title}</h3>
${tile.icon ? html`
<dees-icon class="tile-icon" .iconFA=${tile.icon} size="small"></dees-icon>
` : ''}
</div>
<div class="tile-content">
${this.renderTileContent(tile)}
</div>
${tile.description ? html`
<div class="tile-description">${tile.description}</div>
` : ''}
</div>
`;
}
private renderTileContent(tile: IStatsTile): TemplateResult {
switch (tile.type) {
case 'number':
return html`
<div class="tile-value" style="${tile.color ? `color: ${tile.color}` : ''}">
<span>${tile.value}</span>
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''}
</div>
`;
case 'gauge':
return this.renderGauge(tile);
case 'percentage':
return this.renderPercentage(tile);
case 'trend':
return this.renderTrend(tile);
case 'text':
return html`
<div class="text-value" style="${tile.color ? `color: ${tile.color}` : ''}">
${tile.value}
</div>
`;
default:
return html`<div class="tile-value">${tile.value}</div>`;
}
}
private renderGauge(tile: IStatsTile): TemplateResult {
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const options = tile.gaugeOptions || { min: 0, max: 100 };
const percentage = ((value - options.min) / (options.max - options.min)) * 100;
const strokeDasharray = 188.5; // Circumference of circle with r=30
const strokeDashoffset = strokeDasharray - (strokeDasharray * percentage) / 100;
let strokeColor = tile.color || cssManager.bdTheme('#0084ff', '#0066cc');
if (options.thresholds) {
for (const threshold of options.thresholds.reverse()) {
if (value >= threshold.value) {
strokeColor = threshold.color;
break;
}
}
}
return html`
<div class="gauge-container">
<svg class="gauge-svg" viewBox="0 0 80 80">
<circle
class="gauge-background"
cx="40"
cy="40"
r="30"
transform="rotate(-90 40 40)"
/>
<circle
class="gauge-fill"
cx="40"
cy="40"
r="30"
transform="rotate(-90 40 40)"
stroke="${strokeColor}"
stroke-dasharray="${strokeDasharray}"
stroke-dashoffset="${strokeDashoffset}"
/>
<text class="gauge-text" x="40" y="40" dy="0.35em">
${value}${tile.unit || ''}
</text>
</svg>
</div>
`;
}
private renderPercentage(tile: IStatsTile): TemplateResult {
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const percentage = Math.min(100, Math.max(0, value));
return html`
<div class="percentage-container">
<div
class="percentage-fill"
style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}"
></div>
<div class="percentage-text">${percentage}%</div>
</div>
`;
}
private renderTrend(tile: IStatsTile): TemplateResult {
if (!tile.trendData || tile.trendData.length < 2) {
return html`<div class="tile-value">${tile.value}</div>`;
}
const data = tile.trendData;
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
const width = 200;
const height = 40;
const points = data.map((value, index) => {
const x = (index / (data.length - 1)) * width;
const y = height - ((value - min) / range) * height;
return `${x},${y}`;
}).join(' ');
const areaPoints = `0,${height} ${points} ${width},${height}`;
return html`
<div class="trend-container">
<svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
<polygon class="trend-area" points="${areaPoints}" />
<polyline class="trend-line" points="${points}" />
</svg>
<div class="trend-value">
<span>${tile.value}</span>
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''}
</div>
</div>
`;
}
private async handleGridAction(action: plugins.tsclass.website.IMenuItem) {
if (action.action) {
await action.action();
}
}
private async handleTileAction(action: plugins.tsclass.website.IMenuItem, _tile: IStatsTile) {
if (action.action) {
await action.action();
}
// Note: tile data is available through closure when defining actions
}
private showContextMenu(event: MouseEvent, tile: IStatsTile) {
if (!tile.actions || tile.actions.length === 0) return;
event.preventDefault();
this.contextMenuPosition = { x: event.clientX, y: event.clientY };
this.contextMenuActions = tile.actions;
this.contextMenuVisible = true;
// Close context menu on click outside
const closeHandler = () => {
this.contextMenuVisible = false;
document.removeEventListener('click', closeHandler);
};
setTimeout(() => {
document.addEventListener('click', closeHandler);
}, 100);
}
}

View File

@@ -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>
`;
};

View File

@@ -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 static styles = [
cssManager.defaultStyles,
css`
: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 { 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` return html`
${domtools.elementBasic.styles} <div class="toast" @click=${this.dismiss}>
<style></style> <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);
}
}

View File

@@ -41,6 +41,7 @@ export * from './dees-simple-appdash.js';
export * from './dees-simple-login.js'; export * from './dees-simple-login.js';
export * from './dees-speechbubble.js'; export * from './dees-speechbubble.js';
export * from './dees-spinner.js'; export * from './dees-spinner.js';
export * from './dees-statsgrid.js';
export * from './dees-stepper.js'; export * from './dees-stepper.js';
export * from './dees-table.js'; export * from './dees-table.js';
export * from './dees-terminal.js'; export * from './dees-terminal.js';