Compare commits

..

30 Commits

Author SHA1 Message Date
ac15da9c82 1.8.3
Some checks failed
Default (tags) / security (push) Failing after 34s
Default (tags) / test (push) Failing after 19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:09:27 +00:00
b9432c8489 feat(dees-chart-area): Enhance chart component with dynamic datasets, real-time updates, and improved demo features 2025-06-12 11:09:14 +00:00
b35b1fbae7 1.8.2
Some checks failed
Default (tags) / security (push) Failing after 36s
Default (tags) / test (push) Failing after 21s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-12 11:01:20 +00:00
e39590df2c fix(dees-chart-area): Improve resize handling and initial rendering for better responsiveness
fix(dees-chart-log): Simplify firstUpdated method by removing unnecessary variable
2025-06-12 11:00:33 +00:00
fad7fda2a6 feat(dees-chart-log): Enhance log component with realistic log simulation and improved UI controls 2025-06-12 10:44:21 +00:00
987f557c60 Enhance DeesToast component with new features and improved demo
- Updated README to reflect new toast positions and convenience methods.
- Expanded demo functionality to showcase various toast types, positions, and durations.
- Added programmatic control for toast dismissal and multiple toast notifications.
- Introduced new toast positions: top-center and bottom-center.
- Implemented a progress bar for auto-dismiss functionality.
- Improved styling and animations for better user experience.
2025-06-12 09:33:46 +00:00
4eef9fc731 1.8.1
Some checks failed
Default (tags) / security (push) Failing after 25s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-10 18:58:05 +00:00
cd86001713 fix(dees-statsgrid): Adjust stats grid styling for better alignment and improved visualizations in gauge and trend tiles. 2025-06-10 18:58:05 +00:00
f7e4582fde feat(dees-statsgrid): Add dees-statsgrid component with demo and integration in the main export 2025-06-10 18:29:37 +00:00
4635e3fce5 1.8.0
Some checks failed
Default (tags) / security (push) Failing after 20s
Default (tags) / test (push) Failing after 9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-25 14:27:51 +00:00
af3dc5c466 feat(dees-pagination): Add new pagination component to the library along with its demo and integration in the main export. 2025-04-25 14:27:51 +00:00
12861b2230 1.7.0
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-22 12:49:57 +00:00
b7f672e0f2 feat(dees-searchbar): Add dees-searchbar component with live search and filter demo 2025-04-22 12:49:57 +00:00
fcb44dfd24 1.6.0
Some checks failed
Default (tags) / security (push) Failing after 20s
Default (tags) / test (push) Failing after 9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-22 12:30:22 +00:00
f17b880b59 feat(documentation/dees-heading): Add codex documentation overview and dees-heading component demo 2025-04-22 12:30:22 +00:00
68785d9a72 1.5.6
Some checks failed
Default (tags) / security (push) Failing after 20s
Default (tags) / test (push) Failing after 8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-18 17:07:43 +00:00
ab4396297a fix(dependencies): Bump dependency versions and update demo code references 2025-04-18 17:07:43 +00:00
ef369f2955 update 2025-04-13 17:32:44 +00:00
1e73a9527b 1.5.5
Some checks failed
Default (tags) / security (push) Failing after 19s
Default (tags) / test (push) Failing after 9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-12 14:35:56 +00:00
23a4faa5d1 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. 2025-04-12 14:35:55 +00:00
b0020ace16 1.5.4
Some checks failed
Default (tags) / security (push) Failing after 12s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-11 11:19:58 +00:00
bb78d32dbf fix(readme): Update readme with company and trademark guidelines, clarifying legal usage without exposing licensing details. 2025-04-11 11:19:58 +00:00
e83ad8d504 1.5.3
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-11 11:19:24 +00:00
765b01afe0 fix(readme): Update readme.md: remove redundant usage section and refine component documentation with improved examples. 2025-04-11 11:19:23 +00:00
00e34e7e6c 1.5.2
Some checks failed
Default (tags) / security (push) Failing after 9s
Default (tags) / test (push) Failing after 10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-11 10:24:28 +00:00
bf2ee25390 fix(ci): Remove obsolete GitLab CI configuration file 2025-04-11 10:24:28 +00:00
bf6d8d0bc6 1.5.1
Some checks failed
Default (tags) / security (push) Failing after 18s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-11 10:20:35 +00:00
3399004e75 fix(readme): Update readme with comprehensive reference documentation: add a usage snippet for components like DeesButton, introduce a detailed overview table of all component categories, and enhance documentation sections for each component group. 2025-04-11 10:20:35 +00:00
6c2f36f020 1.5.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-04-11 10:15:00 +00:00
71f4d44782 feat(badge): Add dees-badge component with demo file and update packageManager field in package.json 2025-04-11 10:15:00 +00:00
29 changed files with 7286 additions and 2713 deletions

View File

@ -1,128 +0,0 @@
# gitzone ci_default
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
cache:
paths:
- .npmci_cache/
key: '$CI_BUILD_STAGE'
stages:
- security
- test
- release
- metadata
before_script:
- pnpm install -g pnpm
- pnpm install -g @shipzone/npmci
- npmci npm prepare
# ====================
# security stage
# ====================
# ====================
# security stage
# ====================
auditProductionDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci command npm config set registry https://registry.npmjs.org
- npmci command pnpm audit --audit-level=high --prod
tags:
- lossless
- docker
allow_failure: true
auditDevDependencies:
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
stage: security
script:
- npmci command npm config set registry https://registry.npmjs.org
- npmci command pnpm audit --audit-level=high --dev
tags:
- lossless
- docker
allow_failure: true
# ====================
# test stage
# ====================
testStable:
stage: test
script:
- npmci node install stable
- npmci npm install
- npmci npm test
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- docker
testBuild:
stage: test
script:
- npmci node install stable
- npmci npm install
- npmci npm build
coverage: /\d+.?\d+?\%\s*coverage/
tags:
- docker
release:
stage: release
script:
- npmci node install stable
- npmci npm publish
only:
- tags
tags:
- lossless
- docker
- notpriv
# ====================
# metadata stage
# ====================
codequality:
stage: metadata
allow_failure: true
only:
- tags
script:
- npmci command npm install -g typescript
- npmci npm prepare
- npmci npm install
tags:
- lossless
- docker
- priv
trigger:
stage: metadata
script:
- npmci trigger
only:
- tags
tags:
- lossless
- docker
- notpriv
pages:
stage: metadata
script:
- npmci node install stable
- npmci npm install
- npmci command npm run buildDocs
tags:
- lossless
- docker
- notpriv
only:
- tags
artifacts:
expire_in: 1 week
paths:
- public
allow_failure: true

View File

@ -1,5 +1,93 @@
# 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 realtime 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
- Deleted .gitlab-ci.yml as the CI pipeline configuration is now managed elsewhere.
- Cleaned up CI stages for security, testing, release, and metadata.
## 2025-04-11 - 1.5.1 - fix(readme)
Update readme with comprehensive reference documentation: add a usage snippet for components like DeesButton, introduce a detailed overview table of all component categories, and enhance documentation sections for each component group.
- Added a code example showing how to import and use DeesButton.
- Introduced a components overview table that categorizes Core UI, Forms, Layout, Data Display, Visualization, Dialogs & Overlays, Navigation, and Development components.
- Expanded detailed documentation with usage examples for each component type.
- Reorganized content to improve clarity and ease of navigation for developers.
## 2025-04-11 - 1.5.0 - feat(badge)
Add dees-badge component with demo file and update packageManager field in package.json
- Introduce a new badge component allowing different types (default, primary, success, warning, error) with an optional rounded style
- Provide a demo for the badge component
- Export the badge component in the main elements index
- Update package.json to include an explicit packageManager field
## 2025-01-20 - 1.4.1 - fix(dependencies)
Update dependency versions for smartpromise, webcontainer/api, tapbundle, and @types/node

43
codex.md Normal file
View 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.

View File

@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "1.4.1",
"version": "1.8.3",
"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",
"@webcontainer/api": "1.2.0",
"apexcharts": "^4.3.0",
"@tsclass/tsclass": "^9.2.0",
"@webcontainer/api": "1.6.1",
"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/**/*",
@ -84,5 +85,6 @@
"Web Applications",
"Modern Web",
"Frontend Development"
]
],
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
}

4659
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -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)

1445
readme.md

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '1.4.1',
version: '1.8.1',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@ -0,0 +1,12 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<div style="display: flex; gap: 8px; align-items: center;">
<dees-badge .text=${'Default'}></dees-badge>
<dees-badge .type=${'primary'} .text=${'Primary'}></dees-badge>
<dees-badge .type=${'success'} .text=${'Success'}></dees-badge>
<dees-badge .type=${'warning'} .text=${'Warning'}></dees-badge>
<dees-badge .type=${'error'} .text=${'Error'}></dees-badge>
<dees-badge .type=${'primary'} .rounded=${true} .text=${'Rounded'}></dees-badge>
</div>
`;

View File

@ -0,0 +1,96 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
type CSSResult,
type TemplateResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-badge.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-badge': DeesBadge;
}
}
@customElement('dees-badge')
export class DeesBadge extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
public type: 'default' | 'primary' | 'success' | 'warning' | 'error' = 'default';
@property({ type: String })
public text: string = '';
@property({ type: Boolean })
public rounded: boolean = false;
constructor() {
super();
domtools.elementBasic.setup();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: inline-block;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
line-height: 1.5;
border-radius: 4px;
white-space: nowrap;
}
.badge.rounded {
border-radius: 12px;
}
.badge.default {
background: ${cssManager.bdTheme('#f5f5f5', '#333')};
color: ${cssManager.bdTheme('#666', '#ccc')};
}
.badge.primary {
background: #0050b9;
color: #ffffff;
}
.badge.success {
background: #2e7d32;
color: #ffffff;
}
.badge.warning {
background: #ed6c02;
color: #ffffff;
}
.badge.error {
background: #e4002b;
color: #ffffff;
}
`,
];
public render(): TemplateResult {
return html`
<div class="badge ${this.type} ${this.rounded ? 'rounded' : ''}">
${this.text}
</div>
`;
}
}

View File

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

View File

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

View File

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

View File

@ -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' },
];
public async updateLog() {
this.logEntries = demoLogs;
this.scrollToBottom();
// For demo purposes, store reference globally
if ((window as any).__demoLogElement === undefined) {
(window as any).__demoLogElement = this;
}
}
public async updateLog(entries?: ILogEntry[]) {
if (entries) {
// Add new entries
this.logEntries = [...this.logEntries, ...entries];
// Trim if exceeds max entries
if (this.logEntries.length > this.maxEntries) {
this.logEntries = this.logEntries.slice(-this.maxEntries);
}
// Trigger re-render
this.requestUpdate();
// Auto-scroll if enabled
await this.updateComplete;
if (this.autoScroll) {
this.scrollToBottom();
}
}
}
public clearLogs() {
this.logEntries = [];
this.requestUpdate();
}
private scrollToBottom() {
if (this.logContainer) {
this.logContainer.scrollTop = this.logContainer.scrollHeight;
}
}
public addLog(level: ILogEntry['level'], message: string, source?: string) {
const newEntry: ILogEntry = {
timestamp: new Date().toISOString(),
level,
message,
source
};
this.updateLog([newEntry]);
}
}

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

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

View File

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

View File

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

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

View 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,
})
);
}
}

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

View 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 }
}));
}
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,262 @@
import { html } from '@design.estate/dees-element';
import { html, css, cssManager } from '@design.estate/dees-element';
import { DeesToast } from './dees-toast.js';
import './dees-button.js';
export const demoFunc = async () => {
return html`<dees-toast></dees-toast>`;
return html`
<style>
.demo-container {
padding: 32px;
min-height: 100vh;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
}
.demo-section {
margin-bottom: 48px;
}
.demo-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.demo-description {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#aaa')};
margin-bottom: 24px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
z-index: 100;
}
</style>
<div class="demo-container">
<dees-button class="theme-toggle" @clicked=${() => {
document.body.classList.toggle('bright');
}}>Toggle Theme</dees-button>
<div class="demo-section">
<h2 class="demo-title">Toast Types</h2>
<p class="demo-description">
Different toast types for various notification scenarios. Click any button to show a toast.
</p>
<div class="button-grid">
<dees-button @clicked=${() => {
DeesToast.info('This is an informational message');
}}>Info Toast</dees-button>
<dees-button type="highlighted" @clicked=${() => {
DeesToast.success('Operation completed successfully!');
}}>Success Toast</dees-button>
<dees-button @clicked=${() => {
DeesToast.warning('Please review before proceeding');
}}>Warning Toast</dees-button>
<dees-button @clicked=${() => {
DeesToast.error('An error occurred while processing');
}}>Error Toast</dees-button>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Toast Positions</h2>
<p class="demo-description">
Toasts can appear in different positions on the screen.
</p>
<div class="button-grid">
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Top Right Position',
type: 'info',
position: 'top-right'
});
}}>Top Right</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Top Left Position',
type: 'info',
position: 'top-left'
});
}}>Top Left</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Bottom Right Position',
type: 'info',
position: 'bottom-right'
});
}}>Bottom Right</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Bottom Left Position',
type: 'info',
position: 'bottom-left'
});
}}>Bottom Left</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Top Center Position',
type: 'info',
position: 'top-center'
});
}}>Top Center</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Bottom Center Position',
type: 'info',
position: 'bottom-center'
});
}}>Bottom Center</dees-button>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Duration Options</h2>
<p class="demo-description">
Control how long toasts stay visible. Duration in milliseconds.
</p>
<div class="button-grid">
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Quick toast (1 second)',
type: 'info',
duration: 1000
});
}}>1 Second</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Standard toast (3 seconds)',
type: 'info',
duration: 3000
});
}}>3 Seconds (Default)</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Long toast (5 seconds)',
type: 'info',
duration: 5000
});
}}>5 Seconds</dees-button>
<dees-button @clicked=${() => {
DeesToast.show({
message: 'Manual dismiss only (click to close)',
type: 'warning',
duration: 0
});
}}>No Auto-Dismiss</dees-button>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Multiple Toasts</h2>
<p class="demo-description">
Multiple toasts stack automatically. They maintain their order and animate smoothly.
</p>
<div class="button-grid">
<dees-button @clicked=${() => {
DeesToast.info('First notification');
setTimeout(() => DeesToast.success('Second notification'), 200);
setTimeout(() => DeesToast.warning('Third notification'), 400);
setTimeout(() => DeesToast.error('Fourth notification'), 600);
}}>Show Multiple</dees-button>
<dees-button @clicked=${() => {
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
DeesToast.show({
message: `Notification #${i}`,
type: i % 2 === 0 ? 'success' : 'info',
duration: 2000 + (i * 500)
});
}, i * 100);
}
}}>Rapid Fire</dees-button>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Real-World Examples</h2>
<p class="demo-description">
Common use cases for toast notifications in applications.
</p>
<div class="button-grid">
<dees-button @clicked=${async () => {
const toast = await DeesToast.show({
message: 'Saving changes...',
type: 'info',
duration: 0
});
// Simulate save operation
setTimeout(() => {
toast.dismiss();
DeesToast.success('Changes saved successfully!');
}, 2000);
}}>Save Operation</dees-button>
<dees-button @clicked=${() => {
DeesToast.error('Failed to connect to server. Please check your internet connection.');
}}>Network Error</dees-button>
<dees-button @clicked=${() => {
DeesToast.warning('Your session will expire in 5 minutes');
}}>Session Warning</dees-button>
<dees-button @clicked=${() => {
DeesToast.success('File uploaded successfully!');
}}>Upload Complete</dees-button>
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Programmatic Control</h2>
<p class="demo-description">
Advanced control over toast behavior.
</p>
<div class="button-grid">
<dees-button @clicked=${async () => {
const toast = await DeesToast.show({
message: 'This toast can be dismissed programmatically',
type: 'info',
duration: 0
});
setTimeout(() => {
toast.dismiss();
DeesToast.success('Toast dismissed after 2 seconds');
}, 2000);
}}>Programmatic Dismiss</dees-button>
<dees-button @clicked=${() => {
// Using the convenience methods
DeesToast.info('Info message', 2000);
setTimeout(() => DeesToast.success('Success message', 2000), 500);
setTimeout(() => DeesToast.warning('Warning message', 2000), 1000);
setTimeout(() => DeesToast.error('Error message', 2000), 1500);
}}>Convenience Methods</dees-button>
</div>
</div>
</div>
`;
};

View File

@ -1,4 +1,4 @@
import { customElement, DeesElement, type TemplateResult, html, type CSSResult, } from '@design.estate/dees-element';
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { 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);
}
}

View File

@ -4,6 +4,7 @@ export * from './dees-appui-base.js';
export * from './dees-appui-maincontent.js';
export * from './dees-appui-mainmenu.js';
export * from './dees-appui-mainselector.js';
export * from './dees-badge.js';
export * from './dees-button-exit.js';
export * from './dees-button.js';
export * from './dees-chart-area.js';
@ -17,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';
@ -34,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';
@ -45,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';