Compare commits
94 Commits
Author | SHA1 | Date | |
---|---|---|---|
22e6b74c4f | |||
4de835474b | |||
024d8af40d | |||
808b74fa17 | |||
202881ef1a | |||
7de3d451ad | |||
f0e0430016 | |||
873579fc97 | |||
d321db363d | |||
73c1874e3f | |||
1aa06398a0 | |||
99b23236a1 | |||
d1e7e5447c | |||
4f22a98b78 | |||
eb09aee264 | |||
c3fca1db36 | |||
2a5e6ee37a | |||
41e2125dc7 | |||
2a76b67e9a | |||
d697958536 | |||
1789807f90 | |||
03315db863 | |||
79b1a4ea9f | |||
8fb5e2e2a2 | |||
640a69f4cd | |||
bdb666cbe2 | |||
8a1d830376 | |||
c1e8f8c2a6 | |||
a8f0e5659e | |||
cd3c7c8e63 | |||
5b4319432c | |||
e33f4e7a70 | |||
f101df9329 | |||
d926f5c5e4 | |||
8ad754c9bc | |||
ed20e04e96 | |||
daef1aa841 | |||
339ea2d7d4 | |||
036bba44ae | |||
48fbeb397d | |||
346abfa685 | |||
f1123f319f | |||
ac15da9c82 | |||
b9432c8489 | |||
b35b1fbae7 | |||
e39590df2c | |||
fad7fda2a6 | |||
987f557c60 | |||
4eef9fc731 | |||
cd86001713 | |||
f7e4582fde | |||
4635e3fce5 | |||
af3dc5c466 | |||
12861b2230 | |||
b7f672e0f2 | |||
fcb44dfd24 | |||
f17b880b59 | |||
68785d9a72 | |||
ab4396297a | |||
ef369f2955 | |||
1e73a9527b | |||
23a4faa5d1 | |||
b0020ace16 | |||
bb78d32dbf | |||
e83ad8d504 | |||
765b01afe0 | |||
00e34e7e6c | |||
bf2ee25390 | |||
bf6d8d0bc6 | |||
3399004e75 | |||
6c2f36f020 | |||
71f4d44782 | |||
6df2eb5acc | |||
469f8e0f21 | |||
3712f6ef90 | |||
d2646cd62c | |||
f29ca0ba0b | |||
0c273a818d | |||
6e8099c6f4 | |||
07c68b82a4 | |||
afd19dc912 | |||
f02572665f | |||
f93082e9b0 | |||
08f3bad5f9 | |||
563958813e | |||
1ae1703133 | |||
d2771dfc31 | |||
dd46d3e2f4 | |||
ae641801e1 | |||
719e63c667 | |||
e26da1bb8f | |||
f5fad07038 | |||
68992301ff | |||
d529c27a68 |
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
|
173
changelog.md
173
changelog.md
@ -1,5 +1,178 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-06-22 - 1.9.0 - feat(form-inputs)
|
||||
Improve form input consistency and auto spacing across inputs and buttons
|
||||
|
||||
- Add an 'insideForm' property to dees-button for auto-detection and proper margin adjustment in forms.
|
||||
- Update dees-input-radio to include a 'name' property so that radio buttons in the same group are mutually exclusive.
|
||||
- Enhance dees-form to group radio inputs properly when collecting form data.
|
||||
- Revise readme.hints.md and readme.plan.md to document changes and provide guidance for dees-input-radio.
|
||||
- Update demos for dees-button and dees-form to showcase correct spacing in vertical and horizontal layouts.
|
||||
|
||||
## 2025-06-20 - 1.8.20 - fix(deps)
|
||||
Update dependency versions: bump @design.estate/dees-domtools from ^2.1.1 to ^2.3.3, @design.estate/dees-element from ^2.0.42 to ^2.0.44, lucide from ^0.515.0 to ^0.518.0, and @git.zone/tsbundle from ^2.0.15 to ^2.4.0
|
||||
|
||||
- Upgrade @design.estate/dees-domtools from ^2.1.1 to ^2.3.3
|
||||
- Upgrade @design.estate/dees-element from ^2.0.42 to ^2.0.44
|
||||
- Upgrade lucide from ^0.515.0 to ^0.518.0
|
||||
- Upgrade @git.zone/tsbundle from ^2.0.15 to ^2.4.0
|
||||
|
||||
## 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)
|
||||
Updated package metadata and readme for better project description and structure.
|
||||
|
||||
- Updated package.json and npmextra.json with a detailed project description and list of keywords.
|
||||
- Enhanced readme.md with installation instructions, component usage examples, and detailed component descriptions for clarity.
|
||||
|
||||
## 2024-11-07 - 1.3.1 - fix(DeesSimpleAppDash)
|
||||
Fix: add border to controlbar in DeesSimpleAppDash
|
||||
|
||||
- Fixed the missing border at the top of the controlbar in DeesSimpleAppDash.
|
||||
|
||||
## 2024-11-07 - 1.3.0 - feat(dees-simple-appdash)
|
||||
Enhance responsive styling and terminal setup command
|
||||
|
||||
- Added a new property `terminalSetupCommand` for configuring terminal setup commands.
|
||||
- Improved responsive styling and positioning for components to achieve a fluid layout.
|
||||
- Fixed layout shifts by switching positions to `absolute` for `appbar` and `appcontent`.
|
||||
|
||||
## 2024-10-07 - 1.2.0 - feat(index.ts)
|
||||
Add export for colors module in index.ts
|
||||
|
||||
- The index.ts file now exports the colors module, making color utilities available for external use.
|
||||
|
||||
## 2024-10-06 - 1.1.13 - fix(dees-button)
|
||||
Fix styling issue in button component.
|
||||
|
||||
- Moved the .button.disabled styling block to its correct position after the .button.highlighted block.
|
||||
|
||||
## 2024-10-06 - 1.1.12 - fix(dees-button)
|
||||
Fix reflect attribute for disabled property on dees-button component
|
||||
|
||||
- Added reflect: true to the 'disabled' property ensuring changes reflect in the DOM attribute.
|
||||
|
||||
## 2024-10-05 - 1.1.11 - fix(DeesStepper)
|
||||
Adjusted CSS properties in DeesStepper component
|
||||
|
||||
- Increased border-radius from 8px to 16px for step container elements
|
||||
- Adjusted font-size and font-weight for the title in the step container to improve readability
|
||||
|
||||
## 2024-10-04 - 1.1.10 - fix(dependencies)
|
||||
Reverted @webcontainer/api version
|
||||
|
||||
|
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.
|
@ -5,23 +5,35 @@
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "design.estate",
|
||||
"gitrepo": "dees-catalog",
|
||||
"description": "A library for building components and other projects",
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"npmPackagename": "@design.estate/dees-catalog",
|
||||
"license": "MIT",
|
||||
"projectDomain": "design.estate",
|
||||
"keywords": [
|
||||
"Web Components",
|
||||
"User Interface",
|
||||
"Design System",
|
||||
"UI Library",
|
||||
"Component Library",
|
||||
"Web Development",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"Dynamic Components",
|
||||
"Modular Architecture",
|
||||
"Reusable Components",
|
||||
"Web Development",
|
||||
"Application UI",
|
||||
"Custom Elements",
|
||||
"Shadow DOM",
|
||||
"CSS",
|
||||
"HTML"
|
||||
"UI Elements",
|
||||
"Dashboard Interfaces",
|
||||
"Form Handling",
|
||||
"Data Display",
|
||||
"Visualization",
|
||||
"Charting",
|
||||
"Interactive Components",
|
||||
"Responsive Design",
|
||||
"Web Applications",
|
||||
"Modern Web",
|
||||
"Frontend Development"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
66
package.json
66
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "1.1.10",
|
||||
"version": "1.9.0",
|
||||
"private": false,
|
||||
"description": "A library for building components and other projects",
|
||||
"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",
|
||||
"typings": "dist_ts_web/index.d.ts",
|
||||
"type": "module",
|
||||
@ -15,34 +15,35 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.0.61",
|
||||
"@design.estate/dees-element": "^2.0.39",
|
||||
"@design.estate/dees-wcctools": "^1.0.90",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@design.estate/dees-domtools": "^2.3.3",
|
||||
"@design.estate/dees-element": "^2.0.44",
|
||||
"@design.estate/dees-wcctools": "^1.0.98",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@push.rocks/smarti18n": "^1.0.4",
|
||||
"@push.rocks/smartpromise": "^4.0.4",
|
||||
"@push.rocks/smartpromise": "^4.2.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@tsclass/tsclass": "^4.1.2",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@webcontainer/api": "1.2.0",
|
||||
"apexcharts": "^3.54.0",
|
||||
"highlight.js": "11.10.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"pdfjs-dist": "^4.6.82",
|
||||
"lucide": "^0.518.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.84",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tstest": "^1.0.90",
|
||||
"@git.zone/tswatch": "^2.0.23",
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbundle": "^2.4.0",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tswatch": "^2.0.37",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/tapbundle": "^5.3.0",
|
||||
"@types/node": "^22.7.4"
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.0.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@ -62,15 +63,28 @@
|
||||
"keywords": [
|
||||
"Web Components",
|
||||
"User Interface",
|
||||
"Design System",
|
||||
"UI Library",
|
||||
"Component Library",
|
||||
"Web Development",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"Dynamic Components",
|
||||
"Modular Architecture",
|
||||
"Reusable Components",
|
||||
"Web Development",
|
||||
"Application UI",
|
||||
"Custom Elements",
|
||||
"Shadow DOM",
|
||||
"CSS",
|
||||
"HTML"
|
||||
]
|
||||
"UI Elements",
|
||||
"Dashboard Interfaces",
|
||||
"Form Handling",
|
||||
"Data Display",
|
||||
"Visualization",
|
||||
"Charting",
|
||||
"Interactive Components",
|
||||
"Responsive Design",
|
||||
"Web Applications",
|
||||
"Modern Web",
|
||||
"Frontend Development"
|
||||
],
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
7152
pnpm-lock.yaml
generated
7152
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
513
readme.appui-architecture.md
Normal file
513
readme.appui-architecture.md
Normal file
@ -0,0 +1,513 @@
|
||||
# Building Applications with dees-appui Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The dees-appui system provides a comprehensive framework for building desktop-style web applications with a consistent layout, navigation, and view management system. This document outlines the architecture and best practices for building applications using these components.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Component Hierarchy
|
||||
|
||||
```
|
||||
dees-appui-base
|
||||
├── dees-appui-appbar (top menu bar)
|
||||
├── dees-appui-mainmenu (left sidebar - primary navigation)
|
||||
├── dees-appui-mainselector (second sidebar - contextual navigation)
|
||||
├── dees-appui-maincontent (main content area)
|
||||
│ └── dees-appui-view (view container)
|
||||
│ └── dees-appui-tabs (tab navigation within views)
|
||||
└── dees-appui-activitylog (right sidebar - optional)
|
||||
```
|
||||
|
||||
### View-Based Architecture
|
||||
|
||||
The system is built around the concept of **Views** - self-contained modules that represent different sections of your application. Each view can have:
|
||||
|
||||
- Its own tabs for sub-navigation
|
||||
- Menu items for the selector (contextual navigation)
|
||||
- Content areas with dynamic loading
|
||||
- State management
|
||||
- Event handling
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Application Shell Setup
|
||||
|
||||
```typescript
|
||||
// app-shell.ts
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import type { IAppView } from '@design.estate/dees-catalog';
|
||||
|
||||
@customElement('my-app-shell')
|
||||
export class MyAppShell extends LitElement {
|
||||
@property({ type: Array })
|
||||
views: IAppView[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
activeViewId: string = '';
|
||||
|
||||
render() {
|
||||
const activeView = this.views.find(v => v.id === this.activeViewId);
|
||||
|
||||
return html`
|
||||
<dees-appui-base
|
||||
.appbarMenuItems=${this.getAppBarMenuItems()}
|
||||
.appbarBreadcrumbs=${this.getBreadcrumbs()}
|
||||
.appbarTheme=${'dark'}
|
||||
.appbarUser=${{ name: 'User', status: 'online' }}
|
||||
.mainmenuTabs=${this.getMainMenuTabs()}
|
||||
.mainselectorOptions=${activeView?.menuItems || []}
|
||||
@mainmenu-tab-select=${this.handleMainMenuSelect}
|
||||
@mainselector-option-select=${this.handleSelectorSelect}
|
||||
>
|
||||
<dees-appui-view
|
||||
slot="maincontent"
|
||||
.viewConfig=${activeView}
|
||||
@view-tab-select=${this.handleViewTabSelect}
|
||||
></dees-appui-view>
|
||||
</dees-appui-base>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: View Definition
|
||||
|
||||
```typescript
|
||||
// views/dashboard-view.ts
|
||||
export const dashboardView: IAppView = {
|
||||
id: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
description: 'System overview and metrics',
|
||||
iconName: 'home',
|
||||
tabs: [
|
||||
{
|
||||
key: 'overview',
|
||||
iconName: 'chart-line',
|
||||
action: () => console.log('Overview selected'),
|
||||
content: () => html`
|
||||
<dashboard-overview></dashboard-overview>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'metrics',
|
||||
iconName: 'tachometer-alt',
|
||||
action: () => console.log('Metrics selected'),
|
||||
content: () => html`
|
||||
<dashboard-metrics></dashboard-metrics>
|
||||
`
|
||||
},
|
||||
{
|
||||
key: 'alerts',
|
||||
iconName: 'bell',
|
||||
action: () => console.log('Alerts selected'),
|
||||
content: () => html`
|
||||
<dashboard-alerts></dashboard-alerts>
|
||||
`
|
||||
}
|
||||
],
|
||||
menuItems: [
|
||||
{ key: 'Time Range', action: () => showTimeRangeSelector() },
|
||||
{ key: 'Refresh Rate', action: () => showRefreshSettings() },
|
||||
{ key: 'Export Data', action: () => exportDashboardData() }
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 3: View Management System
|
||||
|
||||
```typescript
|
||||
// services/view-manager.ts
|
||||
export class ViewManager {
|
||||
private views: Map<string, IAppView> = new Map();
|
||||
private activeView: IAppView | null = null;
|
||||
private viewCache: Map<string, any> = new Map();
|
||||
|
||||
registerView(view: IAppView) {
|
||||
this.views.set(view.id, view);
|
||||
}
|
||||
|
||||
async activateView(viewId: string) {
|
||||
const view = this.views.get(viewId);
|
||||
if (!view) throw new Error(`View ${viewId} not found`);
|
||||
|
||||
// Deactivate current view
|
||||
if (this.activeView) {
|
||||
await this.deactivateView(this.activeView.id);
|
||||
}
|
||||
|
||||
// Activate new view
|
||||
this.activeView = view;
|
||||
|
||||
// Update navigation
|
||||
this.updateMainSelector(view.menuItems);
|
||||
this.updateBreadcrumbs(view);
|
||||
|
||||
// Load view data if needed
|
||||
if (!this.viewCache.has(viewId)) {
|
||||
await this.loadViewData(view);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private async loadViewData(view: IAppView) {
|
||||
// Implement lazy loading of view data
|
||||
const viewData = await import(`./views/${view.id}/data.js`);
|
||||
this.viewCache.set(view.id, viewData);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Navigation Integration
|
||||
|
||||
```typescript
|
||||
// navigation/app-navigation.ts
|
||||
export class AppNavigation {
|
||||
constructor(
|
||||
private viewManager: ViewManager,
|
||||
private appShell: MyAppShell
|
||||
) {}
|
||||
|
||||
setupMainMenu(): ITab[] {
|
||||
return [
|
||||
{
|
||||
key: 'dashboard',
|
||||
iconName: 'home',
|
||||
action: () => this.navigateToView('dashboard')
|
||||
},
|
||||
{
|
||||
key: 'projects',
|
||||
iconName: 'folder',
|
||||
action: () => this.navigateToView('projects')
|
||||
},
|
||||
{
|
||||
key: 'analytics',
|
||||
iconName: 'chart-bar',
|
||||
action: () => this.navigateToView('analytics')
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
iconName: 'cog',
|
||||
action: () => this.navigateToView('settings')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async navigateToView(viewId: string) {
|
||||
const view = await this.viewManager.activateView(viewId);
|
||||
this.appShell.activeViewId = viewId;
|
||||
|
||||
// Update URL
|
||||
window.history.pushState(
|
||||
{ viewId },
|
||||
view.name,
|
||||
`/${viewId}`
|
||||
);
|
||||
}
|
||||
|
||||
handleBrowserNavigation() {
|
||||
window.addEventListener('popstate', (event) => {
|
||||
if (event.state?.viewId) {
|
||||
this.navigateToView(event.state.viewId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Dynamic View Loading
|
||||
|
||||
```typescript
|
||||
// views/view-loader.ts
|
||||
export class ViewLoader {
|
||||
private loadedViews: Set<string> = new Set();
|
||||
|
||||
async loadView(viewId: string): Promise<IAppView> {
|
||||
if (this.loadedViews.has(viewId)) {
|
||||
return this.getViewConfig(viewId);
|
||||
}
|
||||
|
||||
// Dynamic import
|
||||
const viewModule = await import(`./views/${viewId}/index.js`);
|
||||
const viewConfig = viewModule.default as IAppView;
|
||||
|
||||
// Register custom elements if needed
|
||||
if (viewModule.registerElements) {
|
||||
await viewModule.registerElements();
|
||||
}
|
||||
|
||||
this.loadedViews.add(viewId);
|
||||
return viewConfig;
|
||||
}
|
||||
|
||||
async preloadViews(viewIds: string[]) {
|
||||
const promises = viewIds.map(id => this.loadView(id));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. View Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── views/
|
||||
│ ├── dashboard/
|
||||
│ │ ├── index.ts # View configuration
|
||||
│ │ ├── data.ts # Data fetching/management
|
||||
│ │ ├── components/ # View-specific components
|
||||
│ │ │ ├── dashboard-overview.ts
|
||||
│ │ │ ├── dashboard-metrics.ts
|
||||
│ │ │ └── dashboard-alerts.ts
|
||||
│ │ └── styles.ts # View-specific styles
|
||||
│ ├── projects/
|
||||
│ │ └── ...
|
||||
│ └── settings/
|
||||
│ └── ...
|
||||
├── services/
|
||||
│ ├── view-manager.ts
|
||||
│ ├── navigation.ts
|
||||
│ └── state-manager.ts
|
||||
└── app-shell.ts
|
||||
```
|
||||
|
||||
### 2. State Management
|
||||
|
||||
```typescript
|
||||
// services/state-manager.ts
|
||||
export class StateManager {
|
||||
private viewStates: Map<string, any> = new Map();
|
||||
|
||||
saveViewState(viewId: string, state: any) {
|
||||
this.viewStates.set(viewId, {
|
||||
...this.getViewState(viewId),
|
||||
...state,
|
||||
lastUpdated: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
getViewState(viewId: string): any {
|
||||
return this.viewStates.get(viewId) || {};
|
||||
}
|
||||
|
||||
// Persist to localStorage
|
||||
persistState() {
|
||||
const serialized = JSON.stringify(
|
||||
Array.from(this.viewStates.entries())
|
||||
);
|
||||
localStorage.setItem('app-state', serialized);
|
||||
}
|
||||
|
||||
restoreState() {
|
||||
const saved = localStorage.getItem('app-state');
|
||||
if (saved) {
|
||||
const entries = JSON.parse(saved);
|
||||
this.viewStates = new Map(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. View Communication
|
||||
|
||||
```typescript
|
||||
// events/view-events.ts
|
||||
export class ViewEventBus {
|
||||
private eventTarget = new EventTarget();
|
||||
|
||||
emit(eventName: string, detail: any) {
|
||||
this.eventTarget.dispatchEvent(
|
||||
new CustomEvent(eventName, { detail })
|
||||
);
|
||||
}
|
||||
|
||||
on(eventName: string, handler: (detail: any) => void) {
|
||||
this.eventTarget.addEventListener(eventName, (e: CustomEvent) => {
|
||||
handler(e.detail);
|
||||
});
|
||||
}
|
||||
|
||||
// Cross-view communication
|
||||
sendMessage(fromView: string, toView: string, message: any) {
|
||||
this.emit('view-message', {
|
||||
from: fromView,
|
||||
to: toView,
|
||||
message
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Responsive Design
|
||||
|
||||
```typescript
|
||||
// views/responsive-view.ts
|
||||
export const createResponsiveView = (config: IAppView): IAppView => {
|
||||
return {
|
||||
...config,
|
||||
tabs: config.tabs.map(tab => ({
|
||||
...tab,
|
||||
content: () => html`
|
||||
<div class="view-content ${getDeviceClass()}">
|
||||
${tab.content()}
|
||||
</div>
|
||||
`
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
function getDeviceClass(): string {
|
||||
const width = window.innerWidth;
|
||||
if (width < 768) return 'mobile';
|
||||
if (width < 1024) return 'tablet';
|
||||
return 'desktop';
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Performance Optimization
|
||||
|
||||
```typescript
|
||||
// optimization/lazy-components.ts
|
||||
export const lazyComponent = (
|
||||
importFn: () => Promise<any>,
|
||||
componentName: string
|
||||
) => {
|
||||
let loaded = false;
|
||||
|
||||
return () => {
|
||||
if (!loaded) {
|
||||
importFn().then(() => {
|
||||
loaded = true;
|
||||
});
|
||||
return html`<dees-spinner></dees-spinner>`;
|
||||
}
|
||||
|
||||
return html`<${componentName}></${componentName}>`;
|
||||
};
|
||||
};
|
||||
|
||||
// Usage in view
|
||||
tabs: [
|
||||
{
|
||||
key: 'heavy-component',
|
||||
content: lazyComponent(
|
||||
() => import('./components/heavy-component.js'),
|
||||
'heavy-component'
|
||||
)
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### 1. View Permissions
|
||||
|
||||
```typescript
|
||||
interface IAppViewWithPermissions extends IAppView {
|
||||
requiredPermissions?: string[];
|
||||
visibleTo?: (user: User) => boolean;
|
||||
}
|
||||
|
||||
class PermissionManager {
|
||||
canAccessView(view: IAppViewWithPermissions, user: User): boolean {
|
||||
if (view.visibleTo) {
|
||||
return view.visibleTo(user);
|
||||
}
|
||||
|
||||
if (view.requiredPermissions) {
|
||||
return view.requiredPermissions.every(
|
||||
perm => user.permissions.includes(perm)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. View Lifecycle Hooks
|
||||
|
||||
```typescript
|
||||
interface IAppViewLifecycle extends IAppView {
|
||||
onActivate?: () => Promise<void>;
|
||||
onDeactivate?: () => Promise<void>;
|
||||
onTabChange?: (oldTab: string, newTab: string) => void;
|
||||
onDestroy?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Dynamic Menu Generation
|
||||
|
||||
```typescript
|
||||
class DynamicMenuBuilder {
|
||||
buildMainMenu(views: IAppView[], user: User): ITab[] {
|
||||
return views
|
||||
.filter(view => this.canShowInMenu(view, user))
|
||||
.map(view => ({
|
||||
key: view.id,
|
||||
iconName: view.iconName || 'file',
|
||||
action: () => this.navigation.navigateToView(view.id)
|
||||
}));
|
||||
}
|
||||
|
||||
buildSelectorMenu(view: IAppView, context: any): ISelectionOption[] {
|
||||
const baseItems = view.menuItems || [];
|
||||
const contextItems = this.getContextualItems(view, context);
|
||||
|
||||
return [...baseItems, ...contextItems];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
For existing applications:
|
||||
|
||||
1. **Identify Views**: Map existing routes/pages to views
|
||||
2. **Extract Components**: Move page-specific components into view folders
|
||||
3. **Define View Configs**: Create IAppView configurations
|
||||
4. **Update Navigation**: Replace existing routing with view navigation
|
||||
5. **Migrate State**: Move page state to ViewManager
|
||||
6. **Test & Optimize**: Ensure smooth transitions and performance
|
||||
|
||||
## Example Application Structure
|
||||
|
||||
```typescript
|
||||
// main.ts
|
||||
import { ViewManager } from './services/view-manager.js';
|
||||
import { AppNavigation } from './services/navigation.js';
|
||||
import { dashboardView } from './views/dashboard/index.js';
|
||||
import { projectsView } from './views/projects/index.js';
|
||||
import { settingsView } from './views/settings/index.js';
|
||||
|
||||
const app = new MyAppShell();
|
||||
const viewManager = new ViewManager();
|
||||
const navigation = new AppNavigation(viewManager, app);
|
||||
|
||||
// Register views
|
||||
viewManager.registerView(dashboardView);
|
||||
viewManager.registerView(projectsView);
|
||||
viewManager.registerView(settingsView);
|
||||
|
||||
// Setup navigation
|
||||
app.views = [dashboardView, projectsView, settingsView];
|
||||
navigation.setupMainMenu();
|
||||
navigation.handleBrowserNavigation();
|
||||
|
||||
// Initial navigation
|
||||
navigation.navigateToView('dashboard');
|
||||
|
||||
document.body.appendChild(app);
|
||||
```
|
||||
|
||||
This architecture provides:
|
||||
- **Modularity**: Each view is self-contained
|
||||
- **Scalability**: Easy to add new views
|
||||
- **Performance**: Lazy loading and caching
|
||||
- **Consistency**: Unified navigation and layout
|
||||
- **Flexibility**: Customizable per view
|
||||
- **Maintainability**: Clear separation of concerns
|
@ -1 +1,79 @@
|
||||
!!! 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.
|
||||
* Try to list all components in a summary.
|
||||
* Then list all components with a short description.
|
||||
|
||||
## Chart Components
|
||||
|
||||
### dees-chart-area
|
||||
- Fully functional area chart component using ApexCharts
|
||||
- Displays time-series data with gradient fills
|
||||
- Responsive with ResizeObserver (debounced to prevent flicker)
|
||||
- Fixed: Chart now properly respects container boundaries on initial render
|
||||
- Overflow prevention with proper CSS containment
|
||||
- Enhanced demo features:
|
||||
- Multiple dataset examples (System Usage, Network Traffic, Sales Analytics)
|
||||
- Real-time data simulation with automatic updates
|
||||
- Dynamic dataset switching
|
||||
- Customizable Y-axis formatters (percentages, currency, units)
|
||||
- Data randomization for testing
|
||||
- Manual data point addition
|
||||
- Properties:
|
||||
- `label`: Chart title
|
||||
- `series`: ApexAxisChartSeries data
|
||||
- `yAxisFormatter`: Custom Y-axis label formatter function
|
||||
- Methods:
|
||||
- `updateSeries()`: Update chart data
|
||||
- `appendData()`: Add new data points to existing series
|
||||
- Demo uses global reference to access chart element (window.__demoChartElement)
|
||||
|
||||
### 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)
|
||||
|
||||
## UI Components
|
||||
|
||||
### dees-button-group
|
||||
- Groups multiple buttons together with a unified background
|
||||
- Properties:
|
||||
- `label`: Optional label text displayed before the buttons
|
||||
- `direction`: 'horizontal' | 'vertical' layout
|
||||
- Features:
|
||||
- Light/dark theme support
|
||||
- Flexible layout with proper spacing
|
||||
- Works with all button types (normal, highlighted, success, danger)
|
||||
- Use cases:
|
||||
- View mode selectors
|
||||
- Action grouping
|
||||
- Navigation options
|
||||
- Filter controls
|
||||
|
||||
## Form Components
|
||||
|
||||
### dees-input-radio
|
||||
- Radio button component with proper group behavior
|
||||
- Properties:
|
||||
- `name`: Group name for mutually exclusive selection
|
||||
- `key`: Unique identifier for the radio option
|
||||
- `value`: Boolean indicating selection state
|
||||
- `label`: Display label
|
||||
- Features:
|
||||
- Automatic group management (radios with same name are mutually exclusive)
|
||||
- Cannot be deselected by clicking (proper radio behavior)
|
||||
- Form integration: Radio groups are collected by name, value is the selected radio's key
|
||||
- Works both inside and outside forms
|
||||
- Supports disabled state
|
||||
- Fixed: Radio buttons now properly deselect others in the group on first click
|
||||
- Note: When using in forms, set both `name` (for grouping) and `key` (for the value)
|
159
readme.plan.md
Normal file
159
readme.plan.md
Normal file
@ -0,0 +1,159 @@
|
||||
# Command: cat ~/.claude/CLAUDE.md
|
||||
|
||||
# Margin Harmonization Plan for @design.estate/dees-catalog
|
||||
|
||||
## Implementation Status: Phase 1 Complete ✅
|
||||
|
||||
Phase 1 has been successfully implemented. Buttons now auto-detect form context and apply appropriate spacing.
|
||||
|
||||
## Objective
|
||||
Implement consistent spacing across all form elements using auto-detection and CSS-based approach for buttons while maintaining the existing input spacing system.
|
||||
|
||||
## Current Issues
|
||||
- Buttons have no default margins (inconsistent with inputs)
|
||||
- Manual spacing required when mixing buttons with inputs in forms
|
||||
- No unified spacing constants across components
|
||||
|
||||
## Implementation Plan (Improved)
|
||||
|
||||
### Phase 1: Add Auto-Detected Form Spacing to Buttons ✅
|
||||
- [x] Update `dees-button.ts`
|
||||
- Add `insideForm` property (boolean, reflected) with auto-detection
|
||||
- Add connectedCallback for automatic form detection:
|
||||
```typescript
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public insideForm: boolean = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Auto-detect if inside a form
|
||||
if (!this.insideForm && this.closest('dees-form')) {
|
||||
this.insideForm = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
- Add margin styles for both vertical and horizontal form contexts:
|
||||
```css
|
||||
/* Default vertical form layout */
|
||||
:host([inside-form]) {
|
||||
display: block;
|
||||
margin-bottom: var(--dees-input-vertical-gap);
|
||||
}
|
||||
:host([inside-form]:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal form layout - auto-detected via parent */
|
||||
:host([inside-form]):host-context(dees-form[horizontal-layout]) {
|
||||
display: inline-block;
|
||||
margin-right: var(--dees-input-horizontal-gap);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:host([inside-form]):host-context(dees-form[horizontal-layout]):last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
```
|
||||
- [x] Update `dees-form-submit.ts`
|
||||
- Remove need for manual attribute setting (auto-detection handles it)
|
||||
- Verify integration works correctly
|
||||
|
||||
### Phase 2: Create Unified Spacing Constants (Optional Enhancement)
|
||||
- [ ] Add CSS custom properties to `dees-input-base.ts`:
|
||||
```css
|
||||
:root {
|
||||
--dees-form-gap: 16px;
|
||||
}
|
||||
```
|
||||
- [ ] Update existing `--dees-input-vertical-gap` to use `--dees-form-gap`
|
||||
- [ ] Update button margins to use the same variable
|
||||
|
||||
### Phase 3: Testing and Documentation
|
||||
- [ ] Test mixed forms with inputs and buttons (both vertical and horizontal)
|
||||
- [ ] Verify last-child margin removal works
|
||||
- [ ] Test auto-detection behavior
|
||||
- [ ] Update demos:
|
||||
- `dees-form.demo.ts` - show buttons auto-detecting form context
|
||||
- `dees-button.demo.ts` - add form context example with manual override
|
||||
- [ ] Document the `insideForm` property and auto-detection in readme.md
|
||||
|
||||
### Phase 4: Clean Up
|
||||
- [ ] Ensure all spacing uses consistent values (16px)
|
||||
- [ ] Verify no breaking changes
|
||||
- [ ] Update changelog.md
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Button Form Integration
|
||||
```typescript
|
||||
// In dees-button.ts
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public insideForm: boolean = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Auto-detect if inside a form
|
||||
if (!this.insideForm && this.closest('dees-form')) {
|
||||
this.insideForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// CSS addition
|
||||
/* Default vertical form layout */
|
||||
:host([inside-form]) {
|
||||
display: block;
|
||||
margin-bottom: var(--dees-input-vertical-gap);
|
||||
}
|
||||
:host([inside-form]:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal form layout - auto-detected via parent */
|
||||
:host([inside-form]):host-context(dees-form[horizontal-layout]) {
|
||||
display: inline-block;
|
||||
margin-right: var(--dees-input-horizontal-gap);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:host([inside-form]):host-context(dees-form[horizontal-layout]):last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
```html
|
||||
<!-- Automatic detection - buttons get form spacing automatically -->
|
||||
<dees-form>
|
||||
<dees-input-text label="Name"></dees-input-text>
|
||||
<dees-input-email label="Email"></dees-input-email>
|
||||
<dees-button>Save Draft</dees-button> <!-- Auto-detects form context -->
|
||||
<dees-form-submit>Submit</dees-form-submit> <!-- Auto-detects form context -->
|
||||
</dees-form>
|
||||
|
||||
<!-- Manual override if needed -->
|
||||
<div class="custom-container">
|
||||
<dees-button inside-form="true">Standalone button with form spacing</dees-button>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal form - spacing adjusts automatically -->
|
||||
<dees-form horizontal-layout>
|
||||
<dees-input-text label="Search"></dees-input-text>
|
||||
<dees-button>Search</dees-button> <!-- Gets right margin instead of bottom -->
|
||||
</dees-form>
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
1. Buttons automatically detect form context and apply appropriate spacing
|
||||
2. Manual override available via `insideForm` property
|
||||
3. Supports both vertical and horizontal form layouts
|
||||
4. No breaking changes for existing implementations
|
||||
5. Consistent 16px spacing between all form elements
|
||||
6. Clear documentation and examples
|
||||
7. All tests pass
|
||||
|
||||
## Benefits of This Approach
|
||||
- **Automatic behavior** - Works out of the box, no manual attributes needed
|
||||
- **Consistent with inputs** - Follows the same pattern as existing form elements
|
||||
- **Layout aware** - Automatically adapts to vertical/horizontal forms
|
||||
- **Minimal code** - Simple CSS-based solution with light JS detection
|
||||
- **Backward compatible** - Existing code continues to work
|
||||
- **Override capability** - Manual control when needed
|
||||
- **Uses existing variables** - Leverages `--dees-input-vertical-gap` and `--dees-input-horizontal-gap`
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '1.1.10',
|
||||
description: 'A library for building components and other projects'
|
||||
version: '1.9.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
@ -22,15 +22,15 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #fff;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100%;
|
||||
background: #111c28;
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#111c28')};
|
||||
font-family: 'Intel One Mono', sans-serif;
|
||||
border-left: 1px solid #202020;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
cursor: default;
|
||||
}
|
||||
.maincontainer {
|
||||
@ -47,7 +47,8 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
padding: 0px 12px 0px 12px;
|
||||
background: #0e151f;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.topbar .heading {
|
||||
@ -57,6 +58,7 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
}
|
||||
|
||||
.activityContainer {
|
||||
@ -73,7 +75,7 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
color: #888
|
||||
color: ${cssManager.bdTheme('#666', '#888')}
|
||||
}
|
||||
|
||||
.streamingIndicator.bottom {
|
||||
@ -85,19 +87,19 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
min-height: 30px;
|
||||
font-size: 12px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px dotted #ffffff20;
|
||||
border-bottom: 1px dotted ${cssManager.bdTheme('#00000020', '#ffffff20')};
|
||||
}
|
||||
|
||||
.activityentry:last-of-type {
|
||||
border-bottom: 1px solid #ffffff00;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.activityentry:hover {
|
||||
background: #00000080;
|
||||
background: ${cssManager.bdTheme('#00000005', '#00000080')};
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #ff8787;
|
||||
color: ${cssManager.bdTheme('#e57373', '#ff8787')};
|
||||
}
|
||||
|
||||
.searchbox {
|
||||
@ -105,10 +107,11 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #0e151f;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
.searchbox input {
|
||||
color: #fff;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
@ -127,7 +130,10 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
bottom: 40px;
|
||||
background: linear-gradient(180deg, #111c2800 0%, #0e151f 100%);
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(180deg, #f8f8f800 0%, #ffffff 100%)',
|
||||
'linear-gradient(180deg, #111c2800 0%, #0e151f 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
}
|
||||
.topShadow {
|
||||
@ -135,7 +141,10 @@ export class DeesAppuiActivitylog extends DeesElement {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
top: 32px;
|
||||
background: linear-gradient(0deg, #111c2800 0%, #0e151f 100%);
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(0deg, #f8f8f800 0%, #ffffff 100%)',
|
||||
'linear-gradient(0deg, #111c2800 0%, #0e151f 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
|
211
ts_web/elements/dees-appui-appbar.demo.ts
Normal file
211
ts_web/elements/dees-appui-appbar.demo.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import type { DeesAppuiBar } from './dees-appui-appbar.js';
|
||||
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Sample menu items with various configurations
|
||||
// Note: Following standard desktop UI patterns, top-level menu items don't have icons
|
||||
// Icons are only used in dropdown menu items for better visual hierarchy
|
||||
const menuItems: IAppBarMenuItem[] = [
|
||||
{
|
||||
name: 'File',
|
||||
action: async () => {}, // No-op action for menu with submenu
|
||||
submenu: [
|
||||
{ name: 'New File', shortcut: 'Cmd+N', iconName: 'file-plus', action: async () => console.log('New file') },
|
||||
{ name: 'Open...', shortcut: 'Cmd+O', iconName: 'folder-open', action: async () => console.log('Open') },
|
||||
{ name: 'Open Recent', action: async () => {}, submenu: [
|
||||
{ name: 'project-alpha.ts', action: async () => console.log('Open recent 1') },
|
||||
{ name: 'config.json', action: async () => console.log('Open recent 2') },
|
||||
{ name: 'readme.md', action: async () => console.log('Open recent 3') },
|
||||
]},
|
||||
{ divider: true },
|
||||
{ name: 'Save', shortcut: 'Cmd+S', iconName: 'save', action: async () => console.log('Save') },
|
||||
{ name: 'Save As...', shortcut: 'Cmd+Shift+S', action: async () => console.log('Save as'), disabled: true },
|
||||
{ divider: true },
|
||||
{ name: 'Exit', shortcut: 'Cmd+Q', action: async () => console.log('Exit') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
action: async () => {}, // No-op action for menu with submenu
|
||||
submenu: [
|
||||
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
|
||||
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
|
||||
{ divider: true },
|
||||
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
|
||||
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
|
||||
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
|
||||
{ divider: true },
|
||||
{ name: 'Find', shortcut: 'Cmd+F', iconName: 'search', action: async () => console.log('Find') },
|
||||
{ name: 'Replace', shortcut: 'Cmd+H', action: async () => console.log('Replace') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View',
|
||||
action: async () => {}, // No-op action for menu with submenu
|
||||
submenu: [
|
||||
{ name: 'Toggle Fullscreen', shortcut: 'F11', iconName: 'expand', action: async () => console.log('Fullscreen') },
|
||||
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoom-in', action: async () => console.log('Zoom in') },
|
||||
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoom-out', action: async () => console.log('Zoom out') },
|
||||
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
|
||||
{ divider: true },
|
||||
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
|
||||
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Help',
|
||||
action: async () => {}, // No-op action for menu with submenu
|
||||
submenu: [
|
||||
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
|
||||
{ name: 'Release Notes', iconName: 'file-text', action: async () => console.log('Release notes') },
|
||||
{ divider: true },
|
||||
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
|
||||
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const appbar = elementArg.querySelector('#appbar') as DeesAppuiBar;
|
||||
|
||||
// Set up status toggle
|
||||
const statusButtons = elementArg.querySelectorAll('.status-toggle dees-button');
|
||||
statusButtons[0].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'online' };
|
||||
});
|
||||
statusButtons[1].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'busy' };
|
||||
});
|
||||
statusButtons[2].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'away' };
|
||||
});
|
||||
statusButtons[3].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'offline' };
|
||||
});
|
||||
|
||||
// Set up window controls toggle
|
||||
const windowControlsButton = elementArg.querySelector('.window-controls-toggle dees-button');
|
||||
windowControlsButton.addEventListener('click', () => {
|
||||
appbar.showWindowControls = !appbar.showWindowControls;
|
||||
});
|
||||
|
||||
// Set up breadcrumb buttons
|
||||
const breadcrumbButtons = elementArg.querySelectorAll('.breadcrumb-toggle dees-button');
|
||||
breadcrumbButtons[0].addEventListener('click', () => {
|
||||
appbar.breadcrumbs = 'Home > Documents > Projects > MyApp > src > index.ts';
|
||||
});
|
||||
breadcrumbButtons[1].addEventListener('click', () => {
|
||||
appbar.breadcrumbs = 'Dashboard';
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
background: #1a1a1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-appui-appbar
|
||||
id="appbar"
|
||||
.menuItems=${menuItems}
|
||||
.breadcrumbs=${'Project > src > components > AppBar.ts'}
|
||||
.breadcrumbSeparator=${' > '}
|
||||
.showWindowControls=${true}
|
||||
.showSearch=${true}
|
||||
.theme=${'dark'}
|
||||
.user=${{
|
||||
name: 'John Doe',
|
||||
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
|
||||
}}
|
||||
@menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail.item)}
|
||||
@breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb clicked:', e.detail)}
|
||||
@search-click=${() => console.log('Search clicked')}
|
||||
@user-menu-open=${() => console.log('User menu clicked')}
|
||||
></dees-appui-appbar>
|
||||
|
||||
<div class="content">
|
||||
<h2>App Bar Demo</h2>
|
||||
<p>This demo shows various features of the app bar component:</p>
|
||||
<ul>
|
||||
<li>Dynamic menu items with icons, shortcuts, and submenus</li>
|
||||
<li>Breadcrumb navigation</li>
|
||||
<li>User account section with status indicator</li>
|
||||
<li>Search icon</li>
|
||||
<li>Window controls (platform-specific)</li>
|
||||
<li>Dark/light theme support</li>
|
||||
<li>Keyboard navigation (Tab, Enter, Escape)</li>
|
||||
<li>Custom events for all interactions</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Theme</label>
|
||||
<dees-button-group class="theme-toggle">
|
||||
<dees-button>Dark</dees-button>
|
||||
<dees-button>Light</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>User Status</label>
|
||||
<dees-button-group class="status-toggle">
|
||||
<dees-button>Online</dees-button>
|
||||
<dees-button>Busy</dees-button>
|
||||
<dees-button>Away</dees-button>
|
||||
<dees-button>Offline</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Window Controls</label>
|
||||
<dees-button-group class="window-controls-toggle">
|
||||
<dees-button>Toggle</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label>Breadcrumbs</label>
|
||||
<dees-button-group class="breadcrumb-toggle">
|
||||
<dees-button>Long Path</dees-button>
|
||||
<dees-button>Short Path</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
@ -1,59 +1,310 @@
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import { demoFunc } from './dees-appui-appbar.demo.js';
|
||||
|
||||
// Import required components
|
||||
import './dees-icon.js';
|
||||
import './dees-windowcontrols.js';
|
||||
import './dees-appui-profiledropdown.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-appui-appbar': DeesAppuiBar;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-appui-appbar')
|
||||
export class DeesAppuiBar extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-appbar></dees-appui-appbar>`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE PROPERTIES
|
||||
@property({ type: Array })
|
||||
public menuItems: interfaces.IAppBarMenuItem[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public breadcrumbs: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public breadcrumbSeparator: string = ' > ';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showWindowControls: boolean = true;
|
||||
|
||||
|
||||
@property({ type: Object })
|
||||
public user?: {
|
||||
name: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
status?: 'online' | 'offline' | 'busy' | 'away';
|
||||
};
|
||||
|
||||
@property({ type: Array })
|
||||
public profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showSearch: boolean = false;
|
||||
|
||||
// STATE
|
||||
@state()
|
||||
private activeMenu: string | null = null;
|
||||
|
||||
@state()
|
||||
private openDropdowns: Set<string> = new Set();
|
||||
|
||||
@state()
|
||||
private focusedItem: string | null = null;
|
||||
|
||||
@state()
|
||||
private focusedDropdownItem: number = -1;
|
||||
|
||||
@state()
|
||||
private isProfileDropdownOpen: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
/* CSS Variables for theming */
|
||||
--appbar-height: 40px;
|
||||
--appbar-font-size: 12px;
|
||||
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #202020;
|
||||
background: #000000;
|
||||
color: #ffffff80;
|
||||
font-size: 12px;
|
||||
height: var(--appbar-height);
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
||||
font-size: var(--appbar-font-size);
|
||||
display: grid;
|
||||
grid-template-columns: ${cssManager.cssGridColumns(3, 20)};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.menus {
|
||||
display: flex;
|
||||
padding-left: 8px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
position: relative;
|
||||
line-height: 24px;
|
||||
padding: 0px 8px;
|
||||
padding: 0px 12px;
|
||||
margin: 8px 0px;
|
||||
border-radius: 4px;
|
||||
-webkit-app-region: no-drag;
|
||||
transition: all 0.2s ease;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Optional: Style for menu items with icons (not typically used for top-level items) */
|
||||
.menuItem dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.menuItem:hover {
|
||||
background: #ffffff20;
|
||||
background: ${cssManager.bdTheme('#00000010', '#ffffff20')};
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
}
|
||||
|
||||
.menuItem.active {
|
||||
background: ${cssManager.bdTheme('#00000020', '#ffffff30')};
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
}
|
||||
|
||||
.menuItem[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menuItem:focus-visible {
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
||||
}
|
||||
|
||||
|
||||
/* Dropdown styles */
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
border-radius: 4px;
|
||||
box-shadow: ${cssManager.bdTheme('0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.3)')};
|
||||
margin-top: 4px;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown.open {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 16px;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item.focused {
|
||||
background: ${cssManager.bdTheme('#00000010', '#ffffff20')};
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.dropdown-item[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown-item .shortcut {
|
||||
margin-left: auto;
|
||||
opacity: 0.6;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
||||
cursor: default;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Account section */
|
||||
.account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: default;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.search-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: default;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: ${cssManager.bdTheme('#00000010', '#ffffff20')};
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#00000020', '#ffffff30')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -2px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
}
|
||||
|
||||
.user-status.online {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.user-status.offline {
|
||||
background: #757575;
|
||||
}
|
||||
|
||||
.user-status.busy {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.user-status.away {
|
||||
background: #ff9800;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -62,16 +313,391 @@ export class DeesAppuiBar extends DeesElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="menus">
|
||||
<dees-windowcontrols></dees-windowcontrols>
|
||||
<div class="menuItem">File</div>
|
||||
<div class="menuItem">View</div>
|
||||
<div class="menuItem">Help</div>
|
||||
<div class="menuItem">Terminal</div>
|
||||
${this.showWindowControls ? html`<dees-windowcontrols></dees-windowcontrols>` : ''}
|
||||
${this.renderMenuItems()}
|
||||
</div>
|
||||
<div class="breadcrumbs">
|
||||
tool:social.io > org:design.estate > prop:lossless.com
|
||||
${this.renderBreadcrumbs()}
|
||||
</div>
|
||||
<div class="account">
|
||||
${this.renderAccountSection()}
|
||||
</div>
|
||||
<div class="account"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMenuItems(): TemplateResult {
|
||||
return html`
|
||||
${this.menuItems.map((item, index) => this.renderMenuItem(item, `menu-${index}`))}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMenuItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult {
|
||||
if ('divider' in item && item.divider) {
|
||||
return html`<div class="dropdown-divider"></div>`;
|
||||
}
|
||||
|
||||
const menuItem = item as interfaces.IAppBarMenuItemRegular;
|
||||
const isActive = this.activeMenu === itemId;
|
||||
const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="menuItem ${isActive ? 'active' : ''}"
|
||||
?disabled=${menuItem.disabled}
|
||||
tabindex="${menuItem.disabled ? -1 : 0}"
|
||||
data-item-id="${itemId}"
|
||||
@click=${() => this.handleMenuClick(menuItem, itemId)}
|
||||
@keydown=${(e: KeyboardEvent) => this.handleMenuKeydown(e, menuItem, itemId)}
|
||||
role="menuitem"
|
||||
aria-haspopup="${hasSubmenu}"
|
||||
aria-expanded="${isActive}"
|
||||
>
|
||||
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
|
||||
${menuItem.name}
|
||||
${hasSubmenu ? this.renderDropdown(menuItem.submenu, itemId, isActive) : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDropdown(items: interfaces.IAppBarMenuItem[], parentId: string, isOpen: boolean): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="dropdown ${isOpen ? 'open' : ''}"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
@keydown=${(e: KeyboardEvent) => this.handleDropdownKeydown(e, items, parentId)}
|
||||
tabindex="${isOpen ? 0 : -1}"
|
||||
role="menu"
|
||||
>
|
||||
${items.map((item, index) => this.renderDropdownItem(item, `${parentId}-${index}`))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDropdownItem(item: interfaces.IAppBarMenuItem, itemId: string): TemplateResult {
|
||||
if ('divider' in item && item.divider) {
|
||||
return html`<div class="dropdown-divider"></div>`;
|
||||
}
|
||||
|
||||
const menuItem = item as interfaces.IAppBarMenuItemRegular;
|
||||
const itemIndex = parseInt(itemId.split('-').pop() || '0');
|
||||
const isFocused = this.focusedDropdownItem === itemIndex;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="dropdown-item ${isFocused ? 'focused' : ''}"
|
||||
?disabled=${menuItem.disabled}
|
||||
@click=${() => this.handleDropdownItemClick(menuItem)}
|
||||
@mouseenter=${() => this.focusedDropdownItem = itemIndex}
|
||||
role="menuitem"
|
||||
tabindex="${menuItem.disabled ? -1 : 0}"
|
||||
>
|
||||
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
|
||||
<span>${menuItem.name}</span>
|
||||
${menuItem.shortcut ? html`<span class="shortcut">${menuItem.shortcut}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBreadcrumbs(): TemplateResult {
|
||||
if (!this.breadcrumbs) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const parts = this.breadcrumbs.split(this.breadcrumbSeparator);
|
||||
return html`
|
||||
${parts.map((part, index) => html`
|
||||
${index > 0 ? html`<span class="breadcrumb-separator">${this.breadcrumbSeparator}</span>` : ''}
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.handleBreadcrumbClick(part, index)}
|
||||
>
|
||||
${part}
|
||||
</span>
|
||||
`)}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAccountSection(): TemplateResult {
|
||||
return html`
|
||||
${this.showSearch ? html`
|
||||
<dees-icon
|
||||
class="search-icon"
|
||||
.icon=${'lucide:search'}
|
||||
@click=${this.handleSearchClick}
|
||||
></dees-icon>
|
||||
` : ''}
|
||||
${this.user ? html`
|
||||
<div style="position: relative;">
|
||||
<div class="user-info" @click=${this.handleUserClick}>
|
||||
<div class="user-avatar">
|
||||
${this.user.avatar ?
|
||||
html`<img src="${this.user.avatar}" alt="${this.user.name}">` :
|
||||
html`${this.user.name.charAt(0).toUpperCase()}`
|
||||
}
|
||||
${this.user.status ? html`
|
||||
<div class="user-status ${this.user.status}"></div>
|
||||
` : ''}
|
||||
</div>
|
||||
<span>${this.user.name}</span>
|
||||
</div>
|
||||
<dees-appui-profiledropdown
|
||||
.user=${this.user}
|
||||
.menuItems=${this.profileMenuItems}
|
||||
.isOpen=${this.isProfileDropdownOpen}
|
||||
.position=${'top-right'}
|
||||
@menu-select=${(e: CustomEvent) => this.handleProfileMenuSelect(e)}
|
||||
></dees-appui-profiledropdown>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
private handleMenuClick(item: interfaces.IAppBarMenuItemRegular, itemId: string) {
|
||||
if (item.disabled) return;
|
||||
|
||||
if (item.submenu && item.submenu.length > 0) {
|
||||
// Toggle dropdown
|
||||
if (this.activeMenu === itemId) {
|
||||
this.activeMenu = null;
|
||||
} else {
|
||||
this.activeMenu = itemId;
|
||||
}
|
||||
} else {
|
||||
// Execute action
|
||||
this.activeMenu = null;
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('menu-select', {
|
||||
detail: { item },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private handleDropdownItemClick(item: interfaces.IAppBarMenuItemRegular) {
|
||||
if (item.disabled) return;
|
||||
|
||||
this.activeMenu = null;
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('menu-select', {
|
||||
detail: { item },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleMenuKeydown(e: KeyboardEvent, item: interfaces.IAppBarMenuItemRegular, itemId: string) {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
this.handleMenuClick(item, itemId);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (item.submenu && this.activeMenu === itemId) {
|
||||
e.preventDefault();
|
||||
// Focus first non-disabled item in dropdown
|
||||
this.focusedDropdownItem = 0;
|
||||
const firstValidItem = this.findNextValidItem(item.submenu, -1, 1);
|
||||
if (firstValidItem !== -1) {
|
||||
this.focusedDropdownItem = firstValidItem;
|
||||
// Focus the dropdown element
|
||||
setTimeout(() => {
|
||||
const dropdown = this.renderRoot.querySelector('.dropdown.open');
|
||||
if (dropdown) {
|
||||
(dropdown as HTMLElement).focus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.activeMenu = null;
|
||||
this.focusedDropdownItem = -1;
|
||||
break;
|
||||
case 'Tab':
|
||||
// Let default tab navigation work but close dropdown
|
||||
if (this.activeMenu === itemId) {
|
||||
this.activeMenu = null;
|
||||
this.focusedDropdownItem = -1;
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
this.focusNextMenuItem(itemId, 1);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.focusNextMenuItem(itemId, -1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleBreadcrumbClick(breadcrumb: string, index: number) {
|
||||
this.dispatchEvent(new CustomEvent('breadcrumb-navigate', {
|
||||
detail: { breadcrumb, index },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleSearchClick() {
|
||||
this.dispatchEvent(new CustomEvent('search-click', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleUserClick() {
|
||||
this.isProfileDropdownOpen = !this.isProfileDropdownOpen;
|
||||
|
||||
// Also emit the event for backward compatibility
|
||||
this.dispatchEvent(new CustomEvent('user-menu-open', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleProfileMenuSelect(e: CustomEvent) {
|
||||
this.isProfileDropdownOpen = false;
|
||||
|
||||
// Re-emit the event
|
||||
this.dispatchEvent(new CustomEvent('profile-menu-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Add global click listener to close dropdowns
|
||||
this.addEventListener('click', this.handleGlobalClick);
|
||||
document.addEventListener('click', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
private handleGlobalClick = (e: Event) => {
|
||||
// Prevent closing when clicking inside
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
private handleDocumentClick = () => {
|
||||
// Close all dropdowns when clicking outside
|
||||
this.activeMenu = null;
|
||||
this.focusedDropdownItem = -1;
|
||||
// Note: Profile dropdown handles its own outside clicks
|
||||
}
|
||||
|
||||
private handleDropdownKeydown(e: KeyboardEvent, items: interfaces.IAppBarMenuItem[], _parentId: string) {
|
||||
const validItems = items.filter(item => !('divider' in item && item.divider));
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
const nextIndex = this.findNextValidItem(items, this.focusedDropdownItem, 1);
|
||||
if (nextIndex !== -1) {
|
||||
this.focusedDropdownItem = nextIndex;
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
const prevIndex = this.findNextValidItem(items, this.focusedDropdownItem, -1);
|
||||
if (prevIndex !== -1) {
|
||||
this.focusedDropdownItem = prevIndex;
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (this.focusedDropdownItem !== -1) {
|
||||
const focusedItem = validItems[this.focusedDropdownItem];
|
||||
if (focusedItem && 'action' in focusedItem && !focusedItem.disabled) {
|
||||
this.handleDropdownItemClick(focusedItem as interfaces.IAppBarMenuItemRegular);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
const firstIndex = this.findNextValidItem(items, -1, 1);
|
||||
if (firstIndex !== -1) {
|
||||
this.focusedDropdownItem = firstIndex;
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
const lastIndex = this.findNextValidItem(items, items.length, -1);
|
||||
if (lastIndex !== -1) {
|
||||
this.focusedDropdownItem = lastIndex;
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
this.activeMenu = null;
|
||||
this.focusedDropdownItem = -1;
|
||||
// Return focus to menu item
|
||||
const menuItem = this.renderRoot.querySelector(`.menuItem.active`);
|
||||
if (menuItem) {
|
||||
(menuItem as HTMLElement).focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private findNextValidItem(items: interfaces.IAppBarMenuItem[], currentIndex: number, direction: number): number {
|
||||
let index = currentIndex + direction;
|
||||
|
||||
while (index >= 0 && index < items.length) {
|
||||
const item = items[index];
|
||||
// Skip dividers and disabled items
|
||||
if (!('divider' in item && item.divider) && !('disabled' in item && item.disabled)) {
|
||||
return index;
|
||||
}
|
||||
index += direction;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private focusNextMenuItem(currentItemId: string, direction: number) {
|
||||
const menuItems = Array.from(this.renderRoot.querySelectorAll('.menuItem'));
|
||||
const currentIndex = menuItems.findIndex(item => item.getAttribute('data-item-id') === currentItemId);
|
||||
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
let nextIndex = currentIndex + direction;
|
||||
|
||||
// Wrap around
|
||||
if (nextIndex < 0) {
|
||||
nextIndex = menuItems.length - 1;
|
||||
} else if (nextIndex >= menuItems.length) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
|
||||
// Find next non-disabled item
|
||||
let attempts = 0;
|
||||
while (attempts < menuItems.length) {
|
||||
const nextItem = menuItems[nextIndex] as HTMLElement;
|
||||
if (!nextItem.hasAttribute('disabled')) {
|
||||
nextItem.focus();
|
||||
// Close current dropdown if open
|
||||
if (this.activeMenu) {
|
||||
this.activeMenu = null;
|
||||
this.focusedDropdownItem = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
nextIndex = (nextIndex + direction + menuItems.length) % menuItems.length;
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
157
ts_web/elements/dees-appui-base.demo.ts
Normal file
157
ts_web/elements/dees-appui-base.demo.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import type { DeesAppuiBase } from './dees-appui-base.js';
|
||||
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
|
||||
import type { ITab } from './interfaces/tab.js';
|
||||
import type { ISelectionOption } from './interfaces/selectionoption.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Menu items for the appbar
|
||||
const menuItems: IAppBarMenuItem[] = [
|
||||
{
|
||||
name: 'File',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'New Project', shortcut: 'Cmd+N', iconName: 'filePlus', action: async () => console.log('New project') },
|
||||
{ name: 'Open Project...', shortcut: 'Cmd+O', iconName: 'folderOpen', action: async () => console.log('Open project') },
|
||||
{ name: 'Recent Projects', action: async () => {}, submenu: [
|
||||
{ name: 'my-app', action: async () => console.log('Open my-app') },
|
||||
{ name: 'component-lib', action: async () => console.log('Open component-lib') },
|
||||
{ name: 'api-server', action: async () => console.log('Open api-server') },
|
||||
]},
|
||||
{ divider: true },
|
||||
{ name: 'Save All', shortcut: 'Cmd+Shift+S', iconName: 'save', action: async () => console.log('Save all') },
|
||||
{ divider: true },
|
||||
{ name: 'Close Project', action: async () => console.log('Close project') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
|
||||
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
|
||||
{ divider: true },
|
||||
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
|
||||
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
|
||||
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'View',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
|
||||
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
|
||||
{ divider: true },
|
||||
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoomIn', action: async () => console.log('Zoom in') },
|
||||
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoomOut', action: async () => console.log('Zoom out') },
|
||||
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Help',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
|
||||
{ name: 'Release Notes', iconName: 'fileText', action: async () => console.log('Release notes') },
|
||||
{ divider: true },
|
||||
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
|
||||
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Main menu tabs (left sidebar)
|
||||
const mainMenuTabs: ITab[] = [
|
||||
{ key: 'dashboard', iconName: 'home', action: () => console.log('Dashboard selected') },
|
||||
{ key: 'projects', iconName: 'folder', action: () => console.log('Projects selected') },
|
||||
{ key: 'analytics', iconName: 'lineChart', action: () => console.log('Analytics selected') },
|
||||
{ key: 'settings', iconName: 'settings', action: () => console.log('Settings selected') },
|
||||
];
|
||||
|
||||
// Selector options (second sidebar)
|
||||
const selectorOptions: (ISelectionOption | { divider: true })[] = [
|
||||
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview selected') },
|
||||
{ key: 'Components', iconName: 'package', action: () => console.log('Components selected') },
|
||||
{ key: 'Services', iconName: 'server', action: () => console.log('Services selected') },
|
||||
{ divider: true },
|
||||
{ key: 'Database', iconName: 'database', action: () => console.log('Database selected') },
|
||||
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings selected') },
|
||||
];
|
||||
|
||||
// Main content tabs
|
||||
const mainContentTabs: ITab[] = [
|
||||
{ key: 'Details', iconName: 'file', action: () => console.log('Details tab') },
|
||||
{ key: 'Logs', iconName: 'list', action: () => console.log('Logs tab') },
|
||||
{ key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') },
|
||||
];
|
||||
|
||||
// Profile menu items
|
||||
const profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [
|
||||
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile settings') },
|
||||
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account settings') },
|
||||
{ divider: true },
|
||||
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
|
||||
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
|
||||
{ divider: true },
|
||||
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-appui-base
|
||||
.appbarMenuItems=${menuItems}
|
||||
.appbarBreadcrumbs=${'Dashboard'}
|
||||
.appbarUser=${{
|
||||
name: 'Jane Smith',
|
||||
email: 'jane.smith@example.com',
|
||||
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
|
||||
}}
|
||||
.appbarProfileMenuItems=${profileMenuItems}
|
||||
.appbarShowWindowControls=${true}
|
||||
.appbarShowSearch=${true}
|
||||
.mainmenuTabs=${mainMenuTabs}
|
||||
.mainselectorOptions=${selectorOptions}
|
||||
.maincontentTabs=${mainContentTabs}
|
||||
@appbar-menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail)}
|
||||
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
|
||||
@appbar-search-click=${() => console.log('Search clicked')}
|
||||
@appbar-user-menu-open=${() => console.log('User menu opened')}
|
||||
@appbar-profile-menu-select=${(e: CustomEvent) => console.log('Profile menu selected:', e.detail)}
|
||||
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
|
||||
@mainselector-option-select=${(e: CustomEvent) => console.log('Option selected:', e.detail)}
|
||||
>
|
||||
<div slot="maincontent" style="padding: 40px; color: #ccc;">
|
||||
<h1>Application Content</h1>
|
||||
<p>This is the main content area where your application's primary interface would be displayed.</p>
|
||||
<p>The layout includes:</p>
|
||||
<ul>
|
||||
<li>App bar with menus, breadcrumbs, and user account</li>
|
||||
<li>Main menu (left sidebar) for primary navigation</li>
|
||||
<li>Selector menu (second sidebar) for sub-navigation</li>
|
||||
<li>Main content area (this section)</li>
|
||||
<li>Activity log (right sidebar)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</dees-appui-base>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
@ -6,11 +6,89 @@ import {
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import type { DeesAppuiBar } from './dees-appui-appbar.js';
|
||||
import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js';
|
||||
import type { DeesAppuiMainselector } from './dees-appui-mainselector.js';
|
||||
import type { DeesAppuiMaincontent } from './dees-appui-maincontent.js';
|
||||
import type { DeesAppuiActivitylog } from './dees-appui-activitylog.js';
|
||||
import { demoFunc } from './dees-appui-base.demo.js';
|
||||
|
||||
// Import child components
|
||||
import './dees-appui-appbar.js';
|
||||
import './dees-appui-mainmenu.js';
|
||||
import './dees-appui-mainselector.js';
|
||||
import './dees-appui-maincontent.js';
|
||||
import './dees-appui-activitylog.js';
|
||||
|
||||
@customElement('dees-appui-base')
|
||||
export class DeesAppuiBase extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-base></dees-appui-base>`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// Properties for appbar
|
||||
@property({ type: Array })
|
||||
public appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public appbarBreadcrumbs: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public appbarBreadcrumbSeparator: string = ' > ';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public appbarShowWindowControls: boolean = true;
|
||||
|
||||
|
||||
@property({ type: Object })
|
||||
public appbarUser?: {
|
||||
name: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
status?: 'online' | 'offline' | 'busy' | 'away';
|
||||
};
|
||||
|
||||
@property({ type: Array })
|
||||
public appbarProfileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public appbarShowSearch: boolean = false;
|
||||
|
||||
// Properties for mainmenu
|
||||
@property({ type: Array })
|
||||
public mainmenuTabs: interfaces.ITab[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
public mainmenuSelectedTab?: interfaces.ITab;
|
||||
|
||||
// Properties for mainselector
|
||||
@property({ type: Array })
|
||||
public mainselectorOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
public mainselectorSelectedOption?: interfaces.ISelectionOption;
|
||||
|
||||
// Properties for maincontent
|
||||
@property({ type: Array })
|
||||
public maincontentTabs: interfaces.ITab[] = [];
|
||||
|
||||
// References to child components
|
||||
@state()
|
||||
public appbar?: DeesAppuiBar;
|
||||
|
||||
@state()
|
||||
public mainmenu?: DeesAppuiMainmenu;
|
||||
|
||||
@state()
|
||||
public mainselector?: DeesAppuiMainselector;
|
||||
|
||||
@state()
|
||||
public maincontent?: DeesAppuiMaincontent;
|
||||
|
||||
@state()
|
||||
public activitylog?: DeesAppuiActivitylog;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@ -19,6 +97,7 @@ export class DeesAppuiBase extends DeesElement {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
||||
}
|
||||
.maingrid {
|
||||
position: absolute;
|
||||
@ -26,7 +105,7 @@ export class DeesAppuiBase extends DeesElement {
|
||||
height: calc(100% - 40px);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 240px auto 240px;
|
||||
grid-template-columns: 60px 240px 1fr 240px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -35,13 +114,106 @@ export class DeesAppuiBase extends DeesElement {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<dees-appui-appbar></dees-appui-appbar>
|
||||
<dees-appui-appbar
|
||||
.menuItems=${this.appbarMenuItems}
|
||||
.breadcrumbs=${this.appbarBreadcrumbs}
|
||||
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
|
||||
.showWindowControls=${this.appbarShowWindowControls}
|
||||
.user=${this.appbarUser}
|
||||
.profileMenuItems=${this.appbarProfileMenuItems}
|
||||
.showSearch=${this.appbarShowSearch}
|
||||
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
||||
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
||||
@search-click=${() => this.handleAppbarSearchClick()}
|
||||
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
||||
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
|
||||
></dees-appui-appbar>
|
||||
<div class="maingrid">
|
||||
<dees-appui-mainmenu></dees-appui-mainmenu>
|
||||
<dees-appui-mainselector></dees-appui-mainselector>
|
||||
<dees-appui-maincontent></dees-appui-maincontent>
|
||||
<dees-appui-mainmenu
|
||||
.tabs=${this.mainmenuTabs}
|
||||
.selectedTab=${this.mainmenuSelectedTab}
|
||||
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
|
||||
></dees-appui-mainmenu>
|
||||
<dees-appui-mainselector
|
||||
.selectionOptions=${this.mainselectorOptions}
|
||||
.selectedOption=${this.mainselectorSelectedOption}
|
||||
@option-select=${(e: CustomEvent) => this.handleMainselectorOptionSelect(e)}
|
||||
></dees-appui-mainselector>
|
||||
<dees-appui-maincontent
|
||||
.tabs=${this.maincontentTabs}
|
||||
>
|
||||
<slot name="maincontent"></slot>
|
||||
</dees-appui-maincontent>
|
||||
<dees-appui-activitylog></dees-appui-activitylog>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
// Get references to child components
|
||||
this.appbar = this.shadowRoot.querySelector('dees-appui-appbar');
|
||||
this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu');
|
||||
this.mainselector = this.shadowRoot.querySelector('dees-appui-mainselector');
|
||||
this.maincontent = this.shadowRoot.querySelector('dees-appui-maincontent');
|
||||
this.activitylog = this.shadowRoot.querySelector('dees-appui-activitylog');
|
||||
}
|
||||
|
||||
// Event handlers for appbar
|
||||
private handleAppbarMenuSelect(e: CustomEvent) {
|
||||
this.dispatchEvent(new CustomEvent('appbar-menu-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleAppbarBreadcrumbNavigate(e: CustomEvent) {
|
||||
this.dispatchEvent(new CustomEvent('appbar-breadcrumb-navigate', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleAppbarSearchClick() {
|
||||
this.dispatchEvent(new CustomEvent('appbar-search-click', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleAppbarUserMenuOpen() {
|
||||
this.dispatchEvent(new CustomEvent('appbar-user-menu-open', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleAppbarProfileMenuSelect(e: CustomEvent) {
|
||||
this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Event handlers for mainmenu
|
||||
private handleMainmenuTabSelect(e: CustomEvent) {
|
||||
this.mainmenuSelectedTab = e.detail.tab;
|
||||
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Event handlers for mainselector
|
||||
private handleMainselectorOptionSelect(e: CustomEvent) {
|
||||
this.mainselectorSelectedOption = e.detail.option;
|
||||
this.dispatchEvent(new CustomEvent('mainselector-option-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -11,35 +11,47 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import './dees-appui-tabs.js';
|
||||
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
|
||||
|
||||
@customElement('dees-appui-maincontent')
|
||||
export class DeesAppuiMaincontent extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-maincontent></dees-appui-maincontent>`;
|
||||
public static demo = () => html`
|
||||
<dees-appui-maincontent
|
||||
.tabs=${[
|
||||
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
|
||||
{ key: 'Details', iconName: 'file', action: () => console.log('Details') },
|
||||
{ key: 'Settings', iconName: 'cog', action: () => console.log('Settings') },
|
||||
]}
|
||||
>
|
||||
<div slot="content" style="padding: 40px; color: #ccc;">
|
||||
<h1>Main Content Area</h1>
|
||||
<p>This is where your application content goes.</p>
|
||||
</div>
|
||||
</dees-appui-maincontent>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public tabs: interfaces.ITab[] = [
|
||||
{ key: 'option 1', action: () => {} },
|
||||
{ key: 'a very long option', action: () => {} },
|
||||
{ key: 'reminder: set your tabs', action: () => {} },
|
||||
{ key: 'option 4', action: () => {} },
|
||||
{ key: '⚠️ Please set tabs', action: () => console.warn('No tabs configured for maincontent') },
|
||||
];
|
||||
|
||||
@property()
|
||||
public selectedTab = null;
|
||||
@property({ type: Object })
|
||||
public selectedTab: interfaces.ITab | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #fff;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #161616;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
}
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
@ -52,110 +64,58 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: #000000;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 0px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
margin-left: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer .tab {
|
||||
color: #a0a0a0;
|
||||
white-space: nowrap;
|
||||
margin-right: 30px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer .tab:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.topbar .tabsContainer .tab.selectedTab {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.topbar .tabIndicator {
|
||||
.content-area {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 40px;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: #161616;
|
||||
transition: all 0.1s;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 1px solid #444444;
|
||||
}
|
||||
|
||||
.mainicon {
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.topbar .tabsContainer {
|
||||
grid-template-columns: repeat(${this.tabs.length}, min-content);
|
||||
}
|
||||
</style>
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<div class="tabsContainer">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : null}"
|
||||
@click="${() => {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
tabArg.action();
|
||||
}}"
|
||||
>
|
||||
${tabArg.key}
|
||||
<dees-appui-tabs
|
||||
.tabs=${this.tabs}
|
||||
.selectedTab=${this.selectedTab}
|
||||
.showTabIndicator=${true}
|
||||
.tabStyle=${'horizontal'}
|
||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||
></dees-appui-tabs>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="tabIndicator"></div>
|
||||
<div class="content-area">
|
||||
<slot></slot>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the indicator
|
||||
*/
|
||||
private updateTabIndicator() {
|
||||
let selectedTab = this.selectedTab;
|
||||
const tabIndex = this.tabs.indexOf(selectedTab);
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabsContainer');
|
||||
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabIndicator');
|
||||
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
|
||||
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
|
||||
private handleTabSelect(e: CustomEvent) {
|
||||
this.selectedTab = e.detail.tab;
|
||||
|
||||
// Re-emit the event
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: e.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private updateTab(tabArg: interfaces.ITab) {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
this.selectedTab.action();
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.updateTab(this.tabs[0]);
|
||||
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
await super.firstUpdated(_changedProperties);
|
||||
// Tab selection is now handled by the dees-appui-tabs component
|
||||
// But we need to ensure the tabs component is ready
|
||||
const tabsComponent = this.shadowRoot.querySelector('dees-appui-tabs') as DeesAppuiTabs;
|
||||
if (tabsComponent) {
|
||||
await tabsComponent.updateComplete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,23 @@ import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
*/
|
||||
@customElement('dees-appui-mainmenu')
|
||||
export class DeesAppuiMainmenu extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-mainmenu></dees-appui-mainmenu>`;
|
||||
public static demo = () => html`
|
||||
<dees-appui-mainmenu
|
||||
.tabs=${[
|
||||
{ key: 'Dashboard', iconName: 'home', action: () => console.log('Dashboard') },
|
||||
{ key: 'Projects', iconName: 'folder', action: () => console.log('Projects') },
|
||||
{ key: 'Analytics', iconName: 'lineChart', action: () => console.log('Analytics') },
|
||||
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
|
||||
]}
|
||||
></dees-appui-mainmenu>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
|
||||
// INSTANCE
|
||||
@property()
|
||||
@property({ type: Array })
|
||||
public tabs: interfaces.ITab[] = [
|
||||
{ key: 'option 1', iconName: 'building', action: () => {} },
|
||||
{ key: 'option 2', iconName: 'building', action: () => {} },
|
||||
{ key: 'option 3', iconName: 'building', action: () => {} },
|
||||
{ key: 'option 4', iconName: 'building', action: () => {} },
|
||||
{ key: '⚠️ Please set tabs', iconName: 'alertTriangle', action: () => console.warn('No tabs configured for mainmenu') },
|
||||
];
|
||||
|
||||
@property()
|
||||
@ -39,16 +45,16 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
css`
|
||||
.mainContainer {
|
||||
--menuSize: 60px;
|
||||
color: #ccc;
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
z-index: 10;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: var(--menuSize);
|
||||
height: 100%;
|
||||
background: #000000;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
|
||||
box-shadow: ${cssManager.bdTheme('0px 0px 5px rgba(0, 0, 0, 0.1)', '0px 0px 5px rgba(0, 0, 0, 0.5)')};
|
||||
user-select: none;
|
||||
border-right: 1px solid #202020;
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
@ -64,17 +70,17 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.15)')};
|
||||
}
|
||||
|
||||
.tab.selectedTab {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
}
|
||||
|
||||
.tabIndicator {
|
||||
opacity: 0;
|
||||
background: #4e729a;
|
||||
background: ${cssManager.bdTheme('#2196f3', '#4e729a')};
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: calc((var(--menuSize) / 3) * 2);
|
||||
@ -105,7 +111,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
this.updateTab(tabArg);
|
||||
}}"
|
||||
>
|
||||
<dees-icon iconFA="${tabArg.iconName as any}"></dees-icon>
|
||||
<dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@ -115,7 +121,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async updateTabIndicator() {
|
||||
private updateTabIndicator() {
|
||||
let selectedTab = this.selectedTab;
|
||||
if (!selectedTab) {
|
||||
selectedTab = this.tabs[0];
|
||||
@ -124,7 +130,12 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
|
||||
if (!selectedTabElement) return;
|
||||
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabIndicator');
|
||||
if (!tabIndicator) return;
|
||||
|
||||
const offsetTop = selectedTabElement.offsetTop;
|
||||
tabIndicator.style.opacity = `1`;
|
||||
tabIndicator.style.top = `calc(${offsetTop}px + (var(--menuSize) / 6))`;
|
||||
@ -134,6 +145,13 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
this.selectedTab.action();
|
||||
|
||||
// Emit tab-select event
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: { tab: tabArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
|
@ -2,6 +2,7 @@ import * as plugins from './00plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
import './dees-icon.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@ -19,22 +20,22 @@ import {
|
||||
*/
|
||||
@customElement('dees-appui-mainselector')
|
||||
export class DeesAppuiMainselector extends DeesElement {
|
||||
public static demo = () => html`<dees-appui-mainselector></dees-appui-mainselector>`;
|
||||
public static demo = () => html`
|
||||
<dees-appui-mainselector
|
||||
.selectionOptions=${[
|
||||
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
|
||||
{ key: 'Components', iconName: 'package', action: () => console.log('Components') },
|
||||
{ key: 'Services', iconName: 'server', action: () => console.log('Services') },
|
||||
{ key: 'Database', iconName: 'database', action: () => console.log('Database') },
|
||||
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
|
||||
]}
|
||||
></dees-appui-mainselector>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property()
|
||||
public selectionOptions: interfaces.ISelectionOption[] = [
|
||||
{
|
||||
key: 'Overview',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
key: 'option 1',
|
||||
action: () => {},
|
||||
},
|
||||
{ key: 'option 2', action: () => {} },
|
||||
{ key: 'option 3', action: () => {} },
|
||||
{ key: 'option 4', action: () => {} },
|
||||
@property({ type: Array })
|
||||
public selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = [
|
||||
{ key: '⚠️ Please set selection options', action: () => console.warn('No selection options configured for mainselector') },
|
||||
];
|
||||
|
||||
@property()
|
||||
@ -44,14 +45,14 @@ export class DeesAppuiMainselector extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #fff;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100%;
|
||||
background: #000000;
|
||||
border-right: 1px solid #222222;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#000000')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
@ -63,52 +64,79 @@ export class DeesAppuiMainselector extends DeesElement {
|
||||
|
||||
.topbar {
|
||||
position: absolute;
|
||||
height: 32px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.topbar .heading {
|
||||
padding-left: 16px;
|
||||
padding-top: 8px;
|
||||
line-height: 24px;
|
||||
padding-left: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.selectionOptions {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
padding-top: 8px;
|
||||
top: 40px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption {
|
||||
cursor: default;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-top: 1px dotted #303030;
|
||||
border-left: 0px solid rgba(0, 0, 0, 0);
|
||||
transition: all 0.1s;
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
transition: background 0.1s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption:hover {
|
||||
border-left: 2px solid #26a69a50;
|
||||
padding-left: 8px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption:first-child {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0);
|
||||
.selectionOptions .selectionOption:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption.selectedOption {
|
||||
border-left: 4px solid #26a69a;
|
||||
padding-left: 10px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption.selectedOption::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: ${cssManager.bdTheme('#26a69a', '#26a69a')};
|
||||
}
|
||||
|
||||
.selectionOption {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selection-divider {
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
margin: 4px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -118,17 +146,22 @@ export class DeesAppuiMainselector extends DeesElement {
|
||||
<style></style>
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<div class="heading">Properties</div>
|
||||
<div class="heading">Selector</div>
|
||||
</div>
|
||||
<div class="selectionOptions">
|
||||
${this.selectionOptions.map((selectionOptionArg) => {
|
||||
if ('divider' in selectionOptionArg && selectionOptionArg.divider) {
|
||||
return html`<div class="selection-divider"></div>`;
|
||||
}
|
||||
|
||||
const option = selectionOptionArg as interfaces.ISelectionOption;
|
||||
return html`
|
||||
<div
|
||||
class="selectionOption ${this.selectedOption === selectionOptionArg
|
||||
class="selectionOption ${this.selectedOption === option
|
||||
? 'selectedOption'
|
||||
: null}"
|
||||
@click="${() => {
|
||||
this.selectOption(selectionOptionArg);
|
||||
this.selectOption(option);
|
||||
}}"
|
||||
@contextmenu="${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
@ -140,7 +173,10 @@ export class DeesAppuiMainselector extends DeesElement {
|
||||
]);
|
||||
}}"
|
||||
>
|
||||
${selectionOptionArg.key}
|
||||
${option.iconName ? html`
|
||||
<dees-icon .icon="${`lucide:${option.iconName}`}" style="font-size: 14px; opacity: 0.7;"></dees-icon>
|
||||
` : ''}
|
||||
<span style="flex: 1;">${option.key}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@ -152,9 +188,24 @@ export class DeesAppuiMainselector extends DeesElement {
|
||||
private selectOption(optionArg: interfaces.ISelectionOption) {
|
||||
this.selectedOption = optionArg;
|
||||
this.selectedOption.action();
|
||||
|
||||
// Emit option-select event
|
||||
this.dispatchEvent(new CustomEvent('option-select', {
|
||||
detail: { option: optionArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.selectOption(this.selectionOptions[0]);
|
||||
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
await super.firstUpdated(_changedProperties);
|
||||
if (this.selectionOptions && this.selectionOptions.length > 0) {
|
||||
await this.updateComplete;
|
||||
// Find first non-divider option
|
||||
const firstOption = this.selectionOptions.find(option => !('divider' in option)) as interfaces.ISelectionOption;
|
||||
if (firstOption) {
|
||||
this.selectOption(firstOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
401
ts_web/elements/dees-appui-profiledropdown.ts
Normal file
401
ts_web/elements/dees-appui-profiledropdown.ts
Normal file
@ -0,0 +1,401 @@
|
||||
import * as plugins from './00plugins.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('dees-appui-profiledropdown')
|
||||
export class DeesAppuiProfileDropdown extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-profiledropdown
|
||||
.user=${{
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
|
||||
status: 'online' as 'online'
|
||||
}}
|
||||
.menuItems=${[
|
||||
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile') },
|
||||
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account') },
|
||||
{ divider: true },
|
||||
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
|
||||
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
|
||||
{ divider: true },
|
||||
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
|
||||
]}
|
||||
.isOpen=${true}
|
||||
></dees-appui-profiledropdown>
|
||||
`;
|
||||
|
||||
@property({ type: Object })
|
||||
public user?: {
|
||||
name: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
status?: 'online' | 'offline' | 'busy' | 'away';
|
||||
};
|
||||
|
||||
@property({ type: Array })
|
||||
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isOpen: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
public position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'top-right';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
min-width: 220px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
border-radius: 4px;
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
||||
)};
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:host([isopen]) .dropdown {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Position variants */
|
||||
.dropdown.top-right {
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dropdown.top-left {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dropdown.bottom-right {
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dropdown.bottom-left {
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* User section */
|
||||
.user-section {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
}
|
||||
|
||||
.user-status.online {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.user-status.offline {
|
||||
background: #757575;
|
||||
}
|
||||
|
||||
.user-status.busy {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.user-status.away {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Menu section */
|
||||
.menu-section {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: default;
|
||||
transition: background 0.1s;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
}
|
||||
|
||||
.menu-item dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-shortcut {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Backdrop for mobile */
|
||||
@media (max-width: 768px) {
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([isopen]) .backdrop {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
transform: translate(-50%, -50%) scale(0.95);
|
||||
margin: 0;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:host([isopen]) .dropdown {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="backdrop" @click=${() => this.close()}></div>
|
||||
<div class="dropdown ${this.position}">
|
||||
${this.user ? html`
|
||||
<div class="user-section">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
${this.user.avatar
|
||||
? html`<img src="${this.user.avatar}" alt="${this.user.name}">`
|
||||
: this.getInitials(this.user.name)
|
||||
}
|
||||
${this.user.status ? html`
|
||||
<div class="user-status ${this.user.status}"></div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<div class="user-name">${this.user.name}</div>
|
||||
${this.user.email ? html`
|
||||
<div class="user-email">${this.user.email}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="menu-section">
|
||||
${this.menuItems.map(item => this.renderMenuItem(item))}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMenuItem(item: plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true }): TemplateResult {
|
||||
if ('divider' in item && item.divider) {
|
||||
return html`<div class="menu-divider"></div>`;
|
||||
}
|
||||
|
||||
const menuItem = item as plugins.tsclass.website.IMenuItem & { iconName?: string; shortcut?: string };
|
||||
return html`
|
||||
<div class="menu-item" @click=${() => this.handleMenuClick(menuItem)}>
|
||||
${menuItem.iconName ? html`
|
||||
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
|
||||
` : ''}
|
||||
<span class="menu-item-text">${menuItem.name}</span>
|
||||
${menuItem.shortcut ? html`
|
||||
<span class="menu-shortcut">${menuItem.shortcut}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map(part => part[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
}
|
||||
|
||||
private async handleMenuClick(item: plugins.tsclass.website.IMenuItem & { iconName?: string; shortcut?: string }) {
|
||||
await item.action();
|
||||
this.close();
|
||||
|
||||
// Emit menu-select event
|
||||
this.dispatchEvent(new CustomEvent('menu-select', {
|
||||
detail: { item },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
public toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
// Handle clicks outside the dropdown
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
this.handleOutsideClick = this.handleOutsideClick.bind(this);
|
||||
document.addEventListener('click', this.handleOutsideClick);
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.handleOutsideClick);
|
||||
}
|
||||
|
||||
private handleOutsideClick(event: MouseEvent) {
|
||||
if (this.isOpen && !this.contains(event.target as Node)) {
|
||||
// Check if the click is on the parent element (which contains the profile button)
|
||||
const parentElement = this.parentElement;
|
||||
if (parentElement && parentElement.contains(event.target as Node)) {
|
||||
// Don't close if clicking within the parent element (e.g., on the profile button)
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
247
ts_web/elements/dees-appui-tabs.ts
Normal file
247
ts_web/elements/dees-appui-tabs.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
@customElement('dees-appui-tabs')
|
||||
export class DeesAppuiTabs extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-tabs
|
||||
.tabs=${[
|
||||
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') },
|
||||
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') },
|
||||
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') },
|
||||
]}
|
||||
></dees-appui-tabs>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public tabs: interfaces.ITab[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
public selectedTab: interfaces.ITab | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showTabIndicator: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
public tabStyle: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tabsContainer.horizontal {
|
||||
display: grid;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 0px;
|
||||
margin-left: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tabsContainer.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.horizontal .tab {
|
||||
margin-right: 30px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.vertical .tab {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: ${cssManager.bdTheme('#000', '#ffffff')};
|
||||
}
|
||||
|
||||
.vertical .tab:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
}
|
||||
|
||||
.tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
||||
}
|
||||
|
||||
.vertical .tab.selectedTab {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#000', '#ffffff')};
|
||||
}
|
||||
|
||||
.tab dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabs-wrapper .tabIndicator {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 40px;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
transition: all 0.1s;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
|
||||
}
|
||||
|
||||
.vertical .tabIndicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.tabStyle === 'horizontal' ? html`
|
||||
<style>
|
||||
.tabsContainer.horizontal {
|
||||
grid-template-columns: repeat(${this.tabs.length}, min-content);
|
||||
}
|
||||
</style>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="tabsContainer horizontal">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.showTabIndicator ? html`
|
||||
<div class="tabIndicator"></div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="tabsContainer vertical">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''}
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private selectTab(tabArg: interfaces.ITab) {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
tabArg.action();
|
||||
|
||||
// Emit tab-select event
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: { tab: tabArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the indicator position
|
||||
*/
|
||||
private updateTabIndicator() {
|
||||
if (!this.showTabIndicator || this.tabStyle !== 'horizontal' || !this.selectedTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = this.tabs.indexOf(this.selectedTab);
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabs-wrapper .tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
|
||||
if (!selectedTabElement) return;
|
||||
|
||||
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabsContainer');
|
||||
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabIndicator');
|
||||
|
||||
if (tabIndicator) {
|
||||
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
|
||||
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.tabs && this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
|
||||
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
||||
this.updateTabIndicator();
|
||||
}
|
||||
}
|
||||
}
|
192
ts_web/elements/dees-appui-view.ts
Normal file
192
ts_web/elements/dees-appui-view.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import './dees-appui-tabs.js';
|
||||
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
|
||||
|
||||
export interface IAppViewTab extends interfaces.ITab {
|
||||
content?: TemplateResult | (() => TemplateResult);
|
||||
}
|
||||
|
||||
export interface IAppView {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
iconName?: string;
|
||||
tabs: IAppViewTab[];
|
||||
menuItems?: interfaces.ISelectionOption[];
|
||||
}
|
||||
|
||||
@customElement('dees-appui-view')
|
||||
export class DeesAppuiView extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-view
|
||||
.viewConfig=${{
|
||||
id: 'demo-view',
|
||||
name: 'Demo View',
|
||||
description: 'A demonstration view',
|
||||
iconName: 'home',
|
||||
tabs: [
|
||||
{
|
||||
key: 'overview',
|
||||
iconName: 'chart-line',
|
||||
action: () => console.log('Overview tab'),
|
||||
content: html`<div style="padding: 20px;">Overview Content</div>`
|
||||
},
|
||||
{
|
||||
key: 'details',
|
||||
iconName: 'file-alt',
|
||||
action: () => console.log('Details tab'),
|
||||
content: html`<div style="padding: 20px;">Details Content</div>`
|
||||
}
|
||||
],
|
||||
menuItems: [
|
||||
{ key: 'General', action: () => console.log('General') },
|
||||
{ key: 'Advanced', action: () => console.log('Advanced') },
|
||||
]
|
||||
}}
|
||||
></dees-appui-view>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@property({ type: Object })
|
||||
public viewConfig: IAppView;
|
||||
|
||||
@state()
|
||||
private selectedTab: IAppViewTab | null = null;
|
||||
|
||||
@state()
|
||||
private tabs: DeesAppuiTabs;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #161616;
|
||||
}
|
||||
|
||||
.view-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.view-header {
|
||||
background: #000000;
|
||||
border-bottom: 1px solid #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.view-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
dees-appui-tabs {
|
||||
height: 60px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.viewConfig) {
|
||||
return html`<div>No view configuration provided</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="view-container">
|
||||
<div class="view-header">
|
||||
<dees-appui-tabs
|
||||
.tabs=${this.viewConfig.tabs}
|
||||
.selectedTab=${this.selectedTab}
|
||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||
></dees-appui-tabs>
|
||||
</div>
|
||||
<div class="view-content">
|
||||
${this.viewConfig.tabs.map((tab) => {
|
||||
const isActive = tab === this.selectedTab;
|
||||
const content = typeof tab.content === 'function' ? tab.content() : tab.content;
|
||||
return html`
|
||||
<div class="tab-content ${isActive ? 'active' : ''}">
|
||||
${content || html`<slot name="${tab.key}"></slot>`}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
this.tabs = this.shadowRoot.querySelector('dees-appui-tabs');
|
||||
|
||||
if (this.viewConfig?.tabs?.length > 0) {
|
||||
this.selectedTab = this.viewConfig.tabs[0];
|
||||
}
|
||||
}
|
||||
|
||||
private handleTabSelect(e: CustomEvent) {
|
||||
this.selectedTab = e.detail.tab;
|
||||
|
||||
// Re-emit the event with view context
|
||||
this.dispatchEvent(new CustomEvent('view-tab-select', {
|
||||
detail: {
|
||||
view: this.viewConfig,
|
||||
tab: e.detail.tab
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Public methods for external control
|
||||
public selectTab(tabKey: string) {
|
||||
const tab = this.viewConfig.tabs.find(t => t.key === tabKey);
|
||||
if (tab) {
|
||||
this.selectedTab = tab;
|
||||
if (this.tabs) {
|
||||
this.tabs.selectedTab = tab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getMenuItems(): interfaces.ISelectionOption[] {
|
||||
return this.viewConfig?.menuItems || [];
|
||||
}
|
||||
|
||||
public getTabs(): IAppViewTab[] {
|
||||
return this.viewConfig?.tabs || [];
|
||||
}
|
||||
}
|
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>
|
||||
`;
|
||||
}
|
||||
}
|
114
ts_web/elements/dees-button-group.demo.ts
Normal file
114
ts_web/elements/dees-button-group.demo.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => {
|
||||
return html`
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
background: #000000;
|
||||
padding: 40px;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 24px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Basic Button Groups</h2>
|
||||
<p class="demo-description">Button groups without labels for simple grouping</p>
|
||||
|
||||
<dees-button-group>
|
||||
<dees-button>Option 1</dees-button>
|
||||
<dees-button>Option 2</dees-button>
|
||||
<dees-button>Option 3</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Labeled Button Groups</h2>
|
||||
<p class="demo-description">Button groups with descriptive labels</p>
|
||||
|
||||
<dees-button-group label="View Mode:">
|
||||
<dees-button type="highlighted">Grid</dees-button>
|
||||
<dees-button>List</dees-button>
|
||||
<dees-button>Cards</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Multiple Groups</h2>
|
||||
<p class="demo-description">Multiple button groups used together</p>
|
||||
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
||||
<dees-button-group label="Dataset:">
|
||||
<dees-button type="highlighted">System</dees-button>
|
||||
<dees-button>Network</dees-button>
|
||||
<dees-button>Sales</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Time Range:">
|
||||
<dees-button>1H</dees-button>
|
||||
<dees-button type="highlighted">24H</dees-button>
|
||||
<dees-button>7D</dees-button>
|
||||
<dees-button>30D</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Refresh</dees-button>
|
||||
<dees-button>Export</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Vertical Button Groups</h2>
|
||||
<p class="demo-description">Button groups with vertical layout</p>
|
||||
|
||||
<div style="display: flex; gap: 24px;">
|
||||
<dees-button-group direction="vertical" label="Navigation:">
|
||||
<dees-button>Dashboard</dees-button>
|
||||
<dees-button type="highlighted">Analytics</dees-button>
|
||||
<dees-button>Reports</dees-button>
|
||||
<dees-button>Settings</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group direction="vertical">
|
||||
<dees-button>Add Item</dees-button>
|
||||
<dees-button>Edit Item</dees-button>
|
||||
<dees-button>Delete Item</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Mixed Button Types</h2>
|
||||
<p class="demo-description">Different button types within groups</p>
|
||||
|
||||
<dees-button-group label="Status:">
|
||||
<dees-button type="success">Active</dees-button>
|
||||
<dees-button>Pending</dees-button>
|
||||
<dees-button type="danger">Inactive</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
83
ts_web/elements/dees-button-group.ts
Normal file
83
ts_web/elements/dees-button-group.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
cssManager,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-button-group.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-button-group': DeesButtonGroup;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-button-group')
|
||||
export class DeesButtonGroup extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property()
|
||||
public label: string = '';
|
||||
|
||||
@property()
|
||||
public direction: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.button-group.vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
font-size: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.button-group.vertical .label {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
margin: 0 !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="button-group ${this.direction}">
|
||||
${this.label ? html`<span class="label">${this.label}</span>` : ''}
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,6 +1,42 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
h3 {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
h3 {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.form-demo {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.form-demo {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<h3>Button Types</h3>
|
||||
<dees-button>This is a slotted Text</dees-button>
|
||||
<p>
|
||||
<dees-button text="Highlighted: This text shows" type="highlighted">Highlighted</dees-button>
|
||||
@ -8,8 +44,34 @@ export const demoFunc = () => html`
|
||||
<p><dees-button type="discreet">This is discreete button</dees-button></p>
|
||||
<p><dees-button disabled>This is a disabled button</dees-button></p>
|
||||
<p><dees-button type="big">This is a slotted Text</dees-button></p>
|
||||
|
||||
<h3>Button States</h3>
|
||||
<p><dees-button status="normal">Normal Status</dees-button></p>
|
||||
<p><dees-button disabled status="pending">Pending Status</dees-button></p>
|
||||
<p><dees-button disabled status="success">Success Status</dees-button></p>
|
||||
<p><dees-button disabled status="error">Error Status</dees-button></p>
|
||||
|
||||
<h3>Buttons in Forms (Auto-spacing)</h3>
|
||||
<div class="form-demo">
|
||||
<dees-form>
|
||||
<dees-input-text label="Name" key="name"></dees-input-text>
|
||||
<dees-input-text label="Email" key="email"></dees-input-text>
|
||||
<dees-button>Save Draft</dees-button>
|
||||
<dees-button type="highlighted">Save and Continue</dees-button>
|
||||
<dees-form-submit>Submit Form</dees-form-submit>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<h3>Buttons Outside Forms (No auto-spacing)</h3>
|
||||
<div class="button-group">
|
||||
<dees-button>Button 1</dees-button>
|
||||
<dees-button>Button 2</dees-button>
|
||||
<dees-button>Button 3</dees-button>
|
||||
</div>
|
||||
|
||||
<h3>Manual Form Spacing</h3>
|
||||
<div>
|
||||
<dees-button inside-form="true">Manually spaced button 1</dees-button>
|
||||
<dees-button inside-form="true">Manually spaced button 2</dees-button>
|
||||
</div>
|
||||
`;
|
||||
|
@ -35,7 +35,8 @@ export class DeesButton extends DeesElement {
|
||||
public eventDetailData: string;
|
||||
|
||||
@property({
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
public disabled = false;
|
||||
|
||||
@ -54,10 +55,24 @@ export class DeesButton extends DeesElement {
|
||||
})
|
||||
public status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true
|
||||
})
|
||||
public insideForm: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Auto-detect if inside a form
|
||||
if (!this.insideForm && this.closest('dees-form')) {
|
||||
this.insideForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
@ -70,6 +85,27 @@ export class DeesButton extends DeesElement {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Form spacing styles */
|
||||
/* Default vertical form layout */
|
||||
:host([inside-form]) {
|
||||
margin-bottom: 16px; /* Using standard 16px like inputs */
|
||||
}
|
||||
|
||||
:host([inside-form]:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal form layout - auto-detected via parent */
|
||||
dees-form[horizontal-layout] :host([inside-form]) {
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dees-form[horizontal-layout] :host([inside-form]:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
transition: all 0.1s , color 0s;
|
||||
position: relative;
|
||||
@ -103,13 +139,6 @@ export class DeesButton extends DeesElement {
|
||||
border-top: 1px solid #0069f2;
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
background: ${cssManager.bdTheme('#ffffff00', '#11111100')};
|
||||
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')};
|
||||
color: #9b9b9e;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.button.highlighted {
|
||||
background: #e4002b;
|
||||
border: none;
|
||||
@ -131,6 +160,14 @@ export class DeesButton extends DeesElement {
|
||||
.button.discreet:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
background: ${cssManager.bdTheme('#ffffff00', '#11111100')};
|
||||
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')};
|
||||
color: #9b9b9e;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.button.hidden {
|
||||
display: none;
|
||||
}
|
||||
@ -179,7 +216,7 @@ export class DeesButton extends DeesElement {
|
||||
${this.status === 'normal' ? html``: html`
|
||||
<dees-spinner .bnw=${true} status="${this.status}"></dees-spinner>
|
||||
`}
|
||||
<div class="textbox">${this.text ? this.text : this.textContent}</div>
|
||||
<div class="textbox">${this.text || html`<slot>Button</slot>`}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -200,9 +237,6 @@ export class DeesButton extends DeesElement {
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
if (!this.textContent) {
|
||||
this.textContent = 'Button';
|
||||
this.performUpdate();
|
||||
}
|
||||
// Don't set default text here as it interferes with slotted content
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,405 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import type { DeesChartArea } from './dees-chart-area.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Initial dataset values
|
||||
const initialDatasets = {
|
||||
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 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const initialFormatters = {
|
||||
system: (val: number) => `${val}%`,
|
||||
};
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
// Get the chart elements
|
||||
const chartElement = elementArg.querySelector('#main-chart') as DeesChartArea;
|
||||
const connectionsChartElement = elementArg.querySelector('#connections-chart') as DeesChartArea;
|
||||
let intervalId: number;
|
||||
let connectionsIntervalId: number;
|
||||
let currentDataset = 'system';
|
||||
|
||||
// Y-axis formatters for different datasets
|
||||
const formatters = {
|
||||
system: (val: number) => `${val}%`,
|
||||
network: (val: number) => `${val} Mbps`,
|
||||
sales: (val: number) => `$${val.toLocaleString()}`,
|
||||
};
|
||||
|
||||
// Time window configuration (in milliseconds)
|
||||
const TIME_WINDOW = 2 * 60 * 1000; // 2 minutes
|
||||
const UPDATE_INTERVAL = 1000; // 1 second
|
||||
const DATA_POINT_INTERVAL = 5000; // Show data points every 5 seconds
|
||||
|
||||
// Store previous values for smooth transitions
|
||||
let previousValues = {
|
||||
cpu: 30,
|
||||
memory: 50,
|
||||
download: 150,
|
||||
upload: 30,
|
||||
connections: 150
|
||||
};
|
||||
|
||||
// Generate initial data points for time window
|
||||
const generateInitialData = (baseValue: number, variance: number, interval: number = DATA_POINT_INTERVAL) => {
|
||||
const data = [];
|
||||
const now = Date.now();
|
||||
const pointCount = Math.floor(TIME_WINDOW / interval);
|
||||
|
||||
for (let i = pointCount; i >= 0; i--) {
|
||||
const timestamp = new Date(now - (i * interval)).toISOString();
|
||||
const value = baseValue + (Math.random() - 0.5) * variance;
|
||||
data.push({ x: timestamp, y: Math.round(value) });
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
// Different datasets to showcase
|
||||
const datasets = {
|
||||
system: {
|
||||
label: 'System Usage (%)',
|
||||
series: [
|
||||
{
|
||||
name: 'CPU',
|
||||
data: generateInitialData(previousValues.cpu, 10),
|
||||
},
|
||||
{
|
||||
name: 'Memory',
|
||||
data: generateInitialData(previousValues.memory, 8),
|
||||
},
|
||||
],
|
||||
},
|
||||
network: {
|
||||
label: 'Network Traffic (Mbps)',
|
||||
series: [
|
||||
{
|
||||
name: 'Download',
|
||||
data: generateInitialData(previousValues.download, 30),
|
||||
},
|
||||
{
|
||||
name: 'Upload',
|
||||
data: generateInitialData(previousValues.upload, 10),
|
||||
},
|
||||
],
|
||||
},
|
||||
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 smooth value transitions
|
||||
const getNextValue = (current: number, min: number, max: number, maxChange: number = 5) => {
|
||||
// Add some randomness but keep it close to current value
|
||||
const change = (Math.random() - 0.5) * maxChange * 2;
|
||||
let newValue = current + change;
|
||||
|
||||
// Apply some "pressure" to move towards center of range
|
||||
const center = (min + max) / 2;
|
||||
const pressure = (center - newValue) * 0.1;
|
||||
newValue += pressure;
|
||||
|
||||
// Ensure within bounds
|
||||
newValue = Math.max(min, Math.min(max, newValue));
|
||||
return Math.round(newValue);
|
||||
};
|
||||
|
||||
// Track time of last data point
|
||||
let lastDataPointTime = Date.now();
|
||||
let connectionsLastUpdate = Date.now();
|
||||
|
||||
// Add real-time data
|
||||
const addRealtimeData = () => {
|
||||
if (!chartElement) return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Only add new data point every DATA_POINT_INTERVAL
|
||||
const shouldAddPoint = (now - lastDataPointTime) >= DATA_POINT_INTERVAL;
|
||||
|
||||
if (shouldAddPoint) {
|
||||
lastDataPointTime = now;
|
||||
const newTimestamp = new Date(now).toISOString();
|
||||
|
||||
// Generate smooth transitions for new values
|
||||
if (currentDataset === 'system') {
|
||||
// Generate new values
|
||||
previousValues.cpu = getNextValue(previousValues.cpu, 20, 50, 3);
|
||||
previousValues.memory = getNextValue(previousValues.memory, 40, 70, 2);
|
||||
|
||||
// Get current data and add new points
|
||||
const currentSeries = chartElement.chartSeries.map((series, index) => ({
|
||||
name: series.name,
|
||||
data: [
|
||||
...(series.data as Array<{x: any; y: any}>),
|
||||
index === 0
|
||||
? { x: newTimestamp, y: previousValues.cpu }
|
||||
: { x: newTimestamp, y: previousValues.memory }
|
||||
]
|
||||
}));
|
||||
|
||||
chartElement.updateSeries(currentSeries, false);
|
||||
|
||||
} else if (currentDataset === 'network') {
|
||||
// Generate new values
|
||||
previousValues.download = getNextValue(previousValues.download, 100, 200, 10);
|
||||
previousValues.upload = getNextValue(previousValues.upload, 20, 50, 5);
|
||||
|
||||
// Get current data and add new points
|
||||
const currentSeries = chartElement.chartSeries.map((series, index) => ({
|
||||
name: series.name,
|
||||
data: [
|
||||
...(series.data as Array<{x: any; y: any}>),
|
||||
index === 0
|
||||
? { x: newTimestamp, y: previousValues.download }
|
||||
: { x: newTimestamp, y: previousValues.upload }
|
||||
]
|
||||
}));
|
||||
|
||||
chartElement.updateSeries(currentSeries, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update connections chart data
|
||||
const updateConnections = () => {
|
||||
if (!connectionsChartElement) return;
|
||||
|
||||
const now = Date.now();
|
||||
const newTimestamp = new Date(now).toISOString();
|
||||
|
||||
// Generate new connections value with discrete changes
|
||||
const change = Math.floor(Math.random() * 21) - 10; // -10 to +10 connections
|
||||
previousValues.connections = Math.max(50, Math.min(300, previousValues.connections + change));
|
||||
|
||||
// Get current data and add new point
|
||||
const currentSeries = connectionsChartElement.chartSeries;
|
||||
const newData = [{
|
||||
name: currentSeries[0]?.name || 'Connections',
|
||||
data: [
|
||||
...(currentSeries[0]?.data as Array<{x: any; y: any}> || []),
|
||||
{ x: newTimestamp, y: previousValues.connections }
|
||||
]
|
||||
}];
|
||||
|
||||
connectionsChartElement.updateSeries(newData, false);
|
||||
};
|
||||
|
||||
// Switch dataset
|
||||
const switchDataset = (name: string) => {
|
||||
currentDataset = name;
|
||||
const dataset = datasets[name];
|
||||
chartElement.label = dataset.label;
|
||||
chartElement.series = dataset.series;
|
||||
chartElement.yAxisFormatter = formatters[name];
|
||||
|
||||
// Set appropriate y-axis scaling
|
||||
if (name === 'system') {
|
||||
chartElement.yAxisScaling = 'percentage';
|
||||
chartElement.yAxisMax = 100;
|
||||
} else if (name === 'network') {
|
||||
chartElement.yAxisScaling = 'dynamic';
|
||||
} else {
|
||||
chartElement.yAxisScaling = 'dynamic';
|
||||
}
|
||||
|
||||
// Reset last data point time to get fresh data immediately
|
||||
lastDataPointTime = Date.now() - DATA_POINT_INTERVAL;
|
||||
};
|
||||
|
||||
// Start/stop real-time updates
|
||||
const startRealtime = () => {
|
||||
if (!intervalId && (currentDataset === 'system' || currentDataset === 'network')) {
|
||||
chartElement.realtimeMode = true;
|
||||
// Only add data every 5 seconds, chart auto-scrolls independently
|
||||
intervalId = window.setInterval(() => addRealtimeData(), DATA_POINT_INTERVAL);
|
||||
}
|
||||
|
||||
// Start connections updates
|
||||
if (!connectionsIntervalId) {
|
||||
connectionsChartElement.realtimeMode = true;
|
||||
// Update connections every second
|
||||
connectionsIntervalId = window.setInterval(() => updateConnections(), UPDATE_INTERVAL);
|
||||
}
|
||||
};
|
||||
|
||||
const stopRealtime = () => {
|
||||
if (intervalId) {
|
||||
window.clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
chartElement.realtimeMode = false;
|
||||
}
|
||||
|
||||
// Stop connections updates
|
||||
if (connectionsIntervalId) {
|
||||
window.clearInterval(connectionsIntervalId);
|
||||
connectionsIntervalId = null;
|
||||
connectionsChartElement.realtimeMode = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Randomize current data (spike/drop simulation)
|
||||
const randomizeData = () => {
|
||||
if (currentDataset === 'system') {
|
||||
// Simulate CPU/Memory spike
|
||||
previousValues.cpu = Math.random() > 0.5 ? 85 : 25;
|
||||
previousValues.memory = Math.random() > 0.5 ? 80 : 45;
|
||||
} else if (currentDataset === 'network') {
|
||||
// Simulate network traffic spike
|
||||
previousValues.download = Math.random() > 0.5 ? 250 : 100;
|
||||
previousValues.upload = Math.random() > 0.5 ? 80 : 20;
|
||||
}
|
||||
|
||||
// Also spike connections
|
||||
previousValues.connections = Math.random() > 0.5 ? 280 : 80;
|
||||
|
||||
// Force immediate update by resetting timers
|
||||
lastDataPointTime = 0;
|
||||
connectionsLastUpdate = 0;
|
||||
};
|
||||
|
||||
// Wire up button click handlers
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.addEventListener('click', () => switchDataset('system'));
|
||||
} else if (text === 'Network Traffic') {
|
||||
button.addEventListener('click', () => switchDataset('network'));
|
||||
} else if (text === 'Sales Data') {
|
||||
button.addEventListener('click', () => switchDataset('sales'));
|
||||
} else if (text === 'Start Live') {
|
||||
button.addEventListener('click', () => startRealtime());
|
||||
} else if (text === 'Stop Live') {
|
||||
button.addEventListener('click', () => stopRealtime());
|
||||
} else if (text === 'Spike Values') {
|
||||
button.addEventListener('click', () => randomizeData());
|
||||
}
|
||||
});
|
||||
|
||||
// Update button states based on current dataset
|
||||
const updateButtonStates = () => {
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.type = currentDataset === 'system' ? 'highlighted' : 'normal';
|
||||
} else if (text === 'Network Traffic') {
|
||||
button.type = currentDataset === 'network' ? 'highlighted' : 'normal';
|
||||
} else if (text === 'Sales Data') {
|
||||
button.type = currentDataset === 'sales' ? 'highlighted' : 'normal';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Configure main chart with rolling window
|
||||
chartElement.rollingWindow = TIME_WINDOW;
|
||||
chartElement.realtimeMode = false; // Will be enabled when starting live updates
|
||||
chartElement.yAxisScaling = 'percentage'; // Initial system dataset uses percentage
|
||||
chartElement.yAxisMax = 100;
|
||||
chartElement.autoScrollInterval = 1000; // Auto-scroll every second
|
||||
|
||||
// Set initial time window
|
||||
setTimeout(() => {
|
||||
chartElement.updateTimeWindow();
|
||||
}, 100);
|
||||
|
||||
// Update button states when dataset changes
|
||||
const originalSwitchDataset = switchDataset;
|
||||
const switchDatasetWithButtonUpdate = (name: string) => {
|
||||
originalSwitchDataset(name);
|
||||
updateButtonStates();
|
||||
};
|
||||
|
||||
// Replace switchDataset with the one that updates buttons
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.removeEventListener('click', () => switchDataset('system'));
|
||||
button.addEventListener('click', () => switchDatasetWithButtonUpdate('system'));
|
||||
} else if (text === 'Network Traffic') {
|
||||
button.removeEventListener('click', () => switchDataset('network'));
|
||||
button.addEventListener('click', () => switchDatasetWithButtonUpdate('network'));
|
||||
} else if (text === 'Sales Data') {
|
||||
button.removeEventListener('click', () => switchDataset('sales'));
|
||||
button.addEventListener('click', () => switchDatasetWithButtonUpdate('sales'));
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize connections chart with data
|
||||
if (connectionsChartElement) {
|
||||
const initialConnectionsData = generateInitialData(previousValues.connections, 30, UPDATE_INTERVAL);
|
||||
connectionsChartElement.series = [{
|
||||
name: 'Connections',
|
||||
data: initialConnectionsData
|
||||
}];
|
||||
|
||||
// Configure connections chart
|
||||
connectionsChartElement.rollingWindow = TIME_WINDOW;
|
||||
connectionsChartElement.realtimeMode = false; // Will be enabled when starting live updates
|
||||
connectionsChartElement.yAxisScaling = 'fixed';
|
||||
connectionsChartElement.yAxisMax = 350;
|
||||
connectionsChartElement.autoScrollInterval = 1000; // Auto-scroll every second
|
||||
|
||||
// Set initial time window
|
||||
setTimeout(() => {
|
||||
connectionsChartElement.updateTimeWindow();
|
||||
}, 100);
|
||||
}
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: #000000;
|
||||
@ -10,12 +407,77 @@ export const demoFunc = () => {
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="controls">
|
||||
<dees-button-group label="Dataset:">
|
||||
<dees-button type="highlighted">System Usage</dees-button>
|
||||
<dees-button>Network Traffic</dees-button>
|
||||
<dees-button>Sales Data</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Real-time:">
|
||||
<dees-button>Start Live</dees-button>
|
||||
<dees-button>Stop Live</dees-button>
|
||||
</dees-button-group>
|
||||
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Spike Values</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<dees-chart-area
|
||||
.label=${'System Usage'}
|
||||
id="main-chart"
|
||||
.label=${initialDatasets.system.label}
|
||||
.series=${initialDatasets.system.series}
|
||||
.yAxisFormatter=${initialFormatters.system}
|
||||
></dees-chart-area>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" style="margin-top: 20px;">
|
||||
<dees-chart-area
|
||||
id="connections-chart"
|
||||
.label=${'Active Connections'}
|
||||
.series=${[{
|
||||
name: 'Connections',
|
||||
data: [] as Array<{x: any; y: any}>
|
||||
}]}
|
||||
.yAxisFormatter=${(val: number) => `${val}`}
|
||||
></dees-chart-area>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Real-time monitoring with 2-minute rolling window •
|
||||
Updates every second with smooth value transitions •
|
||||
Click 'Spike Values' to simulate load spikes
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
@ -6,7 +6,6 @@ import {
|
||||
html,
|
||||
property,
|
||||
state,
|
||||
type CSSResult,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@ -32,30 +31,72 @@ export class DeesChartArea extends DeesElement {
|
||||
@property()
|
||||
public label: string = 'Untitled Chart';
|
||||
|
||||
@property({ type: Array })
|
||||
public series: ApexAxisChartSeries = [];
|
||||
|
||||
// Override getter to return internal chart data
|
||||
get chartSeries(): ApexAxisChartSeries {
|
||||
return this.internalChartData.length > 0 ? this.internalChartData : this.series;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
|
||||
|
||||
@property({ type: Number })
|
||||
public rollingWindow: number = 0; // 0 means no rolling window
|
||||
|
||||
@property({ type: Boolean })
|
||||
public realtimeMode: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
public yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic';
|
||||
|
||||
@property({ type: Number })
|
||||
public yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage'
|
||||
|
||||
@property({ type: Number })
|
||||
public autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable)
|
||||
|
||||
private resizeObserver: ResizeObserver;
|
||||
private resizeTimeout: number;
|
||||
private internalChartData: ApexAxisChartSeries = [];
|
||||
private autoScrollTimer: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
|
||||
this.resizeObserver = new ResizeObserver(entries => {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
// Debounce resize calls to prevent excessive updates
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
|
||||
this.resizeTimeout = window.setTimeout(() => {
|
||||
for (let entry of entries) {
|
||||
if (entry.target.classList.contains('mainbox')) {
|
||||
this.resizeChart(); // Call resizeChart when the .mainbox size changes
|
||||
if (entry.target.classList.contains('mainbox') && this.chart) {
|
||||
this.resizeChart();
|
||||
}
|
||||
}
|
||||
}, 100); // 100ms debounce
|
||||
});
|
||||
|
||||
this.registerStartupFunction(async () => {
|
||||
this.updateComplete.then(() => {
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
if (mainbox) {
|
||||
this.resizeObserver.observe(mainbox); // Start observing the .mainbox element
|
||||
this.resizeObserver.observe(mainbox);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
this.resizeObserver.disconnect();
|
||||
})
|
||||
this.stopAutoScroll();
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
@ -71,8 +112,9 @@ export class DeesChartArea extends DeesElement {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #222;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
@ -82,6 +124,7 @@ export class DeesChartArea extends DeesElement {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
.chartContainer {
|
||||
position: absolute;
|
||||
@ -90,37 +133,93 @@ export class DeesChartArea extends DeesElement {
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 32px 16px 16px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html` <div class="mainbox">
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="chartTitle">${this.label}</div>
|
||||
<div class="chartContainer"></div>
|
||||
</div> `;
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
const domtoolsInstance = await this.domtoolsPromise;
|
||||
var options: ApexCharts.ApexOptions = {
|
||||
series: [
|
||||
await this.domtoolsPromise;
|
||||
|
||||
// Wait for next animation frame to ensure layout is complete
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
// Get actual dimensions of the container
|
||||
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||
|
||||
if (!mainbox || !chartContainer) {
|
||||
console.error('Chart containers not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate initial dimensions
|
||||
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
||||
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
||||
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
||||
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
||||
|
||||
const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||
const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||
|
||||
// Use provided series data or default demo data
|
||||
const chartSeries = this.series.length > 0 ? this.series : [
|
||||
{
|
||||
name: 'cpu',
|
||||
data: [31, 40, 28, 51, 42, 109, 100],
|
||||
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: [11, 32, 45, 32, 34, 52, 41],
|
||||
},
|
||||
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 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Store internal data
|
||||
this.internalChartData = chartSeries;
|
||||
|
||||
var options: ApexCharts.ApexOptions = {
|
||||
series: chartSeries,
|
||||
chart: {
|
||||
width: 0, // Adjusted for responsive width
|
||||
height: 0, // Adjusted for responsive height
|
||||
width: initialWidth || 100, // Use actual width or fallback
|
||||
height: initialHeight || 100, // Use actual height or fallback
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false, // This line disables the toolbar
|
||||
},
|
||||
animations: {
|
||||
enabled: !this.realtimeMode, // Disable animations in realtime mode
|
||||
speed: 400,
|
||||
animateGradually: {
|
||||
enabled: false, // Disable gradual animation for cleaner updates
|
||||
delay: 0
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: !this.realtimeMode,
|
||||
speed: 350
|
||||
}
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
@ -130,35 +229,59 @@ export class DeesChartArea extends DeesElement {
|
||||
curve: 'smooth',
|
||||
},
|
||||
xaxis: {
|
||||
crosshairs: {
|
||||
stroke: {
|
||||
width: 1,
|
||||
color: '#444',
|
||||
type: 'datetime', // Time-series data
|
||||
labels: {
|
||||
format: 'HH:mm:ss', // Time formatting with seconds
|
||||
datetimeUTC: false,
|
||||
style: {
|
||||
colors: '#9e9e9e', // Label color
|
||||
fontSize: '11px',
|
||||
},
|
||||
},
|
||||
type: 'datetime',
|
||||
categories: [
|
||||
'2018-09-19T00:00:00.000Z',
|
||||
'2018-09-19T01:30:00.000Z',
|
||||
'2018-09-19T02:30:00.000Z',
|
||||
'2018-09-19T03:30:00.000Z',
|
||||
'2018-09-19T04:30:00.000Z',
|
||||
'2018-09-19T05:30:00.000Z',
|
||||
'2018-09-19T06:30:00.000Z',
|
||||
],
|
||||
axisBorder: {
|
||||
show: false, // Hide x-axis border
|
||||
},
|
||||
axisTicks: {
|
||||
show: false, // Hide x-axis ticks
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
crosshairs: {
|
||||
stroke: {
|
||||
width: 1,
|
||||
color: '#444',
|
||||
min: 0,
|
||||
max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax,
|
||||
labels: {
|
||||
formatter: this.yAxisFormatter,
|
||||
style: {
|
||||
colors: '#9e9e9e', // Label color
|
||||
fontSize: '12px',
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
show: false, // Hide y-axis border
|
||||
},
|
||||
axisTicks: {
|
||||
show: false, // Hide y-axis ticks
|
||||
},
|
||||
},
|
||||
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: {
|
||||
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: {
|
||||
xaxis: {
|
||||
@ -171,8 +294,8 @@ export class DeesChartArea extends DeesElement {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
borderColor: '#666', // Set the color of the grid lines
|
||||
strokeDashArray: 2, // Solid line
|
||||
borderColor: '#333', // Set the color of the grid lines
|
||||
strokeDashArray: 0, // Solid line
|
||||
row: {
|
||||
colors: [], // This can be used to alternate the shading of the horizontal rows
|
||||
opacity: 0.1,
|
||||
@ -182,27 +305,204 @@ export class DeesChartArea extends DeesElement {
|
||||
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);
|
||||
await this.chart.render();
|
||||
|
||||
// Give the chart a moment to fully initialize before resizing
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await this.resizeChart();
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Update chart if series data changes
|
||||
if (changedProperties.has('series') && this.chart && this.series.length > 0) {
|
||||
await this.updateSeries(this.series);
|
||||
}
|
||||
|
||||
// Update y-axis formatter if it changes
|
||||
if (changedProperties.has('yAxisFormatter') && this.chart) {
|
||||
await this.chart.updateOptions({
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: this.yAxisFormatter,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle realtime mode changes
|
||||
if (changedProperties.has('realtimeMode') && this.chart) {
|
||||
await this.chart.updateOptions({
|
||||
chart: {
|
||||
animations: {
|
||||
enabled: !this.realtimeMode,
|
||||
speed: 400,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
delay: 0
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: !this.realtimeMode,
|
||||
speed: 350
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start/stop auto-scroll based on realtime mode
|
||||
if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) {
|
||||
this.startAutoScroll();
|
||||
} else {
|
||||
this.stopAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle auto-scroll interval changes
|
||||
if (changedProperties.has('autoScrollInterval') && this.chart) {
|
||||
this.stopAutoScroll();
|
||||
if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) {
|
||||
this.startAutoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle y-axis scaling changes
|
||||
if ((changedProperties.has('yAxisScaling') || changedProperties.has('yAxisMax')) && this.chart) {
|
||||
await this.chart.updateOptions({
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async updateSeries(newSeries: ApexAxisChartSeries, animate: boolean = true) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the new data first
|
||||
this.internalChartData = newSeries;
|
||||
|
||||
// Handle rolling window if enabled
|
||||
if (this.rollingWindow > 0 && this.realtimeMode) {
|
||||
const now = Date.now();
|
||||
const cutoffTime = now - this.rollingWindow;
|
||||
|
||||
// Filter data to only include points within the rolling window
|
||||
const filteredSeries = newSeries.map(series => ({
|
||||
name: series.name,
|
||||
data: (series.data as any[]).filter(point => {
|
||||
if (typeof point === 'object' && point !== null && 'x' in point) {
|
||||
return new Date(point.x).getTime() > cutoffTime;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}));
|
||||
|
||||
// Only update if we have data
|
||||
if (filteredSeries.some(s => s.data.length > 0)) {
|
||||
// Handle y-axis scaling first
|
||||
if (this.yAxisScaling === 'dynamic') {
|
||||
const allValues = filteredSeries.flatMap(s => (s.data as any[]).map(d => d.y));
|
||||
if (allValues.length > 0) {
|
||||
const maxValue = Math.max(...allValues);
|
||||
const dynamicMax = Math.ceil(maxValue * 1.1);
|
||||
await this.chart.updateOptions({
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: dynamicMax
|
||||
}
|
||||
}, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
this.chart.updateSeries(filteredSeries, false);
|
||||
}
|
||||
} else {
|
||||
this.chart.updateSeries(newSeries, animate);
|
||||
}
|
||||
}
|
||||
|
||||
// New method to update just the x-axis for smooth scrolling
|
||||
public async updateTimeWindow() {
|
||||
if (!this.chart || this.rollingWindow <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const cutoffTime = now - this.rollingWindow;
|
||||
|
||||
await this.chart.updateOptions({
|
||||
xaxis: {
|
||||
min: cutoffTime,
|
||||
max: now,
|
||||
labels: {
|
||||
format: 'HH:mm:ss',
|
||||
datetimeUTC: false,
|
||||
style: {
|
||||
colors: '#9e9e9e',
|
||||
fontSize: '11px',
|
||||
},
|
||||
},
|
||||
tickAmount: 6,
|
||||
}
|
||||
}, false, false);
|
||||
}
|
||||
|
||||
public async appendData(newData: { data: any[] }[]) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use ApexCharts' appendData method for smoother real-time updates
|
||||
this.chart.appendData(newData);
|
||||
}
|
||||
|
||||
public async updateOptions(options: ApexCharts.ApexOptions, redrawPaths?: boolean, animate?: boolean) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.chart.updateOptions(options, redrawPaths, animate);
|
||||
}
|
||||
|
||||
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
|
||||
const style = window.getComputedStyle(element);
|
||||
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||
|
||||
// Extract padding values
|
||||
const paddingTop = parseInt(style.paddingTop, 10);
|
||||
const paddingBottom = parseInt(style.paddingBottom, 10);
|
||||
const paddingLeft = parseInt(style.paddingLeft, 10);
|
||||
const paddingRight = parseInt(style.paddingRight, 10);
|
||||
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
||||
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
||||
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
||||
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
||||
|
||||
// Calculate the actual width and height to use, subtracting padding
|
||||
const actualWidth = element.clientWidth - paddingLeft - paddingRight;
|
||||
const actualHeight = element.clientHeight - paddingTop - paddingBottom;
|
||||
const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||
|
||||
await this.chart.updateOptions({
|
||||
chart: {
|
||||
@ -211,4 +511,21 @@ export class DeesChartArea extends DeesElement {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private startAutoScroll() {
|
||||
if (this.autoScrollTimer) {
|
||||
return; // Already running
|
||||
}
|
||||
|
||||
this.autoScrollTimer = window.setInterval(() => {
|
||||
this.updateTimeWindow();
|
||||
}, this.autoScrollInterval);
|
||||
}
|
||||
|
||||
private stopAutoScroll() {
|
||||
if (this.autoScrollTimer) {
|
||||
window.clearInterval(this.autoScrollTimer);
|
||||
this.autoScrollTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,136 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { DeesChartLog } from './dees-chart-log.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => {
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
// Get the log element
|
||||
const logElement = elementArg.querySelector('dees-chart-log') as DeesChartLog;
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Wire up button click handlers
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
if (text === 'Add Single Log') {
|
||||
button.addEventListener('click', () => generateRandomLog());
|
||||
} else if (text === 'Start Simulation') {
|
||||
button.addEventListener('click', () => startSimulation());
|
||||
} else if (text === 'Stop Simulation') {
|
||||
button.addEventListener('click', () => stopSimulation());
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
.demoBox {
|
||||
position: relative;
|
||||
@ -9,12 +138,33 @@ export const demoFunc = () => {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.info {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="controls">
|
||||
<dees-button>Add Single Log</dees-button>
|
||||
<dees-button>Start Simulation</dees-button>
|
||||
<dees-button>Stop Simulation</dees-button>
|
||||
</div>
|
||||
<div class="info">Simulating realistic server logs with various levels and sources</div>
|
||||
<dees-chart-log
|
||||
.label=${'Event Log'}
|
||||
.label=${'Production Server Logs'}
|
||||
></dees-chart-log>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
@ -5,15 +5,12 @@ import {
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
state,
|
||||
type CSSResult,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-chart-log.demo.js';
|
||||
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -21,69 +18,303 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILogEntry {
|
||||
timestamp: string;
|
||||
level: 'debug' | 'info' | 'warn' | 'error' | 'success';
|
||||
message: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
@customElement('dees-chart-log')
|
||||
export class DeesChartLog extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// instance
|
||||
@state()
|
||||
public chart: ApexCharts;
|
||||
|
||||
@property()
|
||||
public label: string = 'Untitled Chart';
|
||||
public label: string = 'Server Logs';
|
||||
|
||||
@property({ type: Array })
|
||||
public logEntries: ILogEntry[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public autoScroll: boolean = true;
|
||||
|
||||
@property({ type: Number })
|
||||
public maxEntries: number = 1000;
|
||||
|
||||
private logContainer: HTMLDivElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace;
|
||||
color: #ccc;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #222;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
border-radius: 8px;
|
||||
padding: 32px 16px 16px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
.header {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chartContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#212529', '#fff')};
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')};
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
color: ${cssManager.bdTheme('#495057', '#ccc')};
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')};
|
||||
border-color: ${cssManager.bdTheme('#adb5bd', '#555')};
|
||||
}
|
||||
|
||||
.control-button.active {
|
||||
background: ${cssManager.bdTheme('#007bff', '#4a4a4a')};
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
}
|
||||
|
||||
.logContainer {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.logEntry {
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.level {
|
||||
margin-right: 8px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.level.debug {
|
||||
color: ${cssManager.bdTheme('#6c757d', '#999')};
|
||||
background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')};
|
||||
}
|
||||
|
||||
.level.info {
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4a9eff')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')};
|
||||
}
|
||||
|
||||
.level.warn {
|
||||
color: ${cssManager.bdTheme('#ff8800', '#ffb84a')};
|
||||
background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')};
|
||||
}
|
||||
|
||||
.level.error {
|
||||
color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')};
|
||||
background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')};
|
||||
}
|
||||
|
||||
.level.success {
|
||||
color: ${cssManager.bdTheme('#28a745', '#4aff88')};
|
||||
background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')};
|
||||
}
|
||||
|
||||
.source {
|
||||
color: ${cssManager.bdTheme('#6c757d', '#888')};
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: ${cssManager.bdTheme('#212529', '#ddd')};
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#6c757d', '#666')};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.logContainer::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-track {
|
||||
background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('#adb5bd', '#444')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('#6c757d', '#555')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html` <div class="mainbox">
|
||||
<div class="chartTitle">${this.label}</div>
|
||||
<div class="chartContainer"></div>
|
||||
</div> `;
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="header">
|
||||
<div class="title">${this.label}</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
class="control-button ${this.autoScroll ? 'active' : ''}"
|
||||
@click=${() => { this.autoScroll = !this.autoScroll; }}
|
||||
>
|
||||
Auto Scroll
|
||||
</button>
|
||||
<button
|
||||
class="control-button"
|
||||
@click=${() => { this.clearLogs(); }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logContainer">
|
||||
${this.logEntries.length === 0
|
||||
? html`<div class="empty-state">No logs to display</div>`
|
||||
: this.logEntries.map(entry => this.renderLogEntry(entry))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderLogEntry(entry: ILogEntry): TemplateResult {
|
||||
const timestamp = new Date(entry.timestamp).toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="logEntry">
|
||||
<span class="timestamp">${timestamp}</span>
|
||||
<span class="level ${entry.level}">${entry.level}</span>
|
||||
${entry.source ? html`<span class="source">[${entry.source}]</span>` : ''}
|
||||
<span class="message">${entry.message}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
const domtoolsInstance = await this.domtoolsPromise;
|
||||
await this.domtoolsPromise;
|
||||
this.logContainer = this.shadowRoot.querySelector('.logContainer');
|
||||
|
||||
// Initialize with demo server logs
|
||||
const demoLogs: ILogEntry[] = [
|
||||
{ timestamp: new Date().toISOString(), level: 'info', message: 'Server started on port 3000', source: 'Server' },
|
||||
{ timestamp: new Date().toISOString(), level: 'debug', message: 'Loading configuration from /etc/app/config.json', source: 'Config' },
|
||||
{ timestamp: new Date().toISOString(), level: 'info', message: 'Connected to MongoDB at mongodb://localhost:27017', source: 'Database' },
|
||||
{ timestamp: new Date().toISOString(), level: 'success', message: 'Database connection established successfully', source: 'Database' },
|
||||
{ timestamp: new Date().toISOString(), level: 'warn', message: 'No SSL certificate found, using self-signed certificate', source: 'Security' },
|
||||
{ timestamp: new Date().toISOString(), level: 'info', message: 'API routes initialized: GET /api/users, POST /api/users, DELETE /api/users/:id', source: 'Router' },
|
||||
{ timestamp: new Date().toISOString(), level: 'debug', message: 'Middleware stack: cors, bodyParser, authentication, errorHandler', source: 'Middleware' },
|
||||
{ timestamp: new Date().toISOString(), level: 'info', message: 'WebSocket server listening on ws://localhost:3001', source: 'WebSocket' },
|
||||
];
|
||||
|
||||
this.logEntries = demoLogs;
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
@ -9,49 +9,143 @@ export const demoFunc = () => html`
|
||||
display: block;
|
||||
margin: 20px;
|
||||
}
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 40px;
|
||||
background: #f5f5f5;
|
||||
min-height: 400px;
|
||||
}
|
||||
.demo-area {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
cursor: context-menu;
|
||||
}
|
||||
</style>
|
||||
<dees-button @contextmenu=${(eventArg) => {
|
||||
<div class="demo-container">
|
||||
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'copy',
|
||||
iconName: 'copySolid',
|
||||
name: 'Cut',
|
||||
iconName: 'scissors',
|
||||
shortcut: 'Cmd+X',
|
||||
action: async () => {
|
||||
return null;
|
||||
console.log('Cut action');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
iconName: 'penToSquare',
|
||||
name: 'Copy',
|
||||
iconName: 'copy',
|
||||
shortcut: 'Cmd+C',
|
||||
action: async () => {
|
||||
return null;
|
||||
console.log('Copy action');
|
||||
},
|
||||
},{
|
||||
name: 'paste',
|
||||
iconName: 'pasteSolid',
|
||||
},
|
||||
{
|
||||
name: 'Paste',
|
||||
iconName: 'clipboard',
|
||||
shortcut: 'Cmd+V',
|
||||
action: async () => {
|
||||
return null;
|
||||
console.log('Paste action');
|
||||
},
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'trash2',
|
||||
action: async () => {
|
||||
console.log('Delete action');
|
||||
},
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Select All',
|
||||
shortcut: 'Cmd+A',
|
||||
action: async () => {
|
||||
console.log('Select All action');
|
||||
},
|
||||
},
|
||||
]);
|
||||
}}>Right-Click for contextmenu</dees-button>
|
||||
<dees-contextmenu class="withMargin"></dees-contextmenu>
|
||||
}}>
|
||||
<h3>Right-click anywhere in this area</h3>
|
||||
<p>A context menu will appear with various options</p>
|
||||
</div>
|
||||
|
||||
<dees-button @contextmenu=${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'Button Action 1',
|
||||
iconName: 'play',
|
||||
action: async () => {
|
||||
console.log('Button action 1');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Button Action 2',
|
||||
iconName: 'pause',
|
||||
action: async () => {
|
||||
console.log('Button action 2');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Disabled Action',
|
||||
iconName: 'ban',
|
||||
disabled: true,
|
||||
action: async () => {
|
||||
console.log('This should not run');
|
||||
},
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'settings',
|
||||
action: async () => {
|
||||
console.log('Settings');
|
||||
},
|
||||
},
|
||||
]);
|
||||
}}>Right-click on this button for a different menu</dees-button>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<h4>Static Context Menu (always visible):</h4>
|
||||
<dees-contextmenu
|
||||
class="withMargin"
|
||||
.menuItems=${[
|
||||
{
|
||||
name: 'copy',
|
||||
iconName: 'copySolid',
|
||||
action: async () => {},
|
||||
name: 'New File',
|
||||
iconName: 'filePlus',
|
||||
shortcut: 'Cmd+N',
|
||||
action: async () => console.log('New file'),
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
iconName: 'penToSquare',
|
||||
action: async () => {},
|
||||
},{
|
||||
name: 'paste',
|
||||
iconName: 'pasteSolid',
|
||||
action: async () => {},
|
||||
name: 'Open File',
|
||||
iconName: 'folderOpen',
|
||||
shortcut: 'Cmd+O',
|
||||
action: async () => console.log('Open file'),
|
||||
},
|
||||
] as plugins.tsclass.website.IMenuItem[]}
|
||||
{
|
||||
name: 'Save',
|
||||
iconName: 'save',
|
||||
shortcut: 'Cmd+S',
|
||||
action: async () => console.log('Save'),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Export',
|
||||
iconName: 'download',
|
||||
action: async () => console.log('Export'),
|
||||
},
|
||||
{
|
||||
name: 'Import',
|
||||
iconName: 'upload',
|
||||
action: async () => console.log('Import'),
|
||||
},
|
||||
]}
|
||||
></dees-contextmenu>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,4 +1,3 @@
|
||||
import * as colors from './00colors.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
import { demoFunc } from './dees-contextmenu.demo.js';
|
||||
import {
|
||||
@ -15,6 +14,7 @@ import {
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesWindowLayer } from './dees-windowlayer.js';
|
||||
import './dees-icon.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -30,7 +30,7 @@ export class DeesContextmenu extends DeesElement {
|
||||
// STATIC
|
||||
// This will store all the accumulated menu items
|
||||
public static contextMenuDeactivated = false;
|
||||
public static accumulatedMenuItems: plugins.tsclass.website.IMenuItem[] = [];
|
||||
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] = [];
|
||||
|
||||
// Add a global event listener for the right-click context menu
|
||||
public static initializeGlobalListener() {
|
||||
@ -49,7 +49,13 @@ export class DeesContextmenu extends DeesElement {
|
||||
// Traverse up the DOM tree to accumulate menu items
|
||||
while (target) {
|
||||
if ((target as any).getContextMenuItems) {
|
||||
DeesContextmenu.accumulatedMenuItems.push(...(target as any).getContextMenuItems());
|
||||
const items = (target as any).getContextMenuItems();
|
||||
if (items && items.length > 0) {
|
||||
if (DeesContextmenu.accumulatedMenuItems.length > 0) {
|
||||
DeesContextmenu.accumulatedMenuItems.push({ divider: true });
|
||||
}
|
||||
DeesContextmenu.accumulatedMenuItems.push(...items);
|
||||
}
|
||||
}
|
||||
target = (target as Node).parentNode;
|
||||
}
|
||||
@ -60,7 +66,7 @@ export class DeesContextmenu extends DeesElement {
|
||||
}
|
||||
|
||||
// allows opening of a contextmenu with options
|
||||
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: plugins.tsclass.website.IMenuItem[]) {
|
||||
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]) {
|
||||
if (this.contextMenuDeactivated) {
|
||||
return;
|
||||
}
|
||||
@ -68,32 +74,60 @@ export class DeesContextmenu extends DeesElement {
|
||||
eventArg.stopPropagation();
|
||||
const contextMenu = new DeesContextmenu();
|
||||
contextMenu.style.position = 'fixed';
|
||||
contextMenu.style.zIndex = '2000';
|
||||
contextMenu.style.top = `${eventArg.clientY.toString()}px`;
|
||||
contextMenu.style.left = `${eventArg.clientX.toString()}px`;
|
||||
contextMenu.style.zIndex = '10000';
|
||||
contextMenu.style.opacity = '0';
|
||||
contextMenu.style.transform = 'scale(0.95,0.95)';
|
||||
contextMenu.style.transformOrigin = 'top left';
|
||||
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
|
||||
contextMenu.menuItems = menuItemsArg;
|
||||
contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
|
||||
contextMenu.windowLayer.addEventListener('click', async () => {
|
||||
await contextMenu.destroy();
|
||||
})
|
||||
document.body.append(contextMenu);
|
||||
|
||||
// Get dimensions after adding to DOM
|
||||
await domtools.plugins.smartdelay.delayFor(0);
|
||||
const rect = contextMenu.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// Calculate position
|
||||
let top = eventArg.clientY;
|
||||
let left = eventArg.clientX;
|
||||
|
||||
// Adjust if menu would go off right edge
|
||||
if (left + rect.width > windowWidth) {
|
||||
left = windowWidth - rect.width - 10;
|
||||
}
|
||||
|
||||
// Adjust if menu would go off bottom edge
|
||||
if (top + rect.height > windowHeight) {
|
||||
top = windowHeight - rect.height - 10;
|
||||
}
|
||||
|
||||
// Ensure menu doesn't go off left or top edge
|
||||
if (left < 10) left = 10;
|
||||
if (top < 10) top = 10;
|
||||
|
||||
contextMenu.style.top = `${top}px`;
|
||||
contextMenu.style.left = `${left}px`;
|
||||
contextMenu.style.transformOrigin = 'top left';
|
||||
|
||||
// Animate in
|
||||
await domtools.plugins.smartdelay.delayFor(0);
|
||||
contextMenu.style.opacity = '1';
|
||||
contextMenu.style.transform = 'scale(1,1)';
|
||||
contextMenu.style.transform = 'scale(1) translateY(0)';
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public menuItems: plugins.tsclass.website.IMenuItem[] = [];
|
||||
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = [];
|
||||
windowLayer: DeesWindowLayer;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.tabIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,40 +138,70 @@ export class DeesContextmenu extends DeesElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
transition: all 0.1s;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
color: ${cssManager.bdTheme('#222', '#ccc')};
|
||||
font-size: 14px;
|
||||
width: 200px;
|
||||
border: 1px solid ${cssManager.bdTheme('#fff', '#ffffff10')};
|
||||
min-height: 34px;
|
||||
border-radius: 3px;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
box-shadow: 0px 1px 4px ${cssManager.bdTheme('#00000020', '#000000')};
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
border-radius: 4px;
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
||||
)};
|
||||
user-select: none;
|
||||
padding: 4px;
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.mainbox .menuitem {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
.menuitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: default;
|
||||
transition: background 0.1s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mainbox .menuitem dees-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
width: 14px;
|
||||
transform: translateY(2px);
|
||||
.menuitem:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.mainbox .menuitem:hover {
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
.menuitem:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
}
|
||||
|
||||
.mainbox .menuitem:active {
|
||||
background: #ffffff05;
|
||||
.menuitem.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menuitem dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menuitem-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menuitem-shortcut {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
margin: 4px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -146,10 +210,20 @@ export class DeesContextmenu extends DeesElement {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
${this.menuItems.map((menuItemArg) => {
|
||||
if ('divider' in menuItemArg && menuItemArg.divider) {
|
||||
return html`<div class="menu-divider"></div>`;
|
||||
}
|
||||
|
||||
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean };
|
||||
return html`
|
||||
<div class="menuitem" @click=${() => this.handleClick(menuItemArg)}>
|
||||
<dees-icon .iconFA=${(menuItemArg.iconName as any) || 'minus'}></dees-icon
|
||||
>${menuItemArg.name}
|
||||
<div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}>
|
||||
${menuItem.iconName ? html`
|
||||
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
|
||||
` : ''}
|
||||
<span class="menuitem-text">${menuItem.name}</span>
|
||||
${menuItem.shortcut ? html`
|
||||
<span class="menuitem-shortcut">${menuItem.shortcut}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@ -158,8 +232,8 @@ export class DeesContextmenu extends DeesElement {
|
||||
DeesContextmenu.contextMenuDeactivated = true;
|
||||
this.destroy();
|
||||
}}>
|
||||
<dees-icon .iconFA=${'xmark'}></dees-icon
|
||||
>allow native context
|
||||
<dees-icon .icon="lucide:x"></dees-icon>
|
||||
<span class="menuitem-text">Allow native context</span>
|
||||
</div>
|
||||
` : html``}
|
||||
</div>
|
||||
@ -167,10 +241,45 @@ export class DeesContextmenu extends DeesElement {
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
// Focus on the menu for keyboard navigation
|
||||
this.focus();
|
||||
|
||||
// Add keyboard event listeners
|
||||
this.addEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem) {
|
||||
private handleKeydown = (event: KeyboardEvent) => {
|
||||
const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem:not(.disabled)'));
|
||||
const currentIndex = menuItems.findIndex(item => item.matches(':hover'));
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
const nextIndex = currentIndex + 1 < menuItems.length ? currentIndex + 1 : 0;
|
||||
(menuItems[nextIndex] as HTMLElement).dispatchEvent(new MouseEvent('mouseenter'));
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
const prevIndex = currentIndex - 1 >= 0 ? currentIndex - 1 : menuItems.length - 1;
|
||||
(menuItems[prevIndex] as HTMLElement).dispatchEvent(new MouseEvent('mouseenter'));
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (currentIndex >= 0) {
|
||||
(menuItems[currentIndex] as HTMLElement).click();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
this.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
|
||||
menuItem.action();
|
||||
await this.destroy();
|
||||
}
|
||||
@ -180,7 +289,7 @@ export class DeesContextmenu extends DeesElement {
|
||||
this.windowLayer.destroy();
|
||||
}
|
||||
this.style.opacity = '0';
|
||||
this.style.transform = 'scale(0.95,0,95)';
|
||||
this.style.transform = 'scale(0.95) translateY(-10px)';
|
||||
await domtools.plugins.smartdelay.delayFor(100);
|
||||
this.parentElement.removeChild(this);
|
||||
}
|
||||
|
3
ts_web/elements/dees-form-submit.demo.ts
Normal file
3
ts_web/elements/dees-form-submit.demo.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`<dees-form-submit>Submit Form</dees-form-submit>`;
|
@ -1,3 +1,4 @@
|
||||
import { demoFunc } from './dees-form-submit.demo.js';
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
@ -5,9 +6,8 @@ import {
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesForm } from './dees-form.js';
|
||||
import type { DeesForm } from './dees-form.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -17,7 +17,7 @@ declare global {
|
||||
|
||||
@customElement('dees-form-submit')
|
||||
export class DeesFormSubmit extends DeesElement {
|
||||
public static demo = () => html`<dees-form-submit>This is a sloted text</dees-form-submit>`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
@ -44,11 +44,11 @@ export class DeesFormSubmit extends DeesElement {
|
||||
public render() {
|
||||
return html`
|
||||
<dees-button
|
||||
status=${this.status}
|
||||
@click=${this.submit}
|
||||
.disabled=${this.disabled}
|
||||
.text=${this.text ? this.text : this.textContent}
|
||||
status="${this.status}"
|
||||
@click="${this.submit}"
|
||||
?disabled="${this.disabled}"
|
||||
>
|
||||
${this.text || html`<slot></slot>`}
|
||||
</dees-button>
|
||||
`;
|
||||
}
|
||||
@ -58,13 +58,15 @@ export class DeesFormSubmit extends DeesElement {
|
||||
return;
|
||||
}
|
||||
const parentElement: DeesForm = this.parentElement as DeesForm;
|
||||
if (parentElement && parentElement.gatherAndDispatch) {
|
||||
parentElement.gatherAndDispatch();
|
||||
}
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
if (!this.disabled) {
|
||||
domtools.convenience.smartdelay.delayFor(0);
|
||||
await domtools.convenience.smartdelay.delayFor(0);
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
|
@ -1,69 +1,187 @@
|
||||
import { html, domtools, cssManager } from '@design.estate/dees-element';
|
||||
import { html, css, domtools, cssManager } from '@design.estate/dees-element';
|
||||
import type { DeesForm } from './dees-form.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
.demoContainer {
|
||||
max-width: 400px;
|
||||
margin: 24px auto;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#111')};
|
||||
box-shadow: 0px 1px 3px #00000030;
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
dees-panel {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoContainer">
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .heading="Complete Form Example" .description="A comprehensive form with various input types, validation, and form submission handling">
|
||||
<dees-form
|
||||
style="display: block; margin:auto; max-width: 500px; padding: 20px"
|
||||
@formData=${async (eventArg) => {
|
||||
const form: DeesForm = eventArg.currentTarget;
|
||||
form.setStatus('pending', 'authenticating...');
|
||||
await domtools.plugins.smartdelay.delayFor(1000);
|
||||
form.setStatus('success', 'authenticated!');
|
||||
form.setStatus('pending', 'Processing...');
|
||||
await domtools.plugins.smartdelay.delayFor(2000);
|
||||
form.setStatus('success', 'Form submitted successfully!');
|
||||
await domtools.plugins.smartdelay.delayFor(2000);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<dees-input-text
|
||||
.required=${true}
|
||||
key="firstName"
|
||||
label="First Name"
|
||||
.description=${'Your given name'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.required=${true}
|
||||
key="lastName"
|
||||
label="Last Name"
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.required=${true}
|
||||
key="email"
|
||||
label="Email Address"
|
||||
.description=${'We will use this to contact you'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'title'}
|
||||
.required=${true}
|
||||
key="country"
|
||||
.label=${'Country'}
|
||||
.options=${[
|
||||
{ option: 'option 1', key: 'option1' },
|
||||
{ option: 'option 2', key: 'option2' },
|
||||
{ option: 'option 3', key: 'option3' },
|
||||
{ option: 'United States', key: 'us' },
|
||||
{ option: 'Canada', key: 'ca' },
|
||||
{ option: 'Germany', key: 'de' },
|
||||
{ option: 'France', key: 'fr' },
|
||||
{ option: 'United Kingdom', key: 'uk' },
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-multiselect
|
||||
.label=${'title'}
|
||||
.options=${[
|
||||
{ option: 'option 1', key: 'option1' },
|
||||
{ option: 'option 2', key: 'option2' },
|
||||
{ option: 'option 3', key: 'option3' },
|
||||
]}></dees-input-multiselect>
|
||||
<dees-input-typelist
|
||||
.label=${'a type list'}
|
||||
></dees-input-typelist>
|
||||
<dees-input-text .required="${true}" key="hello1" label="a text" .description=${`
|
||||
This is an awesome description.
|
||||
`}></dees-input-text>
|
||||
<dees-input-text .required="${true}" key="hello2" label="also a text"></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.required="${true}"
|
||||
key="hello3"
|
||||
label="a password"
|
||||
.required=${true}
|
||||
key="password"
|
||||
label="Password"
|
||||
isPasswordBool
|
||||
.description=${'Minimum 8 characters'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-checkbox
|
||||
.required="${true}"
|
||||
key="hello3"
|
||||
label="another text"
|
||||
.required=${true}
|
||||
key="terms"
|
||||
label="I agree to the Terms and Conditions"
|
||||
></dees-input-checkbox>
|
||||
<dees-input-iban></dees-input-iban>
|
||||
<dees-input-multitoggle
|
||||
.label=${'multi select'}
|
||||
.options=${['option 1', 'option 2', 'option 3']}
|
||||
.selectedOption=${'option 1'}
|
||||
></dees-input-multitoggle>
|
||||
<dees-input-fileupload
|
||||
.label=${'attachments'}
|
||||
></dees-input-fileupload>
|
||||
<dees-form-submit>Submit</dees-form-submit>
|
||||
|
||||
<dees-input-checkbox
|
||||
key="newsletter"
|
||||
label="Send me promotional emails"
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-form-submit>Create Account</dees-form-submit>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .heading="Horizontal Form Layout" .description="Compact form with inputs arranged horizontally - perfect for filters and quick forms">
|
||||
<dees-form horizontal-layout>
|
||||
<dees-input-text
|
||||
key="search"
|
||||
label="Search"
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-dropdown
|
||||
key="category"
|
||||
.label=${'Category'}
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: 'All', key: 'all' },
|
||||
{ option: 'Products', key: 'products' },
|
||||
{ option: 'Services', key: 'services' },
|
||||
{ option: 'Support', key: 'support' },
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
key="sort"
|
||||
.label=${'Sort By'}
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: 'Newest', key: 'newest' },
|
||||
{ option: 'Popular', key: 'popular' },
|
||||
{ option: 'Price: Low to High', key: 'price_asc' },
|
||||
{ option: 'Price: High to Low', key: 'price_desc' },
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-checkbox
|
||||
key="inStock"
|
||||
label="In Stock Only"
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .heading="Advanced Form Features" .description="Form with specialized input types and complex validation">
|
||||
<dees-form
|
||||
@formData=${async (eventArg) => {
|
||||
const form: DeesForm = eventArg.currentTarget;
|
||||
const data = eventArg.detail.data;
|
||||
console.log('Form data:', data);
|
||||
form.setStatus('success', 'Data logged to console!');
|
||||
}}
|
||||
>
|
||||
<dees-input-iban
|
||||
key="iban"
|
||||
label="IBAN"
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-phone
|
||||
key="phone"
|
||||
label="Phone Number"
|
||||
.required=${true}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-multitoggle
|
||||
key="preferences"
|
||||
.label=${'Notification Preferences'}
|
||||
.options=${['Email', 'SMS', 'Push', 'In-App']}
|
||||
.selectedOption=${'Email'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multiselect
|
||||
key="interests"
|
||||
.label=${'Areas of Interest'}
|
||||
.options=${[
|
||||
{ option: 'Technology', key: 'tech' },
|
||||
{ option: 'Design', key: 'design' },
|
||||
{ option: 'Business', key: 'business' },
|
||||
{ option: 'Marketing', key: 'marketing' },
|
||||
{ option: 'Sales', key: 'sales' },
|
||||
]}
|
||||
></dees-input-multiselect>
|
||||
|
||||
<dees-input-fileupload
|
||||
key="documents"
|
||||
.label=${'Upload Documents'}
|
||||
.description=${'PDF, DOC, or DOCX files up to 10MB'}
|
||||
></dees-input-fileupload>
|
||||
|
||||
<dees-form-submit>Submit Application</dees-form-submit>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -4,6 +4,7 @@ import {
|
||||
type TemplateResult,
|
||||
DeesElement,
|
||||
type CSSResult,
|
||||
property,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
@ -11,27 +12,42 @@ import { DeesInputCheckbox } from './dees-input-checkbox.js';
|
||||
import { DeesInputText } from './dees-input-text.js';
|
||||
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
|
||||
import { DeesInputRadio } from './dees-input-radio.js';
|
||||
import { DeesInputDropdown } from './dees-input-dropdown.js';
|
||||
import { DeesInputFileupload } from './dees-input-fileupload.js';
|
||||
import { DeesInputIban } from './dees-input-iban.js';
|
||||
import { DeesInputMultitoggle } from './dees-input-multitoggle.js';
|
||||
import { DeesInputPhone } from './dees-input-phone.js';
|
||||
import { DeesInputTypelist } from './dees-input-typelist.js';
|
||||
import { DeesFormSubmit } from './dees-form-submit.js';
|
||||
import { DeesTable } from './dees-table.js';
|
||||
import { demoFunc } from './dees-form.demo.js';
|
||||
import { DeesInputIban } from './dees-input-iban.js';
|
||||
|
||||
// Unified set for form input types
|
||||
const FORM_INPUT_TYPES = [
|
||||
DeesInputCheckbox,
|
||||
DeesInputDropdown,
|
||||
DeesInputFileupload,
|
||||
DeesInputIban,
|
||||
DeesInputText,
|
||||
DeesInputMultitoggle,
|
||||
DeesInputPhone,
|
||||
DeesInputQuantitySelector,
|
||||
DeesInputRadio,
|
||||
DeesInputText,
|
||||
DeesInputTypelist,
|
||||
DeesTable,
|
||||
];
|
||||
|
||||
export type TFormInputElement =
|
||||
| DeesInputCheckbox
|
||||
| DeesInputDropdown
|
||||
| DeesInputFileupload
|
||||
| DeesInputIban
|
||||
| DeesInputText
|
||||
| DeesInputMultitoggle
|
||||
| DeesInputPhone
|
||||
| DeesInputQuantitySelector
|
||||
| DeesInputRadio
|
||||
| DeesInputText
|
||||
| DeesInputTypelist
|
||||
| DeesTable<any>;
|
||||
|
||||
declare global {
|
||||
@ -48,6 +64,13 @@ export class DeesForm extends DeesElement {
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
public readyDeferred = domtools.plugins.smartpromise.defer();
|
||||
|
||||
/**
|
||||
* Controls the layout mode of child input components
|
||||
* When true, sets all child inputs to horizontal layout
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
|
||||
public horizontalLayout: boolean = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
@ -62,6 +85,7 @@ export class DeesForm extends DeesElement {
|
||||
public async firstUpdated() {
|
||||
const formChildren = this.getFormElements();
|
||||
this.updateRequiredStatus();
|
||||
this.updateChildrenLayoutMode();
|
||||
|
||||
for (const child of formChildren) {
|
||||
child.changeSubject.subscribe(async () => {
|
||||
@ -107,13 +131,32 @@ export class DeesForm extends DeesElement {
|
||||
*/
|
||||
public async collectFormData() {
|
||||
const children = this.getFormElements();
|
||||
const valueObject: { [key: string]: string | number | boolean | any[] } = {};
|
||||
const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {};
|
||||
const radioGroups = new Map<string, DeesInputRadio[]>();
|
||||
|
||||
for (const child of children) {
|
||||
if (!child.key) {
|
||||
console.log(`form element with label "${child.label}" has no key. skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle radio buttons specially
|
||||
if (child instanceof DeesInputRadio && child.name) {
|
||||
if (!radioGroups.has(child.name)) {
|
||||
radioGroups.set(child.name, []);
|
||||
}
|
||||
radioGroups.get(child.name).push(child);
|
||||
} else {
|
||||
valueObject[child.key] = child.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Process radio groups - use the name as key and selected radio's key as value
|
||||
for (const [groupName, radios] of radioGroups) {
|
||||
const selectedRadio = radios.find(radio => radio.value === true);
|
||||
valueObject[groupName] = selectedRadio ? selectedRadio.key : null;
|
||||
}
|
||||
|
||||
return valueObject;
|
||||
}
|
||||
|
||||
@ -202,4 +245,28 @@ export class DeesForm extends DeesElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the layout mode of child input components based on form's horizontalLayout property
|
||||
*/
|
||||
private updateChildrenLayoutMode() {
|
||||
const formChildren = this.getFormElements();
|
||||
for (const child of formChildren) {
|
||||
if ('layoutMode' in child) {
|
||||
// The child's auto mode will detect this form's horizontal-layout attribute
|
||||
(child as any).layoutMode = 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when properties change
|
||||
*/
|
||||
updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('horizontalLayout')) {
|
||||
this.updateChildrenLayoutMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
ts_web/elements/dees-heading.demo.ts
Normal file
14
ts_web/elements/dees-heading.demo.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export function demoFunc() {
|
||||
return html`
|
||||
<dees-heading level="1">This is a H1 heading</dees-heading>
|
||||
<dees-heading level="2">This is a H2 heading</dees-heading>
|
||||
<dees-heading level="3">This is a H3 heading</dees-heading>
|
||||
<dees-heading level="4">This is a H4 heading</dees-heading>
|
||||
<dees-heading level="5">This is a H5 heading</dees-heading>
|
||||
<dees-heading level="6">This is a H6 heading</dees-heading>
|
||||
<dees-heading level="hr">This is an hr heading</dees-heading>
|
||||
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
|
||||
`;
|
||||
}
|
115
ts_web/elements/dees-heading.ts
Normal file
115
ts_web/elements/dees-heading.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
property,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
DeesElement,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { demoFunc } from './dees-heading.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-heading': DeesHeading;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-heading')
|
||||
export class DeesHeading extends DeesElement {
|
||||
// demo
|
||||
public static demo = demoFunc;
|
||||
|
||||
// properties
|
||||
/**
|
||||
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
public level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
|
||||
|
||||
// STATIC STYLES
|
||||
public static styles: CSSResult[] = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* Heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
h1 { font-size: 32px; font-family: 'Cal Sans'; letter-spacing: 0.025em;}
|
||||
h2 { font-size: 28px; }
|
||||
h3 { font-size: 24px; }
|
||||
h4 { font-size: 20px; }
|
||||
h5 { font-size: 16px; }
|
||||
h6 { font-size: 14px; }
|
||||
/* Horizontal rule style heading */
|
||||
.heading-hr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
/* Fade lines toward and away from text for hr style */
|
||||
.heading-hr::before {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade in toward center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, transparent, #ccc)',
|
||||
'linear-gradient(to right, transparent, #333)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
}
|
||||
.heading-hr::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade out away from center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, #ccc, transparent)',
|
||||
'linear-gradient(to right, #333, transparent)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
}
|
||||
/* Small hr variant with reduced margins */
|
||||
.heading-hr.heading-hr-small {
|
||||
margin: 8px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.heading-hr.heading-hr-small::before,
|
||||
.heading-hr.heading-hr-small::after {
|
||||
margin: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
// INSTANCE
|
||||
public render(): TemplateResult {
|
||||
switch (this.level) {
|
||||
case '1':
|
||||
return html`<h1><slot></slot></h1>`;
|
||||
case '2':
|
||||
return html`<h2><slot></slot></h2>`;
|
||||
case '3':
|
||||
return html`<h3><slot></slot></h3>`;
|
||||
case '4':
|
||||
return html`<h4><slot></slot></h4>`;
|
||||
case '5':
|
||||
return html`<h5><slot></slot></h5>`;
|
||||
case '6':
|
||||
return html`<h6><slot></slot></h6>`;
|
||||
case 'hr':
|
||||
return html`<div class="heading-hr"><slot></slot></div>`;
|
||||
case 'hr-small':
|
||||
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
||||
default:
|
||||
return html`<h1><slot></slot></h1>`;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,31 +1,155 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { icons, type IconWithPrefix } from './dees-icon.js';
|
||||
import * as lucideIcons from 'lucide';
|
||||
|
||||
import { faIcons } from './dees-icon.js';
|
||||
export const demoFunc = () => {
|
||||
// Group FontAwesome icons by type
|
||||
const faIcons = Object.keys(icons.fa);
|
||||
|
||||
export const demoFunc = () => html`
|
||||
// Extract Lucide icons from the lucideIcons object directly
|
||||
// Log the first few keys to understand the structure
|
||||
console.log('First few Lucide keys:', Object.keys(lucideIcons).slice(0, 5));
|
||||
|
||||
// Get all icon functions from lucideIcons (they have PascalCase names)
|
||||
const lucideIconsList = Object.keys(lucideIcons)
|
||||
.filter(key => {
|
||||
// Skip utility functions and focus on icon components (first letter is uppercase)
|
||||
const isUppercaseFirst = key[0] === key[0].toUpperCase() && key[0] !== key[0].toLowerCase();
|
||||
const isFunction = typeof lucideIcons[key] === 'function';
|
||||
const notUtility = !['createElement', 'createIcons', 'default'].includes(key);
|
||||
return isFunction && isUppercaseFirst && notUtility;
|
||||
})
|
||||
.map(pascalName => {
|
||||
// Convert PascalCase to camelCase
|
||||
return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
||||
});
|
||||
|
||||
// Log how many icons we found
|
||||
console.log(`Found ${lucideIconsList.length} Lucide icons`);
|
||||
|
||||
// If we didn't find any, try an alternative approach
|
||||
if (lucideIconsList.length === 0) {
|
||||
console.log('Trying alternative approach to find Lucide icons');
|
||||
|
||||
// Try to get icon names from a known property if available
|
||||
if (lucideIcons.icons) {
|
||||
const iconSource = lucideIcons.icons || {};
|
||||
lucideIconsList.push(...Object.keys(iconSource));
|
||||
console.log(`Found ${lucideIconsList.length} icons via alternative method`);
|
||||
}
|
||||
}
|
||||
|
||||
// Define the functions in TS scope instead of script tags
|
||||
const searchIcons = (event: InputEvent) => {
|
||||
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim();
|
||||
// Get the demo container first, then search within it
|
||||
const demoContainer = (event.target as HTMLElement).closest('.demoContainer');
|
||||
const containers = demoContainer.querySelectorAll('.iconContainer');
|
||||
|
||||
containers.forEach(container => {
|
||||
const iconName = container.getAttribute('data-name');
|
||||
|
||||
if (searchTerm === '') {
|
||||
container.classList.remove('hidden');
|
||||
} else if (iconName && iconName.includes(searchTerm)) {
|
||||
container.classList.remove('hidden');
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Update counts - search within demoContainer
|
||||
demoContainer.querySelectorAll('.section-container').forEach(section => {
|
||||
const visibleIcons = section.querySelectorAll('.iconContainer:not(.hidden)').length;
|
||||
const countElement = section.querySelector('.icon-count');
|
||||
if (countElement) {
|
||||
const totalIconsCount = section.classList.contains('fa-section')
|
||||
? faIcons.length
|
||||
: lucideIconsList.length;
|
||||
|
||||
countElement.textContent = visibleIcons === totalIconsCount
|
||||
? `${totalIconsCount} icons`
|
||||
: `${visibleIcons} of ${totalIconsCount} icons`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const copyIconName = (iconNameToCopy: string, type: 'fa' | 'lucide') => {
|
||||
// Use the new prefix format
|
||||
const textToCopy = `${type}:${iconNameToCopy}`;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
// Find the event target
|
||||
const currentEvent = window.event as MouseEvent;
|
||||
const currentTarget = currentEvent.currentTarget as HTMLElement;
|
||||
// Show feedback
|
||||
const tooltip = currentTarget.querySelector('.copy-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.textContent = 'Copied!';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.textContent = 'Click to copy';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.demoContainer {
|
||||
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: #111111;
|
||||
padding: 10px; font-size: 30px;
|
||||
padding: 20px;
|
||||
font-size: 30px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#iconSearch {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
#iconSearch:focus {
|
||||
outline: none;
|
||||
border-color: #e4002b;
|
||||
}
|
||||
|
||||
dees-icon {
|
||||
transition: color 0.02s;
|
||||
transition: all 0.2s ease;
|
||||
color: #ffffff;
|
||||
}
|
||||
dees-icon:hover {
|
||||
color: #e4002b;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: block;
|
||||
padding: 16px 16px 0px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 16px 0px 16px;
|
||||
border: 1px solid #333333;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iconContainer:hover {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.iconName {
|
||||
@ -33,23 +157,136 @@ export const demoFunc = () => html`
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
background: #333333;
|
||||
padding: 4px 8px;
|
||||
padding-bottom: 4px;
|
||||
padding: 6px 10px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
margin-top: 16px;
|
||||
margin-top: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
width: 100%;
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
margin: 20px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #333333;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.api-note {
|
||||
font-size: 14px;
|
||||
color: #e4002b;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e4002b;
|
||||
border-radius: 4px;
|
||||
background: rgba(228, 0, 43, 0.1);
|
||||
}
|
||||
|
||||
.icon-count {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
font-weight: normal;
|
||||
background: #222;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.icons-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.copy-tooltip {
|
||||
position: absolute;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
top: -30px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.iconContainer:hover .copy-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconContainer:hover dees-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demoContainer">
|
||||
${Object.keys(faIcons).map(
|
||||
(iconName) => html`
|
||||
<div class="iconContainer">
|
||||
<dees-icon .iconFA=${iconName as any}></dees-icon>
|
||||
<div class="iconName">${iconName}</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div class="search-container">
|
||||
<input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}>
|
||||
</div>
|
||||
|
||||
<div class="api-note">
|
||||
New API: Use <code>icon="fa:iconName"</code> or <code>icon="lucide:iconName"</code> instead of <code>iconFA</code>.
|
||||
Click any icon to copy its new format to clipboard.
|
||||
</div>
|
||||
|
||||
<div class="section-container fa-section">
|
||||
<div class="section-title">
|
||||
FontAwesome Icons
|
||||
<span class="icon-count">${faIcons.length} icons</span>
|
||||
</div>
|
||||
<div class="icons-grid">
|
||||
${faIcons.map(
|
||||
(iconName) => {
|
||||
const prefixedName = `fa:${iconName}`;
|
||||
return html`
|
||||
<div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}>
|
||||
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||
<div class="iconName">${iconName}</div>
|
||||
<span class="copy-tooltip">Click to copy</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-container lucide-section">
|
||||
<div class="section-title">
|
||||
Lucide Icons
|
||||
<span class="icon-count">${lucideIconsList.length} icons</span>
|
||||
</div>
|
||||
<div class="icons-grid">
|
||||
${lucideIconsList.map(
|
||||
(iconName) => {
|
||||
const prefixedName = `lucide:${iconName}`;
|
||||
return html`
|
||||
<div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}>
|
||||
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||
<div class="iconName">${iconName}</div>
|
||||
<span class="copy-tooltip">Click to copy</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
@ -75,7 +75,12 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { demoFunc } from './dees-icon.demo.js';
|
||||
|
||||
export const faIcons = {
|
||||
// Import Lucide icons and the createElement function
|
||||
import * as lucideIcons from 'lucide';
|
||||
import { createElement } from 'lucide';
|
||||
|
||||
// Collect FontAwesome icons
|
||||
const faIcons = {
|
||||
// normal
|
||||
arrowRight: faArrowRightSolid,
|
||||
arrowUpRightFromSquare: faArrowUpRightFromSquareSolid,
|
||||
@ -136,7 +141,32 @@ export const faIcons = {
|
||||
twitter: faTwitter,
|
||||
};
|
||||
|
||||
export type TIconKey = keyof typeof faIcons;
|
||||
// Create a string literal type for all FA icons
|
||||
type FAIconKey = keyof typeof faIcons;
|
||||
|
||||
// Create union types for the icons with prefixes
|
||||
export type IconWithPrefix = `fa:${FAIconKey}` | `lucide:${string}`;
|
||||
|
||||
// Export only FontAwesome icons directly
|
||||
export const icons = {
|
||||
fa: faIcons
|
||||
};
|
||||
|
||||
// Legacy type for backward compatibility
|
||||
export type TIconKey = FAIconKey | `lucide:${string}`;
|
||||
|
||||
// Use a global static cache for all icons to reduce rendering
|
||||
const iconCache = new Map<string, string>();
|
||||
|
||||
// Clear cache items occasionally to prevent memory leaks
|
||||
const MAX_CACHE_SIZE = 500;
|
||||
function limitCacheSize() {
|
||||
if (iconCache.size > MAX_CACHE_SIZE) {
|
||||
// Remove oldest entries (first 20% of items)
|
||||
const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
|
||||
keysToDelete.forEach(key => iconCache.delete(key));
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -148,31 +178,170 @@ declare global {
|
||||
export class DeesIcon extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
/**
|
||||
* @deprecated Use the `icon` property instead with format "fa:iconName" or "lucide:iconName"
|
||||
*/
|
||||
@property({
|
||||
type: String
|
||||
type: String,
|
||||
converter: {
|
||||
// Convert attribute string to property (for reflected attributes)
|
||||
fromAttribute: (value: string): TIconKey => value as TIconKey,
|
||||
// Convert property to attribute (for reflection)
|
||||
toAttribute: (value: TIconKey): string => value
|
||||
}
|
||||
})
|
||||
public iconFA: keyof typeof faIcons;
|
||||
public iconFA?: TIconKey;
|
||||
|
||||
@property()
|
||||
/**
|
||||
* The preferred icon property. Use format "fa:iconName" or "lucide:iconName"
|
||||
* Examples: "fa:check", "lucide:menu"
|
||||
*/
|
||||
@property({
|
||||
type: String,
|
||||
converter: {
|
||||
fromAttribute: (value: string): IconWithPrefix => value as IconWithPrefix,
|
||||
toAttribute: (value: IconWithPrefix): string => value
|
||||
}
|
||||
})
|
||||
public icon?: IconWithPrefix;
|
||||
|
||||
@property({ type: Number })
|
||||
public iconSize: number;
|
||||
|
||||
@property({ type: String })
|
||||
public color: string = 'currentColor';
|
||||
|
||||
@property({ type: Number })
|
||||
public strokeWidth: number = 2;
|
||||
|
||||
// For tracking when we need to re-render
|
||||
private lastIcon: IconWithPrefix | TIconKey | null = null;
|
||||
private lastIconSize: number | null = null;
|
||||
private lastColor: string | null = null;
|
||||
private lastStrokeWidth: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective icon value, supporting both the new `icon` property
|
||||
* and the legacy `iconFA` property for backward compatibility.
|
||||
* Prefers `icon` if both are set.
|
||||
*/
|
||||
private getEffectiveIcon(): IconWithPrefix | TIconKey | null {
|
||||
// Prefer the new API
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
// Fall back to the old API
|
||||
if (this.iconFA) {
|
||||
// If iconFA is already in the proper format (lucide:name), use it directly
|
||||
if (this.iconFA.startsWith('lucide:')) {
|
||||
return this.iconFA;
|
||||
}
|
||||
|
||||
// For FontAwesome icons with no prefix, add the prefix
|
||||
return `fa:${this.iconFA}` as IconWithPrefix;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an icon string into its type and name parts
|
||||
* @param iconStr The icon string in format "type:name"
|
||||
* @returns Object with type and name properties
|
||||
*/
|
||||
private parseIconString(iconStr: string): { type: 'fa' | 'lucide', name: string } {
|
||||
if (iconStr.startsWith('fa:')) {
|
||||
return {
|
||||
type: 'fa',
|
||||
name: iconStr.substring(3) // Remove 'fa:' prefix
|
||||
};
|
||||
} else if (iconStr.startsWith('lucide:')) {
|
||||
return {
|
||||
type: 'lucide',
|
||||
name: iconStr.substring(7) // Remove 'lucide:' prefix
|
||||
};
|
||||
} else {
|
||||
// For backward compatibility, assume FontAwesome if no prefix
|
||||
return {
|
||||
type: 'fa',
|
||||
name: iconStr
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private renderLucideIcon(iconName: string): string {
|
||||
// Create a cache key based on all visual properties
|
||||
const cacheKey = `lucide:${iconName}:${this.iconSize}:${this.color}:${this.strokeWidth}`;
|
||||
|
||||
// Check if we already have this icon in the cache
|
||||
if (iconCache.has(cacheKey)) {
|
||||
return iconCache.get(cacheKey) || '';
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the Pascal case icon name (Menu instead of menu)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1);
|
||||
|
||||
// Check if the icon exists in lucideIcons
|
||||
if (!lucideIcons[pascalCaseName]) {
|
||||
console.warn(`Lucide icon '${pascalCaseName}' not found in lucideIcons object`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use the exact pattern from Lucide documentation
|
||||
const svgElement = createElement(lucideIcons[pascalCaseName], {
|
||||
color: this.color,
|
||||
size: this.iconSize,
|
||||
strokeWidth: this.strokeWidth
|
||||
});
|
||||
|
||||
if (!svgElement) {
|
||||
console.warn(`createElement returned empty result for ${pascalCaseName}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get the HTML
|
||||
const result = svgElement.outerHTML;
|
||||
|
||||
// Cache the result for future use
|
||||
iconCache.set(cacheKey, result);
|
||||
limitCacheSize();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error rendering Lucide icon ${iconName}:`, error);
|
||||
|
||||
// Create a fallback SVG with the icon name
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${this.iconSize}" height="${this.iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.color}" stroke-width="${this.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">
|
||||
<text x="50%" y="50%" font-size="6" text-anchor="middle" dominant-baseline="middle" fill="${this.color}">${iconName}</text>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
* {
|
||||
transition: inherit !important;
|
||||
|
||||
/* Improve rendering performance */
|
||||
#iconContainer svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
will-change: transform; /* Helps with animations */
|
||||
contain: strict; /* Performance optimization */
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -181,8 +350,8 @@ export class DeesIcon extends DeesElement {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
#iconContainer svg {
|
||||
display: block;
|
||||
#iconContainer {
|
||||
width: ${this.iconSize}px;
|
||||
height: ${this.iconSize}px;
|
||||
}
|
||||
</style>
|
||||
@ -190,14 +359,95 @@ export class DeesIcon extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public async updated() {
|
||||
public updated() {
|
||||
// If size is not specified, use font size as a base
|
||||
if (!this.iconSize) {
|
||||
this.iconSize = parseInt(globalThis.getComputedStyle(this).fontSize.replace(/\D/g,''));
|
||||
}
|
||||
if (this.iconFA) {
|
||||
this.shadowRoot.querySelector('#iconContainer').innerHTML = this.iconFA
|
||||
? icon(faIcons[this.iconFA]).html[0]
|
||||
: 'icon not found';
|
||||
|
||||
// Get the effective icon (either from icon or iconFA property)
|
||||
const effectiveIcon = this.getEffectiveIcon();
|
||||
|
||||
// Check if we actually need to update the icon
|
||||
// This prevents unnecessary DOM operations when properties haven't changed
|
||||
if (this.lastIcon === effectiveIcon &&
|
||||
this.lastIconSize === this.iconSize &&
|
||||
this.lastColor === this.color &&
|
||||
this.lastStrokeWidth === this.strokeWidth) {
|
||||
return; // No visual changes - skip update
|
||||
}
|
||||
|
||||
// Update our "last properties" for future change detection
|
||||
this.lastIcon = effectiveIcon;
|
||||
this.lastIconSize = this.iconSize;
|
||||
this.lastColor = this.color;
|
||||
this.lastStrokeWidth = this.strokeWidth;
|
||||
|
||||
const container = this.shadowRoot?.querySelector('#iconContainer');
|
||||
if (!container || !effectiveIcon) return;
|
||||
|
||||
try {
|
||||
// Parse the icon string to get type and name
|
||||
const { type, name } = this.parseIconString(effectiveIcon);
|
||||
|
||||
if (type === 'lucide') {
|
||||
// For Lucide, use direct DOM manipulation as shown in the docs
|
||||
// This approach avoids HTML string issues
|
||||
container.innerHTML = ''; // Clear container
|
||||
|
||||
try {
|
||||
// Convert to PascalCase
|
||||
const pascalCaseName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
if (lucideIcons[pascalCaseName]) {
|
||||
// Use the documented pattern from Lucide docs
|
||||
const svgElement = createElement(lucideIcons[pascalCaseName], {
|
||||
color: this.color,
|
||||
size: this.iconSize,
|
||||
strokeWidth: this.strokeWidth
|
||||
});
|
||||
|
||||
if (svgElement) {
|
||||
// Directly append the element
|
||||
container.appendChild(svgElement);
|
||||
return; // Exit early since we've added the element
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, something went wrong
|
||||
throw new Error(`Could not create element for ${pascalCaseName}`);
|
||||
} catch (error) {
|
||||
console.error(`Error rendering Lucide icon:`, error);
|
||||
|
||||
// Fall back to the string-based approach
|
||||
const iconHtml = this.renderLucideIcon(name);
|
||||
if (iconHtml) {
|
||||
container.innerHTML = iconHtml;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use FontAwesome rendering via HTML string
|
||||
const faIcon = icons.fa[name as FAIconKey];
|
||||
if (faIcon) {
|
||||
const iconHtml = icon(faIcon).html[0];
|
||||
container.innerHTML = iconHtml;
|
||||
} else {
|
||||
console.warn(`FontAwesome icon not found: ${name}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating icon ${effectiveIcon}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources when element is removed
|
||||
async disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// Clear our references
|
||||
this.lastIcon = null;
|
||||
this.lastIconSize = null;
|
||||
this.lastColor = null;
|
||||
this.lastStrokeWidth = null;
|
||||
}
|
||||
}
|
184
ts_web/elements/dees-input-base.ts
Normal file
184
ts_web/elements/dees-input-base.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
css,
|
||||
type CSSResult,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
/**
|
||||
* Base class for all dees-input components
|
||||
* Provides unified margin system and layout mode support
|
||||
*/
|
||||
export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
/**
|
||||
* Layout mode for the input component
|
||||
* - vertical: Traditional form layout (label on top)
|
||||
* - horizontal: Inline layout (label position configurable)
|
||||
* - auto: Detect from parent context
|
||||
*/
|
||||
@property({ type: String })
|
||||
public layoutMode: 'vertical' | 'horizontal' | 'auto' = 'auto';
|
||||
|
||||
/**
|
||||
* Position of the label relative to the input
|
||||
*/
|
||||
@property({ type: String })
|
||||
public labelPosition: 'top' | 'left' | 'right' | 'none' = 'top';
|
||||
|
||||
/**
|
||||
* Common properties for all inputs
|
||||
*/
|
||||
@property({ type: String })
|
||||
public key: string;
|
||||
|
||||
@property({ type: String })
|
||||
public label: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public required: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
public description: string;
|
||||
|
||||
/**
|
||||
* Common styles for all input components
|
||||
*/
|
||||
public static get baseStyles(): CSSResult[] {
|
||||
return [
|
||||
css`
|
||||
/* CSS Variables for consistent spacing */
|
||||
:host {
|
||||
--dees-input-spacing-unit: 8px;
|
||||
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
|
||||
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
|
||||
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
|
||||
}
|
||||
|
||||
/* Default vertical stacking mode (for forms) */
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-bottom: var(--dees-input-vertical-gap);
|
||||
}
|
||||
|
||||
/* Last child in container should have no bottom margin */
|
||||
:host(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal layout mode - activated by attribute */
|
||||
:host([layout-mode="horizontal"]) {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
margin-right: var(--dees-input-horizontal-gap);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:host([layout-mode="horizontal"]:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Auto mode - inherit from parent dees-form if present */
|
||||
|
||||
/* Label position variations */
|
||||
:host([label-position="left"]) .input-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--dees-input-label-gap);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host([label-position="right"]) .input-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--dees-input-label-gap);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host([label-position="top"]) .input-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host([label-position="none"]) dees-label {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subject for value changes that all inputs should implement
|
||||
*/
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<T>();
|
||||
|
||||
/**
|
||||
* Called when the element is connected to the DOM
|
||||
* Sets up layout mode detection
|
||||
*/
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
this.detectLayoutMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the appropriate layout mode based on parent context
|
||||
*/
|
||||
private detectLayoutMode() {
|
||||
if (this.layoutMode !== 'auto') {
|
||||
this.setAttribute('layout-mode', this.layoutMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if parent is a form with horizontal layout
|
||||
const parentForm = this.closest('dees-form');
|
||||
if (parentForm && parentForm.hasAttribute('horizontal-layout')) {
|
||||
this.setAttribute('layout-mode', 'horizontal');
|
||||
} else {
|
||||
this.setAttribute('layout-mode', 'vertical');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the layout mode attribute when property changes
|
||||
*/
|
||||
updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('layoutMode')) {
|
||||
this.detectLayoutMode();
|
||||
}
|
||||
|
||||
if (changedProperties.has('labelPosition')) {
|
||||
this.setAttribute('label-position', this.labelPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard method for freezing input (disabling)
|
||||
*/
|
||||
public async freeze() {
|
||||
this.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard method for unfreezing input (enabling)
|
||||
*/
|
||||
public async unfreeze() {
|
||||
this.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method that child classes must implement to get their value
|
||||
*/
|
||||
public abstract getValue(): any;
|
||||
|
||||
/**
|
||||
* Abstract method that child classes must implement to set their value
|
||||
*/
|
||||
public abstract setValue(value: any): void;
|
||||
}
|
267
ts_web/elements/dees-input-checkbox.demo.ts
Normal file
267
ts_web/elements/dees-input-checkbox.demo.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import type { DeesInputCheckbox } from './dees-input-checkbox.js';
|
||||
import './dees-button.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
// Get all checkboxes for demo interactions
|
||||
const checkboxes = elementArg.querySelectorAll('dees-input-checkbox');
|
||||
|
||||
// Example of programmatic interaction
|
||||
const selectAllBtn = elementArg.querySelector('#select-all-btn');
|
||||
const clearAllBtn = elementArg.querySelector('#clear-all-btn');
|
||||
|
||||
if (selectAllBtn && clearAllBtn) {
|
||||
selectAllBtn.addEventListener('click', () => {
|
||||
checkboxes.forEach((checkbox: DeesInputCheckbox) => {
|
||||
if (!checkbox.disabled && checkbox.key?.startsWith('feature')) {
|
||||
checkbox.value = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
clearAllBtn.addEventListener('click', () => {
|
||||
checkboxes.forEach((checkbox: DeesInputCheckbox) => {
|
||||
if (!checkbox.disabled && checkbox.key?.startsWith('feature')) {
|
||||
checkbox.value = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #0069f2;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section p {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.feature-list {
|
||||
background: #0a0a0a;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Checkboxes</h3>
|
||||
<p>Standard checkbox inputs for boolean selections</p>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'I agree to the Terms and Conditions'}
|
||||
.value=${true}
|
||||
.key=${'terms'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Subscribe to newsletter'}
|
||||
.value=${false}
|
||||
.key=${'newsletter'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Enable notifications'}
|
||||
.required=${true}
|
||||
.key=${'notifications'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Horizontal Layout</h3>
|
||||
<p>Checkboxes arranged horizontally for compact forms</p>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Option A'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionA'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option B'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${true}
|
||||
.key=${'optionB'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option C'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionC'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option D'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${true}
|
||||
.key=${'optionD'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Feature Selection Example</h3>
|
||||
<p>Common use case for feature toggles with batch operations</p>
|
||||
|
||||
<div class="button-group">
|
||||
<dees-button id="select-all-btn" type="secondary">Select All</dees-button>
|
||||
<dees-button id="clear-all-btn" type="secondary">Clear All</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="feature-list">
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Dark Mode Support'}
|
||||
.value=${true}
|
||||
.key=${'feature1'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Email Notifications'}
|
||||
.value=${true}
|
||||
.key=${'feature2'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Two-Factor Authentication'}
|
||||
.value=${false}
|
||||
.key=${'feature3'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'API Access'}
|
||||
.value=${true}
|
||||
.key=${'feature4'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Advanced Analytics'}
|
||||
.value=${false}
|
||||
.key=${'feature5'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>States</h3>
|
||||
<p>Different checkbox states and configurations</p>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled Unchecked'}
|
||||
.disabled=${true}
|
||||
.key=${'disabled1'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled Checked'}
|
||||
.disabled=${true}
|
||||
.value=${true}
|
||||
.key=${'disabled2'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Required Checkbox'}
|
||||
.required=${true}
|
||||
.key=${'required'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Real-world Examples</h3>
|
||||
<p>Common checkbox patterns in applications</p>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Remember me on this device'}
|
||||
.value=${true}
|
||||
.key=${'rememberMe'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Make my profile public'}
|
||||
.value=${false}
|
||||
.key=${'publicProfile'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Allow others to find me by email'}
|
||||
.value=${false}
|
||||
.key=${'findByEmail'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Send me product updates and announcements'}
|
||||
.value=${true}
|
||||
.key=${'productUpdates'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,14 +1,13 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-checkbox.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -17,51 +16,33 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-checkbox')
|
||||
export class DeesInputCheckbox extends DeesElement {
|
||||
export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
||||
// STATIC
|
||||
public static demo = () => html`<dees-input-checkbox></dees-input-checkbox>`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public key: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public label: string = 'Label';
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public value: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean
|
||||
})
|
||||
public disabled: boolean = false;
|
||||
constructor() {
|
||||
super();
|
||||
this.labelPosition = 'right'; // Checkboxes default to label on the right
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 20px 0px;
|
||||
cursor: default;
|
||||
}
|
||||
:host(:hover) {
|
||||
@ -69,21 +50,12 @@ export class DeesInputCheckbox extends DeesElement {
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
display: grid;
|
||||
grid-template-columns: 25px auto;
|
||||
padding: 5px 0px;
|
||||
color: ${this.goBright ? '#333' : '#ccc'};
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.maincontainer:hover {
|
||||
${this.goBright ? '#000' : '#ccc'};
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 15px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
|
||||
input:focus {
|
||||
@ -94,12 +66,12 @@ export class DeesInputCheckbox extends DeesElement {
|
||||
.checkbox {
|
||||
transition: all 0.1s;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid ${this.goBright ? '#CCC' : '#999'};
|
||||
border: 1px solid ${cssManager.bdTheme('#CCC', '#999')};
|
||||
border-radius: 2px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: inline-block;
|
||||
background: ${this.goBright ? '#fafafa' : '#222'};
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222')};
|
||||
}
|
||||
|
||||
.checkbox.selected {
|
||||
@ -146,7 +118,12 @@ export class DeesInputCheckbox extends DeesElement {
|
||||
img {
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<div class="maincontainer" @click="${this.toggleSelected}">
|
||||
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
|
||||
${this.value
|
||||
@ -158,7 +135,8 @@ export class DeesInputCheckbox extends DeesElement {
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
<div class="label">${this.label}</div>
|
||||
</div>
|
||||
<dees-label .label=${this.label}></dees-label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -177,6 +155,14 @@ export class DeesInputCheckbox extends DeesElement {
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public getValue(): boolean {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: boolean): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
const checkboxDiv = this.shadowRoot.querySelector('.checkbox');
|
||||
if (checkboxDiv) {
|
||||
|
@ -1,27 +1,200 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #0069f2;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section p {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Dropdowns</h3>
|
||||
<p>Standard dropdown with search functionality and various options</p>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Country'}
|
||||
.options=${[
|
||||
{option: 'option 1', key: 'option1'},
|
||||
{option: 'option 2', key: 'option2'},
|
||||
{option: 'option 3', key: 'option3'}
|
||||
{ option: 'United States', key: 'us' },
|
||||
{ option: 'Canada', key: 'ca' },
|
||||
{ option: 'Germany', key: 'de' },
|
||||
{ option: 'France', key: 'fr' },
|
||||
{ option: 'United Kingdom', key: 'uk' },
|
||||
{ option: 'Australia', key: 'au' },
|
||||
{ option: 'Japan', key: 'jp' },
|
||||
{ option: 'Brazil', key: 'br' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'United States', key: 'us' }}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Role'}
|
||||
.options=${[
|
||||
{ option: 'Administrator', key: 'admin' },
|
||||
{ option: 'Editor', key: 'editor' },
|
||||
{ option: 'Viewer', key: 'viewer' },
|
||||
{ option: 'Guest', key: 'guest' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Without Search</h3>
|
||||
<p>Dropdown with search functionality disabled for simpler selection</p>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Priority Level'}
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{option: 'option 1', key: 'option1'},
|
||||
{option: 'option 2', key: 'option2'},
|
||||
{option: 'option 3', key: 'option3'}
|
||||
{ option: 'High', key: 'high' },
|
||||
{ option: 'Medium', key: 'medium' },
|
||||
{ option: 'Low', key: 'low' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Medium', key: 'medium' }}
|
||||
></dees-input-dropdown>
|
||||
<div style="height: 300px"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Horizontal Layout</h3>
|
||||
<p>Multiple dropdowns in a horizontal layout for compact forms</p>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-dropdown
|
||||
.label=${'Department'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${[
|
||||
{option: 'option 1', key: 'option1'},
|
||||
{option: 'option 2', key: 'option2'},
|
||||
{option: 'option 3', key: 'option3'}
|
||||
{ option: 'Engineering', key: 'eng' },
|
||||
{ option: 'Design', key: 'design' },
|
||||
{ option: 'Marketing', key: 'marketing' },
|
||||
{ option: 'Sales', key: 'sales' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Team Size'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: '1-5', key: 'small' },
|
||||
{ option: '6-20', key: 'medium' },
|
||||
{ option: '21-50', key: 'large' },
|
||||
{ option: '50+', key: 'xlarge' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Location'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${[
|
||||
{ option: 'Remote', key: 'remote' },
|
||||
{ option: 'On-site', key: 'onsite' },
|
||||
{ option: 'Hybrid', key: 'hybrid' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>States</h3>
|
||||
<p>Different states and configurations</p>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.options=${[
|
||||
{ option: 'Option A', key: 'a' },
|
||||
{ option: 'Option B', key: 'b' },
|
||||
{ option: 'Option C', key: 'c' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Disabled Dropdown'}
|
||||
.disabled=${true}
|
||||
.options=${[
|
||||
{ option: 'Cannot Select', key: 'disabled' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="spacer">
|
||||
(Spacer to test dropdown positioning)
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Bottom Positioning</h3>
|
||||
<p>Dropdown that opens upward when near bottom of viewport</p>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Opens Upward'}
|
||||
.options=${[
|
||||
{ option: 'First Option', key: 'first' },
|
||||
{ option: 'Second Option', key: 'second' },
|
||||
{ option: 'Third Option', key: 'third' },
|
||||
{ option: 'Fourth Option', key: 'fourth' },
|
||||
{ option: 'Fifth Option', key: 'fifth' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`
|
@ -1,17 +1,16 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-input-dropdown.demo.js';
|
||||
import { DeesWindowLayer } from './dees-windowlayer.js';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -20,20 +19,10 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-dropdown')
|
||||
export class DeesInputDropdown extends DeesElement {
|
||||
export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public label: string = 'Label';
|
||||
|
||||
@property()
|
||||
public key: string;
|
||||
|
||||
@property()
|
||||
public options: { option: string; key: string; payload?: any }[] = [];
|
||||
@ -41,20 +30,21 @@ export class DeesInputDropdown extends DeesElement {
|
||||
@property()
|
||||
public selectedOption: { option: string; key: string; payload?: any } = null;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required: boolean = false;
|
||||
// Add value property for form compatibility
|
||||
public get value() {
|
||||
return this.selectedOption;
|
||||
}
|
||||
|
||||
public set value(val: { option: string; key: string; payload?: any }) {
|
||||
this.selectedOption = val;
|
||||
}
|
||||
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public enableSearch: boolean = true;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public disabled: boolean = false;
|
||||
|
||||
@state()
|
||||
public opensToTop: boolean = false;
|
||||
@ -69,6 +59,7 @@ export class DeesInputDropdown extends DeesElement {
|
||||
public isOpened = false;
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
* {
|
||||
@ -78,19 +69,13 @@ export class DeesInputDropdown extends DeesElement {
|
||||
:host {
|
||||
font-family: Roboto;
|
||||
position: relative;
|
||||
display: block;
|
||||
color: ${cssManager.bdTheme('#222', '#fff')};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.selectedBox {
|
||||
user-select: none;
|
||||
@ -205,8 +190,9 @@ export class DeesInputDropdown extends DeesElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label}></dees-label>
|
||||
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}">
|
||||
${this.label ? html`<div class="label">${this.label}</div>` : html``}
|
||||
<div class="selectionBox">
|
||||
${this.enableSearch && !this.opensToTop
|
||||
? html`
|
||||
@ -249,6 +235,7 @@ export class DeesInputDropdown extends DeesElement {
|
||||
${this.selectedOption?.option || 'Select...'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -372,4 +359,12 @@ export class DeesInputDropdown extends DeesElement {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public getValue(): { option: string; key: string; payload?: any } {
|
||||
return this.selectedOption;
|
||||
}
|
||||
|
||||
public setValue(value: { option: string; key: string; payload?: any }): void {
|
||||
this.selectedOption = value;
|
||||
}
|
||||
}
|
||||
|
138
ts_web/elements/dees-input-fileupload.demo.ts
Normal file
138
ts_web/elements/dees-input-fileupload.demo.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.upload-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.upload-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')};
|
||||
}
|
||||
|
||||
.upload-box h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-top: 32px;
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#fff3cd', '#332701')};
|
||||
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#664400')};
|
||||
border-radius: 4px;
|
||||
color: ${cssManager.bdTheme('#856404', '#ffecb5')};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
|
||||
<dees-input-fileupload
|
||||
.label=${'Attachments'}
|
||||
.description=${'Upload files by clicking or dragging'}
|
||||
></dees-input-fileupload>
|
||||
|
||||
<dees-input-fileupload
|
||||
.label=${'Resume'}
|
||||
.description=${'Upload your CV in PDF format'}
|
||||
.buttonText=${'Choose Resume...'}
|
||||
></dees-input-fileupload>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Multiple Upload Areas'} .subtitle=${'Different upload zones for various file types'}>
|
||||
<div class="upload-grid">
|
||||
<div class="upload-box">
|
||||
<h4>Profile Picture</h4>
|
||||
<dees-input-fileupload
|
||||
.label=${'Avatar'}
|
||||
.description=${'JPG, PNG or GIF'}
|
||||
.buttonText=${'Select Image...'}
|
||||
></dees-input-fileupload>
|
||||
</div>
|
||||
|
||||
<div class="upload-box">
|
||||
<h4>Cover Image</h4>
|
||||
<dees-input-fileupload
|
||||
.label=${'Banner'}
|
||||
.description=${'Recommended: 1200x400px'}
|
||||
.buttonText=${'Select Banner...'}
|
||||
></dees-input-fileupload>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different upload states for validation'}>
|
||||
<dees-input-fileupload
|
||||
.label=${'Identity Document'}
|
||||
.description=${'Required for verification'}
|
||||
.required=${true}
|
||||
.buttonText=${'Upload Document...'}
|
||||
></dees-input-fileupload>
|
||||
|
||||
<dees-input-fileupload
|
||||
.label=${'System Files'}
|
||||
.description=${'File upload is disabled'}
|
||||
.disabled=${true}
|
||||
.value=${[]}
|
||||
></dees-input-fileupload>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Application Form'} .subtitle=${'Complete form with file upload integration'}>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .label=${'Email'} .inputType=${'email'} .required=${true}></dees-input-text>
|
||||
<dees-input-fileupload
|
||||
.label=${'Resume'}
|
||||
.description=${'Upload your CV (PDF preferred)'}
|
||||
.required=${true}
|
||||
></dees-input-fileupload>
|
||||
<dees-input-fileupload
|
||||
.label=${'Portfolio'}
|
||||
.description=${'Optional: Upload work samples'}
|
||||
></dees-input-fileupload>
|
||||
<dees-input-text
|
||||
.label=${'Cover Letter'}
|
||||
.inputType=${'textarea'}
|
||||
.description=${'Tell us why you would be a great fit'}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
|
||||
<div class="info-section">
|
||||
<h4>Features:</h4>
|
||||
<ul>
|
||||
<li>Click to select files or drag & drop</li>
|
||||
<li>Multiple file selection support</li>
|
||||
<li>Visual feedback for drag operations</li>
|
||||
<li>Right-click files to remove them</li>
|
||||
<li>Integrates seamlessly with forms</li>
|
||||
</ul>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -2,6 +2,8 @@ import * as colors from './00colors.js';
|
||||
import * as plugins from './00plugins.js';
|
||||
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-fileupload.demo.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
@ -23,23 +25,9 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-fileupload')
|
||||
export class DeesInputFileupload extends DeesElement {
|
||||
public static demo = () =>
|
||||
html`<dees-input-fileupload .label=${'Attachments'}></dees-input-fileupload>`;
|
||||
export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public label: string = null;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public key: string;
|
||||
|
||||
@property({
|
||||
attribute: false,
|
||||
@ -49,16 +37,6 @@ export class DeesInputFileupload extends DeesElement {
|
||||
@property()
|
||||
public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle';
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public disabled: boolean = false;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
@ -69,13 +47,12 @@ export class DeesInputFileupload extends DeesElement {
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: grid;
|
||||
margin: 10px 0px;
|
||||
margin-bottom: 24px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
@ -112,11 +89,6 @@ export class DeesInputFileupload extends DeesElement {
|
||||
background: #00000080;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
@ -173,10 +145,11 @@ export class DeesInputFileupload extends DeesElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<div class="hidden">
|
||||
<input type="file"></div>
|
||||
<input type="file">
|
||||
</div>
|
||||
${this.label ? html`<div class="label">${this.label}</div>` : null}
|
||||
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
|
||||
${this.value.map(
|
||||
(fileArg) => html`
|
||||
@ -205,6 +178,7 @@ export class DeesInputFileupload extends DeesElement {
|
||||
${this.buttonText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -221,7 +195,8 @@ export class DeesInputFileupload extends DeesElement {
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
|
||||
inputFile.addEventListener('change', (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
@ -263,4 +238,12 @@ export class DeesInputFileupload extends DeesElement {
|
||||
dropArea.addEventListener('dragover', handlerFunction, false);
|
||||
dropArea.addEventListener('drop', handlerFunction, false);
|
||||
}
|
||||
|
||||
public getValue(): File[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: File[]): void {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,80 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`<dees-input-iban .label=${'IBAN'}></dees-input-iban>`;
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.payment-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
|
||||
<dees-input-iban
|
||||
.label=${'Bank Account IBAN'}
|
||||
.description=${'Enter your International Bank Account Number'}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Verified IBAN'}
|
||||
.description=${'This IBAN has been verified'}
|
||||
.value=${'DE89370400440532013000'}
|
||||
></dees-input-iban>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Payment Information'} .subtitle=${'IBAN input with horizontal layout for payment forms'}>
|
||||
<div class="payment-group">
|
||||
<dees-input-text
|
||||
.label=${'Account Holder'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'John Doe'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'IBAN'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'GB82WEST12345698765432'}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}>
|
||||
<dees-input-iban
|
||||
.label=${'Payment Account'}
|
||||
.description=${'Required for processing payments'}
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Locked IBAN'}
|
||||
.description=${'This IBAN cannot be changed'}
|
||||
.value=${'FR1420041010050500013M02606'}
|
||||
.disabled=${true}
|
||||
></dees-input-iban>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Bank Transfer Form'} .subtitle=${'Complete form example with IBAN validation'}>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Recipient Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-iban .label=${'Recipient IBAN'} .required=${true}></dees-input-iban>
|
||||
<dees-input-text .label=${'Transfer Reference'} .description=${'Optional reference for the transfer'}></dees-input-text>
|
||||
<dees-input-text .label=${'Amount'} .inputType=${'number'} .required=${true}></dees-input-text>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,18 +1,19 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
state,
|
||||
html,
|
||||
domtools,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import * as ibantools from 'ibantools';
|
||||
import { demoFunc } from './dees-input-iban.demo.js';
|
||||
|
||||
@customElement('dees-input-iban')
|
||||
export class DeesInputIban extends DeesElement {
|
||||
export class DeesInputIban extends DeesInputBase<DeesInputIban> {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
|
||||
@ -23,60 +24,44 @@ export class DeesInputIban extends DeesElement {
|
||||
@state()
|
||||
enteredIbanIsValid: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public disabled = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required = false;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public label = '';
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public key = '';
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public value = '';
|
||||
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesInputIban>();
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* IBAN input specific styles can go here */
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
input[type='text'] {
|
||||
line-height: 20px;
|
||||
padding: 5px;
|
||||
width: 250px;
|
||||
}
|
||||
</style>
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label || 'IBAN'} .description=${this.description}></dees-label>
|
||||
<dees-input-text
|
||||
.label=${'IBAN'}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.placeholder=${'DE89 3704 0044 0532 0130 00'}
|
||||
@input=${(eventArg: InputEvent) => {
|
||||
this.validateIban(eventArg);
|
||||
}}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
const deesInputText = this.shadowRoot.querySelector('dees-input-text');
|
||||
deesInputText.disabled = this.disabled;
|
||||
deesInputText.required = this.required;
|
||||
deesInputText.changeSubject.subscribe(valueArg => {
|
||||
this.value = valueArg.value;
|
||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
const deesInputText = this.shadowRoot.querySelector('dees-input-text') as any;
|
||||
if (deesInputText && deesInputText.changeSubject) {
|
||||
deesInputText.changeSubject.subscribe(() => {
|
||||
this.changeSubject.next(this);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async validateIban(eventArg: InputEvent): Promise<void> {
|
||||
@ -95,4 +80,13 @@ export class DeesInputIban extends DeesElement {
|
||||
const deesInputText = this.shadowRoot.querySelector('dees-input-text');
|
||||
deesInputText.validationText = `IBAN is valid: ${this.enteredIbanIsValid}`;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: string): void {
|
||||
this.value = value;
|
||||
this.enteredString = ibantools.friendlyFormatIBAN(value) || '';
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,128 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Multi-Option Toggle'} .subtitle=${'Select from multiple options with a sliding indicator'}>
|
||||
<dees-input-multitoggle
|
||||
.options=${['option 1', 'option 2', 'a longer option with multiple words']}
|
||||
.selectedOption=${'option 2'}
|
||||
.label=${'Display Mode'}
|
||||
.description=${'Choose how content is displayed'}
|
||||
.options=${['List View', 'Grid View', 'Compact']}
|
||||
.selectedOption=${'Grid View'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'T-Shirt Size'}
|
||||
.description=${'Select your preferred size'}
|
||||
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
|
||||
.selectedOption=${'M'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Boolean Toggle'} .subtitle=${'Simple on/off switches with custom labels'}>
|
||||
<dees-input-multitoggle
|
||||
.label=${'Notifications'}
|
||||
.description=${'Enable or disable push notifications'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'enabled'}
|
||||
.booleanFalseName=${'disabled'}
|
||||
.selectedOption=${'true'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Theme Mode'}
|
||||
.description=${'Switch between light and dark theme'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Dark'}
|
||||
.booleanFalseName=${'Light'}
|
||||
.selectedOption=${'Dark'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Settings Panel'} .subtitle=${'Configuration options in a horizontal layout'}>
|
||||
<div class="settings-grid">
|
||||
<dees-input-multitoggle
|
||||
.label=${'Auto-Save'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Enabled'}
|
||||
.booleanFalseName=${'Disabled'}
|
||||
.selectedOption=${'Enabled'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Language'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${['English', 'German', 'French', 'Spanish']}
|
||||
.selectedOption=${'English'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Quality'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${['Low', 'Medium', 'High', 'Ultra']}
|
||||
.selectedOption=${'High'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Privacy'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Private'}
|
||||
.booleanFalseName=${'Public'}
|
||||
.selectedOption=${'Private'}
|
||||
></dees-input-multitoggle>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'States & Form Integration'} .subtitle=${'Disabled states and form usage'}>
|
||||
<dees-input-multitoggle
|
||||
.label=${'Account Type'}
|
||||
.description=${'This setting is locked'}
|
||||
.options=${['Free', 'Pro', 'Enterprise']}
|
||||
.selectedOption=${'Enterprise'}
|
||||
.disabled=${true}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Project Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-multitoggle
|
||||
.label=${'Visibility'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Public'}
|
||||
.booleanFalseName=${'Private'}
|
||||
.selectedOption=${'Private'}
|
||||
></dees-input-multitoggle>
|
||||
<dees-input-multitoggle
|
||||
.label=${'License'}
|
||||
.options=${['MIT', 'Apache 2.0', 'GPL v3', 'Proprietary']}
|
||||
.selectedOption=${'MIT'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,30 +1,27 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
state,
|
||||
html,
|
||||
domtools,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
|
||||
import * as colors from './00colors.js'
|
||||
|
||||
const { demoFunc } = await import('./dees-input-multitoggle.demo.js');
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-input-multitoggle': DeesInputMultitoggle;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-input-multitoggle')
|
||||
export class DeesInputMultitoggle extends DeesElement {
|
||||
export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public label: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@property()
|
||||
type: 'boolean' | 'multi' | 'single' = 'multi';
|
||||
@ -43,23 +40,38 @@ export class DeesInputMultitoggle extends DeesElement {
|
||||
@property()
|
||||
selectedOption: string = '';
|
||||
|
||||
@property()
|
||||
@property({ type: Boolean })
|
||||
boolValue: boolean = false;
|
||||
|
||||
// Add value property for form compatibility
|
||||
public get value(): string | boolean {
|
||||
if (this.type === 'boolean') {
|
||||
return this.selectedOption === this.booleanTrueName;
|
||||
}
|
||||
return this.selectedOption;
|
||||
}
|
||||
|
||||
public set value(val: string | boolean) {
|
||||
if (this.type === 'boolean' && typeof val === 'boolean') {
|
||||
this.selectedOption = val ? this.booleanTrueName : this.booleanFalseName;
|
||||
} else {
|
||||
this.selectedOption = val as string;
|
||||
}
|
||||
// Defer indicator update to next frame if component not yet updated
|
||||
if (this.hasUpdated) {
|
||||
this.setIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
user-select: none;
|
||||
margin: 8px 0px 24px 0px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.selections {
|
||||
position: relative;
|
||||
@ -70,11 +82,11 @@ export class DeesInputMultitoggle extends DeesElement {
|
||||
width: min-content;
|
||||
border-radius: 20px;
|
||||
height: 32px;
|
||||
border-top: 1px solid #ffffff10;
|
||||
border-top: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
|
||||
}
|
||||
|
||||
.option {
|
||||
color: #ccc;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
position: relative;
|
||||
padding: 0px 16px;
|
||||
line-height: 32px;
|
||||
@ -87,11 +99,11 @@ export class DeesInputMultitoggle extends DeesElement {
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
color: #fff;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
color: #fff;
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
}
|
||||
|
||||
.indicator {
|
||||
@ -101,14 +113,20 @@ export class DeesInputMultitoggle extends DeesElement {
|
||||
left: 4px;
|
||||
top: 3px;
|
||||
border-radius: 16px;
|
||||
background: #0050b9;
|
||||
min-width: 36px;
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
min-width: 24px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.indicator.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<div class="mainbox">
|
||||
<div class="selections">
|
||||
@ -121,14 +139,29 @@ export class DeesInputMultitoggle extends DeesElement {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
public async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Initialize boolean options early
|
||||
if (this.type === 'boolean' && this.options.length === 0) {
|
||||
this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false'];
|
||||
}
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
// Update boolean options if they changed
|
||||
if (this.type === 'boolean') {
|
||||
this.options = [this.booleanTrueName || 'true', this.booleanFalseName || 'false'];
|
||||
}
|
||||
// Wait for the next frame to ensure DOM is fully rendered
|
||||
await this.updateComplete;
|
||||
requestAnimationFrame(() => {
|
||||
this.setIndicator();
|
||||
});
|
||||
}
|
||||
|
||||
public async handleSelection(optionArg: string) {
|
||||
@ -136,18 +169,57 @@ export class DeesInputMultitoggle extends DeesElement {
|
||||
this.setIndicator();
|
||||
}
|
||||
|
||||
private indicatorInitialized = false;
|
||||
|
||||
public async setIndicator() {
|
||||
const indicator: HTMLDivElement = this.shadowRoot.querySelector('.indicator');
|
||||
const selectedIndex = this.options.indexOf(this.selectedOption);
|
||||
|
||||
// If no valid selection, hide indicator
|
||||
if (selectedIndex === -1 || !indicator) {
|
||||
if (indicator) {
|
||||
indicator.style.opacity = '0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const option: HTMLDivElement = this.shadowRoot.querySelector(
|
||||
`.option:nth-child(${this.options.indexOf(this.selectedOption) + 2})`
|
||||
`.option:nth-child(${selectedIndex + 2})`
|
||||
);
|
||||
|
||||
if (indicator && option) {
|
||||
// Only disable transition for the very first positioning
|
||||
if (!this.indicatorInitialized) {
|
||||
indicator.classList.add('no-transition');
|
||||
this.indicatorInitialized = true;
|
||||
|
||||
// Remove the no-transition class after a brief delay
|
||||
setTimeout(() => {
|
||||
indicator.classList.remove('no-transition');
|
||||
}, 50);
|
||||
}
|
||||
|
||||
indicator.style.width = `${option.clientWidth - 8}px`;
|
||||
indicator.style.left = `${option.offsetLeft + 4}px`;
|
||||
indicator.style.opacity = '1';
|
||||
}
|
||||
setTimeout(() => {
|
||||
indicator.style.transition = 'all 0.1s';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public getValue(): string | boolean {
|
||||
if (this.type === 'boolean') {
|
||||
return this.selectedOption === this.booleanTrueName;
|
||||
}
|
||||
return this.selectedOption;
|
||||
}
|
||||
|
||||
public setValue(value: string | boolean): void {
|
||||
if (this.type === 'boolean' && typeof value === 'boolean') {
|
||||
this.selectedOption = value ? (this.booleanTrueName || 'true') : (this.booleanFalseName || 'false');
|
||||
} else {
|
||||
this.selectedOption = value as string;
|
||||
}
|
||||
if (this.hasUpdated) {
|
||||
this.setIndicator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,80 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`<dees-input-phone .label=${'Phone Number'}></dees-input-phone>`;
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}>
|
||||
<dees-input-phone
|
||||
.label=${'Phone Number'}
|
||||
.description=${'Enter your phone number with country code'}
|
||||
.value=${'5551234567'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Contact Phone'}
|
||||
.description=${'Required for account verification'}
|
||||
.required=${true}
|
||||
.placeholder=${'+1 (555) 000-0000'}
|
||||
></dees-input-phone>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-phone
|
||||
.label=${'Mobile'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'4155551234'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Office'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.placeholder=${'+1 (800) 555-0000'}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}>
|
||||
<dees-input-phone
|
||||
.label=${'International Contact'}
|
||||
.description=${'Automatically formats international numbers'}
|
||||
.value=${'441234567890'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Emergency Contact'}
|
||||
.value=${'911'}
|
||||
.disabled=${true}
|
||||
></dees-input-phone>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Form Integration'} .subtitle=${'Phone input as part of a contact form'}>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-phone .label=${'Phone Number'} .required=${true}></dees-input-phone>
|
||||
<dees-input-text .label=${'Email'} .inputType=${'email'}></dees-input-text>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,14 +1,14 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
css,
|
||||
unsafeCSS,
|
||||
cssManager,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-phone.demo.js';
|
||||
|
||||
declare global {
|
||||
@ -18,12 +18,116 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-phone')
|
||||
export class DeesInputPhone extends DeesElement {
|
||||
export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public render() {
|
||||
return html`<div>Phone Input</div>`;
|
||||
@state()
|
||||
protected formattedPhone: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public value: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder: string = '+1 (555) 123-4567';
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* Phone input specific styles can go here */
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<dees-input-text
|
||||
.value=${this.formattedPhone}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.placeholder=${this.placeholder}
|
||||
@input=${(event: InputEvent) => this.handlePhoneInput(event)}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
// Initialize formatted phone from value
|
||||
if (this.value) {
|
||||
this.formattedPhone = this.formatPhoneNumber(this.value);
|
||||
}
|
||||
|
||||
// Subscribe to the inner input's changes
|
||||
const innerInput = this.shadowRoot.querySelector('dees-input-text') as any;
|
||||
if (innerInput && innerInput.changeSubject) {
|
||||
innerInput.changeSubject.subscribe(() => {
|
||||
this.changeSubject.next(this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handlePhoneInput(event: InputEvent) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const cleanedValue = this.cleanPhoneNumber(input.value);
|
||||
const formatted = this.formatPhoneNumber(cleanedValue);
|
||||
|
||||
// Update the input with formatted value
|
||||
if (input.value !== formatted) {
|
||||
const cursorPosition = input.selectionStart || 0;
|
||||
input.value = formatted;
|
||||
|
||||
// Try to maintain cursor position intelligently
|
||||
const newCursorPos = this.calculateCursorPosition(cleanedValue, formatted, cursorPosition);
|
||||
input.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
|
||||
this.formattedPhone = formatted;
|
||||
this.value = cleanedValue;
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
private cleanPhoneNumber(value: string): string {
|
||||
// Remove all non-numeric characters
|
||||
return value.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
private formatPhoneNumber(value: string): string {
|
||||
// Basic US phone number formatting
|
||||
// This can be enhanced to support international formats
|
||||
const cleaned = this.cleanPhoneNumber(value);
|
||||
|
||||
if (cleaned.length === 0) return '';
|
||||
if (cleaned.length <= 3) return cleaned;
|
||||
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
|
||||
if (cleaned.length <= 10) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
|
||||
|
||||
// For numbers longer than 10 digits, format as international
|
||||
return `+${cleaned.slice(0, cleaned.length - 10)} (${cleaned.slice(-10, -7)}) ${cleaned.slice(-7, -4)}-${cleaned.slice(-4)}`;
|
||||
}
|
||||
|
||||
private calculateCursorPosition(cleaned: string, formatted: string, oldPos: number): number {
|
||||
// Simple cursor position calculation
|
||||
// Count how many formatting characters are before the cursor
|
||||
let formattingChars = 0;
|
||||
for (let i = 0; i < oldPos && i < formatted.length; i++) {
|
||||
if (!/\d/.test(formatted[i])) {
|
||||
formattingChars++;
|
||||
}
|
||||
}
|
||||
return Math.min(oldPos + formattingChars, formatted.length);
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: string): void {
|
||||
this.value = value;
|
||||
this.formattedPhone = this.formatPhoneNumber(value);
|
||||
}
|
||||
}
|
127
ts_web/elements/dees-input-quantityselector.demo.ts
Normal file
127
ts_web/elements/dees-input-quantityselector.demo.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.shopping-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
color: #1976d2;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.description=${'Select the desired quantity'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Items in Cart'}
|
||||
.description=${'Adjust the quantity of items'}
|
||||
.value=${3}
|
||||
></dees-input-quantityselector>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Product cards with quantity selectors'}>
|
||||
<div class="shopping-grid">
|
||||
<div class="product-card">
|
||||
<div class="product-name">Premium Headphones</div>
|
||||
<div class="product-price">$199.99</div>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
|
||||
<div class="product-card">
|
||||
<div class="product-name">Wireless Mouse</div>
|
||||
<div class="product-price">$49.99</div>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${2}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
|
||||
<div class="product-card">
|
||||
<div class="product-name">USB-C Cable</div>
|
||||
<div class="product-price">$19.99</div>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Number of Licenses'}
|
||||
.description=${'Select how many licenses you need'}
|
||||
.required=${true}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Fixed Quantity'}
|
||||
.description=${'This quantity cannot be changed'}
|
||||
.disabled=${true}
|
||||
.value=${5}
|
||||
></dees-input-quantityselector>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Order Form'} .subtitle=${'Complete order form with quantity selection'}>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Customer Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-dropdown
|
||||
.label=${'Product'}
|
||||
.options=${['Basic Plan', 'Pro Plan', 'Enterprise Plan']}
|
||||
.required=${true}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.description=${'Number of licenses'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
<dees-input-text
|
||||
.label=${'Special Instructions'}
|
||||
.inputType=${'textarea'}
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,5 +1,7 @@
|
||||
import { customElement, property, html, type TemplateResult, DeesElement, type CSSResult, } from '@design.estate/dees-element';
|
||||
import { customElement, property, html, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-quantityselector.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -8,67 +10,50 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-quantityselector')
|
||||
export class DeesInputQuantitySelector extends DeesElement {
|
||||
public static demo = () => html`<dees-input-quantityselector></dees-input-quantityselector>`;
|
||||
export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySelector> {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
|
||||
@property()
|
||||
public label: string = 'Label';
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public key: string;
|
||||
|
||||
@property({
|
||||
type: Number
|
||||
})
|
||||
public value: number = 1;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean
|
||||
})
|
||||
public disabled: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 110px;
|
||||
width: auto;
|
||||
user-select: none;
|
||||
}
|
||||
.maincontainer {
|
||||
|
||||
.quantity-container {
|
||||
transition: all 0.1s;
|
||||
font-size: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 33% 34% 33%;
|
||||
text-align: center;
|
||||
background:none;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#222222')};
|
||||
line-height: 40px;
|
||||
padding: 0px;
|
||||
min-width: 100px;
|
||||
color: ${this.goBright ? '#666' : '#CCC'};
|
||||
border: ${this.goBright ? '1px solid #333' : '1px solid #CCC'};
|
||||
min-width: 110px;
|
||||
color: ${cssManager.bdTheme('#666', '#CCC')};
|
||||
border: 1px solid ${cssManager.bdTheme('#CCC', '#444')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mainContainer:hover {
|
||||
color: ${this.goBright ? '#333' : '#fff'};
|
||||
border: ${this.goBright ? '1px solid #333' : '1px solid #fff'};
|
||||
.quantity-container.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quantity-container:hover {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
border-color: ${cssManager.bdTheme('#999', '#666')};
|
||||
}
|
||||
|
||||
.minus {
|
||||
@ -91,28 +76,41 @@ export class DeesInputQuantitySelector extends DeesElement {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
</style>
|
||||
|
||||
<div class="maincontainer">
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label}></dees-label>
|
||||
<div class="quantity-container ${this.disabled ? 'disabled' : ''}">
|
||||
<div class="selector minus" @click="${() => {this.decrease();}}">-</div>
|
||||
<div class="quantity">${this.value}</div>
|
||||
<div class="selector plus" @click="${() => {this.increase();}}">+</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public increase() {
|
||||
if (!this.disabled) {
|
||||
this.value++;
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
}
|
||||
|
||||
public decrease() {
|
||||
if (this.value > 0) {
|
||||
if (!this.disabled && this.value > 0) {
|
||||
this.value--;
|
||||
} else {
|
||||
// nothing to do here
|
||||
}
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: number): void {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
267
ts_web/elements/dees-input-radio.demo.ts
Normal file
267
ts_web/elements/dees-input-radio.demo.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import type { DeesInputRadio } from './dees-input-radio.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #0069f2;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section p {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.radio-group {
|
||||
background: #0a0a0a;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.radio-group-title {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Radio Groups</h3>
|
||||
<p>Radio buttons for single-choice selections</p>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-group-title">Select your subscription plan:</div>
|
||||
<dees-input-radio
|
||||
.label=${'Basic Plan - $9/month'}
|
||||
.value=${true}
|
||||
.key=${'plan-basic'}
|
||||
.name=${'plan'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Pro Plan - $29/month'}
|
||||
.key=${'plan-pro'}
|
||||
.name=${'plan'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Enterprise Plan - $99/month'}
|
||||
.key=${'plan-enterprise'}
|
||||
.name=${'plan'}
|
||||
></dees-input-radio>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-group-title">Task Priority:</div>
|
||||
<dees-input-radio
|
||||
.label=${'High Priority'}
|
||||
.key=${'priority-high'}
|
||||
.name=${'priority'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Medium Priority'}
|
||||
.value=${true}
|
||||
.key=${'priority-medium'}
|
||||
.name=${'priority'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Low Priority'}
|
||||
.key=${'priority-low'}
|
||||
.name=${'priority'}
|
||||
></dees-input-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Horizontal Layout</h3>
|
||||
<p>Radio buttons arranged horizontally for yes/no questions</p>
|
||||
|
||||
<div class="radio-group" style="flex-direction: row;">
|
||||
<div style="margin-right: 16px;">Do you agree?</div>
|
||||
<dees-input-radio
|
||||
.label=${'Yes'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${true}
|
||||
.key=${'agree-yes'}
|
||||
.name=${'agreement'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'No'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'agree-no'}
|
||||
.name=${'agreement'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Maybe'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'agree-maybe'}
|
||||
.name=${'agreement'}
|
||||
></dees-input-radio>
|
||||
</div>
|
||||
|
||||
<div class="radio-group" style="flex-direction: row;">
|
||||
<div style="margin-right: 16px;">Experience Level:</div>
|
||||
<dees-input-radio
|
||||
.label=${'Beginner'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'exp-beginner'}
|
||||
.name=${'experience'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Intermediate'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${true}
|
||||
.key=${'exp-intermediate'}
|
||||
.name=${'experience'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Expert'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'exp-expert'}
|
||||
.name=${'experience'}
|
||||
></dees-input-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Survey Example</h3>
|
||||
<p>Multiple radio groups in a survey format</p>
|
||||
|
||||
<div class="grid-layout">
|
||||
<div class="radio-group">
|
||||
<div class="radio-group-title">How satisfied are you?</div>
|
||||
<dees-input-radio .label=${'Very Satisfied'} .key=${'sat-very'} .name=${'satisfaction'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Satisfied'} .value=${true} .key=${'sat-normal'} .name=${'satisfaction'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Neutral'} .key=${'sat-neutral'} .name=${'satisfaction'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Dissatisfied'} .key=${'sat-dis'} .name=${'satisfaction'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Very Dissatisfied'} .key=${'sat-verydis'} .name=${'satisfaction'}></dees-input-radio>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-group-title">Would you recommend us?</div>
|
||||
<dees-input-radio .label=${'Definitely'} .key=${'rec-def'} .name=${'recommend'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Probably'} .value=${true} .key=${'rec-prob'} .name=${'recommend'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Not Sure'} .key=${'rec-unsure'} .name=${'recommend'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Probably Not'} .key=${'rec-probnot'} .name=${'recommend'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Definitely Not'} .key=${'rec-defnot'} .name=${'recommend'}></dees-input-radio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>States</h3>
|
||||
<p>Different radio button states</p>
|
||||
|
||||
<div class="radio-group">
|
||||
<dees-input-radio
|
||||
.label=${'Normal Radio'}
|
||||
.key=${'state-normal'}
|
||||
.name=${'states'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Selected Radio'}
|
||||
.value=${true}
|
||||
.key=${'state-selected'}
|
||||
.name=${'states'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Disabled Unchecked'}
|
||||
.disabled=${true}
|
||||
.key=${'state-disabled1'}
|
||||
.name=${'states2'}
|
||||
></dees-input-radio>
|
||||
<dees-input-radio
|
||||
.label=${'Disabled Checked'}
|
||||
.disabled=${true}
|
||||
.value=${true}
|
||||
.key=${'state-disabled2'}
|
||||
.name=${'states2'}
|
||||
></dees-input-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Settings Example</h3>
|
||||
<p>Common radio button patterns in settings</p>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-group-title">Theme Preference:</div>
|
||||
<dees-input-radio .label=${'Light Theme'} .key=${'theme-light'} .name=${'theme'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Dark Theme'} .value=${true} .key=${'theme-dark'} .name=${'theme'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'System Default'} .key=${'theme-system'} .name=${'theme'}></dees-input-radio>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-group-title">Notification Frequency:</div>
|
||||
<dees-input-radio .label=${'All Notifications'} .key=${'notif-all'} .name=${'notifications'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'Important Only'} .value=${true} .key=${'notif-important'} .name=${'notifications'}></dees-input-radio>
|
||||
<dees-input-radio .label=${'None'} .key=${'notif-none'} .name=${'notifications'}></dees-input-radio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,5 +1,6 @@
|
||||
import {customElement, DeesElement, type TemplateResult, property, html, type CSSResult,} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-radio.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -8,55 +9,36 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-radio')
|
||||
export class DeesInputRadio extends DeesElement {
|
||||
public static demo = () => html`<dees-input-radio></dees-input-radio>`;
|
||||
export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public key: string;
|
||||
|
||||
@property()
|
||||
public label: string = 'Label';
|
||||
|
||||
@property()
|
||||
public value: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean
|
||||
})
|
||||
public disabled: boolean = false;
|
||||
@property({ type: String })
|
||||
public name: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.labelPosition = 'right'; // Radio buttons default to label on the right
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html `
|
||||
<style>
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
transition: all 0.3s;
|
||||
display: grid;
|
||||
grid-template-columns: 25px auto;
|
||||
padding: 5px 0px;
|
||||
color: #ccc;
|
||||
}
|
||||
@ -65,14 +47,6 @@ export class DeesInputRadio extends DeesElement {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 15px;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-bottom: 1px solid #e4002b;
|
||||
@ -106,22 +80,56 @@ export class DeesInputRadio extends DeesElement {
|
||||
height: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<div class="maincontainer" @click="${this.toggleSelected}">
|
||||
<div class="checkbox ${this.value ? 'selected' : ''}">
|
||||
${this.value ? html`<div class="innercircle"></div>`: html``}
|
||||
</div>
|
||||
<div class="label">${this.label}</div>
|
||||
</div>
|
||||
<dees-label .label=${this.label}></dees-label>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async toggleSelected () {
|
||||
this.value = !this.value;
|
||||
// Radio buttons can only be selected, not deselected by clicking
|
||||
if (this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If this radio has a name, find and deselect other radios in the same group
|
||||
if (this.name) {
|
||||
// Try to find a form container first, then fall back to document
|
||||
const container = this.closest('dees-form') ||
|
||||
this.closest('dees-demowrapper') ||
|
||||
this.closest('.radio-group')?.parentElement ||
|
||||
document;
|
||||
const allRadios = container.querySelectorAll(`dees-input-radio[name="${this.name}"]`);
|
||||
allRadios.forEach((radio: DeesInputRadio) => {
|
||||
if (radio !== this && radio.value) {
|
||||
radio.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.value = true;
|
||||
this.dispatchEvent(new CustomEvent('newValue', {
|
||||
detail: this.value,
|
||||
bubbles: true
|
||||
}));
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public getValue(): boolean {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: boolean): void {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
199
ts_web/elements/dees-input-text.demo.ts
Normal file
199
ts_web/elements/dees-input-text.demo.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #0069f2;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.demo-section p {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Text Inputs</h3>
|
||||
<p>Standard text inputs with labels and descriptions</p>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Username'}
|
||||
.value=${'johndoe'}
|
||||
.key=${'username'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Email Address'}
|
||||
.value=${'john@example.com'}
|
||||
.description=${'We will never share your email with anyone'}
|
||||
.key=${'email'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Password'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'secret123'}
|
||||
.key=${'password'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Horizontal Layout</h3>
|
||||
<p>Multiple inputs arranged horizontally for compact forms</p>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-text
|
||||
.label=${'First Name'}
|
||||
.value=${'John'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'firstName'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Last Name'}
|
||||
.value=${'Doe'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'lastName'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Age'}
|
||||
.value=${'28'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'age'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Label Positions</h3>
|
||||
<p>Different label positioning options for various layouts</p>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Label on Top (Default)'}
|
||||
.value=${'Standard layout'}
|
||||
.labelPosition=${'top'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Label on Left'}
|
||||
.value=${'Inline label'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
|
||||
<div class="grid-layout">
|
||||
<dees-input-text
|
||||
.label=${'City'}
|
||||
.value=${'New York'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'ZIP Code'}
|
||||
.value=${'10001'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Validation & States</h3>
|
||||
<p>Different validation states and input configurations</p>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.key=${'requiredField'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Disabled Field'}
|
||||
.value=${'Cannot edit this'}
|
||||
.disabled=${true}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Field with Error'}
|
||||
.value=${'invalid@'}
|
||||
.validationText=${'Please enter a valid email address'}
|
||||
.validationState=${'invalid'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Advanced Features</h3>
|
||||
<p>Password visibility toggle and other advanced features</p>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Password with Toggle'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'mySecurePassword123'}
|
||||
.description=${'Click the eye icon to show/hide password'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'API Key'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'sk-1234567890abcdef'}
|
||||
.description=${'Keep this key secure and never share it'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,16 +1,15 @@
|
||||
import * as colors from './00colors.js';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
import { demoFunc } from './dees-input-text.demo.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -19,47 +18,16 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement('dees-input-text')
|
||||
export class DeesInputText extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-input-text .label=${'this is a label'} .value=${'test'}></dees-input-text>
|
||||
<dees-input-text .isPasswordBool=${true}></dees-input-text>
|
||||
`;
|
||||
export class DeesInputText extends DeesInputBase {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesInputText>();
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public label: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public key: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
public value: string = '';
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public required: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public disabled: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
@ -87,6 +55,7 @@ export class DeesInputText extends DeesElement {
|
||||
validationFunction: (value: string) => boolean;
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
* {
|
||||
@ -95,9 +64,6 @@ export class DeesInputText extends DeesElement {
|
||||
|
||||
:host {
|
||||
position: relative;
|
||||
display: grid;
|
||||
margin: 8px 0px;
|
||||
margin-bottom: 24px;
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
@ -134,7 +100,8 @@ export class DeesInputText extends DeesElement {
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme( colors.bright.blueActive, colors.dark.blueActive)};
|
||||
border-bottom: 1px solid
|
||||
${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
@ -151,6 +118,7 @@ export class DeesInputText extends DeesElement {
|
||||
padding: 4px 0px;
|
||||
width: 40px;
|
||||
z-index: 3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.showPassword:hover {
|
||||
@ -180,12 +148,14 @@ export class DeesInputText extends DeesElement {
|
||||
letter-spacing: ${this.isPasswordBool ? '1px' : 'normal'};
|
||||
color: ${this.goBright ? '#333' : '#ccc'};
|
||||
}
|
||||
${this.validationText ? css`
|
||||
${this.validationText
|
||||
? css`
|
||||
.validationContainer {
|
||||
height: 22px;
|
||||
opacity: 1;
|
||||
}
|
||||
` : css`
|
||||
`
|
||||
: css`
|
||||
.validationContainer {
|
||||
height: 4px;
|
||||
padding: 2px !important;
|
||||
@ -193,17 +163,16 @@ export class DeesInputText extends DeesElement {
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="maincontainer">
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<div class="maincontainer">
|
||||
<input
|
||||
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
|
||||
.value=${this.value}
|
||||
@input="${this.updateValue}"
|
||||
.disabled=${this.disabled}
|
||||
/>
|
||||
<div class="validationContainer">
|
||||
${this.validationText}
|
||||
</div>
|
||||
<div class="validationContainer">${this.validationText}</div>
|
||||
${this.isPasswordBool
|
||||
? html`
|
||||
<div class="showPassword" @click=${this.togglePasswordView}>
|
||||
@ -212,14 +181,12 @@ export class DeesInputText extends DeesElement {
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const input = this.shadowRoot.querySelector('input');
|
||||
input.addEventListener('input', (eventArg: InputEvent) => {
|
||||
|
||||
});
|
||||
// Input event handling is already done in updateValue method
|
||||
}
|
||||
|
||||
public async updateValue(eventArg: Event) {
|
||||
@ -228,16 +195,15 @@ export class DeesInputText extends DeesElement {
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public async freeze() {
|
||||
this.disabled = true;
|
||||
public getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public async unfreeze() {
|
||||
this.disabled = false;
|
||||
public setValue(value: string): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public async togglePasswordView() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
this.showPasswordBool = !this.showPasswordBool;
|
||||
console.log(`this.showPasswordBool is: ${this.showPasswordBool}`);
|
||||
}
|
||||
|
@ -1,15 +1,118 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
.demoContainer {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
padding: 40px;
|
||||
background: #000;
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.info-box {
|
||||
background: #1e3a5f;
|
||||
color: #90caf9;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoContainer">
|
||||
<dees-input-typelist></dees-input-typelist>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}>
|
||||
<dees-input-typelist
|
||||
.label=${'Tags'}
|
||||
.description=${'Add tags by typing and pressing Enter'}
|
||||
.value=${['javascript', 'typescript', 'web-components']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Team Members'}
|
||||
.description=${'Add email addresses of team members'}
|
||||
.value=${['alice@example.com', 'bob@example.com']}
|
||||
></dees-input-typelist>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}>
|
||||
<dees-input-typelist
|
||||
.label=${'Your Skills'}
|
||||
.description=${'List your professional skills'}
|
||||
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-typelist
|
||||
.label=${'Categories'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${['Technology', 'Design', 'Business']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Keywords'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${['innovation', 'startup', 'growth']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}>
|
||||
<dees-input-typelist
|
||||
.label=${'Project Dependencies'}
|
||||
.description=${'List all required npm packages'}
|
||||
.required=${true}
|
||||
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'System Tags'}
|
||||
.description=${'These tags are managed by the system'}
|
||||
.disabled=${true}
|
||||
.value=${['system', 'protected', 'readonly']}
|
||||
></dees-input-typelist>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Article Publishing Form'} .subtitle=${'Complete form with tag management'}>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Article Title'} .required=${true}></dees-input-text>
|
||||
<dees-input-text
|
||||
.label=${'Summary'}
|
||||
.inputType=${'textarea'}
|
||||
.description=${'Brief description of the article'}
|
||||
></dees-input-text>
|
||||
<dees-input-typelist
|
||||
.label=${'Tags'}
|
||||
.description=${'Add relevant tags for better discoverability'}
|
||||
.value=${['tutorial', 'web-development']}
|
||||
></dees-input-typelist>
|
||||
<dees-input-typelist
|
||||
.label=${'Co-Authors'}
|
||||
.description=${'Add email addresses of co-authors'}
|
||||
></dees-input-typelist>
|
||||
</dees-form>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>Tip:</strong> Type a value and press Enter to add it to the list. Click on any item to remove it.
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
@ -1,44 +1,37 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
state,
|
||||
html,
|
||||
domtools,
|
||||
property,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesInputBase } from './dees-input-base.js';
|
||||
|
||||
const { demoFunc } = await import('./dees-input-typelist.demo.js');
|
||||
|
||||
@customElement('dees-input-typelist')
|
||||
export class DeesInputTypelist extends DeesElement {
|
||||
export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
||||
public static demo = demoFunc;
|
||||
|
||||
|
||||
// INSTANCE
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public label: string;
|
||||
@property({ type: Array })
|
||||
public value: string[] = [];
|
||||
|
||||
@state()
|
||||
private inputValue: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
margin: 8px 0px 24px 0px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.mainbox {
|
||||
border-radius: 3px;
|
||||
@ -79,20 +72,89 @@ export class DeesInputTypelist extends DeesElement {
|
||||
input:focus {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#444')};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
margin: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tag .remove {
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tag .remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="label">${this.label}</div>
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<div class="mainbox">
|
||||
<div class="tags" @click=${() => {
|
||||
this.shadowRoot.querySelector('input').focus();
|
||||
}}>
|
||||
<div class="notags">No tags yet</div>
|
||||
${this.value.length === 0
|
||||
? html`<div class="notags">No tags yet</div>`
|
||||
: this.value.map(
|
||||
(tag) => html`
|
||||
<span class="tag">
|
||||
${tag}
|
||||
<span class="remove" @click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
this.removeTag(tag);
|
||||
}}>×</span>
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type, press Enter to add it..."
|
||||
.value=${this.inputValue}
|
||||
@input=${(e: InputEvent) => {
|
||||
this.inputValue = (e.target as HTMLInputElement).value;
|
||||
}}
|
||||
@keydown=${(e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && this.inputValue.trim()) {
|
||||
e.preventDefault();
|
||||
this.addTag(this.inputValue.trim());
|
||||
}
|
||||
}}
|
||||
.disabled=${this.disabled}
|
||||
/>
|
||||
</div>
|
||||
<input type="text" placeholder="Type, press Enter to add it..." />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private addTag(tag: string) {
|
||||
if (!this.value.includes(tag)) {
|
||||
this.value = [...this.value, tag];
|
||||
this.inputValue = '';
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
}
|
||||
|
||||
private removeTag(tag: string) {
|
||||
this.value = this.value.filter((t) => t !== tag);
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public getValue(): string[] {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public setValue(value: string[]): void {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
81
ts_web/elements/dees-panel.demo.ts
Normal file
81
ts_web/elements/dees-panel.demo.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-background {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-background">
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Panel Component'}>
|
||||
<p>The panel component automatically follows the theme and provides consistent styling for grouped content.</p>
|
||||
<p>It's perfect for creating sections in your application with proper spacing and borders.</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Panel with Subtitle'} .subtitle=${'Additional context information'}>
|
||||
<p>Panels can have both a title and subtitle to provide more context.</p>
|
||||
<p>The subtitle appears in a smaller, muted text below the title.</p>
|
||||
</dees-panel>
|
||||
|
||||
<div class="grid-layout">
|
||||
<dees-panel .title=${'Feature 1'}>
|
||||
<p>Grid layouts work great with panels for creating dashboards and feature sections.</p>
|
||||
<dees-button>Action</dees-button>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Feature 2'}>
|
||||
<p>Each panel maintains consistent spacing and styling.</p>
|
||||
<dees-button>Another Action</dees-button>
|
||||
</dees-panel>
|
||||
</div>
|
||||
|
||||
<dees-panel .title=${'Complex Content'}>
|
||||
<h4>Nested Elements</h4>
|
||||
<p>Panels can contain any type of content:</p>
|
||||
<ul>
|
||||
<li>Text and paragraphs</li>
|
||||
<li>Lists and tables</li>
|
||||
<li>Form inputs</li>
|
||||
<li>Other components</li>
|
||||
</ul>
|
||||
|
||||
<dees-input-text .label=${'Example Input'} .description=${'Input inside a panel'}></dees-input-text>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<dees-button>Submit</dees-button>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel>
|
||||
<p>Panels work great even without a title for simple content grouping.</p>
|
||||
<p>They provide visual separation and consistent padding.</p>
|
||||
</dees-panel>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
77
ts_web/elements/dees-panel.ts
Normal file
77
ts_web/elements/dees-panel.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { demoFunc } from './dees-panel.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-panel': DeesPanel;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-panel')
|
||||
export class DeesPanel extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: String })
|
||||
public title: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public subtitle: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#0069f2', '#0099ff')};
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: -12px 0 16px 0;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.content {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
/* Remove margins from first and last children */
|
||||
.content ::slotted(*:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content ::slotted(*:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.title ? html`<h3 class="title">${this.title}</h3>` : ''}
|
||||
${this.subtitle ? html`<p class="subtitle">${this.subtitle}</p>` : ''}
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
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 }
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,21 +1,293 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, DeesElement, customElement, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IView } from './dees-simple-appdash.js';
|
||||
import './dees-form.js';
|
||||
import './dees-input-text.js';
|
||||
import './dees-input-checkbox.js';
|
||||
import './dees-input-dropdown.js';
|
||||
import './dees-input-radio.js';
|
||||
import './dees-form-submit.js';
|
||||
import './dees-statsgrid.js';
|
||||
import type { IStatsTile } from './dees-statsgrid.js';
|
||||
|
||||
// Create demo view components
|
||||
@customElement('demo-view-dashboard')
|
||||
class DemoViewDashboard extends DeesElement {
|
||||
static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 20px 0;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
dees-statsgrid {
|
||||
margin-top: 20px;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
private statsTiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'users',
|
||||
title: 'Active Users',
|
||||
value: 1234,
|
||||
type: 'number',
|
||||
icon: 'faUsers',
|
||||
description: '+15% from last week',
|
||||
color: '#22c55e'
|
||||
},
|
||||
{
|
||||
id: 'pageviews',
|
||||
title: 'Page Views',
|
||||
value: 56700,
|
||||
type: 'number',
|
||||
icon: 'faEye',
|
||||
description: '56.7k total views',
|
||||
color: '#3b82f6'
|
||||
},
|
||||
{
|
||||
id: 'uptime',
|
||||
title: 'System Uptime',
|
||||
value: 89,
|
||||
unit: '%',
|
||||
type: 'gauge',
|
||||
icon: 'faServer',
|
||||
description: 'Last 30 days',
|
||||
color: '#10b981',
|
||||
gaugeOptions: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
thresholds: [
|
||||
{ value: 80, color: '#ef4444' },
|
||||
{ value: 90, color: '#f59e0b' },
|
||||
{ value: 100, color: '#10b981' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'response',
|
||||
title: 'Avg Response Time',
|
||||
value: 3.2,
|
||||
unit: 's',
|
||||
type: 'number',
|
||||
icon: 'faClock',
|
||||
description: '-0.5s improvement',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
title: 'Monthly Revenue',
|
||||
value: 48520,
|
||||
unit: '$',
|
||||
type: 'trend',
|
||||
icon: 'faDollarSign',
|
||||
description: '+8.2% growth',
|
||||
color: '#22c55e',
|
||||
trendData: [35000, 38000, 37500, 41000, 39800, 42000, 44100, 43200, 45600, 47100, 46800, 48520]
|
||||
},
|
||||
{
|
||||
id: 'traffic',
|
||||
title: 'Traffic Trend',
|
||||
value: 1680,
|
||||
type: 'trend',
|
||||
icon: 'faChartLine',
|
||||
description: 'Last 7 days',
|
||||
color: '#3b82f6',
|
||||
trendData: [1200, 1350, 1100, 1450, 1600, 1550, 1680]
|
||||
}
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome to your application dashboard. Here's an overview of your metrics:</p>
|
||||
<dees-statsgrid
|
||||
.tiles=${this.statsTiles}
|
||||
@tile-action=${(e: CustomEvent) => {
|
||||
console.log('Tile action:', e.detail);
|
||||
}}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('demo-view-analytics')
|
||||
class DemoViewAnalytics extends DeesElement {
|
||||
static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 20px 0;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1>Analytics</h1>
|
||||
<p>This is the analytics view. You can add charts and metrics here.</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('demo-view-settings')
|
||||
class DemoViewSettings extends DeesElement {
|
||||
static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 40px;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 20px 0;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
.settings-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.settings-section h2 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 15px 0;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
.horizontal-form-section {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1>Settings</h1>
|
||||
<p>Configure your application settings below:</p>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>General Settings</h2>
|
||||
<dees-form>
|
||||
<dees-input-text key="appName" label="Application Name" value="My App"></dees-input-text>
|
||||
<dees-input-text key="apiEndpoint" label="API Endpoint" value="https://api.example.com"></dees-input-text>
|
||||
<dees-input-dropdown
|
||||
key="environment"
|
||||
label="Environment"
|
||||
.options=${[
|
||||
{ option: 'Development', key: 'dev' },
|
||||
{ option: 'Staging', key: 'staging' },
|
||||
{ option: 'Production', key: 'prod' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Production', key: 'prod' }}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-checkbox key="enableNotifications" label="Enable Notifications" value="true"></dees-input-checkbox>
|
||||
<dees-input-checkbox key="enableAnalytics" label="Enable Analytics" value="false"></dees-input-checkbox>
|
||||
<dees-form-submit>Save General Settings</dees-form-submit>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Display Preferences</h2>
|
||||
<div class="horizontal-form-section">
|
||||
<p style="margin-top: 0; margin-bottom: 16px;">Quick display settings using horizontal layout:</p>
|
||||
<dees-form horizontal-layout>
|
||||
<dees-input-dropdown
|
||||
key="theme"
|
||||
label="Theme"
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: 'Light', key: 'light' },
|
||||
{ option: 'Dark', key: 'dark' },
|
||||
{ option: 'Auto', key: 'auto' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Dark', key: 'dark' }}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-dropdown
|
||||
key="language"
|
||||
label="Language"
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: 'English', key: 'en' },
|
||||
{ option: 'German', key: 'de' },
|
||||
{ option: 'Spanish', key: 'es' },
|
||||
{ option: 'French', key: 'fr' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'English', key: 'en' }}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-checkbox key="compactMode" label="Compact Mode"></dees-input-checkbox>
|
||||
</dees-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Notification Settings</h2>
|
||||
<dees-form>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<div style="font-weight: 500; margin-bottom: 8px;">Email Frequency:</div>
|
||||
<dees-input-radio label="Real-time" value="true" key="email-realtime"></dees-input-radio>
|
||||
<dees-input-radio label="Daily Digest" key="email-daily"></dees-input-radio>
|
||||
<dees-input-radio label="Weekly Summary" key="email-weekly"></dees-input-radio>
|
||||
<dees-input-radio label="Never" key="email-never"></dees-input-radio>
|
||||
</div>
|
||||
<dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox>
|
||||
<dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox>
|
||||
<dees-form-submit>Update Notifications</dees-form-submit>
|
||||
</dees-form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-simple-appdash
|
||||
name="My Application"
|
||||
terminalSetupCommand="echo 'Welcome to the terminal!'"
|
||||
.viewTabs=${[
|
||||
{
|
||||
name: 'View 1',
|
||||
element: null,
|
||||
name: 'Dashboard',
|
||||
iconName: 'home',
|
||||
element: DemoViewDashboard,
|
||||
},
|
||||
{
|
||||
name: 'View 2',
|
||||
element: null,
|
||||
name: 'Analytics',
|
||||
iconName: 'lineChart',
|
||||
element: DemoViewAnalytics,
|
||||
},
|
||||
{
|
||||
name: 'View 3',
|
||||
element: null,
|
||||
name: 'Settings',
|
||||
iconName: 'settings',
|
||||
element: DemoViewSettings,
|
||||
}
|
||||
] as IView[]}
|
||||
>Hello there</dees-simple-appdash>
|
||||
@logout=${() => {
|
||||
console.log('Logout event triggered');
|
||||
alert('Logout clicked!');
|
||||
}}
|
||||
@view-select=${(e: CustomEvent) => {
|
||||
console.log('View selected:', e.detail.view.name);
|
||||
}}
|
||||
></dees-simple-appdash>
|
||||
</div>
|
||||
`;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { demoFunc } from './dees-simple-appdash.demo.js';
|
||||
import * as colors from './00colors.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
@ -14,7 +13,8 @@ import {
|
||||
state,
|
||||
domtools,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesTerminal } from './dees-terminal.js';
|
||||
import './dees-icon.js';
|
||||
import type { DeesTerminal } from './dees-terminal.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@ -24,6 +24,7 @@ declare global {
|
||||
|
||||
export interface IView {
|
||||
name: string;
|
||||
iconName?: string;
|
||||
element: DeesElement['constructor']['prototype'];
|
||||
}
|
||||
|
||||
@ -34,11 +35,18 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
// INSTANCE
|
||||
|
||||
@property()
|
||||
public name = 'Dees Simple Login';
|
||||
public name: string = 'Application Dashboard';
|
||||
|
||||
@property()
|
||||
@property({ type: Array })
|
||||
public viewTabs: IView[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public terminalSetupCommand: string = `echo "Terminal ready"`;
|
||||
|
||||
@state()
|
||||
private selectedView: IView;
|
||||
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
@ -46,83 +54,142 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
user-select: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.appbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: calc(100% - 24px);
|
||||
width: 200px;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#ccc', '#ffffff20')};
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
width: 240px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#000')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
font-size: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
padding: 0px 16px;
|
||||
z-index: 2;
|
||||
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.8);
|
||||
display: grid;
|
||||
grid-template-rows: min-content auto min-content;
|
||||
grid-template-rows: auto min-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.appbar .viewTabs {
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
.sidebar-header {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: top;
|
||||
}
|
||||
|
||||
.viewTab {
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.viewTab:hover {
|
||||
background: ${cssManager.bdTheme('#ccc', '#ffffff10')};
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
|
||||
.viewTab:active {
|
||||
background: ${cssManager.bdTheme('#aaa', '#ffffff20')};
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.appName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
white-space: nowrap;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.viewTabs-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.viewTabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.viewTab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: default;
|
||||
transition: background 0.1s;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.viewTab:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.viewTab:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
}
|
||||
|
||||
.viewTab.selected {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.viewTab.selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: ${cssManager.bdTheme('#26a69a', '#26a69a')};
|
||||
}
|
||||
|
||||
.viewTab dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
|
||||
.appActions {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.appActions .action {
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
transition: background 0.1s;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
.appActions .action:hover {
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
|
||||
.action:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.action dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.appcontent {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
height: calc(100vh - 24px);
|
||||
height: calc(100% - 24px);
|
||||
bottom: 24px;
|
||||
width: calc(100vw - 200px);
|
||||
width: calc(100% - 240px);
|
||||
overflow: auto;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000')};
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
@ -132,22 +199,36 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#00000020', '#ffffff20')};
|
||||
height: 24px;
|
||||
background: ${cssManager.bdTheme(colors.bright.blueMuted, colors.dark.blueMuted)};
|
||||
background: ${cssManager.bdTheme('#2196f3', '#1565c0')};
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: min-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 16px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.control:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
@ -155,31 +236,49 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
return html`
|
||||
<div class="maincontainer">
|
||||
<div class="appbar">
|
||||
<div>
|
||||
<div class="sidebar-header">
|
||||
<dees-icon .icon="lucide:grid3x3" style="font-size: 18px;"></dees-icon>
|
||||
<div class="appName">${this.name}</div>
|
||||
</div>
|
||||
<div class="viewTabs-container">
|
||||
<div class="viewTabs">
|
||||
${this.viewTabs.map(
|
||||
(view) => html`
|
||||
<div class="viewTab" @click=${() => {
|
||||
this.loadView(view);
|
||||
}}>${view.name}</div>
|
||||
<div
|
||||
class="viewTab ${this.selectedView === view ? 'selected' : ''}"
|
||||
@click=${() => this.loadView(view)}
|
||||
>
|
||||
${view.iconName ? html`
|
||||
<dees-icon .icon="${`lucide:${view.iconName}`}"></dees-icon>
|
||||
` : ''}
|
||||
<span style="flex: 1;">${view.name}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appActions">
|
||||
<div class="action" @click=${() => {
|
||||
this.dispatchEvent(new CustomEvent('logout'));
|
||||
}}>Logout</div>
|
||||
this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
|
||||
}}>
|
||||
<dees-icon .icon="lucide:logOut"></dees-icon>
|
||||
<span>Logout</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appcontent">
|
||||
|
||||
<!-- Content goes here -->
|
||||
</div>
|
||||
<div class="controlbar">
|
||||
<div class="control">
|
||||
<dees-icon .iconFA=${'networkWired'}></dees-icon>
|
||||
<dees-icon .icon="lucide:wifi"></dees-icon>
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
<div class="control" @click=${this.launchTerminal}>
|
||||
<dees-icon .iconFA=${'terminal'}></dees-icon>
|
||||
<dees-icon .icon="lucide:terminal"></dees-icon>
|
||||
<span>Terminal</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,27 +288,59 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
public async firstUpdated(_changedProperties): Promise<void> {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
super.firstUpdated(_changedProperties);
|
||||
if (this.viewTabs && this.viewTabs.length > 0) {
|
||||
await this.loadView(this.viewTabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public currentTerminal: DeesTerminal;
|
||||
public async launchTerminal() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
if (this.currentTerminal) {
|
||||
// If terminal already exists, remove it
|
||||
await this.closeTerminal();
|
||||
return;
|
||||
}
|
||||
|
||||
const maincontainer = this.shadowRoot.querySelector('.maincontainer');
|
||||
const { DeesTerminal } = await import('./dees-terminal.js');
|
||||
const terminal = new DeesTerminal();
|
||||
terminal.setupCommand = this.terminalSetupCommand;
|
||||
this.currentTerminal = terminal;
|
||||
maincontainer.appendChild(terminal);
|
||||
terminal.style.position = 'absolute';
|
||||
terminal.style.zIndex = '1';
|
||||
terminal.style.zIndex = '10';
|
||||
terminal.style.top = '0px';
|
||||
terminal.style.left = '200px';
|
||||
terminal.style.left = '240px';
|
||||
terminal.style.right = '0px';
|
||||
terminal.style.bottom = '24px';
|
||||
terminal.style.opacity = '0';
|
||||
terminal.style.transform = 'translateY(20px)';
|
||||
terminal.style.transition = 'all 0.2s';
|
||||
await domtools.plugins.smartdelay.delayFor(0);
|
||||
terminal.style.background = '#000';
|
||||
terminal.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
|
||||
|
||||
// Add close button to terminal
|
||||
terminal.addEventListener('close', () => this.closeTerminal());
|
||||
|
||||
await domtools.convenience.smartdelay.delayFor(0);
|
||||
terminal.style.opacity = '1';
|
||||
terminal.style.transform = 'translateY(0px)';
|
||||
return terminal;
|
||||
}
|
||||
|
||||
private async closeTerminal() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
if (this.currentTerminal) {
|
||||
this.currentTerminal.style.opacity = '0';
|
||||
this.currentTerminal.style.transform = 'translateY(20px)';
|
||||
await domtools.convenience.smartdelay.delayFor(200);
|
||||
this.currentTerminal.remove();
|
||||
this.currentTerminal = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private currentView: DeesElement;
|
||||
public async loadView(viewArg: IView) {
|
||||
const appcontent = this.shadowRoot.querySelector('.appcontent');
|
||||
@ -219,5 +350,13 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
}
|
||||
appcontent.appendChild(view);
|
||||
this.currentView = view;
|
||||
this.selectedView = viewArg;
|
||||
|
||||
// Emit view-select event
|
||||
this.dispatchEvent(new CustomEvent('view-select', {
|
||||
detail: { view: viewArg },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,37 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html` <dees-simple-login name="someapp"> Hello there </dees-simple-login> `;
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-simple-login
|
||||
name="My Application"
|
||||
@login=${(e: CustomEvent) => {
|
||||
console.log('Login event received:', e.detail);
|
||||
const loginData = e.detail?.data || e.detail;
|
||||
if (loginData?.username && loginData?.password) {
|
||||
alert(`Login attempted with:\nUsername: ${loginData.username}\nPassword: ${loginData.password}`);
|
||||
// Here you would typically validate credentials and show the slotted content
|
||||
} else {
|
||||
console.error('Invalid login data structure:', e.detail);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style="padding: 40px; text-align: center;">
|
||||
<h1>Welcome!</h1>
|
||||
<p>This is the slotted content that appears after login.</p>
|
||||
</div>
|
||||
</dees-simple-login>
|
||||
</div>
|
||||
`;
|
||||
|
@ -8,9 +8,6 @@ import {
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
unsafeCSS,
|
||||
type CSSResult,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
declare global {
|
||||
@ -26,51 +23,77 @@ export class DeesSimpleLogin extends DeesElement {
|
||||
// INSTANCE
|
||||
|
||||
@property()
|
||||
public name = 'Dees Simple Login';
|
||||
public name: string = 'Application';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
user-select: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
|
||||
.loginContainer {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center; /* aligns horizontally */
|
||||
align-items: center; /* aligns vertically */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
|
||||
}
|
||||
|
||||
.slotContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login {
|
||||
min-width: 320px;
|
||||
min-height: 100px;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#111')};
|
||||
box-shadow: ${cssManager.bdTheme('0px 1px 4px rgba(0,0,0,0.3)', 'none')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#111')};
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
'0 4px 12px rgba(0, 0, 0, 0.3)'
|
||||
)};
|
||||
border-radius: 8px;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
padding: 24px;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
.slotContainer {
|
||||
opacity:0;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
pointer-events: none;
|
||||
|
||||
.login dees-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.login dees-input-text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login dees-form-submit {
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@ -79,11 +102,11 @@ export class DeesSimpleLogin extends DeesElement {
|
||||
return html`
|
||||
<div class="loginContainer">
|
||||
<div class="login">
|
||||
<dees-form>
|
||||
<div class="header">Login to ${this.name}</div>
|
||||
<dees-input-text key="username" label="username" required></dees-input-text>
|
||||
<dees-input-text key="password" label="password" isPasswordBool required></dees-input-text>
|
||||
<dees-form-submit disabled>login</dees-form-submit>
|
||||
<dees-form>
|
||||
<dees-input-text key="username" label="Username" required></dees-input-text>
|
||||
<dees-input-text key="password" label="Password" isPasswordBool required></dees-input-text>
|
||||
<dees-form-submit>Login</dees-form-submit>
|
||||
</dees-form>
|
||||
</div>
|
||||
</div>
|
||||
@ -93,18 +116,20 @@ export class DeesSimpleLogin extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties): Promise<void> {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||
super.firstUpdated(_changedProperties);
|
||||
const form = this.shadowRoot.querySelector('dees-form');
|
||||
await form.readyDeferred.promise;
|
||||
const username = this.shadowRoot.querySelector('dees-input-text[label="username"]');
|
||||
const password = this.shadowRoot.querySelector('dees-input-text[label="password"]');
|
||||
const submit = this.shadowRoot.querySelector('dees-form-submit');
|
||||
|
||||
const form = this.shadowRoot.querySelector('dees-form') as any;
|
||||
if (form) {
|
||||
form.addEventListener('formData', (event: CustomEvent) => {
|
||||
this.dispatchEvent(new CustomEvent('login', { detail: event.detail }));
|
||||
this.dispatchEvent(new CustomEvent('login', {
|
||||
detail: event.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allows switching to slotted content
|
||||
@ -123,8 +148,5 @@ export class DeesSimpleLogin extends DeesElement {
|
||||
slotContainerDiv.style.transform = 'translateY(0px)';
|
||||
await domtools.convenience.smartdelay.delayFor(300);
|
||||
slotContainerDiv.style.pointerEvents = 'all';
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
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);
|
||||
}
|
||||
}
|
@ -102,7 +102,7 @@ export class DeesStepper extends DeesElement {
|
||||
transition: all 0.7s ease-in-out;
|
||||
max-width: 500px;
|
||||
min-height: 300px;
|
||||
border-radius: 8px;
|
||||
border-radius: 16px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#181818')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#ffffff', '#181818')};
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
@ -175,8 +175,9 @@ export class DeesStepper extends DeesElement {
|
||||
text-align: center;
|
||||
padding-top: 50px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-size: 25px;
|
||||
font-weight: 300;
|
||||
font-size: 22px;
|
||||
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step .content {
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
unsafeCSS,
|
||||
type CSSResult,
|
||||
state,
|
||||
resolveExec,
|
||||
directives,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
@ -415,7 +415,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
<div class="heading heading2">${this.heading2}</div>
|
||||
</div>
|
||||
<div class="headerActions">
|
||||
${resolveExec(async () => {
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes('header')) continue;
|
||||
@ -634,7 +634,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
selected
|
||||
</div>
|
||||
<div class="footerActions">
|
||||
${resolveExec(async () => {
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes('footer')) continue;
|
||||
|
@ -22,13 +22,25 @@ declare global {
|
||||
|
||||
@customElement('dees-terminal')
|
||||
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
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@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() {
|
||||
super();
|
||||
@ -282,8 +294,15 @@ export class DeesTerminal extends DeesElement {
|
||||
input.write(data);
|
||||
});
|
||||
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(`\n`);
|
||||
await this.waitForPrompt(term, '~/');
|
||||
input.write(`clear && echo 'welcome'\n`);
|
||||
this.webcontainerDeferred.resolve(webcontainerInstance);
|
||||
}
|
||||
|
||||
async connectedCallback(): Promise<void> {
|
||||
@ -300,14 +319,16 @@ export class DeesTerminal extends DeesElement {
|
||||
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) => {
|
||||
const checkPrompt = () => {
|
||||
const lines = term.buffer.active;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines.getLine(i);
|
||||
if (line && line.translateToString().includes(prompt)) {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -317,4 +338,18 @@ export class DeesTerminal extends DeesElement {
|
||||
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 () => {
|
||||
return html`<dees-toast></dees-toast>`;
|
||||
return html`
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 32px;
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#aaa')};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-button class="theme-toggle" @clicked=${() => {
|
||||
document.body.classList.toggle('bright');
|
||||
}}>Toggle Theme</dees-button>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Toast Types</h2>
|
||||
<p class="demo-description">
|
||||
Different toast types for various notification scenarios. Click any button to show a toast.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.info('This is an informational message');
|
||||
}}>Info Toast</dees-button>
|
||||
|
||||
<dees-button type="highlighted" @clicked=${() => {
|
||||
DeesToast.success('Operation completed successfully!');
|
||||
}}>Success Toast</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.warning('Please review before proceeding');
|
||||
}}>Warning Toast</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.error('An error occurred while processing');
|
||||
}}>Error Toast</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Toast Positions</h2>
|
||||
<p class="demo-description">
|
||||
Toasts can appear in different positions on the screen.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Top Right Position',
|
||||
type: 'info',
|
||||
position: 'top-right'
|
||||
});
|
||||
}}>Top Right</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Top Left Position',
|
||||
type: 'info',
|
||||
position: 'top-left'
|
||||
});
|
||||
}}>Top Left</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Bottom Right Position',
|
||||
type: 'info',
|
||||
position: 'bottom-right'
|
||||
});
|
||||
}}>Bottom Right</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Bottom Left Position',
|
||||
type: 'info',
|
||||
position: 'bottom-left'
|
||||
});
|
||||
}}>Bottom Left</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Top Center Position',
|
||||
type: 'info',
|
||||
position: 'top-center'
|
||||
});
|
||||
}}>Top Center</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Bottom Center Position',
|
||||
type: 'info',
|
||||
position: 'bottom-center'
|
||||
});
|
||||
}}>Bottom Center</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Duration Options</h2>
|
||||
<p class="demo-description">
|
||||
Control how long toasts stay visible. Duration in milliseconds.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Quick toast (1 second)',
|
||||
type: 'info',
|
||||
duration: 1000
|
||||
});
|
||||
}}>1 Second</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Standard toast (3 seconds)',
|
||||
type: 'info',
|
||||
duration: 3000
|
||||
});
|
||||
}}>3 Seconds (Default)</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Long toast (5 seconds)',
|
||||
type: 'info',
|
||||
duration: 5000
|
||||
});
|
||||
}}>5 Seconds</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.show({
|
||||
message: 'Manual dismiss only (click to close)',
|
||||
type: 'warning',
|
||||
duration: 0
|
||||
});
|
||||
}}>No Auto-Dismiss</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Multiple Toasts</h2>
|
||||
<p class="demo-description">
|
||||
Multiple toasts stack automatically. They maintain their order and animate smoothly.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.info('First notification');
|
||||
setTimeout(() => DeesToast.success('Second notification'), 200);
|
||||
setTimeout(() => DeesToast.warning('Third notification'), 400);
|
||||
setTimeout(() => DeesToast.error('Fourth notification'), 600);
|
||||
}}>Show Multiple</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
setTimeout(() => {
|
||||
DeesToast.show({
|
||||
message: `Notification #${i}`,
|
||||
type: i % 2 === 0 ? 'success' : 'info',
|
||||
duration: 2000 + (i * 500)
|
||||
});
|
||||
}, i * 100);
|
||||
}
|
||||
}}>Rapid Fire</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Real-World Examples</h2>
|
||||
<p class="demo-description">
|
||||
Common use cases for toast notifications in applications.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${async () => {
|
||||
const toast = await DeesToast.show({
|
||||
message: 'Saving changes...',
|
||||
type: 'info',
|
||||
duration: 0
|
||||
});
|
||||
|
||||
// Simulate save operation
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
DeesToast.success('Changes saved successfully!');
|
||||
}, 2000);
|
||||
}}>Save Operation</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.error('Failed to connect to server. Please check your internet connection.');
|
||||
}}>Network Error</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.warning('Your session will expire in 5 minutes');
|
||||
}}>Session Warning</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
DeesToast.success('File uploaded successfully!');
|
||||
}}>Upload Complete</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Programmatic Control</h2>
|
||||
<p class="demo-description">
|
||||
Advanced control over toast behavior.
|
||||
</p>
|
||||
<div class="button-grid">
|
||||
<dees-button @clicked=${async () => {
|
||||
const toast = await DeesToast.show({
|
||||
message: 'This toast can be dismissed programmatically',
|
||||
type: 'info',
|
||||
duration: 0
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
DeesToast.success('Toast dismissed after 2 seconds');
|
||||
}, 2000);
|
||||
}}>Programmatic Dismiss</dees-button>
|
||||
|
||||
<dees-button @clicked=${() => {
|
||||
// Using the convenience methods
|
||||
DeesToast.info('Info message', 2000);
|
||||
setTimeout(() => DeesToast.success('Success message', 2000), 500);
|
||||
setTimeout(() => DeesToast.warning('Warning message', 2000), 1000);
|
||||
setTimeout(() => DeesToast.error('Error message', 2000), 1500);
|
||||
}}>Convenience Methods</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { customElement, DeesElement, type TemplateResult, html, type CSSResult, } from '@design.estate/dees-element';
|
||||
import { customElement, DeesElement, type TemplateResult, html, css, property, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-toast.demo.js';
|
||||
@ -9,20 +9,317 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
||||
export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
|
||||
|
||||
export interface IToastOptions {
|
||||
message: string;
|
||||
type?: ToastType;
|
||||
duration?: number;
|
||||
position?: ToastPosition;
|
||||
}
|
||||
|
||||
@customElement('dees-toast')
|
||||
export class DeesToast extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = demoFunc;
|
||||
|
||||
private static toastContainers = new Map<ToastPosition, HTMLDivElement>();
|
||||
|
||||
private static getOrCreateContainer(position: ToastPosition): HTMLDivElement {
|
||||
if (!this.toastContainers.has(position)) {
|
||||
const container = document.createElement('div');
|
||||
container.className = `toast-container toast-container-${position}`;
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
pointer-events: none;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
// Position the container
|
||||
switch (position) {
|
||||
case 'top-right':
|
||||
container.style.top = '0';
|
||||
container.style.right = '0';
|
||||
break;
|
||||
case 'top-left':
|
||||
container.style.top = '0';
|
||||
container.style.left = '0';
|
||||
break;
|
||||
case 'bottom-right':
|
||||
container.style.bottom = '0';
|
||||
container.style.right = '0';
|
||||
break;
|
||||
case 'bottom-left':
|
||||
container.style.bottom = '0';
|
||||
container.style.left = '0';
|
||||
break;
|
||||
case 'top-center':
|
||||
container.style.top = '0';
|
||||
container.style.left = '50%';
|
||||
container.style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
case 'bottom-center':
|
||||
container.style.bottom = '0';
|
||||
container.style.left = '50%';
|
||||
container.style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
}
|
||||
|
||||
document.body.appendChild(container);
|
||||
this.toastContainers.set(position, container);
|
||||
}
|
||||
return this.toastContainers.get(position)!;
|
||||
}
|
||||
|
||||
public static async show(options: IToastOptions | string) {
|
||||
const opts: IToastOptions = typeof options === 'string'
|
||||
? { message: options }
|
||||
: options;
|
||||
|
||||
const toast = new DeesToast();
|
||||
toast.message = opts.message;
|
||||
toast.type = opts.type || 'info';
|
||||
toast.duration = opts.duration || 3000;
|
||||
|
||||
const container = this.getOrCreateContainer(opts.position || 'top-right');
|
||||
container.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
await toast.updateComplete;
|
||||
requestAnimationFrame(() => {
|
||||
toast.isVisible = true;
|
||||
});
|
||||
|
||||
// Auto dismiss
|
||||
if (toast.duration > 0) {
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
}, toast.duration);
|
||||
}
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
public static info(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'info', duration });
|
||||
}
|
||||
|
||||
public static success(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'success', duration });
|
||||
}
|
||||
|
||||
public static warning(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'warning', duration });
|
||||
}
|
||||
|
||||
public static error(message: string, duration?: number) {
|
||||
return this.show({ message, type: 'error', duration });
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
@property({ type: String })
|
||||
public message: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public type: ToastType = 'info';
|
||||
|
||||
@property({ type: Number })
|
||||
public duration: number = 3000;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isVisible: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style></style>
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
pointer-events: auto;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:host([isvisible]) {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.message {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.close {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.close svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Type-specific styles */
|
||||
:host([type="info"]) .icon {
|
||||
color: #0084ff;
|
||||
}
|
||||
|
||||
:host([type="success"]) .icon {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
:host([type="warning"]) .icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
:host([type="error"]) .icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: currentColor;
|
||||
opacity: 0.2;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
opacity: 0.8;
|
||||
transform-origin: left;
|
||||
animation: progress linear forwards;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
from {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
to {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const icons = {
|
||||
info: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"/>
|
||||
</svg>`,
|
||||
success: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>`,
|
||||
warning: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>`,
|
||||
error: html`<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="toast" @click=${this.dismiss}>
|
||||
<div class="icon">
|
||||
${icons[this.type]}
|
||||
</div>
|
||||
<div class="message">${this.message}</div>
|
||||
<div class="close">
|
||||
<svg viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</div>
|
||||
${this.duration > 0 ? html`
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="animation-duration: ${this.duration}ms"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async dismiss() {
|
||||
this.isVisible = false;
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
this.remove();
|
||||
|
||||
// Clean up empty containers
|
||||
const container = this.parentElement;
|
||||
if (container && container.children.length === 0) {
|
||||
container.remove();
|
||||
for (const [position, cont] of DeesToast.toastContainers.entries()) {
|
||||
if (cont === container) {
|
||||
DeesToast.toastContainers.delete(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
// Set the type attribute for CSS
|
||||
this.setAttribute('type', this.type);
|
||||
}
|
||||
}
|
@ -4,7 +4,12 @@ export * from './dees-appui-base.js';
|
||||
export * from './dees-appui-maincontent.js';
|
||||
export * from './dees-appui-mainmenu.js';
|
||||
export * from './dees-appui-mainselector.js';
|
||||
export * from './dees-appui-profiledropdown.js';
|
||||
export * from './dees-appui-tabs.js';
|
||||
export * from './dees-appui-view.js';
|
||||
export * from './dees-badge.js';
|
||||
export * from './dees-button-exit.js';
|
||||
export * from './dees-button-group.js';
|
||||
export * from './dees-button.js';
|
||||
export * from './dees-chart-area.js';
|
||||
export * from './dees-chart-log.js';
|
||||
@ -17,6 +22,7 @@ export * from './dees-editor-markdown.js';
|
||||
export * from './dees-editor-markdownoutlet.js';
|
||||
export * from './dees-form-submit.js';
|
||||
export * from './dees-form.js';
|
||||
export * from './dees-heading.js';
|
||||
export * from './dees-hint.js';
|
||||
export * from './dees-icon.js';
|
||||
export * from './dees-input-checkbox.js';
|
||||
@ -33,11 +39,14 @@ export * from './dees-label.js';
|
||||
export * from './dees-mobilenavigation.js';
|
||||
export * from './dees-modal.js';
|
||||
export * from './dees-input-multitoggle.js';
|
||||
export * from './dees-panel.js';
|
||||
export * from './dees-pdf.js';
|
||||
export * from './dees-searchbar.js';
|
||||
export * from './dees-simple-appdash.js';
|
||||
export * from './dees-simple-login.js';
|
||||
export * from './dees-speechbubble.js';
|
||||
export * from './dees-spinner.js';
|
||||
export * from './dees-statsgrid.js';
|
||||
export * from './dees-stepper.js';
|
||||
export * from './dees-table.js';
|
||||
export * from './dees-terminal.js';
|
||||
@ -45,3 +54,4 @@ export * from './dees-toast.js';
|
||||
export * from './dees-updater.js';
|
||||
export * from './dees-windowcontrols.js';
|
||||
export * from './dees-windowlayer.js';
|
||||
export * from './dees-pagination.js';
|
||||
|
34
ts_web/elements/interfaces/appbarmenuitem.ts
Normal file
34
ts_web/elements/interfaces/appbarmenuitem.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as plugins from '../00plugins.js';
|
||||
|
||||
/**
|
||||
* Divider menu item
|
||||
*/
|
||||
export interface IAppBarMenuDivider {
|
||||
divider: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular menu item
|
||||
*/
|
||||
export interface IAppBarMenuItemRegular extends plugins.tsclass.website.IMenuItem {
|
||||
id?: string;
|
||||
shortcut?: string; // e.g., "Cmd+S" or "Ctrl+S"
|
||||
submenu?: IAppBarMenuItem[];
|
||||
disabled?: boolean;
|
||||
checked?: boolean; // For checkbox menu items
|
||||
radioGroup?: string; // For radio button menu items
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended menu item interface for app bar menus
|
||||
* Can be either a regular menu item or a divider
|
||||
*/
|
||||
export type IAppBarMenuItem = IAppBarMenuItemRegular | IAppBarMenuDivider;
|
||||
|
||||
/**
|
||||
* Interface for the menu bar configuration
|
||||
*/
|
||||
export interface IMenuBar {
|
||||
menuItems: IAppBarMenuItem[];
|
||||
onMenuSelect?: (item: IAppBarMenuItem) => void;
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from './tab.js';
|
||||
export * from './selectionoption.js';
|
||||
export * from './appbarmenuitem.js';
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface ISelectionOption {
|
||||
key: string;
|
||||
iconName?: string;
|
||||
action: () => void;
|
||||
}
|
@ -1,2 +1,4 @@
|
||||
export * from './elements/index.js';
|
||||
import * as colors from './elements/00colors.js';
|
||||
export { colors };
|
||||
export { commitinfo } from './00_commitinfo_data.js';
|
||||
|
Reference in New Issue
Block a user