Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
346abfa685 | |||
f1123f319f | |||
ac15da9c82 | |||
b9432c8489 | |||
b35b1fbae7 | |||
e39590df2c | |||
fad7fda2a6 | |||
987f557c60 | |||
4eef9fc731 | |||
cd86001713 | |||
f7e4582fde | |||
4635e3fce5 | |||
af3dc5c466 | |||
12861b2230 | |||
b7f672e0f2 | |||
fcb44dfd24 | |||
f17b880b59 | |||
68785d9a72 | |||
ab4396297a | |||
ef369f2955 | |||
1e73a9527b | |||
23a4faa5d1 | |||
b0020ace16 | |||
bb78d32dbf | |||
e83ad8d504 | |||
765b01afe0 |
66
changelog.md
66
changelog.md
@ -1,5 +1,71 @@
|
||||
# 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)
|
||||
Add new pagination component to the library along with its demo and integration in the main export.
|
||||
|
||||
- Introduced dees-pagination component with support for various page range scenarios.
|
||||
- Created demo file to showcase pagination with both small and large sets of pages.
|
||||
- Updated the module's index to export the new pagination component.
|
||||
|
||||
## 2025-04-22 - 1.7.0 - feat(dees-searchbar)
|
||||
Add dees-searchbar component with live search and filter demo
|
||||
|
||||
- Introduces a new dees-searchbar element with an input field, a search button, and filters
|
||||
- Wires up events for 'search-changed' and 'search-submit' to provide real‐time feedback
|
||||
- Adds a demo file to showcase usage and logging of search events
|
||||
|
||||
## 2025-04-22 - 1.6.0 - feat(documentation/dees-heading)
|
||||
Add codex documentation overview and dees-heading component demo
|
||||
|
||||
- Introduce 'codex.md' to provide a high-level overview of project layout, component patterns, and build workflow
|
||||
- Add and update dees-heading component with demo to support multiple heading levels and horizontal rule styles
|
||||
- Update component export index to include dees-heading
|
||||
|
||||
## 2025-04-18 - 1.5.6 - fix(dependencies)
|
||||
Bump dependency versions and update demo code references
|
||||
|
||||
- Upgrade @design.estate/dees-element from ^2.0.39 to ^2.0.41
|
||||
- Upgrade @tsclass/tsclass from ^4.4.0 to ^9.0.0
|
||||
- Upgrade lucide from ^0.488.0 to ^0.501.0
|
||||
- Update @types/node from ^22.10.7 to ^22.14.1
|
||||
- Update dees-icon demo: scope search to demo container and adjust hover scaling
|
||||
- Replace resolveExec with directives.resolveExec in dees-table for proper rendering
|
||||
|
||||
## 2025-04-12 - 1.5.5 - fix(catalog)
|
||||
No code or documentation changes were detected. This commit records an empty update in commit information and confirms that the current state remains stable.
|
||||
|
||||
- Verified that there are no modifications in source, documentation, or demos
|
||||
- Commit metadata and build configuration remain unchanged
|
||||
|
||||
## 2025-04-11 - 1.5.4 - fix(readme)
|
||||
Update readme with company and trademark guidelines, clarifying legal usage without exposing licensing details.
|
||||
|
||||
- Added sections detailing company information and trademark guidelines.
|
||||
- Outlined legal disclaimers for trademark usage.
|
||||
|
||||
## 2025-04-11 - 1.5.3 - fix(readme)
|
||||
Update readme.md: remove redundant usage section and refine component documentation with improved examples.
|
||||
|
||||
- Removed the standalone manual import and usage example for components.
|
||||
- Added refined examples demonstrating both basic and option-based usage (e.g. for DeesButton).
|
||||
- Improved markdown formatting and consistency across component documentation.
|
||||
|
||||
## 2025-04-11 - 1.5.3 - fix(readme)
|
||||
Update readme.md for clearer documentation: removed redundant 'Usage' section and refined component examples (e.g., DeesButton's basic and options usage) for improved clarity and consistency.
|
||||
|
||||
- Removed standalone usage example showing manual import and creation of components
|
||||
- Added refined examples demonstrating both basic and option-based usage of components
|
||||
- Improved overall readme formatting and consistency across component documentation
|
||||
|
||||
## 2025-04-11 - 1.5.2 - fix(ci)
|
||||
Remove obsolete GitLab CI configuration file
|
||||
|
||||
|
43
codex.md
Normal file
43
codex.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Codex: Project Overview and Codebase Structure
|
||||
|
||||
## Project Overview
|
||||
- Package: `@design.estate/dees-catalog`
|
||||
- Focus: Web Components library providing UI elements and layouts for modern web apps.
|
||||
|
||||
## Directory Layout
|
||||
- ts_web/: TypeScript source files
|
||||
- elements/: Individual Web Component definitions
|
||||
- pages/: Page-level templates for composite layouts
|
||||
- html/: Demo/app entry point loading the bundled scripts
|
||||
- dist_bundle/: Bundled browser JS and source maps
|
||||
- dist_ts_web/: ES module outputs for TypeScript/web consumers
|
||||
- dist_watch/: Watch-mode development bundle with live reload
|
||||
- test/: Browser-based tests using `@push.rocks/tapbundle`
|
||||
|
||||
## Component Patterns
|
||||
- Each component in ts_web/elements/:
|
||||
- Decorated with `@customElement('tag-name')`
|
||||
- Extends `DeesElement` from `@design.estate/dees-element`
|
||||
- Uses `@property` for reactive, reflected attributes
|
||||
- Defines `static styles = [cssManager.defaultStyles, css`...`]`
|
||||
- Implements `render()` returning a Lit `html` template with slots or markup
|
||||
- Exposes a demo via `public static demo` linking to `.demo.ts` files
|
||||
|
||||
## Build & Development Workflow
|
||||
- Install dependencies: `npm install` or `pnpm install`
|
||||
- Build production bundle: `npm run build`
|
||||
- Start dev watch mode: `npm run watch`
|
||||
- Run tests: `npm test` (launches browser fixtures)
|
||||
|
||||
## Theming & Utilities
|
||||
- Default global styles via `cssManager.defaultStyles`
|
||||
- Theme-aware values with `cssManager.bdTheme(light, dark)`
|
||||
- DOM utilities set up in `html/index.ts` using `@design.estate/dees-domtools`
|
||||
|
||||
## Documentation
|
||||
- `readme.md` provides an overview of all components and basic usage
|
||||
- Live examples in `.demo.ts` files
|
||||
accessible via component `demo` static property
|
||||
|
||||
## Updates to this file
|
||||
If you have pattern insisights or general changes to the codebase, please update this file.
|
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "1.5.2",
|
||||
"version": "1.8.4",
|
||||
"private": false,
|
||||
"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",
|
||||
@ -16,7 +16,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.1.1",
|
||||
"@design.estate/dees-element": "^2.0.39",
|
||||
"@design.estate/dees-element": "^2.0.42",
|
||||
"@design.estate/dees-wcctools": "^1.0.90",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
@ -25,24 +25,25 @@
|
||||
"@push.rocks/smarti18n": "^1.0.4",
|
||||
"@push.rocks/smartpromise": "^4.2.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@tsclass/tsclass": "^4.4.0",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@webcontainer/api": "1.2.0",
|
||||
"apexcharts": "^4.3.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"lucide": "^0.514.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.84",
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tswatch": "^2.0.37",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/tapbundle": "^5.5.6",
|
||||
"@types/node": "^22.10.7"
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.0.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
2044
pnpm-lock.yaml
generated
2044
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.
|
||||
* Try to list all components in a summary.
|
||||
* 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)
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '1.5.2',
|
||||
version: '1.8.1',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
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`
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: #000000;
|
||||
@ -10,12 +194,92 @@ export const demoFunc = () => {
|
||||
width: 100%;
|
||||
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>
|
||||
<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
|
||||
.label=${'System Usage'}
|
||||
.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>
|
||||
`;
|
||||
};
|
||||
|
@ -32,28 +32,47 @@ export class DeesChartArea extends DeesElement {
|
||||
@property()
|
||||
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 resizeTimeout: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
|
||||
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) {
|
||||
if (entry.target.classList.contains('mainbox')) {
|
||||
this.resizeChart(); // Call resizeChart when the .mainbox size changes
|
||||
if (entry.target.classList.contains('mainbox') && this.chart) {
|
||||
this.resizeChart();
|
||||
}
|
||||
}
|
||||
}, 100); // 100ms debounce
|
||||
});
|
||||
|
||||
this.registerStartupFunction(async () => {
|
||||
this.updateComplete.then(() => {
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
if (mainbox) {
|
||||
this.resizeObserver.observe(mainbox); // Start observing the .mainbox element
|
||||
this.resizeObserver.observe(mainbox);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
this.resizeObserver.disconnect();
|
||||
});
|
||||
}
|
||||
@ -73,6 +92,7 @@ export class DeesChartArea extends DeesElement {
|
||||
height: 400px;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
@ -82,6 +102,7 @@ export class DeesChartArea extends DeesElement {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
.chartContainer {
|
||||
position: absolute;
|
||||
@ -90,6 +111,7 @@ export class DeesChartArea extends DeesElement {
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 32px 16px 16px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -104,9 +126,32 @@ export class DeesChartArea extends DeesElement {
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
const domtoolsInstance = await this.domtoolsPromise;
|
||||
var options: ApexCharts.ApexOptions = {
|
||||
series: [
|
||||
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: [
|
||||
@ -127,14 +172,25 @@ export class DeesChartArea extends DeesElement {
|
||||
{ x: '2025-01-15T19:00:00', y: 40 },
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
var options: ApexCharts.ApexOptions = {
|
||||
series: chartSeries,
|
||||
chart: {
|
||||
width: 0, // Adjusted for responsive width
|
||||
height: 0, // Adjusted for responsive height
|
||||
width: initialWidth || 100, // Use actual width or fallback
|
||||
height: initialHeight || 100, // Use actual height or fallback
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false, // This line disables the toolbar
|
||||
},
|
||||
animations: {
|
||||
enabled: true,
|
||||
speed: 400,
|
||||
animateGradually: {
|
||||
enabled: true,
|
||||
delay: 150
|
||||
},
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
@ -162,9 +218,7 @@ export class DeesChartArea extends DeesElement {
|
||||
yaxis: {
|
||||
min: 0,
|
||||
labels: {
|
||||
formatter: function (val: number) {
|
||||
return `${val} Mbps`; // Format Y-axis labels
|
||||
},
|
||||
formatter: this.yAxisFormatter,
|
||||
style: {
|
||||
colors: '#9e9e9e', // Label color
|
||||
fontSize: '12px',
|
||||
@ -184,14 +238,11 @@ export class DeesChartArea extends DeesElement {
|
||||
x: {
|
||||
format: 'dd/MM/yy HH:mm',
|
||||
},
|
||||
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
|
||||
// Get the x value
|
||||
const xValue = w.globals.labels[dataPointIndex];
|
||||
custom: function ({ series, dataPointIndex, w }: any) {
|
||||
// Iterate through each series and get its value
|
||||
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 value = s[dataPointIndex]; // Get value at data point
|
||||
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);
|
||||
await this.chart.render();
|
||||
|
||||
// Give the chart a moment to fully initialize before resizing
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
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() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||
|
||||
if (!mainbox || !chartContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get computed style of the element
|
||||
const styleMainbox = window.getComputedStyle(mainbox);
|
||||
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||
|
||||
// Extract padding values
|
||||
|
@ -1,6 +1,123 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
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`
|
||||
<style>
|
||||
.demoBox {
|
||||
@ -9,11 +126,31 @@ export const demoFunc = () => {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
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>
|
||||
<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
|
||||
.label=${'Event Log'}
|
||||
.label=${'Production Server Logs'}
|
||||
></dees-chart-log>
|
||||
</div>
|
||||
`;
|
||||
|
@ -5,15 +5,12 @@ import {
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
state,
|
||||
type CSSResult,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-chart-log.demo.js';
|
||||
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
declare global {
|
||||
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')
|
||||
export class DeesChartLog extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// instance
|
||||
@state()
|
||||
public chart: ApexCharts;
|
||||
|
||||
@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() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace;
|
||||
color: #ccc;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #222;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
border-radius: 8px;
|
||||
padding: 32px 16px 16px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
.header {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chartContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
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%;
|
||||
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 {
|
||||
return html` <div class="mainbox">
|
||||
<div class="chartTitle">${this.label}</div>
|
||||
<div class="chartContainer"></div>
|
||||
</div> `;
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="header">
|
||||
<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() {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
14
ts_web/elements/dees-heading.demo.ts
Normal file
14
ts_web/elements/dees-heading.demo.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export function demoFunc() {
|
||||
return html`
|
||||
<dees-heading level="1">This is a H1 heading</dees-heading>
|
||||
<dees-heading level="2">This is a H2 heading</dees-heading>
|
||||
<dees-heading level="3">This is a H3 heading</dees-heading>
|
||||
<dees-heading level="4">This is a H4 heading</dees-heading>
|
||||
<dees-heading level="5">This is a H5 heading</dees-heading>
|
||||
<dees-heading level="6">This is a H6 heading</dees-heading>
|
||||
<dees-heading level="hr">This is an hr heading</dees-heading>
|
||||
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
|
||||
`;
|
||||
}
|
115
ts_web/elements/dees-heading.ts
Normal file
115
ts_web/elements/dees-heading.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
property,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
DeesElement,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { demoFunc } from './dees-heading.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-heading': DeesHeading;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-heading')
|
||||
export class DeesHeading extends DeesElement {
|
||||
// demo
|
||||
public static demo = demoFunc;
|
||||
|
||||
// properties
|
||||
/**
|
||||
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
public level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
|
||||
|
||||
// STATIC STYLES
|
||||
public static styles: CSSResult[] = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* Heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
h1 { font-size: 32px; font-family: 'Cal Sans'; letter-spacing: 0.025em;}
|
||||
h2 { font-size: 28px; }
|
||||
h3 { font-size: 24px; }
|
||||
h4 { font-size: 20px; }
|
||||
h5 { font-size: 16px; }
|
||||
h6 { font-size: 14px; }
|
||||
/* Horizontal rule style heading */
|
||||
.heading-hr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
/* Fade lines toward and away from text for hr style */
|
||||
.heading-hr::before {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade in toward center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, transparent, #ccc)',
|
||||
'linear-gradient(to right, transparent, #333)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
}
|
||||
.heading-hr::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade out away from center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, #ccc, transparent)',
|
||||
'linear-gradient(to right, #333, transparent)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
}
|
||||
/* Small hr variant with reduced margins */
|
||||
.heading-hr.heading-hr-small {
|
||||
margin: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.heading-hr.heading-hr-small::before,
|
||||
.heading-hr.heading-hr-small::after {
|
||||
margin: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
// INSTANCE
|
||||
public render(): TemplateResult {
|
||||
switch (this.level) {
|
||||
case '1':
|
||||
return html`<h1><slot></slot></h1>`;
|
||||
case '2':
|
||||
return html`<h2><slot></slot></h2>`;
|
||||
case '3':
|
||||
return html`<h3><slot></slot></h3>`;
|
||||
case '4':
|
||||
return html`<h4><slot></slot></h4>`;
|
||||
case '5':
|
||||
return html`<h5><slot></slot></h5>`;
|
||||
case '6':
|
||||
return html`<h6><slot></slot></h6>`;
|
||||
case 'hr':
|
||||
return html`<div class="heading-hr"><slot></slot></div>`;
|
||||
case 'hr-small':
|
||||
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
||||
default:
|
||||
return html`<h1><slot></slot></h1>`;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,155 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { icons, type IconWithPrefix } from './dees-icon.js';
|
||||
import * as lucideIcons from 'lucide';
|
||||
|
||||
import { faIcons } from './dees-icon.js';
|
||||
export const demoFunc = () => {
|
||||
// Group FontAwesome icons by type
|
||||
const faIcons = Object.keys(icons.fa);
|
||||
|
||||
export const demoFunc = () => html`
|
||||
// Extract Lucide icons from the lucideIcons object directly
|
||||
// Log the first few keys to understand the structure
|
||||
console.log('First few Lucide keys:', Object.keys(lucideIcons).slice(0, 5));
|
||||
|
||||
// Get all icon functions from lucideIcons (they have PascalCase names)
|
||||
const lucideIconsList = Object.keys(lucideIcons)
|
||||
.filter(key => {
|
||||
// Skip utility functions and focus on icon components (first letter is uppercase)
|
||||
const isUppercaseFirst = key[0] === key[0].toUpperCase() && key[0] !== key[0].toLowerCase();
|
||||
const isFunction = typeof lucideIcons[key] === 'function';
|
||||
const notUtility = !['createElement', 'createIcons', 'default'].includes(key);
|
||||
return isFunction && isUppercaseFirst && notUtility;
|
||||
})
|
||||
.map(pascalName => {
|
||||
// Convert PascalCase to camelCase
|
||||
return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
||||
});
|
||||
|
||||
// Log how many icons we found
|
||||
console.log(`Found ${lucideIconsList.length} Lucide icons`);
|
||||
|
||||
// If we didn't find any, try an alternative approach
|
||||
if (lucideIconsList.length === 0) {
|
||||
console.log('Trying alternative approach to find Lucide icons');
|
||||
|
||||
// Try to get icon names from a known property if available
|
||||
if (lucideIcons.icons) {
|
||||
const iconSource = lucideIcons.icons || {};
|
||||
lucideIconsList.push(...Object.keys(iconSource));
|
||||
console.log(`Found ${lucideIconsList.length} icons via alternative method`);
|
||||
}
|
||||
}
|
||||
|
||||
// Define the functions in TS scope instead of script tags
|
||||
const searchIcons = (event: InputEvent) => {
|
||||
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim();
|
||||
// Get the demo container first, then search within it
|
||||
const demoContainer = (event.target as HTMLElement).closest('.demoContainer');
|
||||
const containers = demoContainer.querySelectorAll('.iconContainer');
|
||||
|
||||
containers.forEach(container => {
|
||||
const iconName = container.getAttribute('data-name');
|
||||
|
||||
if (searchTerm === '') {
|
||||
container.classList.remove('hidden');
|
||||
} else if (iconName && iconName.includes(searchTerm)) {
|
||||
container.classList.remove('hidden');
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Update counts - search within demoContainer
|
||||
demoContainer.querySelectorAll('.section-container').forEach(section => {
|
||||
const visibleIcons = section.querySelectorAll('.iconContainer:not(.hidden)').length;
|
||||
const countElement = section.querySelector('.icon-count');
|
||||
if (countElement) {
|
||||
const totalIconsCount = section.classList.contains('fa-section')
|
||||
? faIcons.length
|
||||
: lucideIconsList.length;
|
||||
|
||||
countElement.textContent = visibleIcons === totalIconsCount
|
||||
? `${totalIconsCount} icons`
|
||||
: `${visibleIcons} of ${totalIconsCount} icons`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const copyIconName = (iconNameToCopy: string, type: 'fa' | 'lucide') => {
|
||||
// Use the new prefix format
|
||||
const textToCopy = `${type}:${iconNameToCopy}`;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
// Find the event target
|
||||
const currentEvent = window.event as MouseEvent;
|
||||
const currentTarget = currentEvent.currentTarget as HTMLElement;
|
||||
// Show feedback
|
||||
const tooltip = currentTarget.querySelector('.copy-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.textContent = 'Copied!';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.textContent = 'Click to copy';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.demoContainer {
|
||||
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: #111111;
|
||||
padding: 10px; font-size: 30px;
|
||||
padding: 20px;
|
||||
font-size: 30px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#iconSearch {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
#iconSearch:focus {
|
||||
outline: none;
|
||||
border-color: #e4002b;
|
||||
}
|
||||
|
||||
dees-icon {
|
||||
transition: color 0.02s;
|
||||
transition: all 0.2s ease;
|
||||
color: #ffffff;
|
||||
}
|
||||
dees-icon:hover {
|
||||
color: #e4002b;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: block;
|
||||
padding: 16px 16px 0px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 16px 0px 16px;
|
||||
border: 1px solid #333333;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iconContainer:hover {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.iconName {
|
||||
@ -33,23 +157,136 @@ export const demoFunc = () => html`
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
background: #333333;
|
||||
padding: 4px 8px;
|
||||
padding-bottom: 4px;
|
||||
padding: 6px 10px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
margin-top: 16px;
|
||||
margin-top: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
width: 100%;
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
margin: 20px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #333333;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.api-note {
|
||||
font-size: 14px;
|
||||
color: #e4002b;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e4002b;
|
||||
border-radius: 4px;
|
||||
background: rgba(228, 0, 43, 0.1);
|
||||
}
|
||||
|
||||
.icon-count {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
font-weight: normal;
|
||||
background: #222;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.icons-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.copy-tooltip {
|
||||
position: absolute;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
top: -30px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.iconContainer:hover .copy-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconContainer:hover dees-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demoContainer">
|
||||
${Object.keys(faIcons).map(
|
||||
(iconName) => html`
|
||||
<div class="iconContainer">
|
||||
<dees-icon .iconFA=${iconName as any}></dees-icon>
|
||||
<div class="iconName">${iconName}</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div class="search-container">
|
||||
<input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}>
|
||||
</div>
|
||||
|
||||
`;
|
||||
<div class="api-note">
|
||||
New API: Use <code>icon="fa:iconName"</code> or <code>icon="lucide:iconName"</code> instead of <code>iconFA</code>.
|
||||
Click any icon to copy its new format to clipboard.
|
||||
</div>
|
||||
|
||||
<div class="section-container fa-section">
|
||||
<div class="section-title">
|
||||
FontAwesome Icons
|
||||
<span class="icon-count">${faIcons.length} icons</span>
|
||||
</div>
|
||||
<div class="icons-grid">
|
||||
${faIcons.map(
|
||||
(iconName) => {
|
||||
const prefixedName = `fa:${iconName}`;
|
||||
return html`
|
||||
<div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}>
|
||||
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||
<div class="iconName">${iconName}</div>
|
||||
<span class="copy-tooltip">Click to copy</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-container lucide-section">
|
||||
<div class="section-title">
|
||||
Lucide Icons
|
||||
<span class="icon-count">${lucideIconsList.length} icons</span>
|
||||
</div>
|
||||
<div class="icons-grid">
|
||||
${lucideIconsList.map(
|
||||
(iconName) => {
|
||||
const prefixedName = `lucide:${iconName}`;
|
||||
return html`
|
||||
<div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}>
|
||||
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||
<div class="iconName">${iconName}</div>
|
||||
<span class="copy-tooltip">Click to copy</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
@ -75,7 +75,12 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { demoFunc } from './dees-icon.demo.js';
|
||||
|
||||
export const faIcons = {
|
||||
// Import Lucide icons and the createElement function
|
||||
import * as lucideIcons from 'lucide';
|
||||
import { createElement } from 'lucide';
|
||||
|
||||
// Collect FontAwesome icons
|
||||
const faIcons = {
|
||||
// normal
|
||||
arrowRight: faArrowRightSolid,
|
||||
arrowUpRightFromSquare: faArrowUpRightFromSquareSolid,
|
||||
@ -136,7 +141,32 @@ export const faIcons = {
|
||||
twitter: faTwitter,
|
||||
};
|
||||
|
||||
export type TIconKey = keyof typeof faIcons;
|
||||
// Create a string literal type for all FA icons
|
||||
type FAIconKey = keyof typeof faIcons;
|
||||
|
||||
// Create union types for the icons with prefixes
|
||||
export type IconWithPrefix = `fa:${FAIconKey}` | `lucide:${string}`;
|
||||
|
||||
// Export only FontAwesome icons directly
|
||||
export const icons = {
|
||||
fa: faIcons
|
||||
};
|
||||
|
||||
// Legacy type for backward compatibility
|
||||
export type TIconKey = FAIconKey | `lucide:${string}`;
|
||||
|
||||
// Use a global static cache for all icons to reduce rendering
|
||||
const iconCache = new Map<string, string>();
|
||||
|
||||
// Clear cache items occasionally to prevent memory leaks
|
||||
const MAX_CACHE_SIZE = 500;
|
||||
function limitCacheSize() {
|
||||
if (iconCache.size > MAX_CACHE_SIZE) {
|
||||
// Remove oldest entries (first 20% of items)
|
||||
const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
|
||||
keysToDelete.forEach(key => iconCache.delete(key));
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -148,31 +178,170 @@ declare global {
|
||||
export class DeesIcon extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
/**
|
||||
* @deprecated Use the `icon` property instead with format "fa:iconName" or "lucide:iconName"
|
||||
*/
|
||||
@property({
|
||||
type: String
|
||||
type: String,
|
||||
converter: {
|
||||
// Convert attribute string to property (for reflected attributes)
|
||||
fromAttribute: (value: string): TIconKey => value as TIconKey,
|
||||
// Convert property to attribute (for reflection)
|
||||
toAttribute: (value: TIconKey): string => value
|
||||
}
|
||||
})
|
||||
public iconFA: keyof typeof faIcons;
|
||||
public iconFA?: TIconKey;
|
||||
|
||||
@property()
|
||||
/**
|
||||
* The preferred icon property. Use format "fa:iconName" or "lucide:iconName"
|
||||
* Examples: "fa:check", "lucide:menu"
|
||||
*/
|
||||
@property({
|
||||
type: String,
|
||||
converter: {
|
||||
fromAttribute: (value: string): IconWithPrefix => value as IconWithPrefix,
|
||||
toAttribute: (value: IconWithPrefix): string => value
|
||||
}
|
||||
})
|
||||
public icon?: IconWithPrefix;
|
||||
|
||||
@property({ type: Number })
|
||||
public iconSize: number;
|
||||
|
||||
@property({ type: String })
|
||||
public color: string = 'currentColor';
|
||||
|
||||
@property({ type: Number })
|
||||
public strokeWidth: number = 2;
|
||||
|
||||
// For tracking when we need to re-render
|
||||
private lastIcon: IconWithPrefix | TIconKey | null = null;
|
||||
private lastIconSize: number | null = null;
|
||||
private lastColor: string | null = null;
|
||||
private lastStrokeWidth: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective icon value, supporting both the new `icon` property
|
||||
* and the legacy `iconFA` property for backward compatibility.
|
||||
* Prefers `icon` if both are set.
|
||||
*/
|
||||
private getEffectiveIcon(): IconWithPrefix | TIconKey | null {
|
||||
// Prefer the new API
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
// Fall back to the old API
|
||||
if (this.iconFA) {
|
||||
// If iconFA is already in the proper format (lucide:name), use it directly
|
||||
if (this.iconFA.startsWith('lucide:')) {
|
||||
return this.iconFA;
|
||||
}
|
||||
|
||||
// For FontAwesome icons with no prefix, add the prefix
|
||||
return `fa:${this.iconFA}` as IconWithPrefix;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an icon string into its type and name parts
|
||||
* @param iconStr The icon string in format "type:name"
|
||||
* @returns Object with type and name properties
|
||||
*/
|
||||
private parseIconString(iconStr: string): { type: 'fa' | 'lucide', name: string } {
|
||||
if (iconStr.startsWith('fa:')) {
|
||||
return {
|
||||
type: 'fa',
|
||||
name: iconStr.substring(3) // Remove 'fa:' prefix
|
||||
};
|
||||
} else if (iconStr.startsWith('lucide:')) {
|
||||
return {
|
||||
type: 'lucide',
|
||||
name: iconStr.substring(7) // Remove 'lucide:' prefix
|
||||
};
|
||||
} else {
|
||||
// For backward compatibility, assume FontAwesome if no prefix
|
||||
return {
|
||||
type: 'fa',
|
||||
name: iconStr
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private renderLucideIcon(iconName: string): string {
|
||||
// Create a cache key based on all visual properties
|
||||
const cacheKey = `lucide:${iconName}:${this.iconSize}:${this.color}:${this.strokeWidth}`;
|
||||
|
||||
// Check if we already have this icon in the cache
|
||||
if (iconCache.has(cacheKey)) {
|
||||
return iconCache.get(cacheKey) || '';
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the Pascal case icon name (Menu instead of menu)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1);
|
||||
|
||||
// Check if the icon exists in lucideIcons
|
||||
if (!lucideIcons[pascalCaseName]) {
|
||||
console.warn(`Lucide icon '${pascalCaseName}' not found in lucideIcons object`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use the exact pattern from Lucide documentation
|
||||
const svgElement = createElement(lucideIcons[pascalCaseName], {
|
||||
color: this.color,
|
||||
size: this.iconSize,
|
||||
strokeWidth: this.strokeWidth
|
||||
});
|
||||
|
||||
if (!svgElement) {
|
||||
console.warn(`createElement returned empty result for ${pascalCaseName}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get the HTML
|
||||
const result = svgElement.outerHTML;
|
||||
|
||||
// Cache the result for future use
|
||||
iconCache.set(cacheKey, result);
|
||||
limitCacheSize();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error rendering Lucide icon ${iconName}:`, error);
|
||||
|
||||
// Create a fallback SVG with the icon name
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${this.iconSize}" height="${this.iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.color}" stroke-width="${this.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">
|
||||
<text x="50%" y="50%" font-size="6" text-anchor="middle" dominant-baseline="middle" fill="${this.color}">${iconName}</text>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
* {
|
||||
transition: inherit !important;
|
||||
|
||||
/* Improve rendering performance */
|
||||
#iconContainer svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
will-change: transform; /* Helps with animations */
|
||||
contain: strict; /* Performance optimization */
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -181,8 +350,8 @@ export class DeesIcon extends DeesElement {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
#iconContainer svg {
|
||||
display: block;
|
||||
#iconContainer {
|
||||
width: ${this.iconSize}px;
|
||||
height: ${this.iconSize}px;
|
||||
}
|
||||
</style>
|
||||
@ -190,14 +359,95 @@ export class DeesIcon extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public async updated() {
|
||||
public updated() {
|
||||
// If size is not specified, use font size as a base
|
||||
if (!this.iconSize) {
|
||||
this.iconSize = parseInt(globalThis.getComputedStyle(this).fontSize.replace(/\D/g,''));
|
||||
}
|
||||
if (this.iconFA) {
|
||||
this.shadowRoot.querySelector('#iconContainer').innerHTML = this.iconFA
|
||||
? icon(faIcons[this.iconFA]).html[0]
|
||||
: 'icon not found';
|
||||
|
||||
// Get the effective icon (either from icon or iconFA property)
|
||||
const effectiveIcon = this.getEffectiveIcon();
|
||||
|
||||
// Check if we actually need to update the icon
|
||||
// This prevents unnecessary DOM operations when properties haven't changed
|
||||
if (this.lastIcon === effectiveIcon &&
|
||||
this.lastIconSize === this.iconSize &&
|
||||
this.lastColor === this.color &&
|
||||
this.lastStrokeWidth === this.strokeWidth) {
|
||||
return; // No visual changes - skip update
|
||||
}
|
||||
|
||||
// Update our "last properties" for future change detection
|
||||
this.lastIcon = effectiveIcon;
|
||||
this.lastIconSize = this.iconSize;
|
||||
this.lastColor = this.color;
|
||||
this.lastStrokeWidth = this.strokeWidth;
|
||||
|
||||
const container = this.shadowRoot?.querySelector('#iconContainer');
|
||||
if (!container || !effectiveIcon) return;
|
||||
|
||||
try {
|
||||
// Parse the icon string to get type and name
|
||||
const { type, name } = this.parseIconString(effectiveIcon);
|
||||
|
||||
if (type === 'lucide') {
|
||||
// For Lucide, use direct DOM manipulation as shown in the docs
|
||||
// This approach avoids HTML string issues
|
||||
container.innerHTML = ''; // Clear container
|
||||
|
||||
try {
|
||||
// Convert to PascalCase
|
||||
const pascalCaseName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
if (lucideIcons[pascalCaseName]) {
|
||||
// Use the documented pattern from Lucide docs
|
||||
const svgElement = createElement(lucideIcons[pascalCaseName], {
|
||||
color: this.color,
|
||||
size: this.iconSize,
|
||||
strokeWidth: this.strokeWidth
|
||||
});
|
||||
|
||||
if (svgElement) {
|
||||
// Directly append the element
|
||||
container.appendChild(svgElement);
|
||||
return; // Exit early since we've added the element
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, something went wrong
|
||||
throw new Error(`Could not create element for ${pascalCaseName}`);
|
||||
} catch (error) {
|
||||
console.error(`Error rendering Lucide icon:`, error);
|
||||
|
||||
// Fall back to the string-based approach
|
||||
const iconHtml = this.renderLucideIcon(name);
|
||||
if (iconHtml) {
|
||||
container.innerHTML = iconHtml;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use FontAwesome rendering via HTML string
|
||||
const faIcon = icons.fa[name as FAIconKey];
|
||||
if (faIcon) {
|
||||
const iconHtml = icon(faIcon).html[0];
|
||||
container.innerHTML = iconHtml;
|
||||
} else {
|
||||
console.warn(`FontAwesome icon not found: ${name}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating icon ${effectiveIcon}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources when element is removed
|
||||
async disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// Clear our references
|
||||
this.lastIcon = null;
|
||||
this.lastIconSize = null;
|
||||
this.lastColor = null;
|
||||
this.lastStrokeWidth = null;
|
||||
}
|
||||
}
|
28
ts_web/elements/dees-pagination.demo.ts
Normal file
28
ts_web/elements/dees-pagination.demo.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
/**
|
||||
* Demo for dees-pagination component
|
||||
*/
|
||||
export const demoFunc = () => html`
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<!-- Small set of pages -->
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<span>5 pages, starting at 1:</span>
|
||||
<dees-pagination
|
||||
.total=${5}
|
||||
.page=${1}
|
||||
@page-change=${(e: CustomEvent) => console.log('Page changed to', e.detail.page)}
|
||||
></dees-pagination>
|
||||
</div>
|
||||
|
||||
<!-- Larger set of pages -->
|
||||
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||
<span>15 pages, starting at 8:</span>
|
||||
<dees-pagination
|
||||
.total=${15}
|
||||
.page=${8}
|
||||
@page-change=${(e: CustomEvent) => console.log('Page changed to', e.detail.page)}
|
||||
></dees-pagination>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
133
ts_web/elements/dees-pagination.ts
Normal file
133
ts_web/elements/dees-pagination.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { customElement, html, DeesElement, property, css, cssManager, type TemplateResult } from '@design.estate/dees-element';
|
||||
import { demoFunc } from './dees-pagination.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-pagination': DeesPagination;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple pagination component.
|
||||
* @fires page-change - Emitted when the page is changed. detail: { page: number }
|
||||
*/
|
||||
@customElement('dees-pagination')
|
||||
export class DeesPagination extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
/** Current page (1-based) */
|
||||
@property({ type: Number, reflect: true })
|
||||
public page = 1;
|
||||
|
||||
/** Total number of pages */
|
||||
@property({ type: Number, reflect: true })
|
||||
public total = 1;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0 2px;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
border-radius: 3px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
background: ${cssManager.bdTheme('#eee', '#444')};
|
||||
}
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||
}
|
||||
button.current {
|
||||
background: #0050b9;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
span.ellipsis {
|
||||
margin: 0 4px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private get pages(): (number | string)[] {
|
||||
const pages: (number | string)[] = [];
|
||||
const total = this.total;
|
||||
const current = this.page;
|
||||
if (total <= 7) {
|
||||
for (let i = 1; i <= total; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (current > 4) {
|
||||
pages.push('...');
|
||||
}
|
||||
const start = Math.max(2, current - 2);
|
||||
const end = Math.min(total - 1, current + 2);
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
if (current < total - 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
pages.push(total);
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<button
|
||||
@click=${() => this.changePage(this.page - 1)}
|
||||
?disabled=${this.page <= 1}
|
||||
aria-label="Previous page"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
${this.pages.map((p) =>
|
||||
p === '...'
|
||||
? html`<span class="ellipsis">…</span>`
|
||||
: html`
|
||||
<button
|
||||
class="${p === this.page ? 'current' : ''}"
|
||||
@click=${() => this.changePage(p as number)}
|
||||
?disabled=${p === this.page}
|
||||
aria-label="Page ${p}"
|
||||
>
|
||||
${p}
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
<button
|
||||
@click=${() => this.changePage(this.page + 1)}
|
||||
?disabled=${this.page >= this.total}
|
||||
aria-label="Next page"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private changePage(newPage: number) {
|
||||
if (newPage < 1 || newPage > this.total || newPage === this.page) {
|
||||
return;
|
||||
}
|
||||
this.page = newPage;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('page-change', {
|
||||
detail: { page: this.page },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
46
ts_web/elements/dees-searchbar.demo.ts
Normal file
46
ts_web/elements/dees-searchbar.demo.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const onChanged = (e: CustomEvent) => {
|
||||
// find the demo wrapper and update the 'changed' log inside it
|
||||
const wrapper = (e.target as HTMLElement).closest('.demoWrapper');
|
||||
const el = wrapper?.querySelector('#changed');
|
||||
if (el) el.textContent = `search-changed: ${e.detail.value}`;
|
||||
};
|
||||
const onSubmit = (e: CustomEvent) => {
|
||||
// find the demo wrapper and update the 'submitted' log inside it
|
||||
const wrapper = (e.target as HTMLElement).closest('.demoWrapper');
|
||||
const el = wrapper?.querySelector('#submitted');
|
||||
if (el) el.textContent = `search-submit: ${e.detail.value}`;
|
||||
};
|
||||
return html`
|
||||
<style>
|
||||
.demoWrapper {
|
||||
display: block;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
background: #888888;
|
||||
}
|
||||
.logs {
|
||||
padding: 16px;
|
||||
width: 600px;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
}
|
||||
.logs div {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
<div class="demoWrapper">
|
||||
<dees-searchbar
|
||||
@search-changed=${onChanged}
|
||||
@search-submit=${onSubmit}
|
||||
></dees-searchbar>
|
||||
<div class="logs">
|
||||
<div id="changed">search-changed:</div>
|
||||
<div id="submitted">search-submit:</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
160
ts_web/elements/dees-searchbar.ts
Normal file
160
ts_web/elements/dees-searchbar.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
css,
|
||||
type TemplateResult,
|
||||
domtools,
|
||||
query,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as colors from './00colors.js';
|
||||
import { demoFunc } from './dees-searchbar.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-searchbar': DeesSearchbar;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-searchbar')
|
||||
export class DeesSearchbar extends DeesElement {
|
||||
// DEMO
|
||||
public static demo = demoFunc;
|
||||
|
||||
// STATIC
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 40px;
|
||||
font-family: Dees Sans;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||
}
|
||||
|
||||
.searchboxContainer {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
background: ${cssManager.bdTheme('#00000015', '#ffffff15')};
|
||||
--boxHeight: 60px;
|
||||
height: var(--boxHeight);
|
||||
border-radius: var(--boxHeight);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 140px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#00000015', '#ffffff20')};
|
||||
}
|
||||
|
||||
input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
color: ${cssManager.bdTheme('#000000', '#eeeeeb')};
|
||||
padding-left: 25px;
|
||||
margin-right: -8px;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
--buttonPadding: 8px;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||
color: ${cssManager.bdTheme('#000000', '#eeeeeb')};
|
||||
line-height: calc(var(--boxHeight) - (var(--buttonPadding) * 2));
|
||||
border-radius: var(--boxHeight);
|
||||
transform: scale(1) ;
|
||||
transform-origin: 50% 50%;
|
||||
text-align: center;
|
||||
|
||||
transition: transform 0.1s, background 0.1s;
|
||||
margin-right: var(--buttonPadding);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.searchButton:hover {
|
||||
color: #fff;
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
}
|
||||
|
||||
.searchButton:active {
|
||||
color: #fff;
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// INSTANCE
|
||||
|
||||
@property()
|
||||
public filters = [];
|
||||
|
||||
|
||||
@query('input')
|
||||
public searchInput!: HTMLInputElement;
|
||||
@query('.searchButton')
|
||||
public searchButton!: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="searchboxContainer">
|
||||
<input type="text" placeholder="Your Skills (e.g. TypeScript, Rust, Projectmanagement)" />
|
||||
<div class="searchButton">Search -></div>
|
||||
</div>
|
||||
${this.filters.length > 0 ? html`
|
||||
<div class="filters">
|
||||
<dees-heading level="hr-small">Filters</dees-heading>
|
||||
<dees-input-dropdown .label=${'location'}></dees-input-dropdown>
|
||||
</div>
|
||||
` : html``}
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* Lifecycle: after first render, wire up events for input and submit actions
|
||||
*/
|
||||
public firstUpdated(): void {
|
||||
// dispatch change on each input
|
||||
this.searchInput.addEventListener('input', () => {
|
||||
this.dispatchEvent(new CustomEvent('search-changed', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { value: this.searchInput.value }
|
||||
}));
|
||||
});
|
||||
// submit on Enter key
|
||||
this.searchInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
this._dispatchSubmit();
|
||||
}
|
||||
});
|
||||
// submit on button click
|
||||
this.searchButton.addEventListener('click', () => this._dispatchSubmit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a submit event with the current search value
|
||||
*/
|
||||
private _dispatchSubmit(): void {
|
||||
this.dispatchEvent(new CustomEvent('search-submit', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { value: this.searchInput.value }
|
||||
}));
|
||||
}
|
||||
}
|
389
ts_web/elements/dees-statsgrid.demo.ts
Normal file
389
ts_web/elements/dees-statsgrid.demo.ts
Normal 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>
|
||||
`;
|
||||
};
|
518
ts_web/elements/dees-statsgrid.ts
Normal file
518
ts_web/elements/dees-statsgrid.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import {
|
||||
unsafeCSS,
|
||||
type CSSResult,
|
||||
state,
|
||||
resolveExec,
|
||||
directives,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
@ -415,7 +415,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
<div class="heading heading2">${this.heading2}</div>
|
||||
</div>
|
||||
<div class="headerActions">
|
||||
${resolveExec(async () => {
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes('header')) continue;
|
||||
@ -634,7 +634,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
selected
|
||||
</div>
|
||||
<div class="footerActions">
|
||||
${resolveExec(async () => {
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes('footer')) continue;
|
||||
|
@ -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 () => {
|
||||
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 { 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')
|
||||
export class DeesToast extends DeesElement {
|
||||
// STATIC
|
||||
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() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style></style>
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ export * from './dees-editor-markdown.js';
|
||||
export * from './dees-editor-markdownoutlet.js';
|
||||
export * from './dees-form-submit.js';
|
||||
export * from './dees-form.js';
|
||||
export * from './dees-heading.js';
|
||||
export * from './dees-hint.js';
|
||||
export * from './dees-icon.js';
|
||||
export * from './dees-input-checkbox.js';
|
||||
@ -35,10 +36,12 @@ export * from './dees-mobilenavigation.js';
|
||||
export * from './dees-modal.js';
|
||||
export * from './dees-input-multitoggle.js';
|
||||
export * from './dees-pdf.js';
|
||||
export * from './dees-searchbar.js';
|
||||
export * from './dees-simple-appdash.js';
|
||||
export * from './dees-simple-login.js';
|
||||
export * from './dees-speechbubble.js';
|
||||
export * from './dees-spinner.js';
|
||||
export * from './dees-statsgrid.js';
|
||||
export * from './dees-stepper.js';
|
||||
export * from './dees-table.js';
|
||||
export * from './dees-terminal.js';
|
||||
@ -46,3 +49,4 @@ export * from './dees-toast.js';
|
||||
export * from './dees-updater.js';
|
||||
export * from './dees-windowcontrols.js';
|
||||
export * from './dees-windowlayer.js';
|
||||
export * from './dees-pagination.js';
|
||||
|
Reference in New Issue
Block a user