Compare commits
161 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 | |||
56e8d868c9 | |||
14e1d4a9f5 | |||
efe49469aa | |||
39500f802a | |||
a64d38dbb1 | |||
e2d3ae8949 | |||
1c4fd0a2da | |||
d0e85da308 | |||
6a0b0aa741 | |||
4c620f480c | |||
b7ebcc4cef | |||
e6275bfeb3 | |||
d4585654b6 | |||
1a8562dc6f | |||
2148959c35 | |||
28a84f7c1a | |||
e935dc3983 | |||
ab30b51c9e | |||
8377d94f81 | |||
14ebeeaee8 | |||
fdc8cb44ab | |||
f72936d1a7 | |||
04170d4834 | |||
9006e023bd | |||
8c8d5e9329 | |||
f4fee46439 | |||
af2ee9a6a5 | |||
a3012b5ec1 | |||
0f414ae24a | |||
1fa9b410ed | |||
90c7890cbf | |||
55e601b806 | |||
45952914ba | |||
5f83b01f0e | |||
c03563f2fc | |||
edf50d2f8d | |||
d0789ab279 | |||
55bac1bf7b | |||
2957121473 | |||
020a1bfb75 | |||
bb246c4f8e | |||
cf258a8d59 | |||
38b4df1ce3 | |||
2f7fdc16c7 | |||
f4b426bc7f | |||
9577cc9ebf | |||
be5124217a | |||
d029a6c346 | |||
69bb03dcdf | |||
5eb2f4cebc | |||
c021a84788 | |||
321ce99338 | |||
6cfe89645c | |||
11f900beeb | |||
39ca21e57c | |||
87872191e2 | |||
2d3fd74333 | |||
eebe898dcc | |||
ca73d00f68 | |||
14bdc46073 | |||
bf04ce6a8f | |||
9dc6dab3a5 | |||
a21a3b6918 | |||
6ae21d73aa | |||
941871991f | |||
497c38f426 | |||
bfaa1623d9 |
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
|
526
changelog.md
Normal file
526
changelog.md
Normal file
@ -0,0 +1,526 @@
|
||||
# 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
|
||||
|
||||
- Changed @webcontainer/api version from ^1.3.0 to 1.2.0 in package.json
|
||||
|
||||
## 2024-10-04 - 1.1.9 - fix(dependencies)
|
||||
Update various dependencies for compatibility and stability.
|
||||
|
||||
- Update @design.estate/dees-domtools to version ^2.0.61
|
||||
- Update @design.estate/dees-element to version ^2.0.39
|
||||
- Update @webcontainer/api to version ^1.3.0
|
||||
- Update apexcharts to version ^3.54.0
|
||||
- Update monaco-editor to version ^0.52.0
|
||||
- Update pdfjs-dist to version ^4.6.82
|
||||
- Update @push.rocks/tapbundle to version ^5.3.0
|
||||
- Update @types/node to version ^22.7.4
|
||||
|
||||
## 2024-09-02 - 1.1.8 - fix(dees-simple-appdash)
|
||||
Corrected viewTab active background color
|
||||
|
||||
- Corrected the background color of the viewTab in active state for better visual consistency.
|
||||
|
||||
## 2024-09-02 - 1.1.7 - fix(dependencies)
|
||||
Update dependencies to their latest versions
|
||||
|
||||
- Update dependency versions in package.json:
|
||||
- - @design.estate/dees-element from ^2.0.34 to ^2.0.36
|
||||
- - @fortawesome/fontawesome-svg-core from ^6.5.2 to ^6.6.0
|
||||
- - @fortawesome/free-brands-svg-icons from ^6.5.2 to ^6.6.0
|
||||
- - @fortawesome/free-regular-svg-icons from ^6.5.2 to ^6.6.0
|
||||
- - @fortawesome/free-solid-svg-icons from ^6.5.2 to ^6.6.0
|
||||
- - @tsclass/tsclass from ^4.0.63 to ^4.1.2
|
||||
- - apexcharts from ^3.49.2 to ^3.53.0
|
||||
- - highlight.js from 11.9.0 to 11.10.0
|
||||
- - monaco-editor from ^0.50.0 to ^0.51.0
|
||||
- - pdfjs-dist from ^4.3.136 to ^4.5.136
|
||||
- - @git.zone/tsbuild from ^2.1.82 to ^2.1.84
|
||||
- - @push.rocks/tapbundle from ^5.0.23 to ^5.0.24
|
||||
- - @types/node from ^20.14.9 to ^22.5.2
|
||||
|
||||
## 2024-07-01 - 1.1.6 - fix(dees-dataview-codebox)
|
||||
Corrected the font-family order for better font rendering.
|
||||
|
||||
- Corrected the font-family order in dees-dataview-codebox.ts to ensure 'Intel One Mono' is prioritized over 'Geist Mono'.
|
||||
|
||||
## 2024-07-01 - 1.1.5 - fix(dees-dataview-codebox)
|
||||
Adjusted line number font weight in codebox
|
||||
|
||||
- Changed the line number font weight from 400 to 200 in the codebox for better visual alignment.
|
||||
|
||||
## 2024-07-01 - 1.1.4 - fix(UI)
|
||||
Fixed font-family order for code and value elements
|
||||
|
||||
- Updated font-family order in dees-dataview-codebox.ts
|
||||
- Updated font-family order in dees-dataview-statusobject.ts
|
||||
|
||||
## 2024-07-01 - 1.1.3 - fix(dees-dataview-codebox)
|
||||
Adjusted codebox font weight and font family.
|
||||
|
||||
- Changed font weight from 200 to 400 for better readability.
|
||||
- Updated font-family to include 'Geist Mono' for uniform appearance.
|
||||
|
||||
## 2024-06-30 - 1.1.2 - fix(elements)
|
||||
Fix various UI components and improve styles
|
||||
|
||||
- Updated styles for multiple components for better appearance in dark mode.
|
||||
- Added missing event handling in `dees-input-fileupload` component.
|
||||
- Implemented tooltip support in `dees-label` component.
|
||||
- Fixed alignment issues in `dees-appui-*` components.
|
||||
- Resolved various accessibility issues in form elements.
|
||||
|
||||
## 2024-06-30 - 1.1.1 - fix(build)
|
||||
Fix build script by updating source folders.
|
||||
|
||||
- Updated build script to correctly reference 'tsfolders' folder.
|
||||
|
||||
## 2024-06-30 - 1.1.0 - feat(project dependencies)
|
||||
Updated various dependencies and internal fonts
|
||||
|
||||
- Updated dependencies in package.json with the latest versions.
|
||||
- Updated font-family references from 'Roboto' and 'Inter' to 'Geist Sans' across multiple components.
|
||||
|
||||
## 2024-04-20 - 1.0.289 - Documentation Update
|
||||
Updated the project documentation.
|
||||
|
||||
## 2024-02-05 to 2024-02-13 - 1.0.277 to 1.0.288 - Core Fixes
|
||||
Series of core updates and patches.
|
||||
|
||||
## 2024-01-22 to 2024-02-05 - 1.0.257 to 1.0.276 - Core Fixes
|
||||
More core updates and patches.
|
||||
|
||||
## 2024-01-21 - 1.0.256 to 1.0.257 - Core Fixes
|
||||
Minor bugfixes for core functionalities.
|
||||
|
||||
## 2024-01-18 - 1.0.246 - Core Fixes
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2024-01-15 - 1.0.242 to 1.0.245 - Core Fixes
|
||||
Various core bugfixes.
|
||||
|
||||
## 2024-01-11 - 1.0.241 to 1.0.242 - Core Fixes
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2024-01-10 - 1.0.240 to 1.0.241 - Core Fixes
|
||||
Minor core fixes and improvements.
|
||||
|
||||
## 2024-01-09 - 1.0.239 to 1.0.240 - Core Fixes
|
||||
Core updates and patches.
|
||||
|
||||
## 2023-12-26 - 1.0.238 to 1.0.239 - Core Fixes
|
||||
Updates to core functionalities.
|
||||
|
||||
## 2023-12-20 - 1.0.236 to 1.0.238 - Core Fixes
|
||||
Bugfixes and updates for core functionalities.
|
||||
|
||||
## 2023-12-08 - 1.0.233 to 1.0.236 - Core Fixes
|
||||
Multiple updates to core functionalities.
|
||||
|
||||
## 2023-11-29 - 1.0.231 to 1.0.232 - Core Fixes
|
||||
Minor core patches.
|
||||
|
||||
## 2023-10-31 - 1.0.229 to 1.0.230 - Core Fixes
|
||||
Various updates and fixes for core.
|
||||
|
||||
## 2023-10-24 - 1.0.228 to 1.0.229 - Core Fixes
|
||||
More core updates and patches.
|
||||
|
||||
## 2023-10-23 - 1.0.226 to 1.0.228 - Core Fixes
|
||||
Series of updates and fixes for core functionalities.
|
||||
|
||||
## 2023-10-20 - 1.0.223 to 1.0.225 - Core Fixes
|
||||
Minor updates and fixes for core.
|
||||
|
||||
## 2023-10-18 - 1.0.222 to 1.0.223 - Core Fixes
|
||||
Core patches and bugfixes.
|
||||
|
||||
## 2023-10-17 - 1.0.221 to 1.0.222 - Core Fixes
|
||||
Further updates to core functionalities.
|
||||
|
||||
## 2023-10-12 - 1.0.220 to 1.0.221 - Core Fixes
|
||||
Minor core updates.
|
||||
|
||||
## 2023-10-11 - 1.0.219 to 1.0.220 - Core Fixes
|
||||
Bugfixes and updates for core.
|
||||
|
||||
## 2023-10-07 - 1.0.217 to 1.0.219 - Core Fixes
|
||||
Series of core updates and patches.
|
||||
|
||||
## 2023-10-05 - 1.0.216 to 1.0.217 - Core Fixes
|
||||
Minor updates for core functionalities.
|
||||
|
||||
## 2023-09-22 - 1.0.212 to 1.0.215 - Core Fixes
|
||||
Various updates and fixes for core.
|
||||
|
||||
## 2023-09-20 - 1.0.210 to 1.0.211 - Core Fixes
|
||||
Series of core patches.
|
||||
|
||||
## 2023-09-18 - 1.0.209 to 1.0.210 - Core Fixes
|
||||
Bugfixes and improvements to core.
|
||||
|
||||
## 2023-09-17 - 1.0.207 to 1.0.209 - Core Fixes
|
||||
Minor updates and fixes.
|
||||
|
||||
## 2023-09-15 - 1.0.203 to 1.0.206 - Core Fixes
|
||||
Series of core improvements and fixes.
|
||||
|
||||
## 2023-09-14 - 1.0.199 to 1.0.202 - Core Fixes
|
||||
Patches for core functionalities.
|
||||
|
||||
## 2023-09-13 - 1.0.194 to 1.0.198 - Core Fixes
|
||||
Various bugfixes and updates for core.
|
||||
|
||||
## 2023-09-12 - 1.0.190 to 1.0.193 - Core Fixes
|
||||
Minor core patches.
|
||||
|
||||
## 2023-09-09 - 1.0.188 to 1.0.189 - Core Fixes
|
||||
Updates and bugfixes for core.
|
||||
|
||||
## 2023-09-07 - 1.0.186 to 1.0.187 - Core Fixes
|
||||
Core patches and improvements.
|
||||
|
||||
## 2023-09-04 - 1.0.184 to 1.0.185 - Core Fixes
|
||||
Series of updates and bugfixes for core.
|
||||
|
||||
## 2023-09-01 - 1.0.180 to 1.0.183 - Core Fixes
|
||||
Various core updates and fixes.
|
||||
|
||||
## 2023-08-28 - 1.0.177 to 1.0.179 - Core Fixes
|
||||
Minor bugfixes and updates.
|
||||
|
||||
## 2023-08-26 - 1.0.176 to 1.0.177 - Core Fixes
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2023-08-20 - 1.0.173 to 1.0.175 - Core Fixes
|
||||
Series of updates and fixes for core.
|
||||
|
||||
## 2023-08-08 - 1.0.172 to 1.0.173 - Core Fixes
|
||||
Minor core updates.
|
||||
|
||||
## 2023-08-07 - 1.0.171 to 1.0.172 - Core Fixes
|
||||
Various updates to core.
|
||||
|
||||
## 2023-04-12 - 1.0.169 to 1.0.170 - Core Fixes
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2023-04-10 - 1.0.163 to 1.0.168 - Core Fixes
|
||||
Series of core updates and patches.
|
||||
|
||||
## 2023-04-07 - 1.0.160 to 1.0.162 - Core Fixes
|
||||
Various updates for core functionalities.
|
||||
|
||||
## 2023-04-06 - 1.0.155 to 1.0.159 - Core Fixes
|
||||
Bugfixes and improvements to core.
|
||||
|
||||
## 2023-03-27 - 1.0.150 to 1.0.154 - Core Fixes
|
||||
Minor updates and patches for core functionalities.
|
||||
|
||||
## 2023-03-09 - 1.0.149 to 1.0.150 - Core Fixes
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2023-01-17 - 1.0.146 to 1.0.148 - Core Fixes
|
||||
Updates and fixes for core.
|
||||
|
||||
## 2023-01-16 - 1.0.144 to 1.0.145 - Core Fixes
|
||||
Series of updates and bugfixes for core.
|
||||
|
||||
## 2023-01-13 - 1.0.134 to 1.0.143 - Core Fixes
|
||||
Various core patches and improvements.
|
||||
|
||||
## 2023-01-12 - 1.0.132 to 1.0.133 - Core Fixes
|
||||
Minor fixes for core functionalities.
|
||||
|
||||
## 2023-01-11 - 1.0.126 to 1.0.131 - Core Fixes
|
||||
Series of updates and fixes for core.
|
||||
|
||||
## 2023-01-09 - 1.0.122 to 1.0.123 - Core Fixes
|
||||
Minor core patches and updates.
|
||||
|
||||
## 2023-01-07 - 1.0.113 to 1.0.121 - Core Fixes
|
||||
Updates and bugfixes for core functionalities.
|
||||
|
||||
## 2023-01-03 - 1.0.107 to 1.0.110 - Core Fixes
|
||||
Series of core updates and fixes.
|
||||
|
||||
## 2022-12-12 - 1.0.105 to 1.0.106 - Core Fixes
|
||||
Various core patches.
|
||||
|
||||
## 2022-12-07 - 1.0.104 to 1.0.105 - Core Fixes
|
||||
Bugfixes and improvements to core functionalities.
|
||||
|
||||
## 2022-12-06 - 1.0.103 to 1.0.104 - Core Fixes
|
||||
Series of updates and patches for core.
|
||||
|
||||
## 2022-08-18 - 1.0.98 to 1.0.102 - Core Fixes
|
||||
Updates and bugfixes for core functionalities.
|
||||
|
||||
## 2022-07-14 - 1.0.93 to 1.0.94 - Core Fixes
|
||||
Minor core updates.
|
||||
|
||||
## 2022-06-26 - 1.0.92 to 1.0.93 - Core Fixes
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2022-06-10 - 1.0.91 to 1.0.92 - Core Fixes
|
||||
Updates and patches for core.
|
||||
|
||||
## 2022-05-30 - 1.0.90 to 1.0.91 - Core Fixes
|
||||
Minor updates and bugfixes.
|
||||
|
||||
## 2022-05-26 - 1.0.87 to 1.0.89 - Core Fixes
|
||||
Series of core updates and patches.
|
||||
|
||||
## 2022-05-24 to 2022-05-20 - 1.0.83 to 1.0.86 - Core Fixes
|
||||
Various updates and bugfixes for core functionalities.
|
||||
|
||||
## 2022-05-03 - 1.0.79 to 1.0.80 - Core Fixes
|
||||
Updates and patches for core functionalities.
|
||||
|
||||
## 2022-03-18 - 1.0.74 to 1.0.77 - Core Fixes
|
||||
Bugfixes and updates for core functionalities.
|
||||
|
||||
## 2022-01-24 - 1.0.73 to 1.0.74 - Core Fixes
|
||||
Minor core patches.
|
||||
|
||||
## 2021-12-10 - 1.0.71 to 1.0.72 - Core Fixes
|
||||
Series of updates and fixes for core functionalities.
|
||||
|
||||
## 2021-11-27 - 1.0.70 to 1.0.71 - Core Fixes
|
||||
Bugfixes and improvements to core.
|
||||
|
||||
## 2021-10-08 - 1.0.68 to 1.0.69 - Core Fixes
|
||||
Various core updates and patches.
|
||||
|
||||
## 2021-10-07 - 1.0.67 to 1.0.68 - Core Fixes
|
||||
Minor updates and improvements for core.
|
||||
|
||||
## 2021-09-15 - 1.0.64 to 1.0.66 - Core Fixes
|
||||
Bugfixes and improvements to core functionalities.
|
||||
|
||||
## 2021-09-14 - 1.0.61 to 1.0.62 - Core Fixes
|
||||
Minor patches and updates for core.
|
||||
|
||||
## 2021-09-10 - 1.0.57 to 1.0.60 - Core Fixes
|
||||
Series of updates and fixes for core functionalities.
|
||||
|
||||
## 2021-09-09 - 1.0.54 to 1.0.56 - Core Fixes
|
||||
Various core updates and patches.
|
||||
|
||||
## 2021-09-01 - 1.0.51 to 1.0.53 - Core Fixes
|
||||
Minor updates for core functionalities.
|
||||
|
||||
## 2021-08-29 - 1.0.49 to 1.0.50 - Core Fixes
|
||||
Bugfixes and patches for core.
|
||||
|
||||
## 2021-08-27 - 1.0.44 to 1.0.48 - Core Fixes
|
||||
Series of updates and improvements for core functionalities.
|
||||
|
||||
## 2021-08-26 - 1.0.42 to 1.0.43 - Core Fixes
|
||||
Minor updates and bugfixes for core.
|
||||
|
||||
## 2021-08-25 - 1.0.38 to 1.0.41 - Core Fixes
|
||||
Extracted series of core updates and patches.
|
||||
|
||||
## 2021-08-24 - 1.0.37 - Core Fix
|
||||
Bugfixes for core functionalities.
|
||||
|
||||
## 2021-08-20 - 1.0.34 to 1.0.36 - Core Fixes
|
||||
Core functionalities updates and patches.
|
||||
|
||||
## 2021-05-05 - 1.0.32 to 1.0.33 - Core Fixes
|
||||
Series of core bugfixes.
|
||||
|
||||
## 2021-03-06 - 1.0.29 to 1.0.31 - Core Fixes
|
||||
Updates and improvements for core functionalities.
|
||||
|
||||
## 2021-02-13 - 1.0.27 to 1.0.28 - Core Fixes
|
||||
Bugfixes and patches for core functionalities.
|
||||
|
||||
## 2020-12-10 - 1.0.25 to 1.0.26 - Core Fixes
|
||||
Various core updates and improvements.
|
||||
|
||||
## 2020-12-02 - 1.0.23 to 1.0.24 - Core Fixes
|
||||
Minor updates for core functionalities.
|
||||
|
||||
## 2020-12-01 - 1.0.21 to 1.0.22 - Core Fixes
|
||||
Bugfixes and patches for core.
|
||||
|
||||
## 2020-09-13 - 1.0.19 to 1.0.20 - Core Fixes
|
||||
Updates and bugfixes for core functionalities.
|
||||
|
||||
## 2020-05-11 - 1.0.17 to 1.0.18 - Core Fixes
|
||||
Core functionalities updates.
|
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.
|
@ -2,17 +2,46 @@
|
||||
"gitzone": {
|
||||
"projectType": "wcc",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "design.estate",
|
||||
"gitrepo": "dees-catalog",
|
||||
"description": "a webcomponents catalog for handling daily stuff on the web",
|
||||
"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"
|
||||
"projectDomain": "design.estate",
|
||||
"keywords": [
|
||||
"Web Components",
|
||||
"User Interface",
|
||||
"UI Library",
|
||||
"Component Library",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"Dynamic Components",
|
||||
"Modular Architecture",
|
||||
"Reusable Components",
|
||||
"Web Development",
|
||||
"Application UI",
|
||||
"Custom Elements",
|
||||
"Shadow DOM",
|
||||
"UI Elements",
|
||||
"Dashboard Interfaces",
|
||||
"Form Handling",
|
||||
"Data Display",
|
||||
"Visualization",
|
||||
"Charting",
|
||||
"Interactive Components",
|
||||
"Responsive Design",
|
||||
"Web Applications",
|
||||
"Modern Web",
|
||||
"Frontend Development"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
79
package.json
79
package.json
@ -1,47 +1,49 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "1.0.267",
|
||||
"version": "1.9.0",
|
||||
"private": false,
|
||||
"description": "website for lossless.com",
|
||||
"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",
|
||||
"scripts": {
|
||||
"test": "tstest test/ --web",
|
||||
"build": "tsbuild element --allowimplicitany && tsbundle element --production",
|
||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
||||
"watch": "tswatch element",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.0.57",
|
||||
"@design.estate/dees-element": "^2.0.33",
|
||||
"@design.estate/dees-wcctools": "^1.0.87",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@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.3",
|
||||
"@push.rocks/smartstring": "^4.0.13",
|
||||
"@tsclass/tsclass": "^4.0.46",
|
||||
"@webcontainer/api": "^1.1.8",
|
||||
"highlight.js": "11.9.0",
|
||||
"ibantools": "^4.3.9",
|
||||
"monaco-editor": "^0.45.0",
|
||||
"pdfjs-dist": "^4.0.379",
|
||||
"@push.rocks/smartpromise": "^4.2.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@webcontainer/api": "1.2.0",
|
||||
"apexcharts": "^4.7.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"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.72",
|
||||
"@git.zone/tsbundle": "^2.0.15",
|
||||
"@git.zone/tstest": "^1.0.86",
|
||||
"@git.zone/tswatch": "^2.0.21",
|
||||
"@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.0.15",
|
||||
"@types/node": "^20.11.5"
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.0.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@ -57,5 +59,32 @@
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
]
|
||||
],
|
||||
"keywords": [
|
||||
"Web Components",
|
||||
"User Interface",
|
||||
"UI Library",
|
||||
"Component Library",
|
||||
"JavaScript",
|
||||
"TypeScript",
|
||||
"Dynamic Components",
|
||||
"Modular Architecture",
|
||||
"Reusable Components",
|
||||
"Web Development",
|
||||
"Application UI",
|
||||
"Custom Elements",
|
||||
"Shadow DOM",
|
||||
"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"
|
||||
}
|
||||
|
13669
pnpm-lock.yaml
generated
13669
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
|
79
readme.hints.md
Normal file
79
readme.hints.md
Normal file
@ -0,0 +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`
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '1.0.267',
|
||||
description: 'website for lossless.com'
|
||||
version: '1.9.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
248
ts_web/elements/dees-appui-activitylog.ts
Normal file
248
ts_web/elements/dees-appui-activitylog.ts
Normal file
@ -0,0 +1,248 @@
|
||||
import * as plugins from './00plugins.js';
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
|
||||
@customElement('dees-appui-activitylog')
|
||||
export class DeesAppuiActivitylog extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = () => html`<dees-appui-activitylog></dees-appui-activitylog>`;
|
||||
|
||||
// INSTANCE
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#111c28')};
|
||||
font-family: 'Intel One Mono', sans-serif;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
cursor: default;
|
||||
}
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
padding: 0px 12px 0px 12px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.topbar .heading {
|
||||
text-align: left;
|
||||
line-height: 24px;
|
||||
padding-top: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
}
|
||||
|
||||
.activityContainer {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
bottom: 40px;
|
||||
width: 100%;
|
||||
padding: 8px 0px;
|
||||
overflow-y: scroll;
|
||||
|
||||
}
|
||||
|
||||
.streamingIndicator {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
color: ${cssManager.bdTheme('#666', '#888')}
|
||||
}
|
||||
|
||||
.streamingIndicator.bottom {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.activityentry {
|
||||
min-height: 30px;
|
||||
font-size: 12px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px dotted ${cssManager.bdTheme('#00000020', '#ffffff20')};
|
||||
}
|
||||
|
||||
.activityentry:last-of-type {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.activityentry:hover {
|
||||
background: ${cssManager.bdTheme('#00000005', '#00000080')};
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: ${cssManager.bdTheme('#e57373', '#ff8787')};
|
||||
}
|
||||
|
||||
.searchbox {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0e151f')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
.searchbox input {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 32px;
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
font-family: 'Intel One Mono', sans-serif;
|
||||
}
|
||||
|
||||
.searchbox input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.bottomShadow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
bottom: 40px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(180deg, #f8f8f800 0%, #ffffff 100%)',
|
||||
'linear-gradient(180deg, #111c2800 0%, #0e151f 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
}
|
||||
.topShadow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
top: 32px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(0deg, #f8f8f800 0%, #ffffff 100%)',
|
||||
'linear-gradient(0deg, #111c2800 0%, #0e151f 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style></style>
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<div class="heading">Activity Log</div>
|
||||
</div>
|
||||
<div class="activityContainer">
|
||||
<div class="streamingIndicator">streaming...</div>
|
||||
<div class="activityentry" @contextmenu=${async eventArg => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'app settings',
|
||||
action: async () => {},
|
||||
},
|
||||
{
|
||||
name: 'account settings',
|
||||
action: async () => {},
|
||||
},
|
||||
{
|
||||
name: 'logout',
|
||||
action: async () => {},
|
||||
},
|
||||
]);
|
||||
}}>
|
||||
<span class="timestamp">22:01:</span> Max Mustermann logged in
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:02:</span> Max Mustermann viewed an invoice
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:03:</span> Max Mustermann added a new contact
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:04:</span> Max Mustermann updated account settings
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:05:</span> Max Mustermann logged out
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:06:</span> Max Mustermann logged in
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:07:</span> Max Mustermann created a new invoice
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:08:</span> Max Mustermann sent an invoice
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:09:</span> Max Mustermann viewed reports
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:10:</span> Max Mustermann logged out
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:11:</span> Max Mustermann logged in
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:12:</span> Max Mustermann deleted an invoice
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:13:</span> Max Mustermann contacted support
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:14:</span> Max Mustermann added a new user
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:15:</span> Max Mustermann changed password
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:16:</span> Max Mustermann logged out
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:17:</span> Max Mustermann logged in
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:18:</span> Max Mustermann archived an invoice
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:19:</span> Max Mustermann approved a payment
|
||||
</div>
|
||||
<div class="activityentry">
|
||||
<span class="timestamp">22:20:</span> Max Mustermann logged out
|
||||
</div>
|
||||
<div class="streamingIndicator bottom">loading more...</div>
|
||||
</div>
|
||||
<div class="searchbox">
|
||||
<input type="text" placeholder="Search" />
|
||||
</div>
|
||||
<div class="topShadow"></div>
|
||||
<div class="bottomShadow"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
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>
|
||||
`;
|
||||
};
|
703
ts_web/elements/dees-appui-appbar.ts
Normal file
703
ts_web/elements/dees-appui-appbar.ts
Normal file
@ -0,0 +1,703 @@
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
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 = 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;
|
||||
width: 100%;
|
||||
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;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
position: relative;
|
||||
line-height: 24px;
|
||||
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: ${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;
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// INSTANCE
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="menus">
|
||||
${this.showWindowControls ? html`<dees-windowcontrols></dees-windowcontrols>` : ''}
|
||||
${this.renderMenuItems()}
|
||||
</div>
|
||||
<div class="breadcrumbs">
|
||||
${this.renderBreadcrumbs()}
|
||||
</div>
|
||||
<div class="account">
|
||||
${this.renderAccountSection()}
|
||||
</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>
|
||||
`;
|
||||
};
|
219
ts_web/elements/dees-appui-base.ts
Normal file
219
ts_web/elements/dees-appui-base.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
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 = 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,
|
||||
css`
|
||||
:host {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
||||
}
|
||||
.maingrid {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
height: calc(100% - 40px);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 240px 1fr 240px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// INSTANCE
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<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
|
||||
.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
|
||||
}));
|
||||
}
|
||||
}
|
121
ts_web/elements/dees-appui-maincontent.ts
Normal file
121
ts_web/elements/dees-appui-maincontent.ts
Normal file
@ -0,0 +1,121 @@
|
||||
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';
|
||||
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
|
||||
.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: '⚠️ Please set tabs', action: () => console.warn('No tabs configured for maincontent') },
|
||||
];
|
||||
|
||||
@property({ type: Object })
|
||||
public selectedTab: interfaces.ITab | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
}
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<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 class="content-area">
|
||||
<slot></slot>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
160
ts_web/elements/dees-appui-mainmenu.ts
Normal file
160
ts_web/elements/dees-appui-mainmenu.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import * as plugins from './00plugins.js';
|
||||
import * as interfaces from './interfaces/index.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesContextmenu } from './dees-contextmenu.js';
|
||||
|
||||
/**
|
||||
* the most left menu
|
||||
* usually used as organization selector
|
||||
*/
|
||||
@customElement('dees-appui-mainmenu')
|
||||
export class DeesAppuiMainmenu extends DeesElement {
|
||||
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({ type: Array })
|
||||
public tabs: interfaces.ITab[] = [
|
||||
{ key: '⚠️ Please set tabs', iconName: 'alertTriangle', action: () => console.warn('No tabs configured for mainmenu') },
|
||||
];
|
||||
|
||||
@property()
|
||||
public selectedTab: interfaces.ITab;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
.mainContainer {
|
||||
--menuSize: 60px;
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
z-index: 10;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: var(--menuSize);
|
||||
height: 100%;
|
||||
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 ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding-top: 18px;
|
||||
font-size: 24px;
|
||||
width: var(--menuSize);
|
||||
height: var(--menuSize);
|
||||
text-align: center;
|
||||
transition: color 0.1s, background 0.2s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.15)')};
|
||||
}
|
||||
|
||||
.tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
}
|
||||
|
||||
.tabIndicator {
|
||||
opacity: 0;
|
||||
background: ${cssManager.bdTheme('#2196f3', '#4e729a')};
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: calc((var(--menuSize) / 3) * 2);
|
||||
left: 0px;
|
||||
top: calc(var(--menuSize) - (((var(--menuSize) / 3) * 2) / 2));
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainContainer" @contextmenu=${(eventArg) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [{
|
||||
name: 'app settings',
|
||||
action: async () => {},
|
||||
iconName: 'gear',
|
||||
}])
|
||||
}}>
|
||||
<div class="tabsContainer">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : null}"
|
||||
@click="${() => {
|
||||
this.updateTab(tabArg);
|
||||
}}"
|
||||
>
|
||||
<dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="tabIndicator"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private updateTabIndicator() {
|
||||
let selectedTab = this.selectedTab;
|
||||
if (!selectedTab) {
|
||||
selectedTab = this.tabs[0];
|
||||
}
|
||||
const tabIndex = this.tabs.indexOf(selectedTab);
|
||||
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))`;
|
||||
}
|
||||
|
||||
updateTab(tabArg: interfaces.ITab) {
|
||||
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() {
|
||||
this.updateTab(this.tabs[0]);
|
||||
}
|
||||
}
|
211
ts_web/elements/dees-appui-mainselector.ts
Normal file
211
ts_web/elements/dees-appui-mainselector.ts
Normal file
@ -0,0 +1,211 @@
|
||||
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,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
/**
|
||||
* the property selector menu
|
||||
* mainly used to select assets within in an organization
|
||||
*/
|
||||
@customElement('dees-appui-mainselector')
|
||||
export class DeesAppuiMainselector extends DeesElement {
|
||||
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({ type: Array })
|
||||
public selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = [
|
||||
{ key: '⚠️ Please set selection options', action: () => console.warn('No selection options configured for mainselector') },
|
||||
];
|
||||
|
||||
@property()
|
||||
public selectedOption: interfaces.ISelectionOption = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#000000')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
}
|
||||
|
||||
.topbar .heading {
|
||||
padding-left: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.selectionOptions {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption {
|
||||
cursor: default;
|
||||
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 {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
|
||||
}
|
||||
|
||||
.selectionOptions .selectionOption.selectedOption {
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<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 === option
|
||||
? 'selectedOption'
|
||||
: null}"
|
||||
@click="${() => {
|
||||
this.selectOption(option);
|
||||
}}"
|
||||
@contextmenu="${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'property settings',
|
||||
action: async () => {},
|
||||
iconName: 'gear',
|
||||
},
|
||||
]);
|
||||
}}"
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
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,23 +55,57 @@ 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`
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Roboto', 'monospace';
|
||||
font-family: 'Geist Sans', 'monospace';
|
||||
}
|
||||
:host([hidden]) {
|
||||
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;
|
||||
@ -84,7 +119,7 @@ export class DeesButton extends DeesElement {
|
||||
border: 1px solid ${cssManager.bdTheme('#eee', '#333')};
|
||||
border-top: ${cssManager.bdTheme('1px solid #eee', '1px solid #444')};
|
||||
border-radius: 4px;
|
||||
line-height: 40px;
|
||||
height: 40px;
|
||||
padding: 0px 8px;
|
||||
min-width: 100px;
|
||||
user-select: none;
|
||||
@ -104,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;
|
||||
@ -132,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;
|
||||
}
|
||||
@ -180,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>
|
||||
`;
|
||||
}
|
||||
@ -201,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
|
||||
}
|
||||
}
|
||||
|
483
ts_web/elements/dees-chart-area.demo.ts
Normal file
483
ts_web/elements/dees-chart-area.demo.ts
Normal file
@ -0,0 +1,483 @@
|
||||
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;
|
||||
height: 100%;
|
||||
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
|
||||
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>
|
||||
`;
|
||||
};
|
531
ts_web/elements/dees-chart-area.ts
Normal file
531
ts_web/elements/dees-chart-area.ts
Normal file
@ -0,0 +1,531 @@
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
cssManager,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-chart-area.demo.js';
|
||||
|
||||
import ApexCharts from 'apexcharts';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-chart-area': DeesChartArea;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-chart-area')
|
||||
export class DeesChartArea extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
// instance
|
||||
@state()
|
||||
public chart: ApexCharts;
|
||||
|
||||
@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) => {
|
||||
// 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.chart) {
|
||||
this.resizeChart();
|
||||
}
|
||||
}
|
||||
}, 100); // 100ms debounce
|
||||
});
|
||||
|
||||
this.registerStartupFunction(async () => {
|
||||
this.updateComplete.then(() => {
|
||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
||||
if (mainbox) {
|
||||
this.resizeObserver.observe(mainbox);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.registerGarbageFunction(async () => {
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
this.resizeObserver.disconnect();
|
||||
this.stopAutoScroll();
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: #ccc;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
.chartContainer {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 32px 16px 16px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="chartTitle">${this.label}</div>
|
||||
<div class="chartContainer"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.domtoolsPromise;
|
||||
|
||||
// Wait for next animation frame to ensure layout is complete
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
// Get actual dimensions of the container
|
||||
const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox');
|
||||
const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer');
|
||||
|
||||
if (!mainbox || !chartContainer) {
|
||||
console.error('Chart containers not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate initial dimensions
|
||||
const styleChartContainer = window.getComputedStyle(chartContainer);
|
||||
const paddingTop = parseInt(styleChartContainer.paddingTop, 10);
|
||||
const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10);
|
||||
const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10);
|
||||
const paddingRight = parseInt(styleChartContainer.paddingRight, 10);
|
||||
|
||||
const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||
const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||
|
||||
// Use provided series data or default demo data
|
||||
const chartSeries = this.series.length > 0 ? this.series : [
|
||||
{
|
||||
name: 'cpu',
|
||||
data: [
|
||||
{ x: '2025-01-15T03:00:00', y: 25 },
|
||||
{ x: '2025-01-15T07:00:00', y: 30 },
|
||||
{ x: '2025-01-15T11:00:00', y: 20 },
|
||||
{ x: '2025-01-15T15:00:00', y: 35 },
|
||||
{ x: '2025-01-15T19:00:00', y: 25 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
data: [
|
||||
{ x: '2025-01-15T03:00:00', y: 10 },
|
||||
{ x: '2025-01-15T07:00:00', y: 12 },
|
||||
{ x: '2025-01-15T11:00:00', y: 10 },
|
||||
{ x: '2025-01-15T15:00:00', y: 30 },
|
||||
{ x: '2025-01-15T19:00:00', y: 40 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Store internal data
|
||||
this.internalChartData = chartSeries;
|
||||
|
||||
var options: ApexCharts.ApexOptions = {
|
||||
series: chartSeries,
|
||||
chart: {
|
||||
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,
|
||||
},
|
||||
stroke: {
|
||||
width: 1,
|
||||
curve: 'smooth',
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime', // Time-series data
|
||||
labels: {
|
||||
format: 'HH:mm:ss', // Time formatting with seconds
|
||||
datetimeUTC: false,
|
||||
style: {
|
||||
colors: '#9e9e9e', // Label color
|
||||
fontSize: '11px',
|
||||
},
|
||||
},
|
||||
axisBorder: {
|
||||
show: false, // Hide x-axis border
|
||||
},
|
||||
axisTicks: {
|
||||
show: false, // Hide x-axis ticks
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
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: {
|
||||
lines: {
|
||||
show: true, // This enables the grid lines along the x-axis
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
column: {
|
||||
colors: [], // For vertical column bands, not needed here but available for customization
|
||||
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() {
|
||||
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 styleChartContainer = window.getComputedStyle(chartContainer);
|
||||
|
||||
// Extract padding values
|
||||
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 = mainbox.clientWidth - paddingLeft - paddingRight;
|
||||
const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom;
|
||||
|
||||
await this.chart.updateOptions({
|
||||
chart: {
|
||||
width: actualWidth,
|
||||
height: actualHeight,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
170
ts_web/elements/dees-chart-log.demo.ts
Normal file
170
ts_web/elements/dees-chart-log.demo.ts
Normal file
@ -0,0 +1,170 @@
|
||||
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;
|
||||
background: #000000;
|
||||
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=${'Production Server Logs'}
|
||||
></dees-chart-log>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
320
ts_web/elements/dees-chart-log.ts
Normal file
320
ts_web/elements/dees-chart-log.ts
Normal file
@ -0,0 +1,320 @@
|
||||
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-chart-log.demo.js';
|
||||
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-chart-log': DeesChartLog;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@property()
|
||||
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 Mono', 'Consolas', 'Monaco', monospace;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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="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() {
|
||||
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(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>
|
||||
<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[]}
|
||||
></dees-contextmenu>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
display: block;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
@ -142,7 +142,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
.lineNumbers {
|
||||
line-height: 1.4em;
|
||||
font-weight: 200;
|
||||
font-family: 'Intel One Mono', 'monospace';
|
||||
font-family: 'Intel One Mono', 'Geist Mono', 'monospace';
|
||||
}
|
||||
|
||||
.hljs-string {
|
||||
|
@ -133,7 +133,7 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
|
||||
.detail .detailsText .value {
|
||||
font-size: 14px;
|
||||
font-family: 'Intel One Mono';
|
||||
font-family: 'Intel One Mono', 'Geist Mono';
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@ -50,7 +50,7 @@ export class DeesEditor extends DeesElement {
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
}
|
||||
|
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 () => {
|
||||
@ -101,15 +125,38 @@ export class DeesForm extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* collects the form data
|
||||
* @returns
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@ -198,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-input-dropdown
|
||||
<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' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
<dees-input-dropdown
|
||||
.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' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
<div style="height: 300px"></div>
|
||||
<dees-input-dropdown
|
||||
.selectedOption=${{ option: 'Medium', key: 'medium' }}
|
||||
></dees-input-dropdown>
|
||||
</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>
|
||||
|
||||
<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;
|
||||
@ -142,7 +114,7 @@ export class DeesInputFileupload extends DeesElement {
|
||||
text-align: left;
|
||||
border-radius: 3px;
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
cursor: default;
|
||||
transition: all 0.2s;
|
||||
border-top: 1px solid #ffffff10;
|
||||
@ -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 () {
|
||||
public increase() {
|
||||
if (!this.disabled) {
|
||||
this.value++;
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public decrease () {
|
||||
if (this.value > 0) {
|
||||
this.value--;
|
||||
} else {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
public decrease() {
|
||||
if (!this.disabled && this.value > 0) {
|
||||
this.value--;
|
||||
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 {
|
||||
@ -176,16 +144,18 @@ export class DeesInputText extends DeesElement {
|
||||
return html`
|
||||
<style>
|
||||
input {
|
||||
font-family: ${this.isPasswordBool ? 'monospace' : 'Roboto'};
|
||||
font-family: ${this.isPasswordBool ? 'monospace' : 'Geist Sans'};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export class DeesMobilenavigation extends DeesElement {
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
padding: 8px 0px;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-weight: 300;
|
||||
border-bottom: 1px dashed #444;
|
||||
margin-top: 16px;
|
||||
|
@ -71,7 +71,7 @@ export class DeesModal extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
will-change: transform;
|
||||
}
|
||||
@ -113,7 +113,7 @@ export class DeesModal extends DeesElement {
|
||||
|
||||
.modal .heading {
|
||||
height: 32px;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
|
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>
|
||||
`;
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ export class DeesPdf extends DeesElement {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
max-width: 800px;
|
||||
|
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,5 +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`
|
||||
<dees-simple-appdash>Hello there</dees-simple-appdash>
|
||||
<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: 'Dashboard',
|
||||
iconName: 'home',
|
||||
element: DemoViewDashboard,
|
||||
},
|
||||
{
|
||||
name: 'Analytics',
|
||||
iconName: 'lineChart',
|
||||
element: DemoViewAnalytics,
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'settings',
|
||||
element: DemoViewSettings,
|
||||
}
|
||||
] as IView[]}
|
||||
@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 {
|
||||
@ -22,6 +22,12 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IView {
|
||||
name: string;
|
||||
iconName?: string;
|
||||
element: DeesElement['constructor']['prototype'];
|
||||
}
|
||||
|
||||
@customElement('dees-simple-appdash')
|
||||
export class DeesSimpleAppDash extends DeesElement {
|
||||
// STATIC
|
||||
@ -29,11 +35,17 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
// INSTANCE
|
||||
|
||||
@property()
|
||||
public name = 'Dees Simple Login';
|
||||
public name: string = 'Application Dashboard';
|
||||
|
||||
@property({ type: Array })
|
||||
public viewTabs: IView[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public terminalSetupCommand: string = `echo "Terminal ready"`;
|
||||
|
||||
@state()
|
||||
private selectedView: IView;
|
||||
|
||||
@property()
|
||||
public views: Array<{ name: string; icon: string; viewFunction: () => Promise<TemplateResult> }> =
|
||||
[];
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
@ -42,49 +54,143 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
user-select: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.appbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#ccc', '#ffffff10')};
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
padding: 0px 16px;
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.appbar {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: calc(100% - 24px);
|
||||
width: 240px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#000')};
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
font-size: 12px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
z-index: 2;
|
||||
box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.8);
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr auto;
|
||||
grid-template-rows: auto min-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.appName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
white-space: nowrap;
|
||||
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;
|
||||
top: 32px;
|
||||
height: calc(100vh - 32px - 24px);
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
height: calc(100% - 24px);
|
||||
bottom: 24px;
|
||||
width: 100%;
|
||||
width: calc(100% - 240px);
|
||||
overflow: auto;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000')};
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.controlbar {
|
||||
@ -93,42 +199,87 @@ 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;
|
||||
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;
|
||||
}
|
||||
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
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 class="viewTabs"></div>
|
||||
</div>
|
||||
<div class="viewTabs-container">
|
||||
<div class="viewTabs">
|
||||
${this.viewTabs.map(
|
||||
(view) => html`
|
||||
<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">Logout</div>
|
||||
<div class="action" @click=${() => {
|
||||
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">
|
||||
<slot></slot>
|
||||
<!-- 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>
|
||||
`;
|
||||
@ -137,20 +288,75 @@ 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 appcontent = this.shadowRoot.querySelector('.appcontent');
|
||||
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 = '10';
|
||||
terminal.style.top = '0px';
|
||||
terminal.style.left = '0px';
|
||||
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';
|
||||
appcontent.appendChild(terminal);
|
||||
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');
|
||||
const view = new viewArg.element();
|
||||
if (this.currentView) {
|
||||
this.currentView.remove();
|
||||
}
|
||||
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,19 +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.switchToSlottedContent();
|
||||
this.dispatchEvent(new CustomEvent('login', {
|
||||
detail: event.detail,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* allows switching to slotted content
|
||||
@ -124,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')};
|
||||
@ -174,9 +174,10 @@ export class DeesStepper extends DeesElement {
|
||||
.step .title {
|
||||
text-align: center;
|
||||
padding-top: 50px;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-size: 25px;
|
||||
font-weight: 300;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
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';
|
||||
@ -171,7 +171,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
css`
|
||||
.mainbox {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
padding: 16px;
|
||||
@ -190,7 +190,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
|
||||
.headingContainer {
|
||||
@ -289,7 +289,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
|
||||
th {
|
||||
text-transform: none;
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
th,
|
||||
@ -366,7 +366,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-family: 'Roboto', 'Inter', sans-serif;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#111', '#ffffff90')};
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#00000050')};
|
||||
@ -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,15 +22,39 @@ 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 @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();
|
||||
domtools.DomTools.setupDomTools();
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
// Handle the resize event
|
||||
console.log(`Terminal Resized`);
|
||||
this.handleResize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 20px;
|
||||
@ -231,6 +255,7 @@ export class DeesTerminal extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private fitAddon: FitAddon;
|
||||
public async firstUpdated(
|
||||
_changedProperties: Map<string | number | symbol, unknown>
|
||||
): Promise<void> {
|
||||
@ -242,14 +267,14 @@ export class DeesTerminal extends DeesElement {
|
||||
convertEol: true,
|
||||
cursorBlink: true,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
this.fitAddon = new FitAddon();
|
||||
term.loadAddon(this.fitAddon);
|
||||
|
||||
// Open the terminal in #terminal-container
|
||||
term.open(container);
|
||||
|
||||
// Make the terminal's size and geometry fit the size of #terminal-container
|
||||
fitAddon.fit();
|
||||
this.fitAddon.fit();
|
||||
|
||||
term.write(`dees-terminal custom terminal. \r\n$ `);
|
||||
|
||||
@ -268,7 +293,63 @@ export class DeesTerminal extends DeesElement {
|
||||
term.onData((data) => {
|
||||
input.write(data);
|
||||
});
|
||||
await domtools.convenience.smartdelay.delayFor(5000);
|
||||
input.write(`pnpm add isomorphic-git @git.zone/tsbuild\n`);
|
||||
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);
|
||||
await this.waitForPrompt(term, '~/');
|
||||
input.write(`clear && echo 'welcome'\n`);
|
||||
this.webcontainerDeferred.resolve(webcontainerInstance);
|
||||
}
|
||||
|
||||
async connectedCallback(): Promise<void> {
|
||||
await super.connectedCallback();
|
||||
this.resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
async disconnectedCallback(): Promise<void> {
|
||||
this.resizeObserver.unobserve(this);
|
||||
await super.disconnectedCallback();
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.fitAddon.fit();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
setTimeout(checkPrompt, 100); // check every 100 ms
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
export * from './dees-appui-activitylog.js';
|
||||
export * from './dees-appui-appbar.js';
|
||||
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';
|
||||
export * from './dees-chips.js';
|
||||
export * from './dees-contextmenu.js';
|
||||
export * from './dees-dataview-codebox.js';
|
||||
@ -9,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';
|
||||
@ -25,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';
|
||||
@ -37,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;
|
||||
}
|
3
ts_web/elements/interfaces/index.ts
Normal file
3
ts_web/elements/interfaces/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './tab.js';
|
||||
export * from './selectionoption.js';
|
||||
export * from './appbarmenuitem.js';
|
5
ts_web/elements/interfaces/selectionoption.ts
Normal file
5
ts_web/elements/interfaces/selectionoption.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ISelectionOption {
|
||||
key: string;
|
||||
iconName?: string;
|
||||
action: () => void;
|
||||
}
|
5
ts_web/elements/interfaces/tab.ts
Normal file
5
ts_web/elements/interfaces/tab.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ITab {
|
||||
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