Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
ac15da9c82 | |||
b9432c8489 | |||
b35b1fbae7 | |||
e39590df2c | |||
fad7fda2a6 | |||
987f557c60 | |||
4eef9fc731 | |||
cd86001713 | |||
f7e4582fde | |||
4635e3fce5 | |||
af3dc5c466 | |||
12861b2230 | |||
b7f672e0f2 | |||
fcb44dfd24 | |||
f17b880b59 | |||
68785d9a72 | |||
ab4396297a | |||
ef369f2955 | |||
1e73a9527b | |||
23a4faa5d1 | |||
b0020ace16 | |||
bb78d32dbf | |||
e83ad8d504 | |||
765b01afe0 | |||
00e34e7e6c | |||
bf2ee25390 | |||
bf6d8d0bc6 | |||
3399004e75 | |||
6c2f36f020 | |||
71f4d44782 | |||
6df2eb5acc | |||
469f8e0f21 | |||
3712f6ef90 | |||
d2646cd62c | |||
f29ca0ba0b | |||
0c273a818d | |||
6e8099c6f4 | |||
07c68b82a4 |
128
.gitlab-ci.yml
128
.gitlab-ci.yml
@ -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
|
|
117
changelog.md
117
changelog.md
@ -1,5 +1,122 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-06-10 - 1.8.1 - fix(dees-statsgrid)
|
||||||
|
Adjust stats grid styling for better alignment and improved visualizations in gauge and trend tiles.
|
||||||
|
|
||||||
|
- Center-align tile header elements by setting align-items to center and ensuring full width.
|
||||||
|
- Increase tile content height to 90px and center its content.
|
||||||
|
- Update gauge visualization: reduce circle radius from 40 to 30, adjust stroke dasharray (from 251.2 to 188.5), and decrease gauge text font size.
|
||||||
|
- Refine trend chart layout: set trend-svg height to 40px, center trend value and adjust typography to larger, bolder text.
|
||||||
|
- Ensure overall grid responsiveness with adjusted gap and column sizing.
|
||||||
|
|
||||||
|
## 2025-04-25 - 1.8.0 - feat(dees-pagination)
|
||||||
|
Add new pagination component to the library along with its demo and integration in the main export.
|
||||||
|
|
||||||
|
- Introduced dees-pagination component with support for various page range scenarios.
|
||||||
|
- Created demo file to showcase pagination with both small and large sets of pages.
|
||||||
|
- Updated the module's index to export the new pagination component.
|
||||||
|
|
||||||
|
## 2025-04-22 - 1.7.0 - feat(dees-searchbar)
|
||||||
|
Add dees-searchbar component with live search and filter demo
|
||||||
|
|
||||||
|
- Introduces a new dees-searchbar element with an input field, a search button, and filters
|
||||||
|
- Wires up events for 'search-changed' and 'search-submit' to provide real‐time feedback
|
||||||
|
- Adds a demo file to showcase usage and logging of search events
|
||||||
|
|
||||||
|
## 2025-04-22 - 1.6.0 - feat(documentation/dees-heading)
|
||||||
|
Add codex documentation overview and dees-heading component demo
|
||||||
|
|
||||||
|
- Introduce 'codex.md' to provide a high-level overview of project layout, component patterns, and build workflow
|
||||||
|
- Add and update dees-heading component with demo to support multiple heading levels and horizontal rule styles
|
||||||
|
- Update component export index to include dees-heading
|
||||||
|
|
||||||
|
## 2025-04-18 - 1.5.6 - fix(dependencies)
|
||||||
|
Bump dependency versions and update demo code references
|
||||||
|
|
||||||
|
- Upgrade @design.estate/dees-element from ^2.0.39 to ^2.0.41
|
||||||
|
- Upgrade @tsclass/tsclass from ^4.4.0 to ^9.0.0
|
||||||
|
- Upgrade lucide from ^0.488.0 to ^0.501.0
|
||||||
|
- Update @types/node from ^22.10.7 to ^22.14.1
|
||||||
|
- Update dees-icon demo: scope search to demo container and adjust hover scaling
|
||||||
|
- Replace resolveExec with directives.resolveExec in dees-table for proper rendering
|
||||||
|
|
||||||
|
## 2025-04-12 - 1.5.5 - fix(catalog)
|
||||||
|
No code or documentation changes were detected. This commit records an empty update in commit information and confirms that the current state remains stable.
|
||||||
|
|
||||||
|
- Verified that there are no modifications in source, documentation, or demos
|
||||||
|
- Commit metadata and build configuration remain unchanged
|
||||||
|
|
||||||
|
## 2025-04-11 - 1.5.4 - fix(readme)
|
||||||
|
Update readme with company and trademark guidelines, clarifying legal usage without exposing licensing details.
|
||||||
|
|
||||||
|
- Added sections detailing company information and trademark guidelines.
|
||||||
|
- Outlined legal disclaimers for trademark usage.
|
||||||
|
|
||||||
|
## 2025-04-11 - 1.5.3 - fix(readme)
|
||||||
|
Update readme.md: remove redundant usage section and refine component documentation with improved examples.
|
||||||
|
|
||||||
|
- Removed the standalone manual import and usage example for components.
|
||||||
|
- Added refined examples demonstrating both basic and option-based usage (e.g. for DeesButton).
|
||||||
|
- Improved markdown formatting and consistency across component documentation.
|
||||||
|
|
||||||
|
## 2025-04-11 - 1.5.3 - fix(readme)
|
||||||
|
Update readme.md for clearer documentation: removed redundant 'Usage' section and refined component examples (e.g., DeesButton's basic and options usage) for improved clarity and consistency.
|
||||||
|
|
||||||
|
- Removed standalone usage example showing manual import and creation of components
|
||||||
|
- Added refined examples demonstrating both basic and option-based usage of components
|
||||||
|
- Improved overall readme formatting and consistency across component documentation
|
||||||
|
|
||||||
|
## 2025-04-11 - 1.5.2 - fix(ci)
|
||||||
|
Remove obsolete GitLab CI configuration file
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- Update @push.rocks/smartpromise to version ^4.2.0
|
||||||
|
- Downgrade @webcontainer/api to version 1.2.0
|
||||||
|
- Update @push.rocks/tapbundle to version ^5.5.6
|
||||||
|
- Update @types/node to version ^22.10.7
|
||||||
|
|
||||||
|
## 2025-01-20 - 1.4.0 - feat(dees-terminal)
|
||||||
|
Enhanced the dees-terminal component to support environment variable settings and improved setup command execution.
|
||||||
|
|
||||||
|
- Added environment property to pass custom environment variables.
|
||||||
|
- Introduced webcontainerDeferred to handle the promise for web container creation.
|
||||||
|
- Enhanced demo to illustrate environment variable usage.
|
||||||
|
- Improved async interaction with the terminal for setting environment variables and executing setup commands.
|
||||||
|
|
||||||
|
## 2025-01-15 - 1.3.4 - fix(chart)
|
||||||
|
Fix chart rendering and appearance issues in the DeesChartArea component.
|
||||||
|
|
||||||
|
- Resolved issues with chart dimensions calculation based on padding.
|
||||||
|
- Adjusted grid and axis lines appearance for better visibility.
|
||||||
|
- Updated tooltip and grid line styling for better accessibility.
|
||||||
|
- Improved series data representation as time-series for more accurate display.
|
||||||
|
|
||||||
|
## 2024-12-17 - 1.3.3 - fix(dees-input-multitoggle)
|
||||||
|
Add missing TypeScript declaration for dees-input-multitoggle
|
||||||
|
|
||||||
|
- Added a missing declaration to the HTMLElementTagNameMap for 'dees-input-multitoggle' element.
|
||||||
|
|
||||||
## 2024-12-09 - 1.3.2 - fix(metadata)
|
## 2024-12-09 - 1.3.2 - fix(metadata)
|
||||||
Updated package metadata and readme for better project description and structure.
|
Updated package metadata and readme for better project description and structure.
|
||||||
|
|
||||||
|
43
codex.md
Normal file
43
codex.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Codex: Project Overview and Codebase Structure
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
- Package: `@design.estate/dees-catalog`
|
||||||
|
- Focus: Web Components library providing UI elements and layouts for modern web apps.
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
- ts_web/: TypeScript source files
|
||||||
|
- elements/: Individual Web Component definitions
|
||||||
|
- pages/: Page-level templates for composite layouts
|
||||||
|
- html/: Demo/app entry point loading the bundled scripts
|
||||||
|
- dist_bundle/: Bundled browser JS and source maps
|
||||||
|
- dist_ts_web/: ES module outputs for TypeScript/web consumers
|
||||||
|
- dist_watch/: Watch-mode development bundle with live reload
|
||||||
|
- test/: Browser-based tests using `@push.rocks/tapbundle`
|
||||||
|
|
||||||
|
## Component Patterns
|
||||||
|
- Each component in ts_web/elements/:
|
||||||
|
- Decorated with `@customElement('tag-name')`
|
||||||
|
- Extends `DeesElement` from `@design.estate/dees-element`
|
||||||
|
- Uses `@property` for reactive, reflected attributes
|
||||||
|
- Defines `static styles = [cssManager.defaultStyles, css`...`]`
|
||||||
|
- Implements `render()` returning a Lit `html` template with slots or markup
|
||||||
|
- Exposes a demo via `public static demo` linking to `.demo.ts` files
|
||||||
|
|
||||||
|
## Build & Development Workflow
|
||||||
|
- Install dependencies: `npm install` or `pnpm install`
|
||||||
|
- Build production bundle: `npm run build`
|
||||||
|
- Start dev watch mode: `npm run watch`
|
||||||
|
- Run tests: `npm test` (launches browser fixtures)
|
||||||
|
|
||||||
|
## Theming & Utilities
|
||||||
|
- Default global styles via `cssManager.defaultStyles`
|
||||||
|
- Theme-aware values with `cssManager.bdTheme(light, dark)`
|
||||||
|
- DOM utilities set up in `html/index.ts` using `@design.estate/dees-domtools`
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- `readme.md` provides an overview of all components and basic usage
|
||||||
|
- Live examples in `.demo.ts` files
|
||||||
|
accessible via component `demo` static property
|
||||||
|
|
||||||
|
## Updates to this file
|
||||||
|
If you have pattern insisights or general changes to the codebase, please update this file.
|
42
package.json
42
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "1.3.2",
|
"version": "1.8.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@ -15,34 +15,35 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.0.61",
|
"@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",
|
"@design.estate/dees-wcctools": "^1.0.90",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@push.rocks/smarti18n": "^1.0.4",
|
"@push.rocks/smarti18n": "^1.0.4",
|
||||||
"@push.rocks/smartpromise": "^4.0.4",
|
"@push.rocks/smartpromise": "^4.2.0",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@tsclass/tsclass": "^4.1.2",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"@webcontainer/api": "1.2.0",
|
"@webcontainer/api": "1.6.1",
|
||||||
"apexcharts": "^3.54.0",
|
"apexcharts": "^4.7.0",
|
||||||
"highlight.js": "11.10.0",
|
"highlight.js": "11.11.1",
|
||||||
"ibantools": "^4.5.1",
|
"ibantools": "^4.5.1",
|
||||||
"monaco-editor": "^0.52.0",
|
"lucide": "^0.514.0",
|
||||||
"pdfjs-dist": "^4.6.82",
|
"monaco-editor": "^0.52.2",
|
||||||
|
"pdfjs-dist": "^4.10.38",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0"
|
"xterm-addon-fit": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.84",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsbundle": "^2.0.15",
|
"@git.zone/tsbundle": "^2.0.15",
|
||||||
"@git.zone/tstest": "^1.0.90",
|
"@git.zone/tstest": "^2.3.1",
|
||||||
"@git.zone/tswatch": "^2.0.23",
|
"@git.zone/tswatch": "^2.0.37",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/tapbundle": "^5.3.0",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^22.7.4"
|
"@types/node": "^22.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@ -84,5 +85,6 @@
|
|||||||
"Web Applications",
|
"Web Applications",
|
||||||
"Modern Web",
|
"Modern Web",
|
||||||
"Frontend Development"
|
"Frontend Development"
|
||||||
]
|
],
|
||||||
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
14127
pnpm-lock.yaml
generated
14127
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,43 @@
|
|||||||
!!! Please pay attention to the following points when writing the readme: !!!
|
!!! Please pay attention to the following points when writing the readme: !!!
|
||||||
* Give a short rundown of components and a few points abputspecific features on each.
|
* Give a short rundown of components and a few points abputspecific features on each.
|
||||||
* Try to list all components in a summary.
|
* Try to list all components in a summary.
|
||||||
* Then list all components with a short description.
|
* Then list all components with a short description.
|
||||||
|
|
||||||
|
## Chart Components
|
||||||
|
|
||||||
|
### dees-chart-area
|
||||||
|
- Fully functional area chart component using ApexCharts
|
||||||
|
- Displays time-series data with gradient fills
|
||||||
|
- Responsive with ResizeObserver (debounced to prevent flicker)
|
||||||
|
- Fixed: Chart now properly respects container boundaries on initial render
|
||||||
|
- Overflow prevention with proper CSS containment
|
||||||
|
- Enhanced demo features:
|
||||||
|
- Multiple dataset examples (System Usage, Network Traffic, Sales Analytics)
|
||||||
|
- Real-time data simulation with automatic updates
|
||||||
|
- Dynamic dataset switching
|
||||||
|
- Customizable Y-axis formatters (percentages, currency, units)
|
||||||
|
- Data randomization for testing
|
||||||
|
- Manual data point addition
|
||||||
|
- Properties:
|
||||||
|
- `label`: Chart title
|
||||||
|
- `series`: ApexAxisChartSeries data
|
||||||
|
- `yAxisFormatter`: Custom Y-axis label formatter function
|
||||||
|
- Methods:
|
||||||
|
- `updateSeries()`: Update chart data
|
||||||
|
- `appendData()`: Add new data points to existing series
|
||||||
|
|
||||||
|
### dees-chart-log
|
||||||
|
- Server log viewer component (not a chart despite the name)
|
||||||
|
- Terminal-style interface with monospace font
|
||||||
|
- Supports log levels: debug, info, warn, error, success
|
||||||
|
- Features:
|
||||||
|
- Auto-scroll toggle
|
||||||
|
- Clear logs button
|
||||||
|
- Colored log levels
|
||||||
|
- Timestamp with milliseconds
|
||||||
|
- Source labels for log entries
|
||||||
|
- Maximum 1000 entries (configurable)
|
||||||
|
- Light/dark theme support
|
||||||
|
- Demo includes realistic server log simulation
|
||||||
|
- Note: In demos, buttons use `@clicked` event (not `@click`)
|
||||||
|
- Demo uses global reference to access log element (window.__demoLogElement)
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '1.3.2',
|
version: '1.8.1',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
12
ts_web/elements/dees-badge.demo.ts
Normal file
12
ts_web/elements/dees-badge.demo.ts
Normal 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>
|
||||||
|
`;
|
96
ts_web/elements/dees-badge.ts
Normal file
96
ts_web/elements/dees-badge.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,285 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css } from '@design.estate/dees-element';
|
||||||
|
import type { DeesChartArea } from './dees-chart-area.js';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
|
let chartElement: DeesChartArea;
|
||||||
|
let intervalId: number;
|
||||||
|
let currentDataset = 'system';
|
||||||
|
|
||||||
|
// Get element reference after render
|
||||||
|
setTimeout(() => {
|
||||||
|
const charts = document.querySelectorAll('dees-chart-area');
|
||||||
|
if (charts.length > 0) {
|
||||||
|
chartElement = charts[charts.length - 1] as DeesChartArea;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Y-axis formatters for different datasets
|
||||||
|
const formatters = {
|
||||||
|
system: (val: number) => `${val}%`,
|
||||||
|
network: (val: number) => `${val} Mbps`,
|
||||||
|
sales: (val: number) => `$${val.toLocaleString()}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Different datasets to showcase
|
||||||
|
const datasets = {
|
||||||
|
system: {
|
||||||
|
label: 'System Usage (%)',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'CPU',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 25 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 30 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 28 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 35 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 32 },
|
||||||
|
{ x: new Date().toISOString(), y: 38 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Memory',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 45 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 48 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 46 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 52 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 50 },
|
||||||
|
{ x: new Date().toISOString(), y: 55 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
label: 'Network Traffic (Mbps)',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Download',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 120 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 150 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 180 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 165 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 190 },
|
||||||
|
{ x: new Date().toISOString(), y: 175 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Upload',
|
||||||
|
data: [
|
||||||
|
{ x: new Date(Date.now() - 300000).toISOString(), y: 25 },
|
||||||
|
{ x: new Date(Date.now() - 240000).toISOString(), y: 30 },
|
||||||
|
{ x: new Date(Date.now() - 180000).toISOString(), y: 35 },
|
||||||
|
{ x: new Date(Date.now() - 120000).toISOString(), y: 28 },
|
||||||
|
{ x: new Date(Date.now() - 60000).toISOString(), y: 32 },
|
||||||
|
{ x: new Date().toISOString(), y: 40 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sales: {
|
||||||
|
label: 'Sales Analytics',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Revenue',
|
||||||
|
data: [
|
||||||
|
{ x: '2025-01-01', y: 45000 },
|
||||||
|
{ x: '2025-01-02', y: 52000 },
|
||||||
|
{ x: '2025-01-03', y: 48000 },
|
||||||
|
{ x: '2025-01-04', y: 61000 },
|
||||||
|
{ x: '2025-01-05', y: 58000 },
|
||||||
|
{ x: '2025-01-06', y: 65000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Profit',
|
||||||
|
data: [
|
||||||
|
{ x: '2025-01-01', y: 12000 },
|
||||||
|
{ x: '2025-01-02', y: 14000 },
|
||||||
|
{ x: '2025-01-03', y: 11000 },
|
||||||
|
{ x: '2025-01-04', y: 18000 },
|
||||||
|
{ x: '2025-01-05', y: 16000 },
|
||||||
|
{ x: '2025-01-06', y: 20000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate random value within range
|
||||||
|
const getRandomValue = (min: number, max: number) => {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add real-time data
|
||||||
|
const addRealtimeData = () => {
|
||||||
|
if (!chartElement) return;
|
||||||
|
|
||||||
|
const dataset = datasets[currentDataset];
|
||||||
|
const newTimestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Generate new data points based on dataset type
|
||||||
|
let newData: any[][] = [];
|
||||||
|
|
||||||
|
if (currentDataset === 'system') {
|
||||||
|
newData = [
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(25, 45) }], // CPU
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(45, 65) }], // Memory
|
||||||
|
];
|
||||||
|
} else if (currentDataset === 'network') {
|
||||||
|
newData = [
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(100, 250) }], // Download
|
||||||
|
[{ x: newTimestamp, y: getRandomValue(20, 50) }], // Upload
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only last 10 data points
|
||||||
|
const currentSeries = chartElement.series.map((series, index) => ({
|
||||||
|
...series,
|
||||||
|
data: [...series.data.slice(-9), ...(newData[index] || [])],
|
||||||
|
}));
|
||||||
|
|
||||||
|
chartElement.series = currentSeries;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Switch dataset
|
||||||
|
const switchDataset = (name: string) => {
|
||||||
|
currentDataset = name;
|
||||||
|
if (chartElement) {
|
||||||
|
const dataset = datasets[name];
|
||||||
|
chartElement.label = dataset.label;
|
||||||
|
chartElement.series = dataset.series;
|
||||||
|
chartElement.yAxisFormatter = formatters[name];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start/stop real-time updates
|
||||||
|
const startRealtime = () => {
|
||||||
|
if (!intervalId && (currentDataset === 'system' || currentDataset === 'network')) {
|
||||||
|
intervalId = window.setInterval(() => addRealtimeData(), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRealtime = () => {
|
||||||
|
if (intervalId) {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Randomize current data
|
||||||
|
const randomizeData = () => {
|
||||||
|
if (!chartElement) return;
|
||||||
|
|
||||||
|
const currentSeries = chartElement.series.map(series => ({
|
||||||
|
...series,
|
||||||
|
data: series.data.map(point => ({
|
||||||
|
...point,
|
||||||
|
y: typeof point.y === 'number'
|
||||||
|
? point.y * (0.8 + Math.random() * 0.4) // +/- 20% variation
|
||||||
|
: point.y,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
chartElement.series = currentSeries;
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.demoBox {
|
${css`
|
||||||
position: relative;
|
.demoBox {
|
||||||
background: #000000;
|
position: relative;
|
||||||
height: 100%;
|
background: #000000;
|
||||||
width: 100%;
|
height: 100%;
|
||||||
padding: 40px;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
padding: 40px;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: #666;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
</style>
|
</style>
|
||||||
<div class="demoBox">
|
<div class="demoBox">
|
||||||
<dees-chart-area
|
<div class="controls">
|
||||||
.label=${'System Usage'}
|
<div class="control-section">
|
||||||
></dees-chart-area>
|
<span class="section-label">Dataset:</span>
|
||||||
|
<dees-button
|
||||||
|
@clicked=${() => switchDataset('system')}
|
||||||
|
type=${currentDataset === 'system' ? 'highlighted' : 'normal'}
|
||||||
|
>System Usage</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@clicked=${() => switchDataset('network')}
|
||||||
|
type=${currentDataset === 'network' ? 'highlighted' : 'normal'}
|
||||||
|
>Network Traffic</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@clicked=${() => switchDataset('sales')}
|
||||||
|
type=${currentDataset === 'sales' ? 'highlighted' : 'normal'}
|
||||||
|
>Sales Data</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-section">
|
||||||
|
<span class="section-label">Real-time:</span>
|
||||||
|
<dees-button @clicked=${() => startRealtime()}>Start Live</dees-button>
|
||||||
|
<dees-button @clicked=${() => stopRealtime()}>Stop Live</dees-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-section">
|
||||||
|
<span class="section-label">Actions:</span>
|
||||||
|
<dees-button @clicked=${() => randomizeData()}>Randomize Values</dees-button>
|
||||||
|
<dees-button @clicked=${() => addRealtimeData()}>Add Point</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<dees-chart-area
|
||||||
|
.label=${datasets[currentDataset].label}
|
||||||
|
.series=${datasets[currentDataset].series}
|
||||||
|
.yAxisFormatter=${formatters[currentDataset]}
|
||||||
|
></dees-chart-area>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
Real-time updates work with System Usage and Network Traffic datasets •
|
||||||
|
Chart updates every 2 seconds when live mode is active •
|
||||||
|
Try switching datasets and randomizing values
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
@ -32,30 +32,49 @@ export class DeesChartArea extends DeesElement {
|
|||||||
@property()
|
@property()
|
||||||
public label: string = 'Untitled Chart';
|
public label: string = 'Untitled Chart';
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public series: ApexAxisChartSeries = [];
|
||||||
|
|
||||||
|
@property({ type: Function })
|
||||||
|
public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
|
||||||
|
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
private resizeTimeout: number;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
domtools.elementBasic.setup();
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(entries => {
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (let entry of entries) {
|
// Debounce resize calls to prevent excessive updates
|
||||||
if (entry.target.classList.contains('mainbox')) {
|
if (this.resizeTimeout) {
|
||||||
this.resizeChart(); // Call resizeChart when the .mainbox size changes
|
clearTimeout(this.resizeTimeout);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.resizeTimeout = window.setTimeout(() => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (entry.target.classList.contains('mainbox') && this.chart) {
|
||||||
|
this.resizeChart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100); // 100ms debounce
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerStartupFunction(async () => {
|
this.registerStartupFunction(async () => {
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||||
if (mainbox) {
|
if (mainbox) {
|
||||||
this.resizeObserver.observe(mainbox); // Start observing the .mainbox element
|
this.resizeObserver.observe(mainbox);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerGarbageFunction(async () => {
|
this.registerGarbageFunction(async () => {
|
||||||
|
if (this.resizeTimeout) {
|
||||||
|
clearTimeout(this.resizeTimeout);
|
||||||
|
}
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
@ -71,8 +90,9 @@ export class DeesChartArea extends DeesElement {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
background: #222;
|
background: #111;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartTitle {
|
.chartTitle {
|
||||||
@ -82,6 +102,7 @@ export class DeesChartArea extends DeesElement {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.chartContainer {
|
.chartContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -90,37 +111,86 @@ export class DeesChartArea extends DeesElement {
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
padding: 32px 16px 16px 0px;
|
padding: 32px 16px 16px 0px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html` <div class="mainbox">
|
return html`
|
||||||
<div class="chartTitle">${this.label}</div>
|
<div class="mainbox">
|
||||||
<div class="chartContainer"></div>
|
<div class="chartTitle">${this.label}</div>
|
||||||
</div> `;
|
<div class="chartContainer"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const domtoolsInstance = await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
|
|
||||||
|
// Wait for next animation frame to ensure layout is complete
|
||||||
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
|
||||||
|
// Get actual dimensions of the container
|
||||||
|
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||||
|
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||||
|
|
||||||
|
if (!mainbox || !chartContainer) {
|
||||||
|
console.error('Chart containers not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate initial dimensions
|
||||||
|
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||||
|
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
||||||
|
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
||||||
|
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
||||||
|
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
||||||
|
|
||||||
|
const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||||
|
const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||||
|
|
||||||
|
// Use provided series data or default demo data
|
||||||
|
const chartSeries = this.series.length > 0 ? this.series : [
|
||||||
|
{
|
||||||
|
name: 'cpu',
|
||||||
|
data: [
|
||||||
|
{ x: '2025-01-15T03:00:00', y: 25 },
|
||||||
|
{ x: '2025-01-15T07:00:00', y: 30 },
|
||||||
|
{ x: '2025-01-15T11:00:00', y: 20 },
|
||||||
|
{ x: '2025-01-15T15:00:00', y: 35 },
|
||||||
|
{ x: '2025-01-15T19:00:00', y: 25 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'memory',
|
||||||
|
data: [
|
||||||
|
{ x: '2025-01-15T03:00:00', y: 10 },
|
||||||
|
{ x: '2025-01-15T07:00:00', y: 12 },
|
||||||
|
{ x: '2025-01-15T11:00:00', y: 10 },
|
||||||
|
{ x: '2025-01-15T15:00:00', y: 30 },
|
||||||
|
{ x: '2025-01-15T19:00:00', y: 40 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
var options: ApexCharts.ApexOptions = {
|
var options: ApexCharts.ApexOptions = {
|
||||||
series: [
|
series: chartSeries,
|
||||||
{
|
|
||||||
name: 'cpu',
|
|
||||||
data: [31, 40, 28, 51, 42, 109, 100],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'memory',
|
|
||||||
data: [11, 32, 45, 32, 34, 52, 41],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
chart: {
|
chart: {
|
||||||
width: 0, // Adjusted for responsive width
|
width: initialWidth || 100, // Use actual width or fallback
|
||||||
height: 0, // Adjusted for responsive height
|
height: initialHeight || 100, // Use actual height or fallback
|
||||||
type: 'area',
|
type: 'area',
|
||||||
toolbar: {
|
toolbar: {
|
||||||
show: false, // This line disables the toolbar
|
show: false, // This line disables the toolbar
|
||||||
},
|
},
|
||||||
|
animations: {
|
||||||
|
enabled: true,
|
||||||
|
speed: 400,
|
||||||
|
animateGradually: {
|
||||||
|
enabled: true,
|
||||||
|
delay: 150
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -130,35 +200,57 @@ export class DeesChartArea extends DeesElement {
|
|||||||
curve: 'smooth',
|
curve: 'smooth',
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
crosshairs: {
|
type: 'datetime', // Time-series data
|
||||||
stroke: {
|
labels: {
|
||||||
width: 1,
|
format: 'hh:mm A', // Time formatting
|
||||||
color: '#444',
|
style: {
|
||||||
|
colors: '#9e9e9e', // Label color
|
||||||
|
fontSize: '12px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: 'datetime',
|
axisBorder: {
|
||||||
categories: [
|
show: false, // Hide x-axis border
|
||||||
'2018-09-19T00:00:00.000Z',
|
},
|
||||||
'2018-09-19T01:30:00.000Z',
|
axisTicks: {
|
||||||
'2018-09-19T02:30:00.000Z',
|
show: false, // Hide x-axis ticks
|
||||||
'2018-09-19T03:30:00.000Z',
|
},
|
||||||
'2018-09-19T04:30:00.000Z',
|
|
||||||
'2018-09-19T05:30:00.000Z',
|
|
||||||
'2018-09-19T06:30:00.000Z',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
crosshairs: {
|
min: 0,
|
||||||
stroke: {
|
labels: {
|
||||||
width: 1,
|
formatter: this.yAxisFormatter,
|
||||||
color: '#444',
|
style: {
|
||||||
|
colors: '#9e9e9e', // Label color
|
||||||
|
fontSize: '12px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
axisBorder: {
|
||||||
|
show: false, // Hide y-axis border
|
||||||
|
},
|
||||||
|
axisTicks: {
|
||||||
|
show: false, // Hide y-axis ticks
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
shared: true, // Enables the tooltip to display across series
|
||||||
|
intersect: false, // Allows hovering anywhere on the chart
|
||||||
|
followCursor: true, // Makes tooltip follow mouse even between points
|
||||||
x: {
|
x: {
|
||||||
format: 'dd/MM/yy HH:mm',
|
format: 'dd/MM/yy HH:mm',
|
||||||
},
|
},
|
||||||
|
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;">`;
|
||||||
|
|
||||||
|
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/>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipContent += `</div>`;
|
||||||
|
return tooltipContent;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
xaxis: {
|
xaxis: {
|
||||||
@ -171,8 +263,8 @@ export class DeesChartArea extends DeesElement {
|
|||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderColor: '#666', // Set the color of the grid lines
|
borderColor: '#333', // Set the color of the grid lines
|
||||||
strokeDashArray: 2, // Solid line
|
strokeDashArray: 0, // Solid line
|
||||||
row: {
|
row: {
|
||||||
colors: [], // This can be used to alternate the shading of the horizontal rows
|
colors: [], // This can be used to alternate the shading of the horizontal rows
|
||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
@ -182,27 +274,88 @@ export class DeesChartArea extends DeesElement {
|
|||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient', // Gradient fill for the area
|
||||||
|
gradient: {
|
||||||
|
shade: 'dark',
|
||||||
|
type: 'vertical',
|
||||||
|
gradientToColors: ['#9c27b0'], // Gradient color ending
|
||||||
|
stops: [0, 100],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options);
|
||||||
await this.chart.render();
|
await this.chart.render();
|
||||||
|
|
||||||
|
// Give the chart a moment to fully initialize before resizing
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
await this.resizeChart();
|
await this.resizeChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updated(changedProperties: Map<string, any>) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
// Update chart if series data changes
|
||||||
|
if (changedProperties.has('series') && this.chart && this.series.length > 0) {
|
||||||
|
await this.updateSeries(this.series);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update y-axis formatter if it changes
|
||||||
|
if (changedProperties.has('yAxisFormatter') && this.chart) {
|
||||||
|
await this.chart.updateOptions({
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
formatter: this.yAxisFormatter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateSeries(newSeries: ApexAxisChartSeries) {
|
||||||
|
if (!this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.chart.updateSeries(newSeries, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async appendData(seriesIndex: number, newData: any[]) {
|
||||||
|
if (!this.chart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSeries = [...this.series];
|
||||||
|
if (currentSeries[seriesIndex]) {
|
||||||
|
currentSeries[seriesIndex].data = [...currentSeries[seriesIndex].data, ...newData];
|
||||||
|
await this.updateSeries(currentSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async resizeChart() {
|
public async resizeChart() {
|
||||||
const element = this.shadowRoot.querySelector('.chartContainer');
|
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
|
// Get computed style of the element
|
||||||
const style = window.getComputedStyle(element);
|
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||||
|
|
||||||
// Extract padding values
|
// Extract padding values
|
||||||
const paddingTop = parseInt(style.paddingTop, 10);
|
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
||||||
const paddingBottom = parseInt(style.paddingBottom, 10);
|
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
||||||
const paddingLeft = parseInt(style.paddingLeft, 10);
|
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
||||||
const paddingRight = parseInt(style.paddingRight, 10);
|
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
||||||
|
|
||||||
// Calculate the actual width and height to use, subtracting padding
|
// Calculate the actual width and height to use, subtracting padding
|
||||||
const actualWidth = element.clientWidth - paddingLeft - paddingRight;
|
const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||||
const actualHeight = element.clientHeight - paddingTop - paddingBottom;
|
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||||
|
|
||||||
await this.chart.updateOptions({
|
await this.chart.updateOptions({
|
||||||
chart: {
|
chart: {
|
||||||
|
@ -1,6 +1,123 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => {
|
||||||
|
let intervalId: number;
|
||||||
|
|
||||||
|
const serverSources = ['Server', 'Database', 'API', 'Auth', 'Cache', 'Queue', 'WebSocket', 'Scheduler'];
|
||||||
|
|
||||||
|
const logTemplates = {
|
||||||
|
debug: [
|
||||||
|
'Loading module: {{module}}',
|
||||||
|
'Cache hit for key: {{key}}',
|
||||||
|
'SQL query executed in {{time}}ms',
|
||||||
|
'Request headers: {{headers}}',
|
||||||
|
'Environment variable loaded: {{var}}',
|
||||||
|
],
|
||||||
|
info: [
|
||||||
|
'Request received: {{method}} {{path}}',
|
||||||
|
'User {{userId}} authenticated successfully',
|
||||||
|
'Processing job {{jobId}} from queue',
|
||||||
|
'Scheduled task "{{task}}" started',
|
||||||
|
'WebSocket connection established from {{ip}}',
|
||||||
|
],
|
||||||
|
warn: [
|
||||||
|
'Slow query detected: {{query}} ({{time}}ms)',
|
||||||
|
'Memory usage at {{percent}}%',
|
||||||
|
'Rate limit approaching for IP {{ip}}',
|
||||||
|
'Deprecated API endpoint called: {{endpoint}}',
|
||||||
|
'Certificate expires in {{days}} days',
|
||||||
|
],
|
||||||
|
error: [
|
||||||
|
'Database connection lost: {{error}}',
|
||||||
|
'Failed to process request: {{error}}',
|
||||||
|
'Authentication failed for user {{user}}',
|
||||||
|
'File not found: {{path}}',
|
||||||
|
'Service unavailable: {{service}}',
|
||||||
|
],
|
||||||
|
success: [
|
||||||
|
'Server started successfully on port {{port}}',
|
||||||
|
'Database migration completed',
|
||||||
|
'Backup completed: {{size}} MB',
|
||||||
|
'SSL certificate renewed',
|
||||||
|
'Health check passed: all systems operational',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateRandomLog = () => {
|
||||||
|
const logElement = (window as any).__demoLogElement;
|
||||||
|
if (!logElement) {
|
||||||
|
console.warn('Log element not ready yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const levels: Array<'debug' | 'info' | 'warn' | 'error' | 'success'> = ['debug', 'info', 'warn', 'error', 'success'];
|
||||||
|
const weights = [0.2, 0.5, 0.15, 0.1, 0.05]; // Weighted probability
|
||||||
|
|
||||||
|
const random = Math.random();
|
||||||
|
let cumulative = 0;
|
||||||
|
let level: typeof levels[0] = 'info';
|
||||||
|
|
||||||
|
for (let i = 0; i < weights.length; i++) {
|
||||||
|
cumulative += weights[i];
|
||||||
|
if (random < cumulative) {
|
||||||
|
level = levels[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = serverSources[Math.floor(Math.random() * serverSources.length)];
|
||||||
|
const templates = logTemplates[level];
|
||||||
|
const template = templates[Math.floor(Math.random() * templates.length)];
|
||||||
|
|
||||||
|
// Replace placeholders with random values
|
||||||
|
const message = template
|
||||||
|
.replace('{{module}}', ['express', 'mongoose', 'redis', 'socket.io'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{key}}', 'user:' + Math.floor(Math.random() * 1000))
|
||||||
|
.replace('{{time}}', String(Math.floor(Math.random() * 500) + 50))
|
||||||
|
.replace('{{headers}}', 'Content-Type: application/json, Authorization: Bearer ...')
|
||||||
|
.replace('{{var}}', ['NODE_ENV', 'DATABASE_URL', 'API_KEY', 'PORT'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{method}}', ['GET', 'POST', 'PUT', 'DELETE'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{path}}', ['/api/users', '/api/auth/login', '/api/products', '/health'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{userId}}', String(Math.floor(Math.random() * 10000)))
|
||||||
|
.replace('{{jobId}}', 'job_' + Math.random().toString(36).substring(2, 11))
|
||||||
|
.replace('{{task}}', ['cleanup', 'backup', 'report-generation', 'cache-refresh'][Math.floor(Math.random() * 4)])
|
||||||
|
.replace('{{ip}}', `192.168.1.${Math.floor(Math.random() * 255)}`)
|
||||||
|
.replace('{{query}}', 'SELECT * FROM users WHERE ...')
|
||||||
|
.replace('{{percent}}', String(Math.floor(Math.random() * 30) + 70))
|
||||||
|
.replace('{{endpoint}}', '/api/v1/legacy')
|
||||||
|
.replace('{{days}}', String(Math.floor(Math.random() * 30) + 1))
|
||||||
|
.replace('{{error}}', ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'][Math.floor(Math.random() * 3)])
|
||||||
|
.replace('{{user}}', 'user_' + Math.floor(Math.random() * 1000))
|
||||||
|
.replace('{{service}}', ['Redis', 'MongoDB', 'ElasticSearch'][Math.floor(Math.random() * 3)])
|
||||||
|
.replace('{{port}}', String(3000 + Math.floor(Math.random() * 10)))
|
||||||
|
.replace('{{size}}', String(Math.floor(Math.random() * 500) + 100));
|
||||||
|
|
||||||
|
logElement.addLog(level, message, source);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startSimulation = () => {
|
||||||
|
if (!intervalId) {
|
||||||
|
// Generate logs at random intervals between 500ms and 2500ms
|
||||||
|
const scheduleNext = () => {
|
||||||
|
generateRandomLog();
|
||||||
|
const nextDelay = Math.random() * 2000 + 500;
|
||||||
|
intervalId = window.setTimeout(() => {
|
||||||
|
if (intervalId) {
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
}, nextDelay);
|
||||||
|
};
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSimulation = () => {
|
||||||
|
if (intervalId) {
|
||||||
|
window.clearTimeout(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.demoBox {
|
.demoBox {
|
||||||
@ -9,11 +126,31 @@ export const demoFunc = () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
color: #888;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="demoBox">
|
<div class="demoBox">
|
||||||
|
<div class="controls">
|
||||||
|
<dees-button @clicked=${() => generateRandomLog()}>Add Single Log</dees-button>
|
||||||
|
<dees-button @clicked=${() => startSimulation()}>Start Simulation</dees-button>
|
||||||
|
<dees-button @clicked=${() => stopSimulation()}>Stop Simulation</dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="info">Simulating realistic server logs with various levels and sources</div>
|
||||||
<dees-chart-log
|
<dees-chart-log
|
||||||
.label=${'Event Log'}
|
.label=${'Production Server Logs'}
|
||||||
></dees-chart-log>
|
></dees-chart-log>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -5,15 +5,12 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
property,
|
property,
|
||||||
state,
|
|
||||||
type CSSResult,
|
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { demoFunc } from './dees-chart-log.demo.js';
|
import { demoFunc } from './dees-chart-log.demo.js';
|
||||||
|
|
||||||
import ApexCharts from 'apexcharts';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -21,69 +18,308 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ILogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: 'debug' | 'info' | 'warn' | 'error' | 'success';
|
||||||
|
message: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('dees-chart-log')
|
@customElement('dees-chart-log')
|
||||||
export class DeesChartLog extends DeesElement {
|
export class DeesChartLog extends DeesElement {
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
// instance
|
|
||||||
@state()
|
|
||||||
public chart: ApexCharts;
|
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public label: string = 'Untitled Chart';
|
public label: string = 'Server Logs';
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public logEntries: ILogEntry[] = [];
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public autoScroll: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public maxEntries: number = 1000;
|
||||||
|
|
||||||
|
private logContainer: HTMLDivElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
domtools.elementBasic.setup();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
font-family: 'Geist Sans', sans-serif;
|
font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.mainbox {
|
.mainbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
background: #222;
|
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 32px 16px 16px 0px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartTitle {
|
.header {
|
||||||
position: absolute;
|
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||||
top: 0;
|
padding: 8px 16px;
|
||||||
left: 0;
|
border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||||
width: 100%;
|
display: flex;
|
||||||
text-align: center;
|
justify-content: space-between;
|
||||||
padding-top: 16px;
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.chartContainer {
|
|
||||||
position: relative;
|
.title {
|
||||||
width: 100%;
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#212529', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')};
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: ${cssManager.bdTheme('#495057', '#ccc')};
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')};
|
||||||
|
border-color: ${cssManager.bdTheme('#adb5bd', '#555')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button.active {
|
||||||
|
background: ${cssManager.bdTheme('#007bff', '#4a4a4a')};
|
||||||
|
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEntry {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: flex;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
margin-right: 8px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.debug {
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#999')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.info {
|
||||||
|
color: ${cssManager.bdTheme('#0066cc', '#4a9eff')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.warn {
|
||||||
|
color: ${cssManager.bdTheme('#ff8800', '#ffb84a')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.error {
|
||||||
|
color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.level.success {
|
||||||
|
color: ${cssManager.bdTheme('#28a745', '#4aff88')};
|
||||||
|
background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#888')};
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: ${cssManager.bdTheme('#212529', '#ddd')};
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
.logContainer::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer::-webkit-scrollbar-track {
|
||||||
|
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer::-webkit-scrollbar-thumb {
|
||||||
|
background: ${cssManager.bdTheme('#adb5bd', '#444')};
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logContainer::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${cssManager.bdTheme('#6c757d', '#555')};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html` <div class="mainbox">
|
return html`
|
||||||
<div class="chartTitle">${this.label}</div>
|
<div class="mainbox">
|
||||||
<div class="chartContainer"></div>
|
<div class="header">
|
||||||
</div> `;
|
<div class="title">${this.label}</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button
|
||||||
|
class="control-button ${this.autoScroll ? 'active' : ''}"
|
||||||
|
@click=${() => { this.autoScroll = !this.autoScroll; }}
|
||||||
|
>
|
||||||
|
Auto Scroll
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="control-button"
|
||||||
|
@click=${() => { this.clearLogs(); }}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="logContainer">
|
||||||
|
${this.logEntries.length === 0
|
||||||
|
? html`<div class="empty-state">No logs to display</div>`
|
||||||
|
: this.logEntries.map(entry => this.renderLogEntry(entry))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLogEntry(entry: ILogEntry): TemplateResult {
|
||||||
|
const timestamp = new Date(entry.timestamp).toLocaleTimeString('en-US', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
fractionalSecondDigits: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="logEntry">
|
||||||
|
<span class="timestamp">${timestamp}</span>
|
||||||
|
<span class="level ${entry.level}">${entry.level}</span>
|
||||||
|
${entry.source ? html`<span class="source">[${entry.source}]</span>` : ''}
|
||||||
|
<span class="message">${entry.message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const domtoolsInstance = await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
|
this.logContainer = this.shadowRoot.querySelector('.logContainer');
|
||||||
|
|
||||||
|
// Initialize with demo server logs
|
||||||
|
const demoLogs: ILogEntry[] = [
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'Server started on port 3000', source: 'Server' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'debug', message: 'Loading configuration from /etc/app/config.json', source: 'Config' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'Connected to MongoDB at mongodb://localhost:27017', source: 'Database' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'success', message: 'Database connection established successfully', source: 'Database' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'warn', message: 'No SSL certificate found, using self-signed certificate', source: 'Security' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'API routes initialized: GET /api/users, POST /api/users, DELETE /api/users/:id', source: 'Router' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'debug', message: 'Middleware stack: cors, bodyParser, authentication, errorHandler', source: 'Middleware' },
|
||||||
|
{ timestamp: new Date().toISOString(), level: 'info', message: 'WebSocket server listening on ws://localhost:3001', source: 'WebSocket' },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.logEntries = demoLogs;
|
||||||
|
this.scrollToBottom();
|
||||||
|
|
||||||
|
// For demo purposes, store reference globally
|
||||||
|
if ((window as any).__demoLogElement === undefined) {
|
||||||
|
(window as any).__demoLogElement = this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateLog() {
|
public async updateLog(entries?: ILogEntry[]) {
|
||||||
|
if (entries) {
|
||||||
|
// Add new entries
|
||||||
|
this.logEntries = [...this.logEntries, ...entries];
|
||||||
|
|
||||||
|
// Trim if exceeds max entries
|
||||||
|
if (this.logEntries.length > this.maxEntries) {
|
||||||
|
this.logEntries = this.logEntries.slice(-this.maxEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger re-render
|
||||||
|
this.requestUpdate();
|
||||||
|
|
||||||
|
// Auto-scroll if enabled
|
||||||
|
await this.updateComplete;
|
||||||
|
if (this.autoScroll) {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearLogs() {
|
||||||
|
this.logEntries = [];
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollToBottom() {
|
||||||
|
if (this.logContainer) {
|
||||||
|
this.logContainer.scrollTop = this.logContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addLog(level: ILogEntry['level'], message: string, source?: string) {
|
||||||
|
const newEntry: ILogEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
source
|
||||||
|
};
|
||||||
|
this.updateLog([newEntry]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
ts_web/elements/dees-heading.demo.ts
Normal file
14
ts_web/elements/dees-heading.demo.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
export function demoFunc() {
|
||||||
|
return html`
|
||||||
|
<dees-heading level="1">This is a H1 heading</dees-heading>
|
||||||
|
<dees-heading level="2">This is a H2 heading</dees-heading>
|
||||||
|
<dees-heading level="3">This is a H3 heading</dees-heading>
|
||||||
|
<dees-heading level="4">This is a H4 heading</dees-heading>
|
||||||
|
<dees-heading level="5">This is a H5 heading</dees-heading>
|
||||||
|
<dees-heading level="6">This is a H6 heading</dees-heading>
|
||||||
|
<dees-heading level="hr">This is an hr heading</dees-heading>
|
||||||
|
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
|
||||||
|
`;
|
||||||
|
}
|
115
ts_web/elements/dees-heading.ts
Normal file
115
ts_web/elements/dees-heading.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
property,
|
||||||
|
cssManager,
|
||||||
|
type TemplateResult,
|
||||||
|
DeesElement,
|
||||||
|
type CSSResult,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import { demoFunc } from './dees-heading.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-heading': DeesHeading;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-heading')
|
||||||
|
export class DeesHeading extends DeesElement {
|
||||||
|
// demo
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
// properties
|
||||||
|
/**
|
||||||
|
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
|
||||||
|
*/
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
public level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
|
||||||
|
|
||||||
|
// STATIC STYLES
|
||||||
|
public static styles: CSSResult[] = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
/* Heading styles */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||||
|
}
|
||||||
|
h1 { font-size: 32px; font-family: 'Cal Sans'; letter-spacing: 0.025em;}
|
||||||
|
h2 { font-size: 28px; }
|
||||||
|
h3 { font-size: 24px; }
|
||||||
|
h4 { font-size: 20px; }
|
||||||
|
h5 { font-size: 16px; }
|
||||||
|
h6 { font-size: 14px; }
|
||||||
|
/* Horizontal rule style heading */
|
||||||
|
.heading-hr {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||||
|
}
|
||||||
|
/* Fade lines toward and away from text for hr style */
|
||||||
|
.heading-hr::before {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
/* fade in toward center */
|
||||||
|
background: ${cssManager.bdTheme(
|
||||||
|
'linear-gradient(to right, transparent, #ccc)',
|
||||||
|
'linear-gradient(to right, transparent, #333)'
|
||||||
|
)};
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
.heading-hr::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
/* fade out away from center */
|
||||||
|
background: ${cssManager.bdTheme(
|
||||||
|
'linear-gradient(to right, #ccc, transparent)',
|
||||||
|
'linear-gradient(to right, #333, transparent)'
|
||||||
|
)};
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
/* Small hr variant with reduced margins */
|
||||||
|
.heading-hr.heading-hr-small {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.heading-hr.heading-hr-small::before,
|
||||||
|
.heading-hr.heading-hr-small::after {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
public render(): TemplateResult {
|
||||||
|
switch (this.level) {
|
||||||
|
case '1':
|
||||||
|
return html`<h1><slot></slot></h1>`;
|
||||||
|
case '2':
|
||||||
|
return html`<h2><slot></slot></h2>`;
|
||||||
|
case '3':
|
||||||
|
return html`<h3><slot></slot></h3>`;
|
||||||
|
case '4':
|
||||||
|
return html`<h4><slot></slot></h4>`;
|
||||||
|
case '5':
|
||||||
|
return html`<h5><slot></slot></h5>`;
|
||||||
|
case '6':
|
||||||
|
return html`<h6><slot></slot></h6>`;
|
||||||
|
case 'hr':
|
||||||
|
return html`<div class="heading-hr"><slot></slot></div>`;
|
||||||
|
case 'hr-small':
|
||||||
|
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
||||||
|
default:
|
||||||
|
return html`<h1><slot></slot></h1>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,155 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { 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);
|
||||||
|
|
||||||
|
// 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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const demoFunc = () => html`
|
// 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>
|
<style>
|
||||||
.demoContainer {
|
.demoContainer {
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background: #111111;
|
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 {
|
dees-icon {
|
||||||
transition: color 0.02s;
|
transition: all 0.2s ease;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
dees-icon:hover {
|
|
||||||
color: #e4002b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconContainer {
|
.iconContainer {
|
||||||
display: block;
|
display: flex;
|
||||||
padding: 16px 16px 0px 16px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 16px 0px 16px;
|
||||||
border: 1px solid #333333;
|
border: 1px solid #333333;
|
||||||
margin-right: 8px;
|
margin-right: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconContainer:hover {
|
||||||
|
background-color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconName {
|
.iconName {
|
||||||
@ -33,23 +157,136 @@ export const demoFunc = () => html`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
background: #333333;
|
background: #333333;
|
||||||
padding: 4px 8px;
|
padding: 6px 10px;
|
||||||
padding-bottom: 4px;
|
|
||||||
margin-left: -16px;
|
margin-left: -16px;
|
||||||
margin-right: -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>
|
</style>
|
||||||
|
|
||||||
<div class="demoContainer">
|
<div class="demoContainer">
|
||||||
${Object.keys(faIcons).map(
|
<div class="search-container">
|
||||||
(iconName) => html`
|
<input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}>
|
||||||
<div class="iconContainer">
|
|
||||||
<dees-icon .iconFA=${iconName as any}></dees-icon>
|
|
||||||
<div class="iconName">${iconName}</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
`;
|
<div class="api-note">
|
||||||
|
New API: Use <code>icon="fa:iconName"</code> or <code>icon="lucide:iconName"</code> instead of <code>iconFA</code>.
|
||||||
|
Click any icon to copy its new format to clipboard.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-container fa-section">
|
||||||
|
<div class="section-title">
|
||||||
|
FontAwesome Icons
|
||||||
|
<span class="icon-count">${faIcons.length} icons</span>
|
||||||
|
</div>
|
||||||
|
<div class="icons-grid">
|
||||||
|
${faIcons.map(
|
||||||
|
(iconName) => {
|
||||||
|
const prefixedName = `fa:${iconName}`;
|
||||||
|
return html`
|
||||||
|
<div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}>
|
||||||
|
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||||
|
<div class="iconName">${iconName}</div>
|
||||||
|
<span class="copy-tooltip">Click to copy</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-container lucide-section">
|
||||||
|
<div class="section-title">
|
||||||
|
Lucide Icons
|
||||||
|
<span class="icon-count">${lucideIconsList.length} icons</span>
|
||||||
|
</div>
|
||||||
|
<div class="icons-grid">
|
||||||
|
${lucideIconsList.map(
|
||||||
|
(iconName) => {
|
||||||
|
const prefixedName = `lucide:${iconName}`;
|
||||||
|
return html`
|
||||||
|
<div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}>
|
||||||
|
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||||
|
<div class="iconName">${iconName}</div>
|
||||||
|
<span class="copy-tooltip">Click to copy</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
@ -75,7 +75,12 @@ import {
|
|||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { demoFunc } from './dees-icon.demo.js';
|
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
|
// normal
|
||||||
arrowRight: faArrowRightSolid,
|
arrowRight: faArrowRightSolid,
|
||||||
arrowUpRightFromSquare: faArrowUpRightFromSquareSolid,
|
arrowUpRightFromSquare: faArrowUpRightFromSquareSolid,
|
||||||
@ -136,7 +141,32 @@ export const faIcons = {
|
|||||||
twitter: faTwitter,
|
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 {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -148,31 +178,170 @@ declare global {
|
|||||||
export class DeesIcon extends DeesElement {
|
export class DeesIcon extends DeesElement {
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use the `icon` property instead with format "fa:iconName" or "lucide:iconName"
|
||||||
|
*/
|
||||||
@property({
|
@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;
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
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 = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: inline-flex;
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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`
|
return html`
|
||||||
${domtools.elementBasic.styles}
|
${domtools.elementBasic.styles}
|
||||||
<style>
|
<style>
|
||||||
#iconContainer svg {
|
#iconContainer {
|
||||||
display: block;
|
width: ${this.iconSize}px;
|
||||||
height: ${this.iconSize}px;
|
height: ${this.iconSize}px;
|
||||||
}
|
}
|
||||||
</style>
|
</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) {
|
if (!this.iconSize) {
|
||||||
this.iconSize = parseInt(globalThis.getComputedStyle(this).fontSize.replace(/\D/g,''));
|
this.iconSize = parseInt(globalThis.getComputedStyle(this).fontSize.replace(/\D/g,''));
|
||||||
}
|
}
|
||||||
if (this.iconFA) {
|
|
||||||
this.shadowRoot.querySelector('#iconContainer').innerHTML = this.iconFA
|
// Get the effective icon (either from icon or iconFA property)
|
||||||
? icon(faIcons[this.iconFA]).html[0]
|
const effectiveIcon = this.getEffectiveIcon();
|
||||||
: 'icon not found';
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,12 @@ import {
|
|||||||
|
|
||||||
const { demoFunc } = await import('./dees-input-multitoggle.demo.js');
|
const { demoFunc } = await import('./dees-input-multitoggle.demo.js');
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-input-multitoggle': DeesInputMultitoggle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('dees-input-multitoggle')
|
@customElement('dees-input-multitoggle')
|
||||||
export class DeesInputMultitoggle extends DeesElement {
|
export class DeesInputMultitoggle extends DeesElement {
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
28
ts_web/elements/dees-pagination.demo.ts
Normal file
28
ts_web/elements/dees-pagination.demo.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demo for dees-pagination component
|
||||||
|
*/
|
||||||
|
export const demoFunc = () => html`
|
||||||
|
<div style="display: flex; align-items: center; gap: 16px;">
|
||||||
|
<!-- Small set of pages -->
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||||
|
<span>5 pages, starting at 1:</span>
|
||||||
|
<dees-pagination
|
||||||
|
.total=${5}
|
||||||
|
.page=${1}
|
||||||
|
@page-change=${(e: CustomEvent) => console.log('Page changed to', e.detail.page)}
|
||||||
|
></dees-pagination>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Larger set of pages -->
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 4px;">
|
||||||
|
<span>15 pages, starting at 8:</span>
|
||||||
|
<dees-pagination
|
||||||
|
.total=${15}
|
||||||
|
.page=${8}
|
||||||
|
@page-change=${(e: CustomEvent) => console.log('Page changed to', e.detail.page)}
|
||||||
|
></dees-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
133
ts_web/elements/dees-pagination.ts
Normal file
133
ts_web/elements/dees-pagination.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { customElement, html, DeesElement, property, css, cssManager, type TemplateResult } from '@design.estate/dees-element';
|
||||||
|
import { demoFunc } from './dees-pagination.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-pagination': DeesPagination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple pagination component.
|
||||||
|
* @fires page-change - Emitted when the page is changed. detail: { page: number }
|
||||||
|
*/
|
||||||
|
@customElement('dees-pagination')
|
||||||
|
export class DeesPagination extends DeesElement {
|
||||||
|
public static demo = demoFunc;
|
||||||
|
/** Current page (1-based) */
|
||||||
|
@property({ type: Number, reflect: true })
|
||||||
|
public page = 1;
|
||||||
|
|
||||||
|
/** Total number of pages */
|
||||||
|
@property({ type: Number, reflect: true })
|
||||||
|
public total = 1;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background: ${cssManager.bdTheme('#eee', '#444')};
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
color: ${cssManager.bdTheme('#aaa', '#666')};
|
||||||
|
}
|
||||||
|
button.current {
|
||||||
|
background: #0050b9;
|
||||||
|
color: #fff;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
span.ellipsis {
|
||||||
|
margin: 0 4px;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private get pages(): (number | string)[] {
|
||||||
|
const pages: (number | string)[] = [];
|
||||||
|
const total = this.total;
|
||||||
|
const current = this.page;
|
||||||
|
if (total <= 7) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push(1);
|
||||||
|
if (current > 4) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
const start = Math.max(2, current - 2);
|
||||||
|
const end = Math.min(total - 1, current + 2);
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
if (current < total - 3) {
|
||||||
|
pages.push('...');
|
||||||
|
}
|
||||||
|
pages.push(total);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
@click=${() => this.changePage(this.page - 1)}
|
||||||
|
?disabled=${this.page <= 1}
|
||||||
|
aria-label="Previous page"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
${this.pages.map((p) =>
|
||||||
|
p === '...'
|
||||||
|
? html`<span class="ellipsis">…</span>`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="${p === this.page ? 'current' : ''}"
|
||||||
|
@click=${() => this.changePage(p as number)}
|
||||||
|
?disabled=${p === this.page}
|
||||||
|
aria-label="Page ${p}"
|
||||||
|
>
|
||||||
|
${p}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
@click=${() => this.changePage(this.page + 1)}
|
||||||
|
?disabled=${this.page >= this.total}
|
||||||
|
aria-label="Next page"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private changePage(newPage: number) {
|
||||||
|
if (newPage < 1 || newPage > this.total || newPage === this.page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.page = newPage;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('page-change', {
|
||||||
|
detail: { page: this.page },
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
46
ts_web/elements/dees-searchbar.demo.ts
Normal file
46
ts_web/elements/dees-searchbar.demo.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
export const demoFunc = () => {
|
||||||
|
const onChanged = (e: CustomEvent) => {
|
||||||
|
// find the demo wrapper and update the 'changed' log inside it
|
||||||
|
const wrapper = (e.target as HTMLElement).closest('.demoWrapper');
|
||||||
|
const el = wrapper?.querySelector('#changed');
|
||||||
|
if (el) el.textContent = `search-changed: ${e.detail.value}`;
|
||||||
|
};
|
||||||
|
const onSubmit = (e: CustomEvent) => {
|
||||||
|
// find the demo wrapper and update the 'submitted' log inside it
|
||||||
|
const wrapper = (e.target as HTMLElement).closest('.demoWrapper');
|
||||||
|
const el = wrapper?.querySelector('#submitted');
|
||||||
|
if (el) el.textContent = `search-submit: ${e.detail.value}`;
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.demoWrapper {
|
||||||
|
display: block;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: #888888;
|
||||||
|
}
|
||||||
|
.logs {
|
||||||
|
padding: 16px;
|
||||||
|
width: 600px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.logs div {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="demoWrapper">
|
||||||
|
<dees-searchbar
|
||||||
|
@search-changed=${onChanged}
|
||||||
|
@search-submit=${onSubmit}
|
||||||
|
></dees-searchbar>
|
||||||
|
<div class="logs">
|
||||||
|
<div id="changed">search-changed:</div>
|
||||||
|
<div id="submitted">search-submit:</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
160
ts_web/elements/dees-searchbar.ts
Normal file
160
ts_web/elements/dees-searchbar.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
DeesElement,
|
||||||
|
property,
|
||||||
|
html,
|
||||||
|
cssManager,
|
||||||
|
unsafeCSS,
|
||||||
|
css,
|
||||||
|
type TemplateResult,
|
||||||
|
domtools,
|
||||||
|
query,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as colors from './00colors.js';
|
||||||
|
import { demoFunc } from './dees-searchbar.demo.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-searchbar': DeesSearchbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-searchbar')
|
||||||
|
export class DeesSearchbar extends DeesElement {
|
||||||
|
// DEMO
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
// STATIC
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
padding: 40px;
|
||||||
|
font-family: Dees Sans;
|
||||||
|
display: block;
|
||||||
|
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchboxContainer {
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
background: ${cssManager.bdTheme('#00000015', '#ffffff15')};
|
||||||
|
--boxHeight: 60px;
|
||||||
|
height: var(--boxHeight);
|
||||||
|
border-radius: var(--boxHeight);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 140px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid ${cssManager.bdTheme('#00000015', '#ffffff20')};
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: ${cssManager.bdTheme('#000000', '#eeeeeb')};
|
||||||
|
padding-left: 25px;
|
||||||
|
margin-right: -8px;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
--buttonPadding: 8px;
|
||||||
|
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||||
|
color: ${cssManager.bdTheme('#000000', '#eeeeeb')};
|
||||||
|
line-height: calc(var(--boxHeight) - (var(--buttonPadding) * 2));
|
||||||
|
border-radius: var(--boxHeight);
|
||||||
|
transform: scale(1) ;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
transition: transform 0.1s, background 0.1s;
|
||||||
|
margin-right: var(--buttonPadding);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton:active {
|
||||||
|
color: #fff;
|
||||||
|
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public filters = [];
|
||||||
|
|
||||||
|
|
||||||
|
@query('input')
|
||||||
|
public searchInput!: HTMLInputElement;
|
||||||
|
@query('.searchButton')
|
||||||
|
public searchButton!: HTMLElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="searchboxContainer">
|
||||||
|
<input type="text" placeholder="Your Skills (e.g. TypeScript, Rust, Projectmanagement)" />
|
||||||
|
<div class="searchButton">Search -></div>
|
||||||
|
</div>
|
||||||
|
${this.filters.length > 0 ? html`
|
||||||
|
<div class="filters">
|
||||||
|
<dees-heading level="hr-small">Filters</dees-heading>
|
||||||
|
<dees-input-dropdown .label=${'location'}></dees-input-dropdown>
|
||||||
|
</div>
|
||||||
|
` : html``}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Lifecycle: after first render, wire up events for input and submit actions
|
||||||
|
*/
|
||||||
|
public firstUpdated(): void {
|
||||||
|
// dispatch change on each input
|
||||||
|
this.searchInput.addEventListener('input', () => {
|
||||||
|
this.dispatchEvent(new CustomEvent('search-changed', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { value: this.searchInput.value }
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
// submit on Enter key
|
||||||
|
this.searchInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this._dispatchSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// submit on button click
|
||||||
|
this.searchButton.addEventListener('click', () => this._dispatchSubmit());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a submit event with the current search value
|
||||||
|
*/
|
||||||
|
private _dispatchSubmit(): void {
|
||||||
|
this.dispatchEvent(new CustomEvent('search-submit', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { value: this.searchInput.value }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
389
ts_web/elements/dees-statsgrid.demo.ts
Normal file
389
ts_web/elements/dees-statsgrid.demo.ts
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
import { html, cssManager } from '@design.estate/dees-element';
|
||||||
|
import type { IStatsTile } from './dees-statsgrid.js';
|
||||||
|
|
||||||
|
export const demoFunc = () => {
|
||||||
|
// Demo data with different tile types
|
||||||
|
const demoTiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'revenue',
|
||||||
|
title: 'Total Revenue',
|
||||||
|
value: 125420,
|
||||||
|
unit: '$',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faDollarSign',
|
||||||
|
description: '+12.5% from last month',
|
||||||
|
color: '#22c55e',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'View Details',
|
||||||
|
iconName: 'faChartLine',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Viewing revenue details for tile:', 'revenue');
|
||||||
|
console.log('Current value:', 125420);
|
||||||
|
alert(`Revenue Details: $125,420 (+12.5%)`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Export Data',
|
||||||
|
iconName: 'faFileExport',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Exporting revenue data');
|
||||||
|
alert('Revenue data exported to CSV');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'users',
|
||||||
|
title: 'Active Users',
|
||||||
|
value: 3847,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faUsers',
|
||||||
|
description: '324 new this week',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'View User List',
|
||||||
|
iconName: 'faList',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Viewing user list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cpu',
|
||||||
|
title: 'CPU Usage',
|
||||||
|
value: 73,
|
||||||
|
type: 'gauge',
|
||||||
|
icon: 'faMicrochip',
|
||||||
|
gaugeOptions: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
thresholds: [
|
||||||
|
{ value: 0, color: '#22c55e' },
|
||||||
|
{ value: 60, color: '#f59e0b' },
|
||||||
|
{ value: 80, color: '#ef4444' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'storage',
|
||||||
|
title: 'Storage Used',
|
||||||
|
value: 65,
|
||||||
|
type: 'percentage',
|
||||||
|
icon: 'faHardDrive',
|
||||||
|
description: '650 GB of 1 TB',
|
||||||
|
color: '#3b82f6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'memory',
|
||||||
|
title: 'Memory Usage',
|
||||||
|
value: 45,
|
||||||
|
type: 'gauge',
|
||||||
|
icon: 'faMemory',
|
||||||
|
gaugeOptions: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
thresholds: [
|
||||||
|
{ value: 0, color: '#22c55e' },
|
||||||
|
{ value: 70, color: '#f59e0b' },
|
||||||
|
{ value: 90, color: '#ef4444' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'requests',
|
||||||
|
title: 'API Requests',
|
||||||
|
value: '1.2k',
|
||||||
|
unit: '/min',
|
||||||
|
type: 'trend',
|
||||||
|
icon: 'faServer',
|
||||||
|
trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'uptime',
|
||||||
|
title: 'System Uptime',
|
||||||
|
value: '99.95%',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'faCheckCircle',
|
||||||
|
color: '#22c55e',
|
||||||
|
description: 'Last 30 days'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'latency',
|
||||||
|
title: 'Response Time',
|
||||||
|
value: 142,
|
||||||
|
unit: 'ms',
|
||||||
|
type: 'trend',
|
||||||
|
icon: 'faClock',
|
||||||
|
trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142],
|
||||||
|
description: 'P95 latency'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'errors',
|
||||||
|
title: 'Error Rate',
|
||||||
|
value: 0.03,
|
||||||
|
unit: '%',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faExclamationTriangle',
|
||||||
|
color: '#ef4444',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'View Error Logs',
|
||||||
|
iconName: 'faFileAlt',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Viewing error logs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Grid actions for the demo
|
||||||
|
const gridActions = [
|
||||||
|
{
|
||||||
|
name: 'Refresh',
|
||||||
|
iconName: 'faSync',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Refreshing stats...');
|
||||||
|
// Simulate refresh animation
|
||||||
|
const grid = document.querySelector('dees-statsgrid');
|
||||||
|
if (grid) {
|
||||||
|
grid.style.opacity = '0.5';
|
||||||
|
setTimeout(() => {
|
||||||
|
grid.style.opacity = '1';
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Export Report',
|
||||||
|
iconName: 'faFileExport',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Exporting stats report...');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Settings',
|
||||||
|
iconName: 'faCog',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Opening settings...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
.demo-container {
|
||||||
|
padding: 32px;
|
||||||
|
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<button class="theme-toggle" @click=${() => {
|
||||||
|
document.body.classList.toggle('bright');
|
||||||
|
}}>Toggle Theme</button>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Full Featured Stats Grid</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
A comprehensive dashboard with various tile types, actions, and real-time updates.
|
||||||
|
</p>
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${demoTiles}
|
||||||
|
.gridActions=${gridActions}
|
||||||
|
.minTileWidth=${250}
|
||||||
|
.gap=${16}
|
||||||
|
></dees-statsgrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Compact Grid (Smaller Tiles)</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Same data displayed with smaller minimum tile width for more compact layouts.
|
||||||
|
</p>
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${demoTiles.slice(0, 6)}
|
||||||
|
.minTileWidth=${180}
|
||||||
|
.gap=${12}
|
||||||
|
></dees-statsgrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Simple Metrics (No Actions)</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Clean display without interactive elements for pure visualization.
|
||||||
|
</p>
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${[
|
||||||
|
{
|
||||||
|
id: 'metric1',
|
||||||
|
title: 'Total Sales',
|
||||||
|
value: 48293,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faShoppingCart'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'metric2',
|
||||||
|
title: 'Conversion Rate',
|
||||||
|
value: 3.4,
|
||||||
|
unit: '%',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faChartLine'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'metric3',
|
||||||
|
title: 'Avg Order Value',
|
||||||
|
value: 127.50,
|
||||||
|
unit: '$',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faReceipt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'metric4',
|
||||||
|
title: 'Customer Satisfaction',
|
||||||
|
value: 92,
|
||||||
|
type: 'percentage',
|
||||||
|
icon: 'faSmile',
|
||||||
|
color: '#22c55e'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
.minTileWidth=${220}
|
||||||
|
.gap=${16}
|
||||||
|
></dees-statsgrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Performance Monitoring</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Real-time performance metrics with gauge visualizations and thresholds.
|
||||||
|
</p>
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${[
|
||||||
|
{
|
||||||
|
id: 'perf1',
|
||||||
|
title: 'Database Load',
|
||||||
|
value: 42,
|
||||||
|
type: 'gauge',
|
||||||
|
icon: 'faDatabase',
|
||||||
|
gaugeOptions: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
thresholds: [
|
||||||
|
{ value: 0, color: '#10b981' },
|
||||||
|
{ value: 50, color: '#f59e0b' },
|
||||||
|
{ value: 75, color: '#ef4444' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perf2',
|
||||||
|
title: 'Network I/O',
|
||||||
|
value: 856,
|
||||||
|
unit: 'MB/s',
|
||||||
|
type: 'trend',
|
||||||
|
icon: 'faNetworkWired',
|
||||||
|
trendData: [720, 780, 823, 845, 812, 876, 856]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perf3',
|
||||||
|
title: 'Cache Hit Rate',
|
||||||
|
value: 94.2,
|
||||||
|
type: 'percentage',
|
||||||
|
icon: 'faBolt',
|
||||||
|
color: '#3b82f6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perf4',
|
||||||
|
title: 'Active Connections',
|
||||||
|
value: 1428,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'faLink',
|
||||||
|
description: 'Peak: 2,100'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
.gridActions=${[
|
||||||
|
{
|
||||||
|
name: 'Auto Refresh',
|
||||||
|
iconName: 'faPlay',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Starting auto refresh...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
.minTileWidth=${280}
|
||||||
|
.gap=${20}
|
||||||
|
></dees-statsgrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Simulate real-time updates
|
||||||
|
setInterval(() => {
|
||||||
|
const grids = document.querySelectorAll('dees-statsgrid');
|
||||||
|
grids.forEach(grid => {
|
||||||
|
if (grid.tiles && grid.tiles.length > 0) {
|
||||||
|
// Update some random values
|
||||||
|
const updatedTiles = [...grid.tiles];
|
||||||
|
|
||||||
|
// Update trends with new data point
|
||||||
|
updatedTiles.forEach(tile => {
|
||||||
|
if (tile.type === 'trend' && tile.trendData) {
|
||||||
|
tile.trendData = [...tile.trendData.slice(1),
|
||||||
|
tile.trendData[tile.trendData.length - 1] + Math.random() * 10 - 5
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Randomly update some numeric values
|
||||||
|
if (tile.type === 'number' && Math.random() > 0.7) {
|
||||||
|
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
|
||||||
|
tile.value = Math.round(currentValue + (Math.random() * 10 - 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update gauge values
|
||||||
|
if (tile.type === 'gauge' && Math.random() > 0.5) {
|
||||||
|
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
|
||||||
|
const newValue = currentValue + (Math.random() * 10 - 5);
|
||||||
|
tile.value = Math.max(tile.gaugeOptions?.min || 0,
|
||||||
|
Math.min(tile.gaugeOptions?.max || 100, Math.round(newValue)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.tiles = updatedTiles;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
518
ts_web/elements/dees-statsgrid.ts
Normal file
518
ts_web/elements/dees-statsgrid.ts
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
import { demoFunc } from './dees-statsgrid.demo.js';
|
||||||
|
import * as plugins from './00plugins.js';
|
||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
DeesElement,
|
||||||
|
property,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
unsafeCSS,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import type { TemplateResult } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import './dees-icon.js';
|
||||||
|
import './dees-contextmenu.js';
|
||||||
|
import './dees-button.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dees-statsgrid': DeesStatsGrid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStatsTile {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
value: number | string;
|
||||||
|
unit?: string;
|
||||||
|
type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text';
|
||||||
|
|
||||||
|
// For gauge type
|
||||||
|
gaugeOptions?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
thresholds?: Array<{value: number; color: string}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For trend type
|
||||||
|
trendData?: number[];
|
||||||
|
|
||||||
|
// Visual customization
|
||||||
|
color?: string;
|
||||||
|
icon?: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
// Tile-specific actions
|
||||||
|
actions?: plugins.tsclass.website.IMenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('dees-statsgrid')
|
||||||
|
export class DeesStatsGrid extends DeesElement {
|
||||||
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public tiles: IStatsTile[] = [];
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public minTileWidth: number = 250;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public gap: number = 16;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
public gridActions: plugins.tsclass.website.IMenuItem[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private contextMenuVisible = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private contextMenuPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private contextMenuActions: plugins.tsclass.website.IMenuItem[] = [];
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: ${unsafeCSS(16)}px;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-actions dees-button {
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(${unsafeCSS(250)}px, 1fr));
|
||||||
|
gap: ${unsafeCSS(16)}px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tile {
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tile:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||||
|
border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-tile.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-icon {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-content {
|
||||||
|
height: 90px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
line-height: 1.2;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-unit {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#777')};
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-background {
|
||||||
|
fill: none;
|
||||||
|
stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
||||||
|
stroke-width: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-fill {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 6;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke-dashoffset 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-text {
|
||||||
|
fill: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentage-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentage-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: ${cssManager.bdTheme('#0084ff', '#0066cc')};
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentage-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-line {
|
||||||
|
fill: none;
|
||||||
|
stroke: ${cssManager.bdTheme('#0084ff', '#0066cc')};
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-area {
|
||||||
|
fill: ${cssManager.bdTheme('rgba(0, 132, 255, 0.1)', 'rgba(0, 102, 204, 0.2)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-value .tile-unit {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-contextmenu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${this.gridActions.length > 0 ? html`
|
||||||
|
<div class="grid-header">
|
||||||
|
<div class="grid-title">Statistics</div>
|
||||||
|
<div class="grid-actions">
|
||||||
|
${this.gridActions.map(action => html`
|
||||||
|
<dees-button @clicked=${() => this.handleGridAction(action)}>
|
||||||
|
${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''}
|
||||||
|
${action.name}
|
||||||
|
</dees-button>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(${this.minTileWidth}px, 1fr)); gap: ${this.gap}px;">
|
||||||
|
${this.tiles.map(tile => this.renderTile(tile))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.contextMenuVisible ? html`
|
||||||
|
<dees-contextmenu
|
||||||
|
.x=${this.contextMenuPosition.x}
|
||||||
|
.y=${this.contextMenuPosition.y}
|
||||||
|
.menuItems=${this.contextMenuActions}
|
||||||
|
@clicked=${() => this.contextMenuVisible = false}
|
||||||
|
></dees-contextmenu>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTile(tile: IStatsTile): TemplateResult {
|
||||||
|
const hasActions = tile.actions && tile.actions.length > 0;
|
||||||
|
const clickable = hasActions && tile.actions.length === 1;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="stats-tile ${clickable ? 'clickable' : ''}"
|
||||||
|
@click=${clickable ? () => this.handleTileAction(tile.actions![0], tile) : undefined}
|
||||||
|
@contextmenu=${hasActions ? (e: MouseEvent) => this.showContextMenu(e, tile) : undefined}
|
||||||
|
>
|
||||||
|
<div class="tile-header">
|
||||||
|
<h3 class="tile-title">${tile.title}</h3>
|
||||||
|
${tile.icon ? html`
|
||||||
|
<dees-icon class="tile-icon" .iconFA=${tile.icon} size="small"></dees-icon>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tile-content">
|
||||||
|
${this.renderTileContent(tile)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${tile.description ? html`
|
||||||
|
<div class="tile-description">${tile.description}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTileContent(tile: IStatsTile): TemplateResult {
|
||||||
|
switch (tile.type) {
|
||||||
|
case 'number':
|
||||||
|
return html`
|
||||||
|
<div class="tile-value" style="${tile.color ? `color: ${tile.color}` : ''}">
|
||||||
|
<span>${tile.value}</span>
|
||||||
|
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
case 'gauge':
|
||||||
|
return this.renderGauge(tile);
|
||||||
|
|
||||||
|
case 'percentage':
|
||||||
|
return this.renderPercentage(tile);
|
||||||
|
|
||||||
|
case 'trend':
|
||||||
|
return this.renderTrend(tile);
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
return html`
|
||||||
|
<div class="text-value" style="${tile.color ? `color: ${tile.color}` : ''}">
|
||||||
|
${tile.value}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return html`<div class="tile-value">${tile.value}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderGauge(tile: IStatsTile): TemplateResult {
|
||||||
|
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
|
||||||
|
const options = tile.gaugeOptions || { min: 0, max: 100 };
|
||||||
|
const percentage = ((value - options.min) / (options.max - options.min)) * 100;
|
||||||
|
const strokeDasharray = 188.5; // Circumference of circle with r=30
|
||||||
|
const strokeDashoffset = strokeDasharray - (strokeDasharray * percentage) / 100;
|
||||||
|
|
||||||
|
let strokeColor = tile.color || cssManager.bdTheme('#0084ff', '#0066cc');
|
||||||
|
if (options.thresholds) {
|
||||||
|
for (const threshold of options.thresholds.reverse()) {
|
||||||
|
if (value >= threshold.value) {
|
||||||
|
strokeColor = threshold.color;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="gauge-container">
|
||||||
|
<svg class="gauge-svg" viewBox="0 0 80 80">
|
||||||
|
<circle
|
||||||
|
class="gauge-background"
|
||||||
|
cx="40"
|
||||||
|
cy="40"
|
||||||
|
r="30"
|
||||||
|
transform="rotate(-90 40 40)"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="gauge-fill"
|
||||||
|
cx="40"
|
||||||
|
cy="40"
|
||||||
|
r="30"
|
||||||
|
transform="rotate(-90 40 40)"
|
||||||
|
stroke="${strokeColor}"
|
||||||
|
stroke-dasharray="${strokeDasharray}"
|
||||||
|
stroke-dashoffset="${strokeDashoffset}"
|
||||||
|
/>
|
||||||
|
<text class="gauge-text" x="40" y="40" dy="0.35em">
|
||||||
|
${value}${tile.unit || ''}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPercentage(tile: IStatsTile): TemplateResult {
|
||||||
|
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
|
||||||
|
const percentage = Math.min(100, Math.max(0, value));
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="percentage-container">
|
||||||
|
<div
|
||||||
|
class="percentage-fill"
|
||||||
|
style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}"
|
||||||
|
></div>
|
||||||
|
<div class="percentage-text">${percentage}%</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTrend(tile: IStatsTile): TemplateResult {
|
||||||
|
if (!tile.trendData || tile.trendData.length < 2) {
|
||||||
|
return html`<div class="tile-value">${tile.value}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = tile.trendData;
|
||||||
|
const max = Math.max(...data);
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const range = max - min || 1;
|
||||||
|
const width = 200;
|
||||||
|
const height = 40;
|
||||||
|
const points = data.map((value, index) => {
|
||||||
|
const x = (index / (data.length - 1)) * width;
|
||||||
|
const y = height - ((value - min) / range) * height;
|
||||||
|
return `${x},${y}`;
|
||||||
|
}).join(' ');
|
||||||
|
|
||||||
|
const areaPoints = `0,${height} ${points} ${width},${height}`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="trend-container">
|
||||||
|
<svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
|
||||||
|
<polygon class="trend-area" points="${areaPoints}" />
|
||||||
|
<polyline class="trend-line" points="${points}" />
|
||||||
|
</svg>
|
||||||
|
<div class="trend-value">
|
||||||
|
<span>${tile.value}</span>
|
||||||
|
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleGridAction(action: plugins.tsclass.website.IMenuItem) {
|
||||||
|
if (action.action) {
|
||||||
|
await action.action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleTileAction(action: plugins.tsclass.website.IMenuItem, _tile: IStatsTile) {
|
||||||
|
if (action.action) {
|
||||||
|
await action.action();
|
||||||
|
}
|
||||||
|
// Note: tile data is available through closure when defining actions
|
||||||
|
}
|
||||||
|
|
||||||
|
private showContextMenu(event: MouseEvent, tile: IStatsTile) {
|
||||||
|
if (!tile.actions || tile.actions.length === 0) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
this.contextMenuPosition = { x: event.clientX, y: event.clientY };
|
||||||
|
this.contextMenuActions = tile.actions;
|
||||||
|
this.contextMenuVisible = true;
|
||||||
|
|
||||||
|
// Close context menu on click outside
|
||||||
|
const closeHandler = () => {
|
||||||
|
this.contextMenuVisible = false;
|
||||||
|
document.removeEventListener('click', closeHandler);
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', closeHandler);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ import {
|
|||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
type CSSResult,
|
type CSSResult,
|
||||||
state,
|
state,
|
||||||
resolveExec,
|
directives,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||||
@ -415,7 +415,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
<div class="heading heading2">${this.heading2}</div>
|
<div class="heading heading2">${this.heading2}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="headerActions">
|
<div class="headerActions">
|
||||||
${resolveExec(async () => {
|
${directives.resolveExec(async () => {
|
||||||
const resultArray: TemplateResult[] = [];
|
const resultArray: TemplateResult[] = [];
|
||||||
for (const action of this.dataActions) {
|
for (const action of this.dataActions) {
|
||||||
if (!action.type.includes('header')) continue;
|
if (!action.type.includes('header')) continue;
|
||||||
@ -634,7 +634,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
selected
|
selected
|
||||||
</div>
|
</div>
|
||||||
<div class="footerActions">
|
<div class="footerActions">
|
||||||
${resolveExec(async () => {
|
${directives.resolveExec(async () => {
|
||||||
const resultArray: TemplateResult[] = [];
|
const resultArray: TemplateResult[] = [];
|
||||||
for (const action of this.dataActions) {
|
for (const action of this.dataActions) {
|
||||||
if (!action.type.includes('footer')) continue;
|
if (!action.type.includes('footer')) continue;
|
||||||
|
@ -22,13 +22,25 @@ declare global {
|
|||||||
|
|
||||||
@customElement('dees-terminal')
|
@customElement('dees-terminal')
|
||||||
export class DeesTerminal extends DeesElement {
|
export class DeesTerminal extends DeesElement {
|
||||||
public static demo = () => html` <dees-terminal></dees-terminal> `;
|
public static demo = () => html` <dees-terminal
|
||||||
|
.environment=${{
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
PORT: '3000',
|
||||||
|
}}
|
||||||
|
></dees-terminal> `;
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
public setupCommand = `pnpm install @git.zone/tsbuild && clear && echo 'welcome'`;
|
public setupCommand = `pnpm install @serve.zone/cli && servezone cli\n`;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
environment: {[key: string]: string} = {};
|
||||||
|
|
||||||
|
// exposing webcontainer
|
||||||
|
private webcontainerDeferred = new domtools.plugins.smartpromise.Deferred<webcontainer.WebContainer>();
|
||||||
|
public webcontainerPromise = this.webcontainerDeferred.promise;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -282,8 +294,15 @@ export class DeesTerminal extends DeesElement {
|
|||||||
input.write(data);
|
input.write(data);
|
||||||
});
|
});
|
||||||
await this.waitForPrompt(term, '~/');
|
await this.waitForPrompt(term, '~/');
|
||||||
|
// lets set the environment variables
|
||||||
|
await this.setEnvironmentVariables(this.environment, webcontainerInstance);
|
||||||
|
input.write(`source source.env\n`);
|
||||||
|
await this.waitForPrompt(term, '~/');
|
||||||
|
// lets run the setup command
|
||||||
input.write(this.setupCommand);
|
input.write(this.setupCommand);
|
||||||
input.write(`\n`);
|
await this.waitForPrompt(term, '~/');
|
||||||
|
input.write(`clear && echo 'welcome'\n`);
|
||||||
|
this.webcontainerDeferred.resolve(webcontainerInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectedCallback(): Promise<void> {
|
async connectedCallback(): Promise<void> {
|
||||||
@ -300,14 +319,16 @@ export class DeesTerminal extends DeesElement {
|
|||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForPrompt(term: Terminal, prompt: string): Promise<void> {
|
public async waitForPrompt(term: Terminal, prompt: string): Promise<void> {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
const checkPrompt = () => {
|
const checkPrompt = () => {
|
||||||
const lines = term.buffer.active;
|
const lines = term.buffer.active;
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines.getLine(i);
|
const line = lines.getLine(i);
|
||||||
if (line && line.translateToString().includes(prompt)) {
|
if (line && line.translateToString().includes(prompt)) {
|
||||||
resolve();
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,4 +338,18 @@ export class DeesTerminal extends DeesElement {
|
|||||||
checkPrompt();
|
checkPrompt();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setEnvironmentVariables(envArg: {[key: string]: string}, webcontainerInstanceArg?: webcontainer.WebContainer) {
|
||||||
|
const webcontainerInstance = webcontainerInstanceArg ||await this.webcontainerPromise;
|
||||||
|
let envFile = ``
|
||||||
|
for (const key in envArg) {
|
||||||
|
envFile += `export ${key}="${envArg[key]}"\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await webcontainerInstance.mount({'source.env': {
|
||||||
|
file: {
|
||||||
|
contents: envFile,
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,262 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import { DeesToast } from './dees-toast.js';
|
||||||
|
import './dees-button.js';
|
||||||
|
|
||||||
export const demoFunc = async () => {
|
export const demoFunc = async () => {
|
||||||
return html`<dees-toast></dees-toast>`;
|
return html`
|
||||||
}
|
<style>
|
||||||
|
.demo-container {
|
||||||
|
padding: 32px;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<dees-button class="theme-toggle" @clicked=${() => {
|
||||||
|
document.body.classList.toggle('bright');
|
||||||
|
}}>Toggle Theme</dees-button>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Toast Types</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Different toast types for various notification scenarios. Click any button to show a toast.
|
||||||
|
</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.info('This is an informational message');
|
||||||
|
}}>Info Toast</dees-button>
|
||||||
|
|
||||||
|
<dees-button type="highlighted" @clicked=${() => {
|
||||||
|
DeesToast.success('Operation completed successfully!');
|
||||||
|
}}>Success Toast</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.warning('Please review before proceeding');
|
||||||
|
}}>Warning Toast</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.error('An error occurred while processing');
|
||||||
|
}}>Error Toast</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Toast Positions</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Toasts can appear in different positions on the screen.
|
||||||
|
</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Top Right Position',
|
||||||
|
type: 'info',
|
||||||
|
position: 'top-right'
|
||||||
|
});
|
||||||
|
}}>Top Right</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Top Left Position',
|
||||||
|
type: 'info',
|
||||||
|
position: 'top-left'
|
||||||
|
});
|
||||||
|
}}>Top Left</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Bottom Right Position',
|
||||||
|
type: 'info',
|
||||||
|
position: 'bottom-right'
|
||||||
|
});
|
||||||
|
}}>Bottom Right</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Bottom Left Position',
|
||||||
|
type: 'info',
|
||||||
|
position: 'bottom-left'
|
||||||
|
});
|
||||||
|
}}>Bottom Left</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Top Center Position',
|
||||||
|
type: 'info',
|
||||||
|
position: 'top-center'
|
||||||
|
});
|
||||||
|
}}>Top Center</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Bottom Center Position',
|
||||||
|
type: 'info',
|
||||||
|
position: 'bottom-center'
|
||||||
|
});
|
||||||
|
}}>Bottom Center</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Duration Options</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Control how long toasts stay visible. Duration in milliseconds.
|
||||||
|
</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Quick toast (1 second)',
|
||||||
|
type: 'info',
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
}}>1 Second</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Standard toast (3 seconds)',
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}}>3 Seconds (Default)</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Long toast (5 seconds)',
|
||||||
|
type: 'info',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}}>5 Seconds</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Manual dismiss only (click to close)',
|
||||||
|
type: 'warning',
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
}}>No Auto-Dismiss</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Multiple Toasts</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Multiple toasts stack automatically. They maintain their order and animate smoothly.
|
||||||
|
</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.info('First notification');
|
||||||
|
setTimeout(() => DeesToast.success('Second notification'), 200);
|
||||||
|
setTimeout(() => DeesToast.warning('Third notification'), 400);
|
||||||
|
setTimeout(() => DeesToast.error('Fourth notification'), 600);
|
||||||
|
}}>Show Multiple</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
DeesToast.show({
|
||||||
|
message: `Notification #${i}`,
|
||||||
|
type: i % 2 === 0 ? 'success' : 'info',
|
||||||
|
duration: 2000 + (i * 500)
|
||||||
|
});
|
||||||
|
}, i * 100);
|
||||||
|
}
|
||||||
|
}}>Rapid Fire</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Real-World Examples</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Common use cases for toast notifications in applications.
|
||||||
|
</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @clicked=${async () => {
|
||||||
|
const toast = await DeesToast.show({
|
||||||
|
message: 'Saving changes...',
|
||||||
|
type: 'info',
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate save operation
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.dismiss();
|
||||||
|
DeesToast.success('Changes saved successfully!');
|
||||||
|
}, 2000);
|
||||||
|
}}>Save Operation</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.error('Failed to connect to server. Please check your internet connection.');
|
||||||
|
}}>Network Error</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.warning('Your session will expire in 5 minutes');
|
||||||
|
}}>Session Warning</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
DeesToast.success('File uploaded successfully!');
|
||||||
|
}}>Upload Complete</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Programmatic Control</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Advanced control over toast behavior.
|
||||||
|
</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @clicked=${async () => {
|
||||||
|
const toast = await DeesToast.show({
|
||||||
|
message: 'This toast can be dismissed programmatically',
|
||||||
|
type: 'info',
|
||||||
|
duration: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.dismiss();
|
||||||
|
DeesToast.success('Toast dismissed after 2 seconds');
|
||||||
|
}, 2000);
|
||||||
|
}}>Programmatic Dismiss</dees-button>
|
||||||
|
|
||||||
|
<dees-button @clicked=${() => {
|
||||||
|
// Using the convenience methods
|
||||||
|
DeesToast.info('Info message', 2000);
|
||||||
|
setTimeout(() => DeesToast.success('Success message', 2000), 500);
|
||||||
|
setTimeout(() => DeesToast.warning('Warning message', 2000), 1000);
|
||||||
|
setTimeout(() => DeesToast.error('Error message', 2000), 1500);
|
||||||
|
}}>Convenience Methods</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { customElement, DeesElement, type TemplateResult, html, type CSSResult, } from '@design.estate/dees-element';
|
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { demoFunc } from './dees-toast.demo.js';
|
import { demoFunc } from './dees-toast.demo.js';
|
||||||
@ -9,20 +9,317 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
||||||
|
export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
|
||||||
|
|
||||||
|
export interface IToastOptions {
|
||||||
|
message: string;
|
||||||
|
type?: ToastType;
|
||||||
|
duration?: number;
|
||||||
|
position?: ToastPosition;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('dees-toast')
|
@customElement('dees-toast')
|
||||||
export class DeesToast extends DeesElement {
|
export class DeesToast extends DeesElement {
|
||||||
|
// STATIC
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
|
private static toastContainers = new Map<ToastPosition, HTMLDivElement>();
|
||||||
|
|
||||||
|
private static getOrCreateContainer(position: ToastPosition): HTMLDivElement {
|
||||||
|
if (!this.toastContainers.has(position)) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = `toast-container toast-container-${position}`;
|
||||||
|
container.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Position the container
|
||||||
|
switch (position) {
|
||||||
|
case 'top-right':
|
||||||
|
container.style.top = '0';
|
||||||
|
container.style.right = '0';
|
||||||
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
container.style.top = '0';
|
||||||
|
container.style.left = '0';
|
||||||
|
break;
|
||||||
|
case 'bottom-right':
|
||||||
|
container.style.bottom = '0';
|
||||||
|
container.style.right = '0';
|
||||||
|
break;
|
||||||
|
case 'bottom-left':
|
||||||
|
container.style.bottom = '0';
|
||||||
|
container.style.left = '0';
|
||||||
|
break;
|
||||||
|
case 'top-center':
|
||||||
|
container.style.top = '0';
|
||||||
|
container.style.left = '50%';
|
||||||
|
container.style.transform = 'translateX(-50%)';
|
||||||
|
break;
|
||||||
|
case 'bottom-center':
|
||||||
|
container.style.bottom = '0';
|
||||||
|
container.style.left = '50%';
|
||||||
|
container.style.transform = 'translateX(-50%)';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(container);
|
||||||
|
this.toastContainers.set(position, container);
|
||||||
|
}
|
||||||
|
return this.toastContainers.get(position)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async show(options: IToastOptions | string) {
|
||||||
|
const opts: IToastOptions = typeof options === 'string'
|
||||||
|
? { message: options }
|
||||||
|
: options;
|
||||||
|
|
||||||
|
const toast = new DeesToast();
|
||||||
|
toast.message = opts.message;
|
||||||
|
toast.type = opts.type || 'info';
|
||||||
|
toast.duration = opts.duration || 3000;
|
||||||
|
|
||||||
|
const container = this.getOrCreateContainer(opts.position || 'top-right');
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation
|
||||||
|
await toast.updateComplete;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toast.isVisible = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto dismiss
|
||||||
|
if (toast.duration > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.dismiss();
|
||||||
|
}, toast.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods
|
||||||
|
public static info(message: string, duration?: number) {
|
||||||
|
return this.show({ message, type: 'info', duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static success(message: string, duration?: number) {
|
||||||
|
return this.show({ message, type: 'success', duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static warning(message: string, duration?: number) {
|
||||||
|
return this.show({ message, type: 'warning', duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static error(message: string, duration?: number) {
|
||||||
|
return this.show({ message, type: 'error', duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@property({ type: String })
|
||||||
|
public message: string = '';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public type: ToastType = 'info';
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
public duration: number = 3000;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public isVisible: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
domtools.elementBasic.setup();
|
domtools.elementBasic.setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
pointer-events: auto;
|
||||||
|
font-family: 'Geist Sans', sans-serif;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([isvisible]) {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 500px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Type-specific styles */
|
||||||
|
:host([type="info"]) .icon {
|
||||||
|
color: #0084ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([type="success"]) .icon {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([type="warning"]) .icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([type="error"]) .icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar */
|
||||||
|
.progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.2;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.8;
|
||||||
|
transform-origin: left;
|
||||||
|
animation: progress linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress {
|
||||||
|
from {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
|
const icons = {
|
||||||
|
info: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
success: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
warning: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||||
|
</svg>`,
|
||||||
|
error: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||||
|
</svg>`
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${domtools.elementBasic.styles}
|
<div class="toast" @click=${this.dismiss}>
|
||||||
<style></style>
|
<div class="icon">
|
||||||
|
${icons[this.type]}
|
||||||
|
</div>
|
||||||
|
<div class="message">${this.message}</div>
|
||||||
|
<div class="close">
|
||||||
|
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
${this.duration > 0 ? html`
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="animation-duration: ${this.duration}ms"></div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async dismiss() {
|
||||||
|
this.isVisible = false;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
this.remove();
|
||||||
|
|
||||||
|
// Clean up empty containers
|
||||||
|
const container = this.parentElement;
|
||||||
|
if (container && container.children.length === 0) {
|
||||||
|
container.remove();
|
||||||
|
for (const [position, cont] of DeesToast.toastContainers.entries()) {
|
||||||
|
if (cont === container) {
|
||||||
|
DeesToast.toastContainers.delete(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public firstUpdated() {
|
||||||
|
// Set the type attribute for CSS
|
||||||
|
this.setAttribute('type', this.type);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ export * from './dees-appui-base.js';
|
|||||||
export * from './dees-appui-maincontent.js';
|
export * from './dees-appui-maincontent.js';
|
||||||
export * from './dees-appui-mainmenu.js';
|
export * from './dees-appui-mainmenu.js';
|
||||||
export * from './dees-appui-mainselector.js';
|
export * from './dees-appui-mainselector.js';
|
||||||
|
export * from './dees-badge.js';
|
||||||
export * from './dees-button-exit.js';
|
export * from './dees-button-exit.js';
|
||||||
export * from './dees-button.js';
|
export * from './dees-button.js';
|
||||||
export * from './dees-chart-area.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-editor-markdownoutlet.js';
|
||||||
export * from './dees-form-submit.js';
|
export * from './dees-form-submit.js';
|
||||||
export * from './dees-form.js';
|
export * from './dees-form.js';
|
||||||
|
export * from './dees-heading.js';
|
||||||
export * from './dees-hint.js';
|
export * from './dees-hint.js';
|
||||||
export * from './dees-icon.js';
|
export * from './dees-icon.js';
|
||||||
export * from './dees-input-checkbox.js';
|
export * from './dees-input-checkbox.js';
|
||||||
@ -34,10 +36,12 @@ export * from './dees-mobilenavigation.js';
|
|||||||
export * from './dees-modal.js';
|
export * from './dees-modal.js';
|
||||||
export * from './dees-input-multitoggle.js';
|
export * from './dees-input-multitoggle.js';
|
||||||
export * from './dees-pdf.js';
|
export * from './dees-pdf.js';
|
||||||
|
export * from './dees-searchbar.js';
|
||||||
export * from './dees-simple-appdash.js';
|
export * from './dees-simple-appdash.js';
|
||||||
export * from './dees-simple-login.js';
|
export * from './dees-simple-login.js';
|
||||||
export * from './dees-speechbubble.js';
|
export * from './dees-speechbubble.js';
|
||||||
export * from './dees-spinner.js';
|
export * from './dees-spinner.js';
|
||||||
|
export * from './dees-statsgrid.js';
|
||||||
export * from './dees-stepper.js';
|
export * from './dees-stepper.js';
|
||||||
export * from './dees-table.js';
|
export * from './dees-table.js';
|
||||||
export * from './dees-terminal.js';
|
export * from './dees-terminal.js';
|
||||||
@ -45,3 +49,4 @@ export * from './dees-toast.js';
|
|||||||
export * from './dees-updater.js';
|
export * from './dees-updater.js';
|
||||||
export * from './dees-windowcontrols.js';
|
export * from './dees-windowcontrols.js';
|
||||||
export * from './dees-windowlayer.js';
|
export * from './dees-windowlayer.js';
|
||||||
|
export * from './dees-pagination.js';
|
||||||
|
Reference in New Issue
Block a user