Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb3d396c68 | |||
| 13ba5670f0 | |||
| 961b811b7a | |||
| cd491e1517 | |||
| b8a03def79 | |||
| 2b6798083d | |||
| 3c7b5dc690 | |||
| 2f4afddf73 | |||
| 212a46894e | |||
| 653ef109be | |||
| a0b17132ad | |||
| 486ec11ce6 | |||
| a24d28d4e0 | |||
| 8cc45a53e9 | |||
| edf7a86f07 | |||
| 8b8a8ff943 | |||
| 59610f463e | |||
| c1672bb8ae | |||
| 3e101840a6 | |||
| e87898ab82 | |||
| 8a1be59a51 | |||
| a3b2ace88d | |||
| c34037265e | |||
| 8c230fe3af | |||
| a695d60770 | |||
| ea30cbd381 | |||
| 39a4bf0dd3 | |||
| d5c9bc69b3 | |||
| 27f8ab752d | |||
| 5948fc83ea | |||
| 72e3d6a09e | |||
| de6f4a3ac5 | |||
| eecdc51557 | |||
| c841c49e1e | |||
| 2595d822d0 | |||
| 3ae0541065 | |||
| 4b735b768a | |||
| 9422edbfa1 | |||
| 37c5e92d6d | |||
| c7503de11e | |||
| 408362f3be | |||
| b3f5ab3d31 |
126
changelog.md
126
changelog.md
@@ -1,5 +1,131 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.76.0 - feat(input)
|
||||||
|
separate label info tooltips from description text across input components
|
||||||
|
|
||||||
|
- adds a dedicated infoText property for dees-label tooltips while keeping description available for helper text rendered below inputs
|
||||||
|
- introduces a shared renderDescription() helper in the input base component and updates multiple input components to use the unified description styling
|
||||||
|
- updates demos and consuming components to migrate tooltip content from description to infoText where appropriate
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.75.0 - feat(dees-tile)
|
||||||
|
add configurable overscroll handling to tile content and use it in modals
|
||||||
|
|
||||||
|
- introduces a reflected overscroll property on dees-tile with contain, auto, and none options
|
||||||
|
- moves tile content scrolling and scrollbar styling into dees-tile instead of modal-specific styling
|
||||||
|
- updates dees-modal to enable contained overscroll through the new dees-tile API
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.74.2 - fix(modal,tile,input-text)
|
||||||
|
move scroll handling from tile content to modal and update input text demo to use changeSubject subscriptions
|
||||||
|
|
||||||
|
- bump @design.estate/dees-wcctools from ^3.8.2 to ^3.8.4
|
||||||
|
- set dees-tile content overflow to hidden and apply scroll styling through dees-modal part selectors
|
||||||
|
- simplify the interactive dees-input-text demo by subscribing directly to changeSubject for live value updates
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.74.1 - fix(dees-input-text)
|
||||||
|
adjust password toggle and validation icon alignment in text input
|
||||||
|
|
||||||
|
- positions the password toggle and validation icon with fixed top offsets for improved vertical alignment
|
||||||
|
- updates the validation icon styling to use a larger themed icon without the circular background
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.74.0 - feat(input-text)
|
||||||
|
add validated success state and text editing context menu to text inputs
|
||||||
|
|
||||||
|
- show a delayed checkmark confirmation for successful validation and hide inline validation text afterward
|
||||||
|
- move IBAN validation handling into the shared text input validation function
|
||||||
|
- improve the email validation demo to use a stricter regex-based check
|
||||||
|
- add cut, copy, paste, and select-all context menu actions for text inputs
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.73.2 - fix(input,label)
|
||||||
|
correct validation state attribute handling in text inputs and refine label description icon styling
|
||||||
|
|
||||||
|
- Change dees-input-text validationState to reflect as a string attribute and align validation selectors with the emitted host attribute
|
||||||
|
- Wrap the dees-label description icon in a dedicated container to improve sizing, hover feedback, and alignment
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.73.1 - fix(dees-label)
|
||||||
|
align label content and icon consistently using inline flex layout
|
||||||
|
|
||||||
|
- change the label container from inline-block to inline-flex with centered alignment
|
||||||
|
- remove icon-specific vertical transform in favor of layout-based alignment
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.73.0 - feat(dees-label)
|
||||||
|
expand dees-label demo coverage and update supporting dependencies
|
||||||
|
|
||||||
|
- replace the minimal dees-label demo with a structured showcase for basic, required, description, combined, and empty-label states
|
||||||
|
- add themed demo styling and inline annotations to better document component behavior
|
||||||
|
- update @design.estate/dees-wcctools, lucide, and @types/node dependency versions
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.72.1 - fix(dees-stepper)
|
||||||
|
improve stepper exit animation timing for cancel confirmation flow
|
||||||
|
|
||||||
|
- Animate step tiles downward with fade-out during teardown instead of only fading the container
|
||||||
|
- Delay stepper destruction briefly after dismissing the confirmation modal so both exit transitions render smoothly
|
||||||
|
- Increase teardown delay to match the updated exit animation duration
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.72.0 - feat(dees-stepper)
|
||||||
|
add configurable cancellation flow with confirmation modal
|
||||||
|
|
||||||
|
- adds a cancelable option to control whether steppers can be dismissed
|
||||||
|
- shows a confirmation modal when canceling via the new Cancel button or overlay backdrop
|
||||||
|
- updates footer button rendering and separators to support the new cancel action consistently
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.71.1 - fix(dees-modal)
|
||||||
|
move modal content scrolling into dees-tile so long content stays scrollable with pinned header and actions
|
||||||
|
|
||||||
|
- Update dees-tile content area to use vertical scrolling when constrained by a max-height while keeping horizontal overflow clipped.
|
||||||
|
- Remove duplicate scrolling styles from dees-modal and rely on the shared tile container behavior.
|
||||||
|
- Add modal demo cases for long article, list, and form content to verify internal scrolling.
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.71.0 - feat(dees-stepper)
|
||||||
|
add footer menu actions with form-aware step validation
|
||||||
|
|
||||||
|
- replace step footer submit handling with configurable menuOptions actions
|
||||||
|
- disable the primary footer action until required form fields are completed and show a completion hint
|
||||||
|
- dispatch form data before running primary step actions and clean up form subscriptions on destroy
|
||||||
|
- adjust overlay host positioning so the stepper container controls viewport layering correctly
|
||||||
|
|
||||||
|
## 2026-04-11 - 3.70.1 - fix(dees-modal)
|
||||||
|
use icon font sizing for modal header buttons
|
||||||
|
|
||||||
|
- replace fixed width and height on header button icons with font-size to align dees-icon rendering
|
||||||
|
|
||||||
|
## 2026-04-08 - 3.70.0 - feat(dees-table)
|
||||||
|
add opt-in flash highlighting for updated table cells
|
||||||
|
|
||||||
|
- introduces highlight-updates and highlight-duration properties for diff-based cell update highlighting
|
||||||
|
- adds a warning banner when flash highlighting is enabled without rowKey
|
||||||
|
- keeps selection stable across data refreshes and avoids flashing user-edited cells
|
||||||
|
- includes a live demo showcasing flashing updates and reduced-motion support
|
||||||
|
|
||||||
|
## 2026-04-08 - 3.69.1 - fix(ui)
|
||||||
|
refine heading emphasis and animate app dashboard subview expansion
|
||||||
|
|
||||||
|
- Adjust heading color hierarchy so h1-h2 use primary text while h3-h6 use secondary text, and reduce h1 font weight for better visual balance
|
||||||
|
- Replace app dashboard subview conditional rendering with animated expand/collapse behavior using grid transitions and inert state handling
|
||||||
|
|
||||||
|
## 2026-04-08 - 3.69.0 - feat(dees-heading)
|
||||||
|
add numeric aliases for horizontal rule heading levels and refine heading spacing styles
|
||||||
|
|
||||||
|
- Support level="7" as an alias for "hr" and level="8" as an alias for "hr-small".
|
||||||
|
- Update heading and hr variant styles to use design tokens for spacing and colors, with per-level margin tuning.
|
||||||
|
- Extend the demo to show both named and numeric hr heading level variants.
|
||||||
|
|
||||||
|
## 2026-04-08 - 3.68.0 - feat(dees-simple-appdash)
|
||||||
|
add nested sidebar subviews and preserve submit labels from slotted text
|
||||||
|
|
||||||
|
- support grouped navigation items with expandable subviews and parent-to-first-subview fallback in the app dashboard
|
||||||
|
- allow dees-form-submit to derive its button text from light DOM content when no explicit text property is set
|
||||||
|
|
||||||
|
## 2026-04-07 - 3.67.1 - fix(repo)
|
||||||
|
no changes to commit
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-04-07 - 3.67.0 - feat(dees-table)
|
||||||
|
improve inline cell editors with integrated input styling and auto-open dropdowns
|
||||||
|
|
||||||
|
- add a visually integrated mode to dees-input-text and dees-input-dropdown for table cell editing
|
||||||
|
- auto-open dropdown editors when a table cell enters edit mode
|
||||||
|
- refine table editing cell outline and dropdown value matching for inline editors
|
||||||
|
|
||||||
## 2026-04-07 - 3.66.0 - feat(dees-table)
|
## 2026-04-07 - 3.66.0 - feat(dees-table)
|
||||||
add virtualized row rendering for large tables and optimize table rendering performance
|
add virtualized row rendering for large tables and optimize table rendering performance
|
||||||
|
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.66.0",
|
"version": "3.76.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||||
"main": "dist_ts_web/index.js",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.5.4",
|
"@design.estate/dees-domtools": "^2.5.4",
|
||||||
"@design.estate/dees-element": "^2.2.4",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@design.estate/dees-wcctools": "^3.8.0",
|
"@design.estate/dees-wcctools": "^3.8.4",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
"@tiptap/starter-kit": "^2.23.0",
|
"@tiptap/starter-kit": "^2.23.0",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"lightweight-charts": "^5.1.0",
|
|
||||||
"highlight.js": "11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"ibantools": "^4.5.1",
|
"ibantools": "^4.5.1",
|
||||||
"lucide": "^0.577.0",
|
"lightweight-charts": "^5.1.0",
|
||||||
|
"lucide": "^1.8.0",
|
||||||
"monaco-editor": "0.55.1",
|
"monaco-editor": "0.55.1",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"pdfjs-dist": "^4.10.38",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"@git.zone/tstest": "^3.6.3",
|
"@git.zone/tstest": "^3.6.3",
|
||||||
"@git.zone/tswatch": "^3.3.2",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@push.rocks/projectinfo": "^5.1.0",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
"@types/node": "^25.5.0"
|
"@types/node": "^25.6.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
|||||||
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
|||||||
specifier: ^2.2.4
|
specifier: ^2.2.4
|
||||||
version: 2.2.4
|
version: 2.2.4
|
||||||
'@design.estate/dees-wcctools':
|
'@design.estate/dees-wcctools':
|
||||||
specifier: ^3.8.0
|
specifier: ^3.8.4
|
||||||
version: 3.8.0
|
version: 3.8.4
|
||||||
'@fortawesome/fontawesome-svg-core':
|
'@fortawesome/fontawesome-svg-core':
|
||||||
specifier: ^7.2.0
|
specifier: ^7.2.0
|
||||||
version: 7.2.0
|
version: 7.2.0
|
||||||
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
lucide:
|
lucide:
|
||||||
specifier: ^0.577.0
|
specifier: ^1.8.0
|
||||||
version: 0.577.0
|
version: 1.8.0
|
||||||
monaco-editor:
|
monaco-editor:
|
||||||
specifier: 0.55.1
|
specifier: 0.55.1
|
||||||
version: 0.55.1
|
version: 0.55.1
|
||||||
@@ -106,8 +106,8 @@ importers:
|
|||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.5.0
|
specifier: ^25.6.0
|
||||||
version: 25.5.0
|
version: 25.6.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -323,8 +323,8 @@ packages:
|
|||||||
'@design.estate/dees-element@2.2.4':
|
'@design.estate/dees-element@2.2.4':
|
||||||
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
||||||
|
|
||||||
'@design.estate/dees-wcctools@3.8.0':
|
'@design.estate/dees-wcctools@3.8.4':
|
||||||
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
|
resolution: {integrity: sha512-KpFK/azK+a/Xpq33pXKcho+tdFKVHhKZM5ArvHqo9QMwTczgp5DZZgowTDUuqAofjZwnuVfCPHK/Pw9e64N46A==}
|
||||||
|
|
||||||
'@emnapi/core@1.8.1':
|
'@emnapi/core@1.8.1':
|
||||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||||
@@ -2121,11 +2121,11 @@ packages:
|
|||||||
'@types/node@16.9.1':
|
'@types/node@16.9.1':
|
||||||
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
|
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
|
||||||
|
|
||||||
'@types/node@22.19.15':
|
'@types/node@22.19.17':
|
||||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
|
||||||
|
|
||||||
'@types/node@25.5.0':
|
'@types/node@25.6.0':
|
||||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||||
|
|
||||||
'@types/randomatic@3.1.5':
|
'@types/randomatic@3.1.5':
|
||||||
resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==}
|
resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==}
|
||||||
@@ -3054,6 +3054,9 @@ packages:
|
|||||||
lucide@0.577.0:
|
lucide@0.577.0:
|
||||||
resolution: {integrity: sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==}
|
resolution: {integrity: sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==}
|
||||||
|
|
||||||
|
lucide@1.8.0:
|
||||||
|
resolution: {integrity: sha512-JjV/QnadgFLj1Pyu9IKl0lknrolFEzo04B64QcYLLeRzZl/iEHpdbSrRRKbyXcv45SZNv+WGjIUCT33e7xHO6Q==}
|
||||||
|
|
||||||
make-dir@3.1.0:
|
make-dir@3.1.0:
|
||||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4002,8 +4005,8 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici-types@7.18.2:
|
undici-types@7.19.2:
|
||||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
@@ -4708,7 +4711,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
'@design.estate/dees-element': 2.2.4
|
'@design.estate/dees-element': 2.2.4
|
||||||
'@design.estate/dees-wcctools': 3.8.0
|
'@design.estate/dees-wcctools': 3.8.4
|
||||||
'@fortawesome/fontawesome-svg-core': 7.2.0
|
'@fortawesome/fontawesome-svg-core': 7.2.0
|
||||||
'@fortawesome/free-brands-svg-icons': 7.2.0
|
'@fortawesome/free-brands-svg-icons': 7.2.0
|
||||||
'@fortawesome/free-regular-svg-icons': 7.2.0
|
'@fortawesome/free-regular-svg-icons': 7.2.0
|
||||||
@@ -4784,7 +4787,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@design.estate/dees-wcctools@3.8.0':
|
'@design.estate/dees-wcctools@3.8.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.5.4
|
'@design.estate/dees-domtools': 2.5.4
|
||||||
'@design.estate/dees-element': 2.2.4
|
'@design.estate/dees-element': 2.2.4
|
||||||
@@ -5257,7 +5260,7 @@ snapshots:
|
|||||||
'@inquirer/figures': 1.0.15
|
'@inquirer/figures': 1.0.15
|
||||||
'@inquirer/type': 2.0.0
|
'@inquirer/type': 2.0.0
|
||||||
'@types/mute-stream': 0.0.4
|
'@types/mute-stream': 0.0.4
|
||||||
'@types/node': 22.19.15
|
'@types/node': 22.19.17
|
||||||
'@types/wrap-ansi': 3.0.0
|
'@types/wrap-ansi': 3.0.0
|
||||||
ansi-escapes: 4.3.2
|
ansi-escapes: 4.3.2
|
||||||
cli-width: 4.1.0
|
cli-width: 4.1.0
|
||||||
@@ -7235,7 +7238,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/clean-css@4.2.11':
|
'@types/clean-css@4.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
@@ -7245,7 +7248,7 @@ snapshots:
|
|||||||
'@types/fs-extra@11.0.4':
|
'@types/fs-extra@11.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jsonfile': 6.1.4
|
'@types/jsonfile': 6.1.4
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7265,7 +7268,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0': {}
|
'@types/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
@@ -7288,21 +7291,21 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mute-stream@0.0.4':
|
'@types/mute-stream@0.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/node-forge@1.3.14':
|
'@types/node-forge@1.3.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/node@16.9.1': {}
|
'@types/node@16.9.1': {}
|
||||||
|
|
||||||
'@types/node@22.19.15':
|
'@types/node@22.19.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@25.5.0':
|
'@types/node@25.6.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.19.2
|
||||||
|
|
||||||
'@types/randomatic@3.1.5': {}
|
'@types/randomatic@3.1.5': {}
|
||||||
|
|
||||||
@@ -7314,11 +7317,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/tar-stream@3.1.4':
|
'@types/tar-stream@3.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/through2@2.0.41':
|
'@types/through2@2.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
@@ -7344,11 +7347,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.5.0
|
'@types/node': 25.6.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@@ -8300,6 +8303,8 @@ snapshots:
|
|||||||
|
|
||||||
lucide@0.577.0: {}
|
lucide@0.577.0: {}
|
||||||
|
|
||||||
|
lucide@1.8.0: {}
|
||||||
|
|
||||||
make-dir@3.1.0:
|
make-dir@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -9570,7 +9575,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici-types@7.18.2: {}
|
undici-types@7.19.2: {}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
352
readme.plan.md
Normal file
352
readme.plan.md
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
# Plan: dees-stepper — adopt dees-tile + optional overlay window layer
|
||||||
|
|
||||||
|
> First line (per CLAUDE.md): Please reread `/home/philkunz/.claude/CLAUDE.md` before continuing.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Today `dees-stepper` is an inline-only layout component: it hard-codes each step as a custom `.step` `<div>` with its own border / background / box-shadow / border-radius, and its `:host` is `position: absolute; width: 100%; height: 100%;` so it can only live inside a bounded parent container.
|
||||||
|
|
||||||
|
The user wants it to behave more like `dees-modal`:
|
||||||
|
|
||||||
|
1. Each step should be wrapped in a `<dees-tile>` — the unified "rounded on rounded" frame used by modals and panels — rather than a bespoke `.step` div.
|
||||||
|
2. A `DeesWindowLayer` should be added behind the stepper, the same way `DeesModal.createAndShow` does, so the stepper can appear as an overlay on top of the page.
|
||||||
|
|
||||||
|
User has confirmed (via AskUserQuestion in this session):
|
||||||
|
- **API**: keep the current inline usage working AND add a static `createAndShow()` like dees-modal.
|
||||||
|
- **Layout**: keep the current vertical stack + SweetScroll behavior inside the overlay (don't switch to single-tile swap).
|
||||||
|
- **Nav placement**: split header/footer — goBack + step counter go into the `dees-tile` header slot; the title stays in the content area; the tile footer is used for optional next/submit buttons supplied per-step.
|
||||||
|
|
||||||
|
No external consumers of `dees-stepper` were found inside this package (`grep dees-stepper|DeesStepper` only matches its own source, demo, index, changelog, readme). External consumers in dependent projects may exist — the refactor is kept backward-compatible for the inline path.
|
||||||
|
|
||||||
|
## Current state (reference)
|
||||||
|
|
||||||
|
**File:** `ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts` (lines 20–299)
|
||||||
|
|
||||||
|
- `IStep` interface — `title`, `content: TemplateResult`, `validationFunc`, `onReturnToStepFunc`, internal flags (lines 20–27).
|
||||||
|
- `:host { position: absolute; width: 100%; height: 100%; }` (lines 59–63).
|
||||||
|
- `.stepperContainer` — absolute, 100% w/h, `overflow: hidden`, holds SweetScroll (lines 64–69).
|
||||||
|
- `.step` — max-width 500, min-height 300, `border-radius: 12px`, theme background, theme border, `box-shadow: 0 8px 32px rgba(0,0,0,0.4)`, `filter: opacity(0.55) saturate(0.85)`, transform transition (lines 71–97). **These frame styles overlap with what `dees-tile` already provides.**
|
||||||
|
- `.step.selected` — `filter: opacity(1) saturate(1)` (lines 89–93). **Scroll-through visual cue, keep.**
|
||||||
|
- `.step.hiddenStep` — `filter: opacity(0)` (line 95). **Keep.**
|
||||||
|
- `.step.entrance` — faster transition variant for first-render (lines 99–105). **Keep.**
|
||||||
|
- `.step .stepCounter` — `position: absolute; top: 12px; right: 12px;` pill (lines 111–121). **Move into header slot as a flex child.**
|
||||||
|
- `.step .goBack` — `position: absolute; top: 12px; left: 12px;` pill + icon + hover (lines 123–161). **Move into header slot as a flex child.**
|
||||||
|
- `.step .title` — centered, 24px, 64px top padding (lines 163–171). **Keep inside the tile's content slot; remove the 64px top padding since goBack/counter no longer overlap it.**
|
||||||
|
- `.step .content` — 32px padding (lines 173–175). **Keep.**
|
||||||
|
- `render()` (lines 179–204) — maps `steps` to `.step` divs.
|
||||||
|
- `setScrollStatus()` (lines 226–263) — SweetScroll container setup + step validation kick-off. **Keep mostly as-is; selectors still target `.step`/`.selected` so rename cautiously.**
|
||||||
|
- `firstUpdated` (lines 210–218), `updated` (lines 220–222), `goBack` (lines 265–282), `goNext` (lines 284–298) — untouched in behavior, only DOM selectors may need adjusting.
|
||||||
|
|
||||||
|
**Reference files (read, do not modify):**
|
||||||
|
- `ts_web/elements/00group-overlay/dees-modal/dees-modal.ts` — canonical `createAndShow` + `destroy` + window-layer coordination + z-index registry usage.
|
||||||
|
- `ts_web/elements/00group-layout/dees-tile/dees-tile.ts` — slot API: `slot="header"`, default slot, `slot="footer"`. Auto-hides footer when slotted nodes are empty. Uses `part="outer"`, `part="header"`, `part="content"`, `part="footer"` for external shadow-part styling.
|
||||||
|
- `ts_web/elements/00group-overlay/dees-windowlayer/dees-windowlayer.ts` — `createAndShow({ blur })`, `destroy()`, dispatches `clicked` event on backdrop click, uses `zIndexRegistry`.
|
||||||
|
- `ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts` — existing inline demo.
|
||||||
|
|
||||||
|
## Target state
|
||||||
|
|
||||||
|
### 1. IStep interface — add one optional field
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface IStep {
|
||||||
|
title: string;
|
||||||
|
content: TemplateResult;
|
||||||
|
footerContent?: TemplateResult; // NEW: optional, rendered in dees-tile footer slot
|
||||||
|
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
||||||
|
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
||||||
|
validationFuncCalled?: boolean;
|
||||||
|
abortController?: AbortController;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Form-based steps don't need `footerContent` — their `dees-form-submit` stays inside the form in the content slot (as today). `footerContent` is for non-form steps that need an explicit primary action, or for any step that wants buttons in the conventional tile footer location.
|
||||||
|
|
||||||
|
### 2. New overlay-mode state + API on DeesStepper
|
||||||
|
|
||||||
|
```ts
|
||||||
|
@state() accessor overlay: boolean = false;
|
||||||
|
@state() accessor stepperZIndex: number = 1000;
|
||||||
|
private windowLayer?: DeesWindowLayer;
|
||||||
|
|
||||||
|
public static async createAndShow(optionsArg: {
|
||||||
|
steps: IStep[];
|
||||||
|
}): Promise<DeesStepper> {
|
||||||
|
const body = document.body;
|
||||||
|
const stepper = new DeesStepper();
|
||||||
|
stepper.steps = optionsArg.steps;
|
||||||
|
stepper.overlay = true;
|
||||||
|
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
|
||||||
|
stepper.windowLayer.addEventListener('click', async () => {
|
||||||
|
await stepper.destroy();
|
||||||
|
});
|
||||||
|
body.append(stepper.windowLayer); // (already appended inside createAndShow, but mirror dees-modal's pattern; see note)
|
||||||
|
body.append(stepper);
|
||||||
|
stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
zIndexRegistry.register(stepper, stepper.stepperZIndex);
|
||||||
|
return stepper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroy() {
|
||||||
|
const domtools = await this.domtoolsPromise;
|
||||||
|
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
||||||
|
container?.classList.add('predestroy');
|
||||||
|
await domtools.convenience.smartdelay.delayFor(200);
|
||||||
|
if (this.parentElement) this.parentElement.removeChild(this);
|
||||||
|
if (this.windowLayer) await this.windowLayer.destroy();
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note on `body.append(windowLayer)`:** `DeesWindowLayer.createAndShow` already appends the window layer to `document.body` (line 27 of `dees-windowlayer.ts`). `dees-modal.ts:71` still calls `body.append(modal.windowLayer)` — that's either a no-op (already-attached nodes) or a re-parent to keep ordering. I will match dees-modal's exact sequence verbatim to avoid introducing subtle differences; if it's a bug in dees-modal it is out of scope for this task.
|
||||||
|
|
||||||
|
**Minimum new scope for createAndShow:** just `steps` for now. No `onComplete`, no `showCloseButton`, no width options. Future-proofing via additional options is an explicit follow-up — this plan keeps scope razor-sharp (per CLAUDE.md). The caller can already wire completion via the last step's `validationFunc` calling back into their own code.
|
||||||
|
|
||||||
|
### 3. Render template — wrap each step in `<dees-tile>`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<div class="stepperContainer ${this.overlay ? 'overlay' : ''}" style="${this.overlay ? `z-index: ${this.stepperZIndex}` : ''}">
|
||||||
|
${this.steps.map((stepArg, i) => {
|
||||||
|
const isSelected = stepArg === this.selectedStep;
|
||||||
|
const isHidden = this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
||||||
|
const isFirst = i === 0;
|
||||||
|
const stepNumber = i + 1;
|
||||||
|
return html`
|
||||||
|
<dees-tile
|
||||||
|
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
|
||||||
|
>
|
||||||
|
<div slot="header" class="step-header">
|
||||||
|
${!isFirst
|
||||||
|
? html`<div class="goBack" @click=${this.goBack}>
|
||||||
|
<span>←</span> go to previous step
|
||||||
|
</div>`
|
||||||
|
: html`<div class="goBack-spacer"></div>`}
|
||||||
|
<div class="stepCounter">Step ${stepNumber} of ${this.steps.length}</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-body">
|
||||||
|
<div class="title">${stepArg.title}</div>
|
||||||
|
<div class="content">${stepArg.content}</div>
|
||||||
|
</div>
|
||||||
|
${stepArg.footerContent
|
||||||
|
? html`<div slot="footer" class="step-footer">${stepArg.footerContent}</div>`
|
||||||
|
: ''}
|
||||||
|
</dees-tile>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key detail:** on the first step, render a `.goBack-spacer` (empty div) in the header instead of nothing — so the `stepCounter` stays right-aligned via `justify-content: space-between`. Without a spacer, flex would left-align the counter on step 1.
|
||||||
|
|
||||||
|
### 4. CSS changes
|
||||||
|
|
||||||
|
**Remove from `.step`:**
|
||||||
|
- `border-radius: 12px;`
|
||||||
|
- `background: ${cssManager.bdTheme(...)};`
|
||||||
|
- `border: 1px solid ${cssManager.bdTheme(...)};`
|
||||||
|
- `color: ${cssManager.bdTheme(...)};`
|
||||||
|
- `box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);`
|
||||||
|
- `overflow: hidden;`
|
||||||
|
|
||||||
|
**Why:** `dees-tile` owns all of these now. The `.step` selector still exists (since `dees-tile` has `class="step ..."` on it), but it only controls the outer animation wrapper: `max-width`, `min-height`, `margin`, `filter`, `transform`, `transition`, `user-select`, `pointer-events`.
|
||||||
|
|
||||||
|
**Keep on `.step`:**
|
||||||
|
- `position: relative;`
|
||||||
|
- `pointer-events: none;` + `.step.selected { pointer-events: all; }`
|
||||||
|
- `max-width: 500px;` / `min-height: 300px;`
|
||||||
|
- `margin: auto; margin-bottom: 20px;`
|
||||||
|
- `filter: opacity(0.55) saturate(0.85);` + `.selected { filter: opacity(1) saturate(1); }`
|
||||||
|
- `.hiddenStep { filter: opacity(0); }`
|
||||||
|
- All the cubic-bezier transitions (transform/filter/box-shadow — but box-shadow is now a no-op since dees-tile provides the shadow; leave the transition spec in so we don't have to re-check browser parsing; or just drop `box-shadow` from the transition list — I'll drop it for cleanliness).
|
||||||
|
- `.step.entrance` + `.step.entrance.hiddenStep { transform: translateY(16px); }`
|
||||||
|
- `.step:last-child { margin-bottom: 100vh; }`
|
||||||
|
|
||||||
|
**Add for dees-tile shadow enhancement:** use `::part(outer)` to apply the modal-style elevated shadow only when in overlay mode (optional polish — inline mode stays flat):
|
||||||
|
```css
|
||||||
|
.stepperContainer.overlay dees-tile.step::part(outer) {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
|
||||||
|
0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
|
||||||
|
0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This exactly mirrors the dees-modal::part(outer) shadow stack (dees-modal.ts:157–161) so the overlay stepper reads as "same visual language as modal."
|
||||||
|
|
||||||
|
**Restyle `.step-header` (NEW — the `<div slot="header">`):**
|
||||||
|
```css
|
||||||
|
.step-header {
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restyle `.step .stepCounter` → `.step-header .stepCounter` (move from absolute to flex child):**
|
||||||
|
- Drop `position: absolute; top: 12px; right: 12px;`
|
||||||
|
- Keep everything else (padding, font-size, border-radius, background, border).
|
||||||
|
|
||||||
|
**Restyle `.step .goBack` → `.step-header .goBack` (move from absolute to flex child):**
|
||||||
|
- Drop `position: absolute; top: 12px; left: 12px;`
|
||||||
|
- Keep everything else (padding, font-size, border-radius, background, border, hover/active states).
|
||||||
|
|
||||||
|
**Add `.goBack-spacer`:**
|
||||||
|
```css
|
||||||
|
.goBack-spacer { width: 1px; } /* placeholder so flex space-between works on step 1 */
|
||||||
|
```
|
||||||
|
|
||||||
|
**Restyle `.step .title`:**
|
||||||
|
- Drop `padding-top: 64px;` — no longer overlaps anything since header is in its own slot.
|
||||||
|
- Keep `text-align: center; font-family: 'Geist Sans', sans-serif; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; color: inherit;`
|
||||||
|
- Add `padding-top: 32px;` (or similar) so there's consistent breathing room above the title inside the tile content.
|
||||||
|
|
||||||
|
**Add `.step-footer` (new container for `stepArg.footerContent`):**
|
||||||
|
```css
|
||||||
|
.step-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add overlay-mode positioning:**
|
||||||
|
```css
|
||||||
|
.stepperContainer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.stepperContainer.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.stepperContainer.predestroy {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Adjust `:host` for dual-mode:**
|
||||||
|
```css
|
||||||
|
:host {
|
||||||
|
position: absolute; /* inline default */
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: ${cssGeistFontFamily};
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
:host([overlay]) {
|
||||||
|
position: fixed; /* overlay mode */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `overlay` @state needs to reflect to an attribute for the `:host([overlay])` selector to work. Since `@state` doesn't reflect attributes, use `@property({ type: Boolean, reflect: true })` instead — change the decorator accordingly.
|
||||||
|
|
||||||
|
### 5. Imports to add in `dees-stepper.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||||
|
import { zIndexRegistry } from '../../00zindex.js';
|
||||||
|
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||||
|
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
`dees-tile` side-effect import registers the custom element. `cssGeistFontFamily` is only needed if I add it to `:host` (which I want, to match modal).
|
||||||
|
|
||||||
|
### 6. SweetScroll selector stability
|
||||||
|
|
||||||
|
`setScrollStatus()` selectors target `.step` and `.selected` (lines 228–229). These continue to match since I'm keeping those class names on the `<dees-tile>` elements. **No selector changes needed.**
|
||||||
|
|
||||||
|
One subtlety: `offsetTop` / `offsetHeight` on `<dees-tile>` should still work — the tile's `:host` is `display: flex; flex-direction: column;` which participates in layout. I'll verify visually in the demo.
|
||||||
|
|
||||||
|
### 7. Demo update
|
||||||
|
|
||||||
|
**File:** `ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts`
|
||||||
|
|
||||||
|
Current demo renders one inline stepper directly. I'll keep that and add an **overlay launcher button** above it:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const stepperDemo = () => html`
|
||||||
|
<div style="padding: 16px;">
|
||||||
|
<dees-button @click=${async () => {
|
||||||
|
const stepper = await DeesStepper.createAndShow({
|
||||||
|
steps: [/* same steps as inline demo */],
|
||||||
|
});
|
||||||
|
}}>Open stepper as overlay</dees-button>
|
||||||
|
</div>
|
||||||
|
<dees-stepper .steps=${[/* ... existing inline demo steps ... */]}></dees-stepper>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the step definitions into a `const demoSteps = [...]` above the template so both the inline and overlay paths reuse them (DRY). Import `DeesStepper` at the top of the demo file.
|
||||||
|
|
||||||
|
## Files to modify
|
||||||
|
|
||||||
|
1. **`ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts`** — main refactor (IStep, imports, render, styles, createAndShow, destroy, overlay state).
|
||||||
|
2. **`ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts`** — add overlay launcher button, extract shared `demoSteps` const, import `DeesStepper`.
|
||||||
|
|
||||||
|
**Files explicitly NOT modified:**
|
||||||
|
- `dees-tile.ts` — used as-is via its slot API.
|
||||||
|
- `dees-windowlayer.ts` — used as-is via `createAndShow` / `destroy` / `click` event.
|
||||||
|
- `dees-modal.ts` — reference only.
|
||||||
|
- `00zindex.ts` — reference only.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. **Build**: `pnpm run build` — must pass with no TS errors. Pure refactor, no new dependencies, no lib-check regressions expected.
|
||||||
|
|
||||||
|
2. **Inline demo (backward compat)**:
|
||||||
|
- Start the demo server (port 8080 is already running) and navigate to the dees-stepper demo page.
|
||||||
|
- Confirm the stepper renders inline exactly like before: first step centered, subsequent steps dimmed below, scroll-through animation on goNext / goBack.
|
||||||
|
- Fill out the first form, submit → stepper scrolls to step 2. Click goBack → scrolls back.
|
||||||
|
- Confirm the `dees-tile` frame is visible on each step (rounded, bordered, themed) and that the title + form are inside the tile's content area.
|
||||||
|
- Confirm goBack button + step counter sit in the tile's header row, space-between, left/right respectively.
|
||||||
|
|
||||||
|
3. **Overlay demo (new path)**:
|
||||||
|
- Click the "Open stepper as overlay" button.
|
||||||
|
- Confirm a `dees-windowlayer` with blur appears behind the stepper.
|
||||||
|
- Confirm the stepper fills the viewport (fixed, 100vw×100vh).
|
||||||
|
- Confirm z-index stacking: stepper above window layer above page content.
|
||||||
|
- Click the window layer (outside the tile) → stepper animates out, then destroys along with the window layer.
|
||||||
|
- Re-open and step through forward & back — behavior identical to inline mode.
|
||||||
|
|
||||||
|
4. **Playwright visual check** (per CLAUDE.md: screenshots MUST go in `.playwright-mcp/`):
|
||||||
|
- `.playwright-mcp/dees-stepper-inline.png` — inline mode, step 1 with form.
|
||||||
|
- `.playwright-mcp/dees-stepper-overlay.png` — overlay mode, same step.
|
||||||
|
- `.playwright-mcp/dees-stepper-overlay-step3.png` — overlay mode mid-flow, to verify scroll-stack visual.
|
||||||
|
- Both light and dark themes if the demo has a theme toggle.
|
||||||
|
|
||||||
|
5. **Grep sanity**:
|
||||||
|
- Confirm `dees-stepper` has no new unexpected match locations: `grep dees-stepper ts_web/` should still only match stepper's own files.
|
||||||
|
- Confirm no `.step` class collisions elsewhere (unlikely — `.step` is a plain class name; all usages should be shadow-scoped to `dees-stepper`).
|
||||||
|
|
||||||
|
## Open assumptions & deferred scope
|
||||||
|
|
||||||
|
These are explicit defaults in this plan. If the user wants different behavior for any of them, they should flag it on review — each is a simple follow-up but not in scope right now (CLAUDE.md: stay focused, no "while we're at it"):
|
||||||
|
|
||||||
|
- **No close button on overlay stepper.** Clicking the window layer backdrop is the only way to dismiss. Matches how dees-modal with `showCloseButton: false` behaves. Can add a close button in a follow-up.
|
||||||
|
- **No `onComplete` callback in `createAndShow`.** The last step doesn't auto-destroy the overlay — the app controls it via the step's `validationFunc`. Can add a callback option in a follow-up.
|
||||||
|
- **No width/size options in `createAndShow`.** The step tile continues to use the stepper's existing `max-width: 500px`. Can parameterize in a follow-up.
|
||||||
|
- **Box-shadow in the `.step` transition list** is dropped from the transition for cleanliness — the box-shadow is now on `dees-tile::part(outer)` and doesn't change between selected/hiddenStep, so transitioning it was already a no-op.
|
||||||
|
- **`pnpm start` / dev server path**: I'll reuse the existing server on port 8080 that was already listening when this session began; if that server doesn't serve the stepper demo, I'll start wcctools manually.
|
||||||
|
|
||||||
|
## Risk
|
||||||
|
|
||||||
|
- **Low-medium.** The change is localized to one component and its demo. No API removal, only an additive `createAndShow` + an optional `footerContent` field. External consumers of the inline API continue to work if they only set `steps` + `selectedStep`.
|
||||||
|
- **Biggest risk:** SweetScroll's `offsetTop` / `offsetHeight` measurements on `<dees-tile>` may compute differently than on the former `<div class="step">` because `dees-tile` has an internal `display: flex; flex-direction: column;` host and a `.tile-outer { flex: 1; min-height: 0; }` inner frame. If the scroll math drifts, the mitigation is to keep the `.step` wrapper as an outer `<div>` that **contains** a `<dees-tile>`, rather than putting the class directly on `<dees-tile>`. That preserves the exact box model SweetScroll was measuring. I'll try the direct-class approach first (simpler) and fall back to the wrapper approach if the scroll target looks off in the demo.
|
||||||
|
- **Second risk:** The `:host([overlay])` attribute selector requires `overlay` to be a reflected `@property`, not `@state`. I've already accounted for this in the plan (decorator change).
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.66.0',
|
version: '3.76.0',
|
||||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type ITableAction } from './dees-table.js';
|
import { type ITableAction } from './dees-table.js';
|
||||||
import * as plugins from '../../00plugins.js';
|
import * as plugins from '../../00plugins.js';
|
||||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
|
||||||
interface ITableDemoData {
|
interface ITableDemoData {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -742,6 +743,71 @@ export const demoFunc = () => html`
|
|||||||
] as ITableAction[]}
|
] as ITableAction[]}
|
||||||
></dees-table>
|
></dees-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||||
|
const tableEl = elementArg.querySelector('#demoLiveFlash') as any;
|
||||||
|
if (!tableEl) return;
|
||||||
|
// Guard against double-start if runAfterRender fires more than once
|
||||||
|
// (e.g. across hot-reload cycles).
|
||||||
|
if (tableEl.__liveFlashTimerId) {
|
||||||
|
window.clearInterval(tableEl.__liveFlashTimerId);
|
||||||
|
}
|
||||||
|
const tick = () => {
|
||||||
|
if (!Array.isArray(tableEl.data) || tableEl.data.length === 0) return;
|
||||||
|
const next = tableEl.data.map((r: any) => ({ ...r }));
|
||||||
|
const count = 1 + Math.floor(Math.random() * 3);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const idx = Math.floor(Math.random() * next.length);
|
||||||
|
const delta = +((Math.random() * 2 - 1) * 3).toFixed(2);
|
||||||
|
const newPrice = Math.max(1, +(next[idx].price + delta).toFixed(2));
|
||||||
|
next[idx] = {
|
||||||
|
...next[idx],
|
||||||
|
price: newPrice,
|
||||||
|
change: delta,
|
||||||
|
updatedAt: new Date().toLocaleTimeString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
tableEl.data = next;
|
||||||
|
};
|
||||||
|
tableEl.__liveFlashTimerId = window.setInterval(tick, 1500);
|
||||||
|
}}>
|
||||||
|
<div class="demo-section">
|
||||||
|
<h2 class="demo-title">Live Updates with Flash Highlighting</h2>
|
||||||
|
<p class="demo-description">
|
||||||
|
Opt-in cell-flash via <code>highlight-updates="flash"</code>. The ticker below mutates
|
||||||
|
random rows every 1.5s and reassigns <code>.data</code>. Updated cells briefly flash
|
||||||
|
amber and fade out. Requires <code>rowKey</code> (here <code>"symbol"</code>). Honors
|
||||||
|
<code>prefers-reduced-motion</code>. Row selection persists across updates — click a
|
||||||
|
row, then watch it stay selected as the data churns.
|
||||||
|
</p>
|
||||||
|
<dees-table
|
||||||
|
id="demoLiveFlash"
|
||||||
|
.rowKey=${'symbol'}
|
||||||
|
highlight-updates="flash"
|
||||||
|
.selectionMode=${'multi'}
|
||||||
|
heading1="Live Market Feed"
|
||||||
|
heading2="Flashing cells indicate updated values"
|
||||||
|
.columns=${[
|
||||||
|
{ key: 'symbol', header: 'Symbol', sortable: true },
|
||||||
|
{ key: 'price', header: 'Price', sortable: true },
|
||||||
|
{ key: 'change', header: 'Δ', sortable: true },
|
||||||
|
{ key: 'updatedAt', header: 'Updated' },
|
||||||
|
]}
|
||||||
|
.data=${[
|
||||||
|
{ symbol: 'AAPL', price: 182.52, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'MSFT', price: 414.18, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'GOOG', price: 168.74, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'AMZN', price: 186.13, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'TSLA', price: 248.50, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'NVDA', price: 877.35, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'META', price: 492.96, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'NFLX', price: 605.88, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'AMD', price: 165.24, change: 0, updatedAt: '—' },
|
||||||
|
{ symbol: 'INTC', price: 42.15, change: 0, updatedAt: '—' },
|
||||||
|
]}
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
</dees-demowrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -214,6 +214,30 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
@property({ type: Number, attribute: 'virtual-overscan' })
|
@property({ type: Number, attribute: 'virtual-overscan' })
|
||||||
accessor virtualOverscan: number = 8;
|
accessor virtualOverscan: number = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opt-in visual indication of cell-value changes across data updates.
|
||||||
|
*
|
||||||
|
* - `'none'` (default): no diffing, zero overhead.
|
||||||
|
* - `'flash'`: when `data` is reassigned to a new array reference, diff the
|
||||||
|
* new rows against the previous snapshot and briefly flash any cells
|
||||||
|
* whose resolved value changed. Equality is strict `===`; object-valued
|
||||||
|
* cells are compared by reference. The currently-edited cell is never
|
||||||
|
* flashed. User-initiated cell edits do not flash.
|
||||||
|
*
|
||||||
|
* Requires `rowKey` to be set — without it, the feature silently no-ops
|
||||||
|
* and renders a visible dev warning banner. Honors `prefers-reduced-motion`
|
||||||
|
* (fades are replaced with a static background hint of the same duration).
|
||||||
|
*/
|
||||||
|
@property({ type: String, attribute: 'highlight-updates' })
|
||||||
|
accessor highlightUpdates: 'none' | 'flash' = 'none';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration of the flash animation in milliseconds. Fed into the
|
||||||
|
* `--dees-table-flash-duration` CSS variable on the host.
|
||||||
|
*/
|
||||||
|
@property({ type: Number, attribute: 'highlight-duration' })
|
||||||
|
accessor highlightDuration: number = 900;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When set, the table renders inside a fixed-height scroll container
|
* When set, the table renders inside a fixed-height scroll container
|
||||||
* (`max-height: var(--table-max-height, 360px)`) and the header sticks
|
* (`max-height: var(--table-max-height, 360px)`) and the header sticks
|
||||||
@@ -268,6 +292,23 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private accessor __floatingActive: boolean = false;
|
private accessor __floatingActive: boolean = false;
|
||||||
|
|
||||||
|
// ─── Flash-on-update state (only populated when highlightUpdates === 'flash') ──
|
||||||
|
/** rowId → set of colKey strings currently flashing. */
|
||||||
|
@state()
|
||||||
|
private accessor __flashingCells: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
|
/** rowId → (colKey → last-seen resolved cell value). Populated per diff pass. */
|
||||||
|
private __prevSnapshot?: Map<string, Map<string, unknown>>;
|
||||||
|
|
||||||
|
/** Single shared timer that clears __flashingCells after highlightDuration ms. */
|
||||||
|
private __flashClearTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
/** Monotonic counter bumped each flash batch so directives.keyed recreates the cell node and restarts the animation. */
|
||||||
|
private __flashTick: number = 0;
|
||||||
|
|
||||||
|
/** One-shot console.warn gate for missing rowKey in flash mode. */
|
||||||
|
private __flashWarnedNoRowKey: boolean = false;
|
||||||
|
|
||||||
// ─── Render memoization ──────────────────────────────────────────────
|
// ─── Render memoization ──────────────────────────────────────────────
|
||||||
// These caches let render() short-circuit when the relevant inputs
|
// These caches let render() short-circuit when the relevant inputs
|
||||||
// (by reference) haven't changed. They are NOT @state — mutating them
|
// (by reference) haven't changed. They are NOT @state — mutating them
|
||||||
@@ -557,10 +598,19 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="headingSeparation"></div>
|
<div class="headingSeparation"></div>
|
||||||
|
${this.highlightUpdates === 'flash' && !this.rowKey
|
||||||
|
? html`<div class="flashConfigWarning" role="alert">
|
||||||
|
<dees-icon .icon=${'lucide:triangleAlert'}></dees-icon>
|
||||||
|
<span>
|
||||||
|
<code>highlight-updates="flash"</code> requires
|
||||||
|
<code>rowKey</code> to be set. Flash is disabled.
|
||||||
|
</span>
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
<div class="searchGrid hidden">
|
<div class="searchGrid hidden">
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'lucene syntax search'}
|
.label=${'lucene syntax search'}
|
||||||
.description=${`
|
.infoText=${`
|
||||||
You can use the lucene syntax to search for data, e.g.:
|
You can use the lucene syntax to search for data, e.g.:
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -573,7 +623,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
.label=${'search mode'}
|
.label=${'search mode'}
|
||||||
.options=${['table', 'data', 'server']}
|
.options=${['table', 'data', 'server']}
|
||||||
.selectedOption=${'table'}
|
.selectedOption=${'table'}
|
||||||
.description=${`
|
.infoText=${`
|
||||||
There are three basic modes:
|
There are three basic modes:
|
||||||
|
|
||||||
* table: only searches data already in the table
|
* table: only searches data already in the table
|
||||||
@@ -606,9 +656,13 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
${useVirtual && topSpacerHeight > 0
|
${useVirtual && topSpacerHeight > 0
|
||||||
? html`<tr aria-hidden="true" style="height:${topSpacerHeight}px"><td></td></tr>`
|
? html`<tr aria-hidden="true" style="height:${topSpacerHeight}px"><td></td></tr>`
|
||||||
: html``}
|
: html``}
|
||||||
${renderRows.map((itemArg, sliceIdx) => {
|
${directives.repeat(
|
||||||
|
renderRows,
|
||||||
|
(itemArg, sliceIdx) => `${this.getRowId(itemArg)}::${renderStart + sliceIdx}`,
|
||||||
|
(itemArg, sliceIdx) => {
|
||||||
const rowIndex = renderStart + sliceIdx;
|
const rowIndex = renderStart + sliceIdx;
|
||||||
const rowId = this.getRowId(itemArg);
|
const rowId = this.getRowId(itemArg);
|
||||||
|
const flashSet = this.__flashingCells.get(rowId);
|
||||||
return html`
|
return html`
|
||||||
<tr
|
<tr
|
||||||
data-row-idx=${rowIndex}
|
data-row-idx=${rowIndex}
|
||||||
@@ -640,6 +694,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
const isEditing =
|
const isEditing =
|
||||||
this.__editingCell?.rowId === rowId &&
|
this.__editingCell?.rowId === rowId &&
|
||||||
this.__editingCell?.colKey === editKey;
|
this.__editingCell?.colKey === editKey;
|
||||||
|
const isFlashing = !!flashSet?.has(editKey);
|
||||||
const cellClasses = [
|
const cellClasses = [
|
||||||
isEditable ? 'editable' : '',
|
isEditable ? 'editable' : '',
|
||||||
isFocused && !isEditing ? 'focused' : '',
|
isFocused && !isEditing ? 'focused' : '',
|
||||||
@@ -647,14 +702,22 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
const innerHtml = html`<div
|
||||||
|
class=${isFlashing ? 'innerCellContainer flashing' : 'innerCellContainer'}
|
||||||
|
>
|
||||||
|
${isEditing ? this.renderCellEditor(itemArg, col) : content}
|
||||||
|
</div>`;
|
||||||
return html`
|
return html`
|
||||||
<td
|
<td
|
||||||
class=${cellClasses}
|
class=${cellClasses}
|
||||||
data-col-key=${editKey}
|
data-col-key=${editKey}
|
||||||
>
|
>
|
||||||
<div class="innerCellContainer">
|
${isFlashing
|
||||||
${isEditing ? this.renderCellEditor(itemArg, col) : content}
|
? directives.keyed(
|
||||||
</div>
|
`${rowId}:${editKey}:${this.__flashTick}`,
|
||||||
|
innerHtml
|
||||||
|
)
|
||||||
|
: innerHtml}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
@@ -685,7 +748,8 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</tr>`;
|
</tr>`;
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
${useVirtual && bottomSpacerHeight > 0
|
${useVirtual && bottomSpacerHeight > 0
|
||||||
? html`<tr aria-hidden="true" style="height:${bottomSpacerHeight}px"><td></td></tr>`
|
? html`<tr aria-hidden="true" style="height:${bottomSpacerHeight}px"><td></td></tr>`
|
||||||
: html``}
|
: html``}
|
||||||
@@ -768,7 +832,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
${effectiveColumns
|
${effectiveColumns
|
||||||
.filter((c) => !c.hidden)
|
.filter((c) => !c.hidden)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
const isSortable = !!col.sortable;
|
const isSortable = col.sortable !== false;
|
||||||
const ariaSort = this.getAriaSort(col);
|
const ariaSort = this.getAriaSort(col);
|
||||||
return html`
|
return html`
|
||||||
<th
|
<th
|
||||||
@@ -801,7 +865,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
const key = String(col.key);
|
const key = String(col.key);
|
||||||
if (col.filterable === false) return html`<th></th>`;
|
if (col.filterable === false) return html`<th></th>`;
|
||||||
return html`<th>
|
return html`<th>
|
||||||
<input type="text" placeholder="Filter..." .value=${this.columnFilters[key] || ''}
|
<input type="text" placeholder="Filter..." data-col-key=${key} .value=${this.columnFilters[key] || ''}
|
||||||
@input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
|
@input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
|
||||||
</th>`;
|
</th>`;
|
||||||
})}
|
})}
|
||||||
@@ -957,6 +1021,84 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
if (fh) fh.classList.remove('active');
|
if (fh) fh.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a filter `<input>` inside the floating-header clone currently has
|
||||||
|
* focus, copy its value, caret, and selection range onto the matching
|
||||||
|
* input in the real header, then focus that real input. This lets the
|
||||||
|
* user keep typing uninterrupted when filter input causes the table to
|
||||||
|
* shrink below the viewport stick line and the floating header has to
|
||||||
|
* unmount.
|
||||||
|
*
|
||||||
|
* Safe to call at any time — it is a no-op unless an input inside the
|
||||||
|
* floating header is focused and has a `data-col-key` attribute that
|
||||||
|
* matches a real-header input.
|
||||||
|
*/
|
||||||
|
private __transferFocusToRealHeader(): void {
|
||||||
|
const fh = this.__floatingHeaderEl;
|
||||||
|
if (!fh) return;
|
||||||
|
const active = this.shadowRoot?.activeElement as HTMLElement | null;
|
||||||
|
if (!active || !fh.contains(active)) return;
|
||||||
|
const colKey = active.getAttribute('data-col-key');
|
||||||
|
if (!colKey) return;
|
||||||
|
const fromInput = active as HTMLInputElement;
|
||||||
|
const real = this.shadowRoot?.querySelector(
|
||||||
|
`.tableScroll > table > thead input[data-col-key="${CSS.escape(colKey)}"]`
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
if (!real || real === fromInput) return;
|
||||||
|
const selStart = fromInput.selectionStart;
|
||||||
|
const selEnd = fromInput.selectionEnd;
|
||||||
|
const selDir = fromInput.selectionDirection as any;
|
||||||
|
real.focus({ preventScroll: true });
|
||||||
|
try {
|
||||||
|
if (selStart != null && selEnd != null) {
|
||||||
|
real.setSelectionRange(selStart, selEnd, selDir || undefined);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* setSelectionRange throws on unsupported input types — ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symmetric counterpart to `__transferFocusToRealHeader`. When the
|
||||||
|
* floating header has just activated and a real-header filter input
|
||||||
|
* was focused (and is now scrolled off-screen behind the floating
|
||||||
|
* clone), move focus to the clone's matching input so the user keeps
|
||||||
|
* typing in the visible one.
|
||||||
|
*
|
||||||
|
* Called from `__syncFloatingHeader` inside the post-activation
|
||||||
|
* `updateComplete` callback — by then the clone subtree exists in the
|
||||||
|
* DOM and can receive focus.
|
||||||
|
*/
|
||||||
|
private __transferFocusToFloatingHeader(): void {
|
||||||
|
const fh = this.__floatingHeaderEl;
|
||||||
|
if (!fh || !this.__floatingActive) return;
|
||||||
|
const active = this.shadowRoot?.activeElement as HTMLElement | null;
|
||||||
|
if (!active) return;
|
||||||
|
// Only handle focus that lives in the real header (not already in the clone).
|
||||||
|
const realThead = this.shadowRoot?.querySelector(
|
||||||
|
'.tableScroll > table > thead'
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (!realThead || !realThead.contains(active)) return;
|
||||||
|
const colKey = active.getAttribute('data-col-key');
|
||||||
|
if (!colKey) return;
|
||||||
|
const fromInput = active as HTMLInputElement;
|
||||||
|
const clone = fh.querySelector(
|
||||||
|
`input[data-col-key="${CSS.escape(colKey)}"]`
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
if (!clone || clone === fromInput) return;
|
||||||
|
const selStart = fromInput.selectionStart;
|
||||||
|
const selEnd = fromInput.selectionEnd;
|
||||||
|
const selDir = fromInput.selectionDirection as any;
|
||||||
|
clone.focus({ preventScroll: true });
|
||||||
|
try {
|
||||||
|
if (selStart != null && selEnd != null) {
|
||||||
|
clone.setSelectionRange(selStart, selEnd, selDir || undefined);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Virtualization ─────────────────────────────────────────────────
|
// ─── Virtualization ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1053,18 +1195,42 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active when the table top is above the stick line and the table bottom
|
// Active when the table top is above the stick line and any pixel of the
|
||||||
// hasn't yet scrolled past it.
|
// table still sits below it. As the table's bottom edge approaches the
|
||||||
const shouldBeActive =
|
// stick line we shrink the floating container and slide the cloned header
|
||||||
tableRect.top < stick.top && tableRect.bottom > stick.top + Math.min(headerHeight, 1);
|
// up inside it, so the header appears to scroll off with the table
|
||||||
|
// instead of snapping away in one frame.
|
||||||
|
const distance = tableRect.bottom - stick.top;
|
||||||
|
const shouldBeActive = tableRect.top < stick.top && distance > 0;
|
||||||
|
|
||||||
if (shouldBeActive !== this.__floatingActive) {
|
if (shouldBeActive !== this.__floatingActive) {
|
||||||
|
if (!shouldBeActive) {
|
||||||
|
// Before we flag the clone for unmount, hand off any focused
|
||||||
|
// filter input to its counterpart in the real header. This is the
|
||||||
|
// "user is typing in a sticky filter input, filter shrinks the
|
||||||
|
// table so the floating header hides" case — without this
|
||||||
|
// handoff the user's focus (and caret position) would be lost
|
||||||
|
// when the clone unmounts.
|
||||||
|
this.__transferFocusToRealHeader();
|
||||||
|
}
|
||||||
this.__floatingActive = shouldBeActive;
|
this.__floatingActive = shouldBeActive;
|
||||||
fh.classList.toggle('active', shouldBeActive);
|
fh.classList.toggle('active', shouldBeActive);
|
||||||
|
if (!shouldBeActive) {
|
||||||
|
// Reset inline geometry so the next activation starts clean.
|
||||||
|
fh.style.height = '';
|
||||||
|
const ft = this.__floatingTableEl;
|
||||||
|
if (ft) ft.style.transform = '';
|
||||||
|
}
|
||||||
if (shouldBeActive) {
|
if (shouldBeActive) {
|
||||||
// Clone subtree doesn't exist yet — wait for the next render to
|
// Clone subtree doesn't exist yet — wait for the next render to
|
||||||
// materialize it, then complete geometry sync.
|
// materialize it, then complete geometry sync. Additionally, if a
|
||||||
this.updateComplete.then(() => this.__syncFloatingHeader());
|
// real-header filter input was focused when we activated, hand
|
||||||
|
// off to the clone once it exists so the user keeps typing in
|
||||||
|
// the visible (floating) input.
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
this.__syncFloatingHeader();
|
||||||
|
this.__transferFocusToFloatingHeader();
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1100,15 +1266,28 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
fh.style.left = `${clipLeft}px`;
|
fh.style.left = `${clipLeft}px`;
|
||||||
fh.style.width = `${clipWidth}px`;
|
fh.style.width = `${clipWidth}px`;
|
||||||
|
|
||||||
|
// Exit animation: when the table's bottom edge is within `headerHeight`
|
||||||
|
// pixels of the stick line, shrink the container and translate the
|
||||||
|
// inner table up by the same amount. overflow:hidden on .floatingHeader
|
||||||
|
// clips the overflow, producing a scroll-off effect.
|
||||||
|
const visibleHeight = Math.min(headerHeight, distance);
|
||||||
|
const exitOffset = headerHeight - visibleHeight;
|
||||||
|
fh.style.height = `${visibleHeight}px`;
|
||||||
|
|
||||||
// The inner table is positioned so the visible region matches the real
|
// The inner table is positioned so the visible region matches the real
|
||||||
// table's left edge — shift it left when we clipped to the container.
|
// table's left edge — shift it left when we clipped to the container.
|
||||||
floatTable.style.width = `${tableRect.width}px`;
|
floatTable.style.width = `${tableRect.width}px`;
|
||||||
floatTable.style.marginLeft = `${tableRect.left - clipLeft}px`;
|
floatTable.style.marginLeft = `${tableRect.left - clipLeft}px`;
|
||||||
|
floatTable.style.transform = exitOffset > 0 ? `translateY(-${exitOffset}px)` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disconnectedCallback() {
|
public async disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.teardownFloatingHeader();
|
this.teardownFloatingHeader();
|
||||||
|
if (this.__flashClearTimer) {
|
||||||
|
clearTimeout(this.__flashClearTimer);
|
||||||
|
this.__flashClearTimer = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
@@ -1116,9 +1295,141 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
// table markup actually exists (it only renders when data.length > 0).
|
// table markup actually exists (it only renders when data.length > 0).
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs before each render. Drives two independent concerns:
|
||||||
|
*
|
||||||
|
* 1. **Selection rebind** — when `data` is reassigned to a fresh array
|
||||||
|
* (typical live-data pattern), `selectedDataRow` still points at the
|
||||||
|
* stale row object from the old array. We re-resolve it by rowKey so
|
||||||
|
* consumers of `selectedDataRow` (footer indicator, header/footer
|
||||||
|
* actions, copy fallback) see the live reference. `selectedIds`,
|
||||||
|
* `__focusedCell`, `__editingCell`, `__selectionAnchorId` are all
|
||||||
|
* keyed by string rowId and persist automatically — no change needed.
|
||||||
|
* This runs regardless of `highlightUpdates` — it is a baseline
|
||||||
|
* correctness fix for live data.
|
||||||
|
*
|
||||||
|
* 2. **Flash diff** — when `highlightUpdates === 'flash'`, diff the new
|
||||||
|
* data against `__prevSnapshot` and populate `__flashingCells` with
|
||||||
|
* the (rowId, colKey) pairs whose resolved cell value changed. A
|
||||||
|
* single shared timer clears `__flashingCells` after
|
||||||
|
* `highlightDuration` ms. Skipped if `rowKey` is missing (with a
|
||||||
|
* one-shot console.warn; the render surface also shows a warning
|
||||||
|
* banner).
|
||||||
|
*/
|
||||||
|
public willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {
|
||||||
|
// --- Phase 1: selection rebind (always runs) ---
|
||||||
|
if (changedProperties.has('data') && this.selectedDataRow && this.rowKey) {
|
||||||
|
const prevId = this.getRowId(this.selectedDataRow);
|
||||||
|
let found: T | undefined;
|
||||||
|
for (const row of this.data) {
|
||||||
|
if (this.getRowId(row) === prevId) {
|
||||||
|
found = row;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found) {
|
||||||
|
if (found !== this.selectedDataRow) this.selectedDataRow = found;
|
||||||
|
} else {
|
||||||
|
this.selectedDataRow = undefined as unknown as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 2: flash diff ---
|
||||||
|
if (this.highlightUpdates !== 'flash') {
|
||||||
|
// Mode was toggled off (or never on) — drop any lingering state so
|
||||||
|
// re-enabling later starts with a clean slate.
|
||||||
|
if (this.__prevSnapshot || this.__flashingCells.size > 0) {
|
||||||
|
this.__prevSnapshot = undefined;
|
||||||
|
if (this.__flashingCells.size > 0) this.__flashingCells = new Map();
|
||||||
|
if (this.__flashClearTimer) {
|
||||||
|
clearTimeout(this.__flashClearTimer);
|
||||||
|
this.__flashClearTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.rowKey) {
|
||||||
|
if (!this.__flashWarnedNoRowKey) {
|
||||||
|
this.__flashWarnedNoRowKey = true;
|
||||||
|
console.warn(
|
||||||
|
'[dees-table] highlightUpdates="flash" requires `rowKey` to be set. Flash is disabled. ' +
|
||||||
|
'Set the rowKey property/attribute to a stable identifier on your row data (e.g. `rowKey="id"`).'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!changedProperties.has('data')) return;
|
||||||
|
|
||||||
|
const effectiveColumns = this.__getEffectiveColumns();
|
||||||
|
const visibleCols = effectiveColumns.filter((c) => !c.hidden);
|
||||||
|
const nextSnapshot = new Map<string, Map<string, unknown>>();
|
||||||
|
const newlyFlashing = new Map<string, Set<string>>();
|
||||||
|
|
||||||
|
for (const row of this.data) {
|
||||||
|
const rowId = this.getRowId(row);
|
||||||
|
const cellMap = new Map<string, unknown>();
|
||||||
|
for (const col of visibleCols) {
|
||||||
|
cellMap.set(String(col.key), getCellValueFn(row, col, this.displayFunction));
|
||||||
|
}
|
||||||
|
nextSnapshot.set(rowId, cellMap);
|
||||||
|
|
||||||
|
const prevCells = this.__prevSnapshot?.get(rowId);
|
||||||
|
if (!prevCells) continue; // new row — not an "update"
|
||||||
|
for (const [colKey, nextVal] of cellMap) {
|
||||||
|
if (prevCells.get(colKey) !== nextVal) {
|
||||||
|
// Don't flash the cell the user is actively editing.
|
||||||
|
if (
|
||||||
|
this.__editingCell &&
|
||||||
|
this.__editingCell.rowId === rowId &&
|
||||||
|
this.__editingCell.colKey === colKey
|
||||||
|
) continue;
|
||||||
|
let set = newlyFlashing.get(rowId);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
newlyFlashing.set(rowId, set);
|
||||||
|
}
|
||||||
|
set.add(colKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hadPrev = !!this.__prevSnapshot;
|
||||||
|
this.__prevSnapshot = nextSnapshot;
|
||||||
|
if (!hadPrev) return; // first time seeing data — no flashes
|
||||||
|
|
||||||
|
if (newlyFlashing.size === 0) return;
|
||||||
|
|
||||||
|
// Merge with any in-flight flashes from a rapid second update so a cell
|
||||||
|
// that changes twice before its animation ends gets a single clean
|
||||||
|
// restart (via __flashTick / directives.keyed) instead of stacking.
|
||||||
|
for (const [rowId, cols] of newlyFlashing) {
|
||||||
|
const existing = this.__flashingCells.get(rowId);
|
||||||
|
if (existing) {
|
||||||
|
for (const c of cols) existing.add(c);
|
||||||
|
} else {
|
||||||
|
this.__flashingCells.set(rowId, cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.__flashTick++;
|
||||||
|
// Reactivity nudge: we've mutated the Map in place, so give Lit a fresh
|
||||||
|
// reference so the @state change fires for render.
|
||||||
|
this.__flashingCells = new Map(this.__flashingCells);
|
||||||
|
if (this.__flashClearTimer) clearTimeout(this.__flashClearTimer);
|
||||||
|
this.__flashClearTimer = setTimeout(() => {
|
||||||
|
this.__flashingCells = new Map();
|
||||||
|
this.__flashClearTimer = undefined;
|
||||||
|
}, Math.max(0, this.highlightDuration));
|
||||||
|
}
|
||||||
|
|
||||||
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
// Feed highlightDuration into the CSS variable so JS and CSS stay in
|
||||||
|
// sync via a single source of truth.
|
||||||
|
if (changedProperties.has('highlightDuration')) {
|
||||||
|
this.style.setProperty('--dees-table-flash-duration', `${this.highlightDuration}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
// Only re-measure column widths when the data or schema actually changed
|
// Only re-measure column widths when the data or schema actually changed
|
||||||
// (or on first paint). `determineColumnWidths` is the single biggest
|
// (or on first paint). `determineColumnWidths` is the single biggest
|
||||||
// first-paint cost — it forces multiple layout flushes per row.
|
// first-paint cost — it forces multiple layout flushes per row.
|
||||||
@@ -1496,7 +1807,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
// Maximum exposed slot: one beyond the current cascade, capped at the
|
// Maximum exposed slot: one beyond the current cascade, capped at the
|
||||||
// number of sortable columns. If the column is already in the cascade we
|
// number of sortable columns. If the column is already in the cascade we
|
||||||
// never need to grow the slot count.
|
// never need to grow the slot count.
|
||||||
const sortableColumnCount = effectiveColumns.filter((c) => !!c.sortable).length;
|
const sortableColumnCount = effectiveColumns.filter((c) => c.sortable !== false).length;
|
||||||
const maxSlot = Math.min(
|
const maxSlot = Math.min(
|
||||||
Math.max(cascadeLen + (existing ? 0 : 1), 1),
|
Math.max(cascadeLen + (existing ? 0 : 1), 1),
|
||||||
Math.max(sortableColumnCount, 1)
|
Math.max(sortableColumnCount, 1)
|
||||||
@@ -1602,6 +1913,17 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.push({ divider: true });
|
||||||
|
items.push({
|
||||||
|
name: this.showColumnFilters ? 'Hide column filters' : 'Show column filters',
|
||||||
|
iconName: this.showColumnFilters ? 'lucide:filterX' : 'lucide:filter',
|
||||||
|
action: async () => {
|
||||||
|
this.showColumnFilters = !this.showColumnFilters;
|
||||||
|
this.requestUpdate();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2023,6 +2345,10 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
'.editingCell dees-input-tags'
|
'.editingCell dees-input-tags'
|
||||||
) as any;
|
) as any;
|
||||||
el?.focus?.();
|
el?.focus?.();
|
||||||
|
// Dropdown editors should auto-open so the user can pick immediately.
|
||||||
|
if (el?.tagName === 'DEES-INPUT-DROPDOWN') {
|
||||||
|
el.updateComplete?.then(() => el.toggleSelectionBox?.());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2057,6 +2383,10 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
}
|
}
|
||||||
if (parsed !== oldValue) {
|
if (parsed !== oldValue) {
|
||||||
(item as any)[col.key] = parsed;
|
(item as any)[col.key] = parsed;
|
||||||
|
// Keep the flash-diff snapshot in sync so the next external update
|
||||||
|
// does not see this user edit as an external change (which would
|
||||||
|
// otherwise flash the cell the user just typed into).
|
||||||
|
this.__recordCellInSnapshot(item, col);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('cellEdit', {
|
new CustomEvent('cellEdit', {
|
||||||
detail: { row: item, key, oldValue, newValue: parsed },
|
detail: { row: item, key, oldValue, newValue: parsed },
|
||||||
@@ -2070,6 +2400,24 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the flash diff snapshot for a single cell to match its current
|
||||||
|
* resolved value. Called from `commitCellEdit` so a user-initiated edit
|
||||||
|
* does not register as an external change on the next diff pass.
|
||||||
|
* No-op when flash mode is off or no snapshot exists yet.
|
||||||
|
*/
|
||||||
|
private __recordCellInSnapshot(item: T, col: Column<T>): void {
|
||||||
|
if (this.highlightUpdates !== 'flash' || !this.__prevSnapshot) return;
|
||||||
|
if (!this.rowKey) return;
|
||||||
|
const rowId = this.getRowId(item);
|
||||||
|
let cellMap = this.__prevSnapshot.get(rowId);
|
||||||
|
if (!cellMap) {
|
||||||
|
cellMap = new Map();
|
||||||
|
this.__prevSnapshot.set(rowId, cellMap);
|
||||||
|
}
|
||||||
|
cellMap.set(String(col.key), getCellValueFn(item, col, this.displayFunction));
|
||||||
|
}
|
||||||
|
|
||||||
/** Renders the appropriate dees-input-* component for this column. */
|
/** Renders the appropriate dees-input-* component for this column. */
|
||||||
private renderCellEditor(item: T, col: Column<T>): TemplateResult {
|
private renderCellEditor(item: T, col: Column<T>): TemplateResult {
|
||||||
const raw = (item as any)[col.key];
|
const raw = (item as any)[col.key];
|
||||||
@@ -2090,8 +2438,13 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
case 'dropdown': {
|
case 'dropdown': {
|
||||||
const options = (col.editorOptions?.options as any[]) ?? [];
|
const options = (col.editorOptions?.options as any[]) ?? [];
|
||||||
const selected =
|
const selected =
|
||||||
options.find((o: any) => (o?.option ?? o?.key ?? o) === value) ?? null;
|
options.find((o: any) => {
|
||||||
|
if (o == null) return false;
|
||||||
|
if (typeof o === 'string') return o === raw;
|
||||||
|
return o.key === raw || o.option === raw;
|
||||||
|
}) ?? null;
|
||||||
return html`<dees-input-dropdown
|
return html`<dees-input-dropdown
|
||||||
|
.vintegrated=${true}
|
||||||
.options=${options}
|
.options=${options}
|
||||||
.selectedOption=${selected}
|
.selectedOption=${selected}
|
||||||
@selectedOption=${(e: CustomEvent<any>) => {
|
@selectedOption=${(e: CustomEvent<any>) => {
|
||||||
@@ -2121,6 +2474,7 @@ export class DeesTable<T> extends DeesElement {
|
|||||||
case 'text':
|
case 'text':
|
||||||
default:
|
default:
|
||||||
return html`<dees-input-text
|
return html`<dees-input-text
|
||||||
|
.vintegrated=${true}
|
||||||
.value=${value == null ? '' : String(value)}
|
.value=${value == null ? '' : String(value)}
|
||||||
@focusout=${(e: any) => onTextCommit(e.target)}
|
@focusout=${(e: any) => onTextCommit(e.target)}
|
||||||
@keydown=${(e: KeyboardEvent) => this.__handleEditorKey(e, item, col)}
|
@keydown=${(e: KeyboardEvent) => this.__handleEditorKey(e, item, col)}
|
||||||
|
|||||||
@@ -373,6 +373,72 @@ export const tableStyles: CSSResult[] = [
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Cell flash highlighting (opt-in via highlight-updates="flash") ----
|
||||||
|
Bloomberg/TradingView-style: the text itself briefly takes an accent
|
||||||
|
color then fades back to the default. No background tint, no layout
|
||||||
|
shift, no weight change. Readable, modern, subtle.
|
||||||
|
Consumers can override per instance:
|
||||||
|
dees-table#myTable { --dees-table-flash-color: hsl(142 76% 40%); }
|
||||||
|
*/
|
||||||
|
:host {
|
||||||
|
--dees-table-flash-color: ${cssManager.bdTheme(
|
||||||
|
'hsl(32 95% 44%)',
|
||||||
|
'hsl(45 93% 62%)'
|
||||||
|
)};
|
||||||
|
--dees-table-flash-easing: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.innerCellContainer.flashing {
|
||||||
|
animation: dees-table-cell-flash
|
||||||
|
var(--dees-table-flash-duration, 900ms)
|
||||||
|
var(--dees-table-flash-easing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hold the accent color briefly, then fade back to the theme's default
|
||||||
|
text color. Inherits to child text and to SVG icons that use
|
||||||
|
currentColor. Cells with explicit color overrides in renderers are
|
||||||
|
intentionally unaffected. */
|
||||||
|
@keyframes dees-table-cell-flash {
|
||||||
|
0%,
|
||||||
|
35% { color: var(--dees-table-flash-color); }
|
||||||
|
100% { color: var(--dees-color-text-primary); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.innerCellContainer.flashing {
|
||||||
|
animation: none;
|
||||||
|
color: var(--dees-table-flash-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dev-time warning banner shown when highlight-updates="flash" but
|
||||||
|
rowKey is missing. Consumers should never ship this to production. */
|
||||||
|
.flashConfigWarning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 16px 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-left: 3px solid ${cssManager.bdTheme('hsl(38 92% 50%)', 'hsl(48 96% 63%)')};
|
||||||
|
background: ${cssManager.bdTheme('hsl(48 96% 89% / 0.6)', 'hsl(48 96% 30% / 0.15)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(32 81% 29%)', 'hsl(48 96% 80%)')};
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.flashConfigWarning dees-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.flashConfigWarning code {
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.6)', 'hsl(0 0% 0% / 0.3)')};
|
||||||
|
font-family: ${cssGeistFontFamily};
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Editable cell affordances */
|
/* Editable cell affordances */
|
||||||
td.editable {
|
td.editable {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
@@ -386,6 +452,11 @@ export const tableStyles: CSSResult[] = [
|
|||||||
}
|
}
|
||||||
td.editingCell {
|
td.editingCell {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
outline: 2px solid ${cssManager.bdTheme(
|
||||||
|
'hsl(222.2 47.4% 51.2% / 0.6)',
|
||||||
|
'hsl(217.2 91.2% 59.8% / 0.6)'
|
||||||
|
)};
|
||||||
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
td.editingCell .innerCellContainer {
|
td.editingCell .innerCellContainer {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface Column<T = any> {
|
|||||||
header?: string | TemplateResult;
|
header?: string | TemplateResult;
|
||||||
value?: (row: T) => any;
|
value?: (row: T) => any;
|
||||||
renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column<T> }) => TemplateResult | string;
|
renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column<T> }) => TemplateResult | string;
|
||||||
|
/** Whether this column can be sorted by clicking its header. Defaults to `true`; set to `false` to disable. */
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
/** whether this column participates in per-column quick filtering (default: true) */
|
/** whether this column participates in per-column quick filtering (default: true) */
|
||||||
filterable?: boolean;
|
filterable?: boolean;
|
||||||
|
|||||||
@@ -75,12 +75,23 @@ export class DeesFormSubmit extends DeesElement {
|
|||||||
.text=${this.text}
|
.text=${this.text}
|
||||||
?disabled=${this.disabled}
|
?disabled=${this.disabled}
|
||||||
@clicked=${this.submit}
|
@clicked=${this.submit}
|
||||||
>
|
></dees-button>
|
||||||
<slot></slot>
|
|
||||||
</dees-button>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async firstUpdated() {
|
||||||
|
// Capture light DOM text content as the button label. dees-button wipes
|
||||||
|
// its own light DOM during extractLightDom(), so we cannot simply forward
|
||||||
|
// a <slot> into it — we have to hoist the text onto the .text property
|
||||||
|
// ourselves before handing it to dees-button.
|
||||||
|
if (!this.text) {
|
||||||
|
const slotText = this.textContent?.trim();
|
||||||
|
if (slotText) {
|
||||||
|
this.text = slotText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async submit() {
|
public async submit() {
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const demoFunc = () => html`
|
|||||||
.required=${true}
|
.required=${true}
|
||||||
key="firstName"
|
key="firstName"
|
||||||
label="First Name"
|
label="First Name"
|
||||||
.description=${'Your given name'}
|
.infoText=${'Your given name'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
@@ -105,7 +105,7 @@ export const demoFunc = () => html`
|
|||||||
.required=${true}
|
.required=${true}
|
||||||
key="email"
|
key="email"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
.description=${'We will use this to contact you'}
|
.infoText=${'We will use this to contact you'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
@@ -126,7 +126,7 @@ export const demoFunc = () => html`
|
|||||||
key="password"
|
key="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
isPasswordBool
|
isPasswordBool
|
||||||
.description=${'Minimum 8 characters'}
|
.infoText=${'Minimum 8 characters'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-checkbox
|
<dees-input-checkbox
|
||||||
@@ -300,7 +300,7 @@ export const demoFunc = () => html`
|
|||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
key="documents"
|
key="documents"
|
||||||
.label=${'Upload Documents'}
|
.label=${'Upload Documents'}
|
||||||
.description=${'PDF, DOC, or DOCX files up to 10MB'}
|
.infoText=${'PDF, DOC, or DOCX files up to 10MB'}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-form-submit>Submit Application</dees-form-submit>
|
<dees-form-submit>Submit Application</dees-form-submit>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
property,
|
property,
|
||||||
|
html,
|
||||||
css,
|
css,
|
||||||
type CSSResult,
|
type CSSResult,
|
||||||
cssManager,
|
cssManager,
|
||||||
@@ -42,6 +43,9 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor disabled: boolean = false;
|
accessor disabled: boolean = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor infoText!: string;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor description!: string;
|
accessor description!: string;
|
||||||
|
|
||||||
@@ -90,6 +94,14 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
|||||||
:host([label-position="none"]) dees-label {
|
:host([label-position="none"]) dees-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Description text below input */
|
||||||
|
.descriptionText {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -155,6 +167,14 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
|||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the description text below the input.
|
||||||
|
* Call ${this.renderDescription()} at the end of your render template.
|
||||||
|
*/
|
||||||
|
public renderDescription() {
|
||||||
|
return this.description ? html`<div class="descriptionText">${this.description}</div>` : '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract method that child classes must implement to get their value
|
* Abstract method that child classes must implement to get their value
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -147,12 +147,6 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
|||||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Description */
|
|
||||||
.description-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -185,7 +179,7 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
|||||||
</div>
|
</div>
|
||||||
<div class="label-container">
|
<div class="label-container">
|
||||||
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
|
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
|
||||||
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>
|
||||||
<dees-tile>
|
<dees-tile>
|
||||||
<div slot="header" class="toolbar">
|
<div slot="header" class="toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
@@ -362,6 +362,7 @@ export class DeesInputCode extends DeesInputBase<string> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dees-tile>
|
</dees-tile>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { DeesInputDatepicker } from './component.js';
|
|||||||
export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult => {
|
export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult => {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${component.label} .description=${component.description} .required=${component.required}></dees-label>
|
<dees-label .label=${component.label} .infoText=${component.infoText} .required=${component.required}></dees-label>
|
||||||
<div class="input-container">
|
<div class="input-container">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -27,6 +27,7 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
|
|||||||
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${component.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-dropdown
|
<dees-input-dropdown
|
||||||
.label=${'Select Country'}
|
.label=${'Select Country'}
|
||||||
|
.description=${'Choose the country where your business is registered'}
|
||||||
.options=${[
|
.options=${[
|
||||||
{ option: 'United States', key: 'us' },
|
{ option: 'United States', key: 'us' },
|
||||||
{ option: 'Canada', key: 'ca' },
|
{ option: 'Canada', key: 'ca' },
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
})
|
})
|
||||||
accessor enableSearch: boolean = true;
|
accessor enableSearch: boolean = true;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
accessor vintegrated: boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor isOpened = false;
|
accessor isOpened = false;
|
||||||
|
|
||||||
@@ -126,13 +132,43 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
.selectedBox.open::after {
|
.selectedBox.open::after {
|
||||||
transform: translateY(-50%) rotate(180deg);
|
transform: translateY(-50%) rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Visually integrated mode: shed chrome to blend into a host component
|
||||||
|
(e.g. a dees-table cell in edit mode). */
|
||||||
|
:host([vintegrated]) dees-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) .maincontainer {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) .selectedBox {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 32px 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) .selectedBox:hover:not(.disabled),
|
||||||
|
:host([vintegrated]) .selectedBox:focus-visible {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) .selectedBox::after {
|
||||||
|
right: 12px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>
|
||||||
<div class="maincontainer">
|
<div class="maincontainer">
|
||||||
<div
|
<div
|
||||||
class="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}"
|
class="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}"
|
||||||
@@ -143,6 +179,7 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
|||||||
${this.selectedOption?.option || 'Select an option'}
|
${this.selectedOption?.option || 'Select an option'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label
|
<dees-label
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.description=${this.description}
|
.infoText=${this.infoText}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
></dees-label>
|
></dees-label>
|
||||||
<dees-tile
|
<dees-tile
|
||||||
@@ -114,6 +114,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
|||||||
${this.validationMessage
|
${this.validationMessage
|
||||||
? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
|
? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
|
||||||
: html``}
|
: html``}
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,14 +59,15 @@ export const demoFunc = () => html`
|
|||||||
<div class="demo-stack">
|
<div class="demo-stack">
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Attachments'}
|
.label=${'Attachments'}
|
||||||
.description=${'Upload supporting documents for your request'}
|
.infoText=${'Upload supporting documents for your request'}
|
||||||
|
.description=${'Accepted formats: images, PDF, and ZIP archives up to 10MB'}
|
||||||
.accept=${'image/*,.pdf,.zip'}
|
.accept=${'image/*,.pdf,.zip'}
|
||||||
.maxSize=${10 * 1024 * 1024}
|
.maxSize=${10 * 1024 * 1024}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Brand assets'}
|
.label=${'Brand assets'}
|
||||||
.description=${'Upload high-resolution imagery (JPG/PNG)'}
|
.infoText=${'Upload high-resolution imagery (JPG/PNG)'}
|
||||||
.accept=${'image/jpeg,image/png'}
|
.accept=${'image/jpeg,image/png'}
|
||||||
.multiple=${false}
|
.multiple=${false}
|
||||||
.maxSize=${5 * 1024 * 1024}
|
.maxSize=${5 * 1024 * 1024}
|
||||||
@@ -77,14 +78,14 @@ export const demoFunc = () => html`
|
|||||||
<div class="demo-stack">
|
<div class="demo-stack">
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Audio uploads'}
|
.label=${'Audio uploads'}
|
||||||
.description=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
|
.infoText=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
|
||||||
.accept=${'audio/*'}
|
.accept=${'audio/*'}
|
||||||
.maxSize=${25 * 1024 * 1024}
|
.maxSize=${25 * 1024 * 1024}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Disabled example'}
|
.label=${'Disabled example'}
|
||||||
.description=${'Uploader is disabled while moderation is pending'}
|
.infoText=${'Uploader is disabled while moderation is pending'}
|
||||||
.disabled=${true}
|
.disabled=${true}
|
||||||
></dees-input-fileupload>
|
></dees-input-fileupload>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +101,7 @@ export const demoFunc = () => html`
|
|||||||
<div class="demo-stack">
|
<div class="demo-stack">
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Project name'}
|
.label=${'Project name'}
|
||||||
.description=${'How should we refer to this project internally?'}
|
.infoText=${'How should we refer to this project internally?'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
.key=${'projectName'}
|
.key=${'projectName'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
@@ -114,7 +115,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Statement of work'}
|
.label=${'Statement of work'}
|
||||||
.description=${'Upload a signed statement of work (PDF, max 15MB)'}
|
.infoText=${'Upload a signed statement of work (PDF, max 15MB)'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
.accept=${'application/pdf'}
|
.accept=${'application/pdf'}
|
||||||
.maxSize=${15 * 1024 * 1024}
|
.maxSize=${15 * 1024 * 1024}
|
||||||
@@ -124,7 +125,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-fileupload
|
<dees-input-fileupload
|
||||||
.label=${'Creative references'}
|
.label=${'Creative references'}
|
||||||
.description=${'Optional. Upload up to five visual references'}
|
.infoText=${'Optional. Upload up to five visual references'}
|
||||||
.accept=${'image/*'}
|
.accept=${'image/*'}
|
||||||
.maxFiles=${5}
|
.maxFiles=${5}
|
||||||
.maxSize=${8 * 1024 * 1024}
|
.maxSize=${8 * 1024 * 1024}
|
||||||
@@ -133,7 +134,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Notes'}
|
.label=${'Notes'}
|
||||||
.description=${'Add optional context for reviewers'}
|
.infoText=${'Add optional context for reviewers'}
|
||||||
.inputType=${'textarea'}
|
.inputType=${'textarea'}
|
||||||
.key=${'notes'}
|
.key=${'notes'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|||||||
@@ -33,12 +33,13 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-iban
|
<dees-input-iban
|
||||||
.label=${'Bank Account IBAN'}
|
.label=${'Bank Account IBAN'}
|
||||||
.description=${'Enter your International Bank Account Number'}
|
.infoText=${'Enter your International Bank Account Number'}
|
||||||
|
.description=${'Your IBAN can be found on your bank statement'}
|
||||||
></dees-input-iban>
|
></dees-input-iban>
|
||||||
|
|
||||||
<dees-input-iban
|
<dees-input-iban
|
||||||
.label=${'Verified IBAN'}
|
.label=${'Verified IBAN'}
|
||||||
.description=${'This IBAN has been verified'}
|
.infoText=${'This IBAN has been verified'}
|
||||||
.value=${'DE89370400440532013000'}
|
.value=${'DE89370400440532013000'}
|
||||||
></dees-input-iban>
|
></dees-input-iban>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,13 +65,13 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-iban
|
<dees-input-iban
|
||||||
.label=${'Payment Account'}
|
.label=${'Payment Account'}
|
||||||
.description=${'Required for processing payments'}
|
.infoText=${'Required for processing payments'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
></dees-input-iban>
|
></dees-input-iban>
|
||||||
|
|
||||||
<dees-input-iban
|
<dees-input-iban
|
||||||
.label=${'Locked IBAN'}
|
.label=${'Locked IBAN'}
|
||||||
.description=${'This IBAN cannot be changed'}
|
.infoText=${'This IBAN cannot be changed'}
|
||||||
.value=${'FR1420041010050500013M02606'}
|
.value=${'FR1420041010050500013M02606'}
|
||||||
.disabled=${true}
|
.disabled=${true}
|
||||||
></dees-input-iban>
|
></dees-input-iban>
|
||||||
@@ -81,7 +82,7 @@ export const demoFunc = () => html`
|
|||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .label=${'Recipient Name'} .required=${true}></dees-input-text>
|
<dees-input-text .label=${'Recipient Name'} .required=${true}></dees-input-text>
|
||||||
<dees-input-iban .label=${'Recipient IBAN'} .required=${true}></dees-input-iban>
|
<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=${'Transfer Reference'} .infoText=${'Optional reference for the transfer'}></dees-input-text>
|
||||||
<dees-input-text .label=${'Amount'} .inputType=${'number'} .required=${true}></dees-input-text>
|
<dees-input-text .label=${'Amount'} .inputType=${'number'} .required=${true}></dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
|
|||||||
@@ -44,16 +44,27 @@ export class DeesInputIban extends DeesInputBase<DeesInputIban> {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label || 'IBAN'} .description=${this.description}></dees-label>
|
<dees-label .label=${this.label || 'IBAN'} .infoText=${this.infoText}></dees-label>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.placeholder=${'DE89 3704 0044 0532 0130 00'}
|
.placeholder=${'DE89 3704 0044 0532 0130 00'}
|
||||||
|
.validationFunction=${(value: string) => {
|
||||||
|
const normalized = value.replace(/ /g, '');
|
||||||
|
if (normalized.length === 0) {
|
||||||
|
return { valid: true, message: '' };
|
||||||
|
}
|
||||||
|
const isValid = ibantools.isValidIBAN(normalized);
|
||||||
|
return isValid
|
||||||
|
? { valid: true, message: 'IBAN is valid' }
|
||||||
|
: { valid: false, message: 'Please enter a valid IBAN' };
|
||||||
|
}}
|
||||||
@input=${(eventArg: InputEvent) => {
|
@input=${(eventArg: InputEvent) => {
|
||||||
this.validateIban(eventArg);
|
this.validateIban(eventArg);
|
||||||
}}
|
}}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -81,10 +92,6 @@ export class DeesInputIban extends DeesInputBase<DeesInputIban> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.enteredIbanIsValid = ibantools.isValidIBAN(this.enteredString.replace(/ /g, ''));
|
this.enteredIbanIsValid = ibantools.isValidIBAN(this.enteredString.replace(/ /g, ''));
|
||||||
const deesInputText = this.shadowRoot!.querySelector('dees-input-text') as any;
|
|
||||||
if (deesInputText) {
|
|
||||||
deesInputText.validationText = `IBAN is valid: ${this.enteredIbanIsValid}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getValue(): string {
|
public getValue(): string {
|
||||||
|
|||||||
@@ -373,13 +373,6 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 4px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
.list-items::-webkit-scrollbar {
|
.list-items::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -546,9 +539,7 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
|||||||
<div class="validation-message">${this.validationText}</div>
|
<div class="validation-message">${this.validationText}</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
${this.description ? html`
|
${this.renderDescription()}
|
||||||
<div class="description">${this.description}</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-multitoggle
|
<dees-input-multitoggle
|
||||||
.label=${'Display Mode'}
|
.label=${'Display Mode'}
|
||||||
.description=${'Choose how content is displayed'}
|
.infoText=${'Choose how content is displayed'}
|
||||||
|
.description=${'This setting affects how items appear on your dashboard'}
|
||||||
.options=${['List View', 'Grid View', 'Compact']}
|
.options=${['List View', 'Grid View', 'Compact']}
|
||||||
.selectedOption=${'Grid View'}
|
.selectedOption=${'Grid View'}
|
||||||
></dees-input-multitoggle>
|
></dees-input-multitoggle>
|
||||||
@@ -64,7 +65,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-multitoggle
|
<dees-input-multitoggle
|
||||||
.label=${'T-Shirt Size'}
|
.label=${'T-Shirt Size'}
|
||||||
.description=${'Select your preferred size'}
|
.infoText=${'Select your preferred size'}
|
||||||
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
|
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
|
||||||
.selectedOption=${'M'}
|
.selectedOption=${'M'}
|
||||||
></dees-input-multitoggle>
|
></dees-input-multitoggle>
|
||||||
@@ -76,7 +77,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-multitoggle
|
<dees-input-multitoggle
|
||||||
.label=${'Notifications'}
|
.label=${'Notifications'}
|
||||||
.description=${'Enable or disable push notifications'}
|
.infoText=${'Enable or disable push notifications'}
|
||||||
.type=${'boolean'}
|
.type=${'boolean'}
|
||||||
.selectedOption=${'true'}
|
.selectedOption=${'true'}
|
||||||
></dees-input-multitoggle>
|
></dees-input-multitoggle>
|
||||||
@@ -85,7 +86,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-multitoggle
|
<dees-input-multitoggle
|
||||||
.label=${'Theme Mode'}
|
.label=${'Theme Mode'}
|
||||||
.description=${'Switch between light and dark theme'}
|
.infoText=${'Switch between light and dark theme'}
|
||||||
.type=${'boolean'}
|
.type=${'boolean'}
|
||||||
.booleanTrueName=${'Dark'}
|
.booleanTrueName=${'Dark'}
|
||||||
.booleanFalseName=${'Light'}
|
.booleanFalseName=${'Light'}
|
||||||
@@ -134,7 +135,7 @@ export const demoFunc = () => html`
|
|||||||
|
|
||||||
<dees-input-multitoggle
|
<dees-input-multitoggle
|
||||||
.label=${'Account Type'}
|
.label=${'Account Type'}
|
||||||
.description=${'This setting is locked'}
|
.infoText=${'This setting is locked'}
|
||||||
.options=${['Free', 'Pro', 'Enterprise']}
|
.options=${['Free', 'Pro', 'Enterprise']}
|
||||||
.selectedOption=${'Enterprise'}
|
.selectedOption=${'Enterprise'}
|
||||||
.disabled=${true}
|
.disabled=${true}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText}></dees-label>
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
<div class="selections">
|
<div class="selections">
|
||||||
<div class="indicator"></div>
|
<div class="indicator"></div>
|
||||||
@@ -158,6 +158,7 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-phone
|
<dees-input-phone
|
||||||
.label=${'Phone Number'}
|
.label=${'Phone Number'}
|
||||||
.description=${'Enter your phone number with country code'}
|
.infoText=${'Enter your phone number with country code'}
|
||||||
|
.description=${'Include country code for international numbers'}
|
||||||
.value=${'5551234567'}
|
.value=${'5551234567'}
|
||||||
></dees-input-phone>
|
></dees-input-phone>
|
||||||
|
|
||||||
<dees-input-phone
|
<dees-input-phone
|
||||||
.label=${'Contact Phone'}
|
.label=${'Contact Phone'}
|
||||||
.description=${'Required for account verification'}
|
.infoText=${'Required for account verification'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
.placeholder=${'+1 (555) 000-0000'}
|
.placeholder=${'+1 (555) 000-0000'}
|
||||||
></dees-input-phone>
|
></dees-input-phone>
|
||||||
@@ -66,7 +67,7 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-phone
|
<dees-input-phone
|
||||||
.label=${'International Contact'}
|
.label=${'International Contact'}
|
||||||
.description=${'Automatically formats international numbers'}
|
.infoText=${'Automatically formats international numbers'}
|
||||||
.value=${'441234567890'}
|
.value=${'441234567890'}
|
||||||
></dees-input-phone>
|
></dees-input-phone>
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText}></dees-label>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.value=${this.formattedPhone}
|
.value=${this.formattedPhone}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@@ -55,6 +55,7 @@ export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
|
|||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
@input=${(event: InputEvent) => this.handlePhoneInput(event)}
|
@input=${(event: InputEvent) => this.handlePhoneInput(event)}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,13 +69,14 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-quantityselector
|
<dees-input-quantityselector
|
||||||
.label=${'Quantity'}
|
.label=${'Quantity'}
|
||||||
.description=${'Select the desired quantity'}
|
.infoText=${'Select the desired quantity'}
|
||||||
|
.description=${'Minimum order quantity is 1 item'}
|
||||||
.value=${1}
|
.value=${1}
|
||||||
></dees-input-quantityselector>
|
></dees-input-quantityselector>
|
||||||
|
|
||||||
<dees-input-quantityselector
|
<dees-input-quantityselector
|
||||||
.label=${'Items in Cart'}
|
.label=${'Items in Cart'}
|
||||||
.description=${'Adjust the quantity of items'}
|
.infoText=${'Adjust the quantity of items'}
|
||||||
.value=${3}
|
.value=${3}
|
||||||
></dees-input-quantityselector>
|
></dees-input-quantityselector>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,14 +181,14 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-quantityselector
|
<dees-input-quantityselector
|
||||||
.label=${'Number of Licenses'}
|
.label=${'Number of Licenses'}
|
||||||
.description=${'Select how many licenses you need'}
|
.infoText=${'Select how many licenses you need'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
.value=${1}
|
.value=${1}
|
||||||
></dees-input-quantityselector>
|
></dees-input-quantityselector>
|
||||||
|
|
||||||
<dees-input-quantityselector
|
<dees-input-quantityselector
|
||||||
.label=${'Fixed Quantity'}
|
.label=${'Fixed Quantity'}
|
||||||
.description=${'This quantity cannot be changed'}
|
.infoText=${'This quantity cannot be changed'}
|
||||||
.disabled=${true}
|
.disabled=${true}
|
||||||
.value=${5}
|
.value=${5}
|
||||||
></dees-input-quantityselector>
|
></dees-input-quantityselector>
|
||||||
@@ -204,7 +205,7 @@ export const demoFunc = () => html`
|
|||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
<dees-input-quantityselector
|
<dees-input-quantityselector
|
||||||
.label=${'Quantity'}
|
.label=${'Quantity'}
|
||||||
.description=${'Number of licenses'}
|
.infoText=${'Number of licenses'}
|
||||||
.value=${1}
|
.value=${1}
|
||||||
></dees-input-quantityselector>
|
></dees-input-quantityselector>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
${this.label ? html`<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>` : ''}
|
${this.label ? html`<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>` : ''}
|
||||||
<div
|
<div
|
||||||
class="quantity-container ${this.disabled ? 'disabled' : ''}"
|
class="quantity-container ${this.disabled ? 'disabled' : ''}"
|
||||||
data-min="${this.value <= 0}"
|
data-min="${this.value <= 0}"
|
||||||
@@ -162,6 +162,7 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
|
|||||||
aria-label="Increase quantity"
|
aria-label="Increase quantity"
|
||||||
>+</div>
|
>+</div>
|
||||||
</div>
|
</div>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,14 +189,6 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
|
||||||
margin-top: 10px;
|
|
||||||
line-height: 1.5;
|
|
||||||
letter-spacing: -0.003em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Validation styles */
|
/* Validation styles */
|
||||||
:host([validationState="invalid"]) .radio-circle {
|
:host([validationState="invalid"]) .radio-circle {
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||||
@@ -256,7 +248,7 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
|||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,13 +278,6 @@ export const richtextStyles = [
|
|||||||
border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([disabled]) dees-tile {
|
:host([disabled]) dees-tile {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const renderRichtext = (component: DeesInputRichtext): TemplateResult =>
|
|||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
</dees-tile>
|
</dees-tile>
|
||||||
${component.description ? html`<div class="description">${component.description}</div>` : ''}
|
${component.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -210,14 +210,6 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Description styles */
|
|
||||||
.description {
|
|
||||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
|
||||||
font-size: 13px;
|
|
||||||
margin-top: 6px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
.suggestions-dropdown::-webkit-scrollbar {
|
.suggestions-dropdown::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -302,9 +294,7 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
|||||||
<div class="validation-message">${this.validationText}</div>
|
<div class="validation-message">${this.validationText}</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
${this.description ? html`
|
${this.renderDescription()}
|
||||||
<div class="description">${this.description}</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,12 +94,13 @@ export const demoFunc = () => html`
|
|||||||
.label=${'Username'}
|
.label=${'Username'}
|
||||||
.value=${'johndoe'}
|
.value=${'johndoe'}
|
||||||
.key=${'username'}
|
.key=${'username'}
|
||||||
|
.description=${'Your username will be visible to other users'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Email Address'}
|
.label=${'Email Address'}
|
||||||
.value=${'john@example.com'}
|
.value=${'john@example.com'}
|
||||||
.description=${'We will never share your email with anyone'}
|
.infoText=${'We will never share your email with anyone'}
|
||||||
.key=${'email'}
|
.key=${'email'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
@@ -210,39 +211,7 @@ export const demoFunc = () => html`
|
|||||||
</dees-panel>
|
</dees-panel>
|
||||||
</dees-demowrapper>
|
</dees-demowrapper>
|
||||||
|
|
||||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
<dees-demowrapper>
|
||||||
// Demonstrate validation states
|
|
||||||
const requiredInput = elementArg.querySelector('dees-input-text[required]') as DeesInputText;
|
|
||||||
const disabledInput = elementArg.querySelector('dees-input-text[disabled]') as DeesInputText;
|
|
||||||
const errorInput = elementArg.querySelector('dees-input-text[validationState="invalid"]') as DeesInputText;
|
|
||||||
|
|
||||||
if (requiredInput) {
|
|
||||||
// Show validation on blur for empty required field
|
|
||||||
requiredInput.addEventListener('blur', () => {
|
|
||||||
if (!requiredInput.getValue()) {
|
|
||||||
console.log('Required field is empty!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disabledInput) {
|
|
||||||
console.log('Disabled input cannot be edited');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorInput) {
|
|
||||||
console.log('Error input shows validation message:', errorInput.validationText);
|
|
||||||
|
|
||||||
// Simulate fixing the error
|
|
||||||
errorInput.addEventListener('changeSubject', () => {
|
|
||||||
const value = errorInput.getValue();
|
|
||||||
if (value.includes('@') && value.includes('.')) {
|
|
||||||
errorInput.validationState = 'valid';
|
|
||||||
errorInput.validationText = '';
|
|
||||||
console.log('Email validation passed!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
|
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
@@ -258,10 +227,15 @@ export const demoFunc = () => html`
|
|||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Field with Error'}
|
.label=${'Email with Validation'}
|
||||||
.value=${'invalid@'}
|
.value=${'invalid@'}
|
||||||
.validationText=${'Please enter a valid email address'}
|
.validationFunction=${(value: string) => {
|
||||||
.validationState=${'invalid'}
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (emailRegex.test(value)) {
|
||||||
|
return { valid: true, message: 'Email address is valid' };
|
||||||
|
}
|
||||||
|
return { valid: false, message: 'Please enter a valid email address' };
|
||||||
|
}}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
</div>
|
</div>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
@@ -297,49 +271,30 @@ export const demoFunc = () => html`
|
|||||||
.label=${'Password with Toggle'}
|
.label=${'Password with Toggle'}
|
||||||
.isPasswordBool=${true}
|
.isPasswordBool=${true}
|
||||||
.value=${'mySecurePassword123'}
|
.value=${'mySecurePassword123'}
|
||||||
.description=${'Click the eye icon to show/hide password'}
|
.infoText=${'Click the eye icon to show/hide password'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'API Key'}
|
.label=${'API Key'}
|
||||||
.isPasswordBool=${true}
|
.isPasswordBool=${true}
|
||||||
.value=${'sk-1234567890abcdef'}
|
.value=${'sk-1234567890abcdef'}
|
||||||
.description=${'Keep this key secure and never share it'}
|
.infoText=${'Keep this key secure and never share it'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
</div>
|
</div>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
</dees-demowrapper>
|
</dees-demowrapper>
|
||||||
|
|
||||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||||
// Set up interactive example
|
const dynamicInput = elementArg.querySelector('dees-input-text') as DeesInputText;
|
||||||
const dynamicInput = elementArg.querySelector('dees-input-text');
|
|
||||||
const output = elementArg.querySelector('#text-input-output');
|
const output = elementArg.querySelector('#text-input-output');
|
||||||
|
|
||||||
if (dynamicInput && output) {
|
if (dynamicInput && output) {
|
||||||
// Update output on every change
|
dynamicInput.changeSubject.subscribe(() => {
|
||||||
dynamicInput.addEventListener('changeSubject', ((event: CustomEvent) => {
|
output.textContent = `Current value: "${dynamicInput.getValue()}"`;
|
||||||
const value = (event.detail as DeesInputText).getValue();
|
|
||||||
output.textContent = `Current value: "${value}"`;
|
|
||||||
}) as EventListener);
|
|
||||||
|
|
||||||
// Also track focus/blur events
|
|
||||||
dynamicInput.addEventListener('focus', () => {
|
|
||||||
console.log('Input focused');
|
|
||||||
});
|
|
||||||
|
|
||||||
dynamicInput.addEventListener('blur', () => {
|
|
||||||
console.log('Input blurred');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track keypress events
|
|
||||||
let keypressCount = 0;
|
|
||||||
dynamicInput.addEventListener('keydown', () => {
|
|
||||||
keypressCount++;
|
|
||||||
console.log(`Keypress count: ${keypressCount}`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<dees-panel .title=${'Interactive Example'} .subtitle=${'Try typing in the inputs to see real-time value changes'}>
|
<dees-panel .title=${'Interactive Example'} .subtitle=${'Try typing in the input to see real-time value changes'}>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Dynamic Input'}
|
.label=${'Dynamic Input'}
|
||||||
.placeholder=${'Type something here...'}
|
.placeholder=${'Type something here...'}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
customElement,
|
customElement,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
property,
|
property,
|
||||||
|
state,
|
||||||
html,
|
html,
|
||||||
cssManager,
|
cssManager,
|
||||||
css,
|
css,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
|
import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -44,7 +46,7 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
accessor showPasswordBool = false;
|
accessor showPasswordBool = false;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: Boolean,
|
type: String,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
})
|
})
|
||||||
accessor validationState!: 'valid' | 'warn' | 'invalid';
|
accessor validationState!: 'valid' | 'warn' | 'invalid';
|
||||||
@@ -54,8 +56,19 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
})
|
})
|
||||||
accessor validationText: string = '';
|
accessor validationText: string = '';
|
||||||
|
|
||||||
@property({})
|
@property({ attribute: false })
|
||||||
accessor validationFunction!: (value: string) => boolean;
|
accessor validationFunction!: (value: string) => { valid: boolean; message?: string };
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
accessor vintegrated: boolean = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor validConfirmed: boolean = false;
|
||||||
|
|
||||||
|
private validTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
@@ -121,7 +134,7 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
.showPassword {
|
.showPassword {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1px;
|
right: 1px;
|
||||||
top: 50%;
|
top: 20px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -139,6 +152,29 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Valid checkmark icon */
|
||||||
|
.validIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.validIcon.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validIcon dees-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Validation styles */
|
/* Validation styles */
|
||||||
.validationContainer {
|
.validationContainer {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
@@ -150,7 +186,7 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.validationContainer.error {
|
.validationContainer.invalid {
|
||||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
|
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
|
||||||
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||||
}
|
}
|
||||||
@@ -166,34 +202,64 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Error state for input */
|
/* Error state for input */
|
||||||
:host([validation-state="invalid"]) input {
|
:host([validationstate="invalid"]) input {
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([validation-state="invalid"]) input:focus {
|
:host([validationstate="invalid"]) input:focus {
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.05)', 'hsl(0 72.2% 50.6% / 0.05)')};
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.05)', 'hsl(0 72.2% 50.6% / 0.05)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Warning state for input */
|
/* Warning state for input */
|
||||||
:host([validation-state="warn"]) input {
|
:host([validationstate="warn"]) input {
|
||||||
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([validation-state="warn"]) input:focus {
|
:host([validationstate="warn"]) input:focus {
|
||||||
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(25 95% 53% / 0.05)', 'hsl(25 95% 63% / 0.05)')};
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(25 95% 53% / 0.05)', 'hsl(25 95% 63% / 0.05)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Valid state for input */
|
/* Valid state for input */
|
||||||
:host([validation-state="valid"]) input {
|
:host([validationstate="valid"]) input {
|
||||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([validation-state="valid"]) input:focus {
|
:host([validationstate="valid"]) input:focus {
|
||||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
|
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Visually integrated mode: shed chrome to blend into a host component
|
||||||
|
(e.g. a dees-table cell in edit mode). */
|
||||||
|
:host([vintegrated]) dees-label,
|
||||||
|
:host([vintegrated]) .validationContainer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) .maincontainer {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) input {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 24px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) input:hover:not(:disabled):not(:focus),
|
||||||
|
:host([vintegrated]) input:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
:host([vintegrated]) .showPassword {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -203,9 +269,9 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
input {
|
input {
|
||||||
font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'};
|
font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'};
|
||||||
letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'};
|
letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'};
|
||||||
padding-right: ${this.isPasswordBool ? '48px' : '12px'};
|
padding-right: ${this.isPasswordBool ? '48px' : this.validConfirmed ? '40px' : '12px'};
|
||||||
}
|
}
|
||||||
${this.validationText
|
${this.validationText && !this.validConfirmed
|
||||||
? css`
|
? css`
|
||||||
.validationContainer {
|
.validationContainer {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -223,7 +289,7 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>
|
||||||
<div class="maincontainer">
|
<div class="maincontainer">
|
||||||
<input
|
<input
|
||||||
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
|
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
|
||||||
@@ -239,28 +305,114 @@ export class DeesInputText extends DeesInputBase {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
|
<div class="validIcon ${this.validConfirmed ? 'show' : ''}">
|
||||||
|
<dees-icon .icon=${'lucide:Check'}></dees-icon>
|
||||||
|
</div>
|
||||||
${this.validationText
|
${this.validationText
|
||||||
? html`
|
? html`
|
||||||
<div class="validationContainer ${this.validationState || 'error'}">
|
<div class="validationContainer ${this.validationState || 'invalid'}">
|
||||||
${this.validationText}
|
${this.validationText}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html`<div class="validationContainer"></div>`}
|
: html`<div class="validationContainer"></div>`}
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
// Input event handling is already done in updateValue method
|
if (this.validationFunction && this.value) {
|
||||||
|
const result = this.validationFunction(this.value);
|
||||||
|
this.validationState = result.valid ? 'valid' : 'invalid';
|
||||||
|
this.validationText = result.message || '';
|
||||||
|
if (result.valid) {
|
||||||
|
this.validConfirmed = false;
|
||||||
|
this.validTimeout = setTimeout(() => {
|
||||||
|
this.validConfirmed = true;
|
||||||
|
this.validationState = undefined as any;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateValue(eventArg: Event) {
|
public async updateValue(eventArg: Event) {
|
||||||
const target: any = eventArg.target;
|
const target: any = eventArg.target;
|
||||||
this.value = target.value;
|
this.value = target.value;
|
||||||
|
if (this.validationFunction) {
|
||||||
|
const result = this.validationFunction(this.value);
|
||||||
|
this.validationState = result.valid ? 'valid' : 'invalid';
|
||||||
|
this.validationText = result.message || '';
|
||||||
|
if (result.valid) {
|
||||||
|
this.validConfirmed = false;
|
||||||
|
clearTimeout(this.validTimeout);
|
||||||
|
this.validTimeout = setTimeout(() => {
|
||||||
|
this.validConfirmed = true;
|
||||||
|
this.validationState = undefined as any;
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
clearTimeout(this.validTimeout);
|
||||||
|
this.validConfirmed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.changeSubject.next(this);
|
this.changeSubject.next(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getContextMenuItems() {
|
||||||
|
const input = this.shadowRoot!.querySelector('input')!;
|
||||||
|
const hasSelection = input.selectionStart !== input.selectionEnd;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Cut',
|
||||||
|
iconName: 'lucide:Scissors',
|
||||||
|
shortcut: 'Cmd+X',
|
||||||
|
disabled: !hasSelection,
|
||||||
|
action: async () => {
|
||||||
|
const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
|
||||||
|
await navigator.clipboard.writeText(selected);
|
||||||
|
const start = input.selectionStart!;
|
||||||
|
this.value = this.value.substring(0, start) + this.value.substring(input.selectionEnd!);
|
||||||
|
input.value = this.value;
|
||||||
|
input.setSelectionRange(start, start);
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Copy',
|
||||||
|
iconName: 'lucide:Copy',
|
||||||
|
shortcut: 'Cmd+C',
|
||||||
|
disabled: !hasSelection,
|
||||||
|
action: async () => {
|
||||||
|
const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
|
||||||
|
await navigator.clipboard.writeText(selected);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Paste',
|
||||||
|
iconName: 'lucide:ClipboardPaste',
|
||||||
|
shortcut: 'Cmd+V',
|
||||||
|
action: async () => {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
const start = input.selectionStart!;
|
||||||
|
const end = input.selectionEnd!;
|
||||||
|
this.value = this.value.substring(0, start) + text + this.value.substring(end);
|
||||||
|
input.value = this.value;
|
||||||
|
input.setSelectionRange(start + text.length, start + text.length);
|
||||||
|
this.changeSubject.next(this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
name: 'Select All',
|
||||||
|
iconName: 'lucide:TextCursorInput',
|
||||||
|
shortcut: 'Cmd+A',
|
||||||
|
action: async () => {
|
||||||
|
input.select();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public getValue(): string {
|
public getValue(): string {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,12 +170,6 @@ export class DeesInputToggle extends DeesInputBase<DeesInputToggle> {
|
|||||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Description */
|
|
||||||
.description-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -199,7 +193,7 @@ export class DeesInputToggle extends DeesInputBase<DeesInputToggle> {
|
|||||||
</div>
|
</div>
|
||||||
<div class="label-container">
|
<div class="label-container">
|
||||||
${this.label ? html`<div class="toggle-label">${this.label}</div>` : ''}
|
${this.label ? html`<div class="toggle-label">${this.label}</div>` : ''}
|
||||||
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,13 +48,14 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'Tags'}
|
.label=${'Tags'}
|
||||||
.description=${'Add tags by typing and pressing Enter'}
|
.infoText=${'Add tags by typing and pressing Enter'}
|
||||||
|
.description=${'Tags help categorize and filter your content'}
|
||||||
.value=${['javascript', 'typescript', 'web-components']}
|
.value=${['javascript', 'typescript', 'web-components']}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
|
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'Team Members'}
|
.label=${'Team Members'}
|
||||||
.description=${'Add email addresses of team members'}
|
.infoText=${'Add email addresses of team members'}
|
||||||
.value=${['alice@example.com', 'bob@example.com']}
|
.value=${['alice@example.com', 'bob@example.com']}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,7 +65,7 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'Your Skills'}
|
.label=${'Your Skills'}
|
||||||
.description=${'List your professional skills'}
|
.infoText=${'List your professional skills'}
|
||||||
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
|
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
|
|
||||||
@@ -88,14 +89,14 @@ export const demoFunc = () => html`
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'Project Dependencies'}
|
.label=${'Project Dependencies'}
|
||||||
.description=${'List all required npm packages'}
|
.infoText=${'List all required npm packages'}
|
||||||
.required=${true}
|
.required=${true}
|
||||||
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
|
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
|
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'System Tags'}
|
.label=${'System Tags'}
|
||||||
.description=${'These tags are managed by the system'}
|
.infoText=${'These tags are managed by the system'}
|
||||||
.disabled=${true}
|
.disabled=${true}
|
||||||
.value=${['system', 'protected', 'readonly']}
|
.value=${['system', 'protected', 'readonly']}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
@@ -108,16 +109,16 @@ export const demoFunc = () => html`
|
|||||||
<dees-input-text
|
<dees-input-text
|
||||||
.label=${'Summary'}
|
.label=${'Summary'}
|
||||||
.inputType=${'textarea'}
|
.inputType=${'textarea'}
|
||||||
.description=${'Brief description of the article'}
|
.infoText=${'Brief description of the article'}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'Tags'}
|
.label=${'Tags'}
|
||||||
.description=${'Add relevant tags for better discoverability'}
|
.infoText=${'Add relevant tags for better discoverability'}
|
||||||
.value=${['tutorial', 'web-development']}
|
.value=${['tutorial', 'web-development']}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
<dees-input-typelist
|
<dees-input-typelist
|
||||||
.label=${'Co-Authors'}
|
.label=${'Co-Authors'}
|
||||||
.description=${'Add email addresses of co-authors'}
|
.infoText=${'Add email addresses of co-authors'}
|
||||||
></dees-input-typelist>
|
></dees-input-typelist>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
|||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText}></dees-label>
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
<div class="tags" @click=${() => {
|
<div class="tags" @click=${() => {
|
||||||
this.shadowRoot!.querySelector('input')!.focus();
|
this.shadowRoot!.querySelector('input')!.focus();
|
||||||
@@ -188,6 +188,7 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
return html`
|
return html`
|
||||||
<dees-label
|
<dees-label
|
||||||
.label="${this.label}"
|
.label="${this.label}"
|
||||||
.description="${this.description}"
|
.infoText="${this.infoText}"
|
||||||
.required="${this.required}"
|
.required="${this.required}"
|
||||||
></dees-label>
|
></dees-label>
|
||||||
<div class="wysiwyg-container">
|
<div class="wysiwyg-container">
|
||||||
@@ -303,6 +303,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
|||||||
<!-- Blocks will be rendered programmatically -->
|
<!-- Blocks will be rendered programmatically -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${this.renderDescription()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
|||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="profile-container"
|
class="profile-container"
|
||||||
@@ -329,6 +329,7 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
|||||||
accept="${this.acceptedFormats.join(',')}"
|
accept="${this.acceptedFormats.join(',')}"
|
||||||
@change=${this.handleFileSelect}
|
@change=${this.handleFileSelect}
|
||||||
/>
|
/>
|
||||||
|
${this.renderDescription()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export function demoFunc() {
|
|||||||
<dees-heading level="4">This is a H4 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="5">This is a H5 heading</dees-heading>
|
||||||
<dees-heading level="6">This is a H6 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">This is an hr heading (level="hr")</dees-heading>
|
||||||
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
|
<dees-heading level="7">This is an hr heading (level="7")</dees-heading>
|
||||||
|
<dees-heading level="hr-small">This is an hr-small heading (level="hr-small")</dees-heading>
|
||||||
|
<dees-heading level="8">This is an hr-small heading (level="8")</dees-heading>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -27,68 +27,104 @@ export class DeesHeading extends DeesElement {
|
|||||||
|
|
||||||
// properties
|
// properties
|
||||||
/**
|
/**
|
||||||
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
|
* Heading level:
|
||||||
|
* '1'-'6' → <h1>..<h6>
|
||||||
|
* '7'|'hr' → horizontal-rule style heading
|
||||||
|
* '8'|'hr-small' → small horizontal-rule style heading
|
||||||
*/
|
*/
|
||||||
@property({ type: String, reflect: true })
|
@property({ type: String, reflect: true })
|
||||||
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
|
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | 'hr' | 'hr-small' = '1';
|
||||||
|
|
||||||
// STATIC STYLES
|
// STATIC STYLES
|
||||||
public static styles: CSSResult[] = [
|
public static styles: CSSResult[] = [
|
||||||
themeDefaultStyles,
|
themeDefaultStyles,
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
:host {
|
||||||
/* Heading styles */
|
display: block;
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
|
||||||
}
|
}
|
||||||
h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;}
|
|
||||||
h2 { font-size: 28px; }
|
/* Heading styles.
|
||||||
h3 { font-size: 24px; }
|
* Color hierarchy: h1-h2 stay prominent with text-primary; h3-h6 step
|
||||||
h4 { font-size: 20px; }
|
* down to text-secondary so they read as subheadings instead of
|
||||||
h5 { font-size: 16px; }
|
* mini-h1s. Keeps the visual loudness out of smaller headings. */
|
||||||
h6 { font-size: 14px; }
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
h1, h2 {
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
h3, h4, h5, h6 {
|
||||||
|
color: var(--dees-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-level typography + spacing.
|
||||||
|
* Margin scales with importance: h1 gets the most breathing room,
|
||||||
|
* h6 the least. Top margin > bottom margin so headings group with
|
||||||
|
* the content that follows them. */
|
||||||
|
h1 {
|
||||||
|
/* h1 uses weight 500, not 600: the Cal Sans display font is
|
||||||
|
* already stylized enough that bold + max contrast + 32px stacks
|
||||||
|
* too much emphasis. 500 keeps the typographic impact without
|
||||||
|
* shouting. */
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 32px;
|
||||||
|
font-family: ${cssCalSansFontFamily};
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
margin: var(--dees-spacing-2xl) 0 var(--dees-spacing-lg);
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: var(--dees-spacing-lg) 0 var(--dees-spacing-sm);
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: var(--dees-spacing-md) 0 var(--dees-spacing-sm);
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: var(--dees-spacing-md) 0 var(--dees-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
/* Horizontal rule style heading */
|
/* Horizontal rule style heading */
|
||||||
.heading-hr {
|
.heading-hr {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 16px 0;
|
margin: var(--dees-spacing-lg) 0;
|
||||||
color: ${cssManager.bdTheme('#999', '#555')};
|
color: var(--dees-color-text-muted);
|
||||||
}
|
}
|
||||||
/* Fade lines toward and away from text for hr style */
|
/* Fade lines toward and away from text for hr style */
|
||||||
.heading-hr::before {
|
.heading-hr::before {
|
||||||
content: '';
|
content: '';
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
/* fade in toward center */
|
background: linear-gradient(to right, transparent, var(--dees-color-border-strong));
|
||||||
background: ${cssManager.bdTheme(
|
margin: 0 var(--dees-spacing-sm);
|
||||||
'linear-gradient(to right, transparent, #ccc)',
|
|
||||||
'linear-gradient(to right, transparent, #333)'
|
|
||||||
)};
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
}
|
||||||
.heading-hr::after {
|
.heading-hr::after {
|
||||||
content: '';
|
content: '';
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
/* fade out away from center */
|
background: linear-gradient(to right, var(--dees-color-border-strong), transparent);
|
||||||
background: ${cssManager.bdTheme(
|
margin: 0 var(--dees-spacing-sm);
|
||||||
'linear-gradient(to right, #ccc, transparent)',
|
|
||||||
'linear-gradient(to right, #333, transparent)'
|
|
||||||
)};
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
}
|
||||||
/* Small hr variant with reduced margins */
|
/* Small hr variant with reduced margins */
|
||||||
.heading-hr.heading-hr-small {
|
.heading-hr.heading-hr-small {
|
||||||
margin: 8px 0;
|
margin: var(--dees-spacing-sm) 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.heading-hr.heading-hr-small::before,
|
.heading-hr.heading-hr-small::before,
|
||||||
.heading-hr.heading-hr-small::after {
|
.heading-hr.heading-hr-small::after {
|
||||||
margin: 0 8px;
|
margin: 0 var(--dees-spacing-sm);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -109,8 +145,10 @@ export class DeesHeading extends DeesElement {
|
|||||||
return html`<h5><slot></slot></h5>`;
|
return html`<h5><slot></slot></h5>`;
|
||||||
case '6':
|
case '6':
|
||||||
return html`<h6><slot></slot></h6>`;
|
return html`<h6><slot></slot></h6>`;
|
||||||
|
case '7':
|
||||||
case 'hr':
|
case 'hr':
|
||||||
return html`<div class="heading-hr"><slot></slot></div>`;
|
return html`<div class="heading-hr"><slot></slot></div>`;
|
||||||
|
case '8':
|
||||||
case 'hr-small':
|
case 'hr-small':
|
||||||
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,7 +1,128 @@
|
|||||||
import { html, cssManager } from '@design.estate/dees-element';
|
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
export const demoFunc = () => {
|
export const demoFunc = () => html`
|
||||||
return html`
|
<style>
|
||||||
<dees-label .label=${'a label'}></dees-label>
|
${css`
|
||||||
`;
|
.demo-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demo-section {
|
||||||
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-section p {
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row .annotation {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: ${cssManager.bdTheme('#888', '#666')};
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="demo-container">
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Basic Label</h3>
|
||||||
|
<p>A simple text label with no additional indicators.</p>
|
||||||
|
<div class="label-grid">
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">label="Username"</span>
|
||||||
|
<dees-label .label=${'Username'}></dees-label>
|
||||||
|
</div>
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">label="Email Address"</span>
|
||||||
|
<dees-label .label=${'Email Address'}></dees-label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Required Indicator</h3>
|
||||||
|
<p>When <code>required</code> is set, a red asterisk appears after the label text.</p>
|
||||||
|
<div class="label-grid">
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">required=${'{true}'}</span>
|
||||||
|
<dees-label .label=${'Full Name'} .required=${true}></dees-label>
|
||||||
|
</div>
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">required=${'{false}'} (default)</span>
|
||||||
|
<dees-label .label=${'Nickname'} .required=${false}></dees-label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Info Text (Info Icon)</h3>
|
||||||
|
<p>When <code>infoText</code> is set, an info icon appears next to the label. Hover over it to see the tooltip.</p>
|
||||||
|
<div class="label-grid">
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">infoText="..."</span>
|
||||||
|
<dees-label .label=${'API Key'} .infoText=${'Your API key can be found in the developer settings dashboard.'}></dees-label>
|
||||||
|
</div>
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">short infoText</span>
|
||||||
|
<dees-label .label=${'Region'} .infoText=${'Select your nearest datacenter.'}></dees-label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Required + Info Text</h3>
|
||||||
|
<p>Both indicators can be combined. The asterisk appears first, then the info icon.</p>
|
||||||
|
<div class="label-grid">
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">required + infoText</span>
|
||||||
|
<dees-label .label=${'Password'} .required=${true} .infoText=${'Must be at least 8 characters with one uppercase letter and one number.'}></dees-label>
|
||||||
|
</div>
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">required + infoText</span>
|
||||||
|
<dees-label .label=${'Email Address'} .required=${true} .infoText=${'We will send a verification link to this address.'}></dees-label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Empty Label</h3>
|
||||||
|
<p>When <code>label</code> is empty or not set, nothing is rendered. The element below has no label text:</p>
|
||||||
|
<div class="label-grid">
|
||||||
|
<div class="label-row">
|
||||||
|
<span class="annotation">label="" (empty)</span>
|
||||||
|
<dees-label .label=${''}></dees-label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class DeesLabel extends DeesElement {
|
|||||||
type: String,
|
type: String,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
})
|
})
|
||||||
accessor description!: string;
|
accessor infoText!: string;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -50,7 +50,8 @@ export class DeesLabel extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -66,13 +67,26 @@ export class DeesLabel extends DeesElement {
|
|||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dees-icon {
|
.description-icon {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
font-size: 12px;
|
align-items: center;
|
||||||
transform: translateY(1px);
|
justify-content: center;
|
||||||
margin-left: 4px;
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: -6px 0 -6px 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||||
cursor: help;
|
}
|
||||||
|
|
||||||
|
.description-icon:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 100% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 80%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-icon dees-icon {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -84,10 +98,12 @@ export class DeesLabel extends DeesElement {
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
${this.label}
|
${this.label}
|
||||||
${this.required ? html`<span class="required">*</span>` : ''}
|
${this.required ? html`<span class="required">*</span>` : ''}
|
||||||
${this.description
|
${this.infoText
|
||||||
? html`
|
? html`
|
||||||
|
<div class="description-icon">
|
||||||
<dees-icon .icon=${'lucide:info'}></dees-icon>
|
<dees-icon .icon=${'lucide:info'}></dees-icon>
|
||||||
<dees-speechbubble .text=${this.description}></dees-speechbubble>
|
</div>
|
||||||
|
<dees-speechbubble .text=${this.infoText}></dees-speechbubble>
|
||||||
`
|
`
|
||||||
: html``}
|
: html``}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
import { DeesStepper, type IStep } from './dees-stepper.js';
|
||||||
|
|
||||||
export const stepperDemo = () => html`
|
const demoSteps: IStep[] = [
|
||||||
<dees-stepper
|
|
||||||
.steps=${[
|
|
||||||
{
|
{
|
||||||
title: 'Account Setup',
|
title: 'Account Setup',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text key="email" label="Work Email" required></dees-input-text>
|
<dees-input-text key="email" label="Work Email" required></dees-input-text>
|
||||||
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Profile Details',
|
title: 'Profile Details',
|
||||||
@@ -23,13 +20,11 @@ export const stepperDemo = () => html`
|
|||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text key="firstName" label="First Name" required></dees-input-text>
|
<dees-input-text key="firstName" label="First Name" required></dees-input-text>
|
||||||
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Contact Information',
|
title: 'Contact Information',
|
||||||
@@ -37,13 +32,11 @@ export const stepperDemo = () => html`
|
|||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone>
|
<dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone>
|
||||||
<dees-input-text key="company" label="Company"></dees-input-text>
|
<dees-input-text key="company" label="Company"></dees-input-text>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Team Size',
|
title: 'Team Size',
|
||||||
@@ -60,13 +53,11 @@ export const stepperDemo = () => html`
|
|||||||
]}
|
]}
|
||||||
required
|
required
|
||||||
></dees-input-dropdown>
|
></dees-input-dropdown>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Goals',
|
title: 'Goals',
|
||||||
@@ -82,13 +73,11 @@ export const stepperDemo = () => html`
|
|||||||
]}
|
]}
|
||||||
required
|
required
|
||||||
></dees-input-multitoggle>
|
></dees-input-multitoggle>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Brand Preferences',
|
title: 'Brand Preferences',
|
||||||
@@ -96,13 +85,11 @@ export const stepperDemo = () => html`
|
|||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
||||||
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Integrations',
|
title: 'Integrations',
|
||||||
@@ -113,13 +100,11 @@ export const stepperDemo = () => html`
|
|||||||
label="Integrations in use"
|
label="Integrations in use"
|
||||||
placeholder="Add integration"
|
placeholder="Add integration"
|
||||||
></dees-input-list>
|
></dees-input-list>
|
||||||
<dees-form-submit>Continue</dees-form-submit>
|
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
validationFunc: async (stepperArg, elementArg) => {
|
menuOptions: [
|
||||||
const deesForm = elementArg.querySelector('dees-form');
|
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
],
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Review & Launch',
|
title: 'Review & Launch',
|
||||||
@@ -128,7 +113,25 @@ export const stepperDemo = () => html`
|
|||||||
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
||||||
</dees-panel>
|
</dees-panel>
|
||||||
`,
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Launch', action: async (stepper) => stepper!.goNext() },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
] as const}
|
];
|
||||||
></dees-stepper>
|
|
||||||
|
const cloneSteps = (): IStep[] => demoSteps.map((step) => ({ ...step }));
|
||||||
|
|
||||||
|
export const stepperDemo = () => html`
|
||||||
|
<div style="position: absolute; inset: 0;">
|
||||||
|
<div
|
||||||
|
style="position: absolute; top: 16px; left: 50%; transform: translateX(-50%); z-index: 10;"
|
||||||
|
>
|
||||||
|
<dees-button
|
||||||
|
@click=${async () => {
|
||||||
|
await DeesStepper.createAndShow({ steps: cloneSteps() });
|
||||||
|
}}
|
||||||
|
>Open stepper as overlay</dees-button>
|
||||||
|
</div>
|
||||||
|
<dees-stepper .steps=${cloneSteps()}></dees-stepper>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as plugins from '../../00plugins.js';
|
import * as plugins from '../../00plugins.js';
|
||||||
import * as colors from '../../00colors.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeesElement,
|
DeesElement,
|
||||||
@@ -16,10 +15,17 @@ import {
|
|||||||
import * as domtools from '@design.estate/dees-domtools';
|
import * as domtools from '@design.estate/dees-domtools';
|
||||||
import { stepperDemo } from './dees-stepper.demo.js';
|
import { stepperDemo } from './dees-stepper.demo.js';
|
||||||
import { themeDefaultStyles } from '../../00theme.js';
|
import { themeDefaultStyles } from '../../00theme.js';
|
||||||
|
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||||
|
import { zIndexRegistry } from '../../00zindex.js';
|
||||||
|
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||||
|
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
||||||
|
import type { DeesForm } from '../../00group-form/dees-form/dees-form.js';
|
||||||
|
import '../dees-tile/dees-tile.js';
|
||||||
|
|
||||||
export interface IStep {
|
export interface IStep {
|
||||||
title: string;
|
title: string;
|
||||||
content: TemplateResult;
|
content: TemplateResult;
|
||||||
|
menuOptions?: plugins.tsclass.website.IMenuItem<DeesStepper>[];
|
||||||
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
||||||
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
||||||
validationFuncCalled?: boolean;
|
validationFuncCalled?: boolean;
|
||||||
@@ -34,9 +40,33 @@ declare global {
|
|||||||
|
|
||||||
@customElement('dees-stepper')
|
@customElement('dees-stepper')
|
||||||
export class DeesStepper extends DeesElement {
|
export class DeesStepper extends DeesElement {
|
||||||
|
// STATIC
|
||||||
public static demo = stepperDemo;
|
public static demo = stepperDemo;
|
||||||
public static demoGroups = ['Layout', 'Form'];
|
public static demoGroups = ['Layout', 'Form'];
|
||||||
|
|
||||||
|
public static async createAndShow(optionsArg: {
|
||||||
|
steps: IStep[];
|
||||||
|
cancelable?: boolean;
|
||||||
|
}): Promise<DeesStepper> {
|
||||||
|
const body = document.body;
|
||||||
|
const stepper = new DeesStepper();
|
||||||
|
stepper.steps = optionsArg.steps;
|
||||||
|
stepper.overlay = true;
|
||||||
|
if (optionsArg.cancelable !== undefined) {
|
||||||
|
stepper.cancelable = optionsArg.cancelable;
|
||||||
|
}
|
||||||
|
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
|
||||||
|
body.append(stepper.windowLayer);
|
||||||
|
body.append(stepper);
|
||||||
|
|
||||||
|
// Get z-index for stepper (should be above window layer)
|
||||||
|
stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
|
||||||
|
zIndexRegistry.register(stepper, stepper.stepperZIndex);
|
||||||
|
|
||||||
|
return stepper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
@property({
|
@property({
|
||||||
type: Array,
|
type: Array,
|
||||||
})
|
})
|
||||||
@@ -47,6 +77,37 @@ export class DeesStepper extends DeesElement {
|
|||||||
})
|
})
|
||||||
accessor selectedStep!: IStep;
|
accessor selectedStep!: IStep;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
accessor overlay: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true (default), the stepper renders a Cancel button in every step's
|
||||||
|
* footer, and clicking the backdrop (overlay mode) triggers the same cancel
|
||||||
|
* confirmation flow. Set to false for forced flows where the user must
|
||||||
|
* complete the stepper — no Cancel button, no backdrop dismissal.
|
||||||
|
*/
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
reflect: true,
|
||||||
|
})
|
||||||
|
accessor cancelable: boolean = true;
|
||||||
|
|
||||||
|
@property({ type: Number, attribute: false })
|
||||||
|
accessor stepperZIndex: number = 1000;
|
||||||
|
|
||||||
|
@property({ type: Object, attribute: false })
|
||||||
|
accessor activeForm: DeesForm | null = null;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: false })
|
||||||
|
accessor activeFormValid: boolean = true;
|
||||||
|
|
||||||
|
private activeFormSubscription?: { unsubscribe: () => void };
|
||||||
|
|
||||||
|
private windowLayer?: DeesWindowLayer;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -60,7 +121,24 @@ export class DeesStepper extends DeesElement {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-family: ${cssGeistFontFamily};
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In overlay mode the host is "transparent" to layout — the inner
|
||||||
|
* .stepperContainer.overlay is what pins to the viewport and carries the
|
||||||
|
* z-index. Keeping :host unpositioned avoids nesting the stacking context
|
||||||
|
* under an auto-z-index parent (which was trapping .stepperContainer
|
||||||
|
* below DeesWindowLayer's sibling layers). This mirrors how dees-modal
|
||||||
|
* keeps its own :host unpositioned and lets .modalContainer drive layout.
|
||||||
|
*/
|
||||||
|
:host([overlay]) {
|
||||||
|
position: static;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.stepperContainer {
|
.stepperContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -68,101 +146,125 @@ export class DeesStepper extends DeesElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step {
|
.stepperContainer.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exit animation — reverse of the entry. Tiles slide DOWN + fade out,
|
||||||
|
mirroring the .entrance slide-up. The transition override is needed
|
||||||
|
because dees-tile.step has its own 0.7s transition for step selection
|
||||||
|
which would otherwise make the exit sluggish. */
|
||||||
|
.stepperContainer.predestroy dees-tile.step {
|
||||||
|
transform: translateY(16px);
|
||||||
|
filter: opacity(0);
|
||||||
|
transition: transform 0.25s, filter 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-tile.step {
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), box-shadow 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1), border 0.7s cubic-bezier(0.87, 0, 0.13, 1);
|
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
border-radius: 12px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0f0f11')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e2e8f0', '#272729')};
|
|
||||||
color: ${cssManager.bdTheme('#0f172a', '#f5f5f5')};
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
filter: opacity(0.55) saturate(0.85);
|
filter: opacity(0.55) saturate(0.85);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
transition: transform 0.7s cubic-bezier(0.87, 0, 0.13, 1), filter 0.7s cubic-bezier(0.87, 0, 0.13, 1);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.selected {
|
dees-tile.step.selected {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
filter: opacity(1) saturate(1);
|
filter: opacity(1) saturate(1);
|
||||||
user-select: auto;
|
user-select: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.hiddenStep {
|
dees-tile.step.hiddenStep {
|
||||||
filter: opacity(0);
|
filter: opacity(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.entrance {
|
dees-tile.step.entrance {
|
||||||
transition: transform 0.35s ease, box-shadow 0.35s ease, filter 0.35s ease, border 0.35s ease;
|
transition: transform 0.35s ease, filter 0.35s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step.entrance.hiddenStep {
|
dees-tile.step.entrance.hiddenStep {
|
||||||
transform: translateY(16px);
|
transform: translateY(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step:last-child {
|
dees-tile.step:last-child {
|
||||||
margin-bottom: 100vh;
|
margin-bottom: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .stepCounter {
|
.stepperContainer.overlay dees-tile.step::part(outer) {
|
||||||
color: ${cssManager.bdTheme('#64748b', '#a1a1aa')};
|
box-shadow:
|
||||||
position: absolute;
|
0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
|
||||||
top: 12px;
|
0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
|
||||||
right: 12px;
|
0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
|
||||||
padding: 6px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.5)', 'rgba(63, 63, 70, 0.45)')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.7)', 'rgba(63, 63, 70, 0.6)')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack {
|
.step-header {
|
||||||
position: absolute;
|
height: 36px;
|
||||||
top: 12px;
|
display: flex;
|
||||||
left: 12px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 8px 0 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goBack-spacer {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-header .goBack {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 999px;
|
line-height: 1;
|
||||||
border: 1px solid ${cssManager.bdTheme('rgba(226, 232, 240, 0.9)', 'rgba(63, 63, 70, 0.85)')};
|
border: none;
|
||||||
background: ${cssManager.bdTheme('rgba(255, 255, 255, 0.9)', 'rgba(39, 39, 42, 0.85)')};
|
background: transparent;
|
||||||
color: ${cssManager.bdTheme('#475569', '#d4d4d8')};
|
color: var(--dees-color-text-muted);
|
||||||
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border 0.2s ease, color 0.2s ease, background 0.2s ease, transform 0.2s ease;
|
transition: background 0.15s ease, color 0.15s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack:hover {
|
.step-header .goBack:hover {
|
||||||
color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
|
background: var(--dees-color-hover);
|
||||||
border-color: ${cssManager.bdTheme(colors.dark.blue, colors.dark.blue)};
|
color: var(--dees-color-text-secondary);
|
||||||
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.95)', 'rgba(63, 63, 70, 0.7)')};
|
|
||||||
transform: translateX(-2px);
|
transform: translateX(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack:active {
|
.step-header .goBack:active {
|
||||||
color: ${cssManager.bdTheme('#0f172a', '#fafafa')};
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')};
|
||||||
border-color: ${cssManager.bdTheme(colors.dark.blueActive, colors.dark.blueActive)};
|
|
||||||
background: ${cssManager.bdTheme('rgba(226, 232, 240, 0.85)', 'rgba(63, 63, 70, 0.6)')};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack span {
|
.step-header .goBack span {
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .goBack:hover span {
|
.step-header .goBack:hover span {
|
||||||
transform: translateX(-2px);
|
transform: translateX(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .title {
|
.step-header .stepCounter {
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-body .title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 64px;
|
padding-top: 32px;
|
||||||
font-family: 'Geist Sans', sans-serif;
|
font-family: 'Geist Sans', sans-serif;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -170,35 +272,149 @@ export class DeesStepper extends DeesElement {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step .content {
|
.step-body .content {
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Footer: modal-style bottom buttons --- */
|
||||||
|
.bottomButtons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 36px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton {
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border-left separator on every button EXCEPT the first one.
|
||||||
|
Uses general sibling so the stepHint (if rendered on the left) does
|
||||||
|
not shift which button counts as "first" and create a phantom border. */
|
||||||
|
.bottomButtons .bottomButton ~ .bottomButton {
|
||||||
|
border-left: 1px solid var(--dees-color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton:hover {
|
||||||
|
background: var(--dees-color-hover);
|
||||||
|
color: var(--dees-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton:active {
|
||||||
|
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 13%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.primary {
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.primary:hover {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||||
|
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.primary:active {
|
||||||
|
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.12)', 'hsl(213.1 93.9% 67.8% / 0.12)')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomButtons .bottomButton.disabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hint shown on the left of the footer when the active step's form has
|
||||||
|
unfilled required fields. Uses margin-right: auto to push right-aligned
|
||||||
|
buttons to the right while keeping the hint flush-left. */
|
||||||
|
.bottomButtons .stepHint {
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--dees-color-text-muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<div class="stepperContainer">
|
<div
|
||||||
${this.steps.map(
|
class="stepperContainer ${this.overlay ? 'overlay' : ''}"
|
||||||
(stepArg) =>
|
style=${this.overlay ? `z-index: ${this.stepperZIndex};` : ''}
|
||||||
html`<div
|
@click=${this.handleOutsideClick}
|
||||||
class="step ${stepArg === this.selectedStep
|
|
||||||
? 'selected'
|
|
||||||
: null} ${this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep)
|
|
||||||
? 'hiddenStep'
|
|
||||||
: ''} ${this.getIndexOfStep(stepArg) === 0 ? 'entrance' : ''}"
|
|
||||||
>
|
>
|
||||||
${this.getIndexOfStep(stepArg) > 0
|
${this.steps.map((stepArg, stepIndex) => {
|
||||||
? html`<div class="goBack" @click=${this.goBack}><span style="font-family: Inter"><-</span> go to previous step</div>`
|
const isSelected = stepArg === this.selectedStep;
|
||||||
: ``}
|
const isHidden =
|
||||||
|
this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
||||||
|
const isFirst = stepIndex === 0;
|
||||||
|
return html`<dees-tile
|
||||||
|
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
|
||||||
|
>
|
||||||
|
<div slot="header" class="step-header">
|
||||||
|
${!isFirst
|
||||||
|
? html`<div class="goBack" @click=${this.goBack}>
|
||||||
|
<span style="font-family: Inter"><-</span> go to previous step
|
||||||
|
</div>`
|
||||||
|
: html`<div class="goBack-spacer"></div>`}
|
||||||
<div class="stepCounter">
|
<div class="stepCounter">
|
||||||
Step ${this.steps.findIndex((elementArg) => elementArg === stepArg) + 1} of
|
Step ${stepIndex + 1} of ${this.steps.length}
|
||||||
${this.steps.length}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-body">
|
||||||
<div class="title">${stepArg.title}</div>
|
<div class="title">${stepArg.title}</div>
|
||||||
<div class="content">${stepArg.content}</div>
|
<div class="content">${stepArg.content}</div>
|
||||||
</div> `
|
</div>
|
||||||
)}
|
<div slot="footer" class="bottomButtons">
|
||||||
|
${isSelected && this.activeForm !== null && !this.activeFormValid
|
||||||
|
? html`<div class="stepHint">Complete form to continue</div>`
|
||||||
|
: ''}
|
||||||
|
${this.cancelable
|
||||||
|
? html`<div
|
||||||
|
class="bottomButton"
|
||||||
|
@click=${() => this.handleCancelRequest()}
|
||||||
|
>Cancel</div>`
|
||||||
|
: ''}
|
||||||
|
${stepArg.menuOptions?.map((actionArg, actionIndex) => {
|
||||||
|
const isPrimary = actionIndex === stepArg.menuOptions!.length - 1;
|
||||||
|
const isDisabled = isPrimary && this.activeForm !== null && !this.activeFormValid;
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="bottomButton ${isPrimary ? 'primary' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||||
|
@click=${() => this.handleMenuOptionClick(actionArg, isPrimary)}
|
||||||
|
>${actionArg.name}</div>
|
||||||
|
`;
|
||||||
|
}) ?? ''}
|
||||||
|
</div>
|
||||||
|
</dees-tile>`;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -230,6 +446,7 @@ export class DeesStepper extends DeesElement {
|
|||||||
if (!selectedStepElement) {
|
if (!selectedStepElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.scanActiveForm(selectedStepElement);
|
||||||
if (!stepperContainer.style.paddingTop) {
|
if (!stepperContainer.style.paddingTop) {
|
||||||
stepperContainer.style.paddingTop = `${
|
stepperContainer.style.paddingTop = `${
|
||||||
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
stepperContainer.offsetHeight / 2 - selectedStepElement.offsetHeight / 2
|
||||||
@@ -296,4 +513,165 @@ export class DeesStepper extends DeesElement {
|
|||||||
nextStep.validationFuncCalled = false;
|
nextStep.validationFuncCalled = false;
|
||||||
this.selectedStep = nextStep;
|
this.selectedStep = nextStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the currently selected step for a <dees-form> in its content. When
|
||||||
|
* found, subscribes to the form's RxJS changeSubject so the primary
|
||||||
|
* menuOption button can auto-enable/disable as required fields are filled.
|
||||||
|
*
|
||||||
|
* If the form reference is the same as the previous activation (e.g. on a
|
||||||
|
* same-step re-render), we just recompute validity without re-subscribing.
|
||||||
|
*/
|
||||||
|
private scanActiveForm(selectedStepElement: HTMLElement) {
|
||||||
|
const form = selectedStepElement.querySelector('dees-form') as DeesForm | null;
|
||||||
|
if (form === this.activeForm) {
|
||||||
|
this.recomputeFormValid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.activeFormSubscription?.unsubscribe();
|
||||||
|
this.activeFormSubscription = undefined;
|
||||||
|
this.activeForm = form;
|
||||||
|
if (!form) {
|
||||||
|
this.activeFormValid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Initial check before subscribing, in case the form's firstUpdated fires
|
||||||
|
// synchronously between scan and subscribe.
|
||||||
|
this.recomputeFormValid();
|
||||||
|
this.activeFormSubscription = form.changeSubject.subscribe(() => {
|
||||||
|
this.recomputeFormValid();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recomputes activeFormValid by checking every required field in the active
|
||||||
|
* form for a non-empty value. Mirrors dees-form.updateRequiredStatus's logic
|
||||||
|
* but stores the result on the stepper instead of mutating a submit button.
|
||||||
|
*/
|
||||||
|
private recomputeFormValid() {
|
||||||
|
const form = this.activeForm;
|
||||||
|
if (!form) {
|
||||||
|
this.activeFormValid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fields = form.getFormElements();
|
||||||
|
this.activeFormValid = fields.every(
|
||||||
|
(field) => !field.required || (field.value !== null && field.value !== undefined && field.value !== ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler for menuOption buttons in the footer. For the primary (last)
|
||||||
|
* button, if an active form is present, gates on required-field validity and
|
||||||
|
* triggers the form's gatherAndDispatch() before running the action. The
|
||||||
|
* action is awaited so any async work (e.g. goNext → scroll animation)
|
||||||
|
* completes before the click handler returns.
|
||||||
|
*/
|
||||||
|
private async handleMenuOptionClick(
|
||||||
|
optionArg: plugins.tsclass.website.IMenuItem<DeesStepper>,
|
||||||
|
isPrimary: boolean,
|
||||||
|
) {
|
||||||
|
const form = this.activeForm;
|
||||||
|
if (isPrimary && form) {
|
||||||
|
if (!this.activeFormValid) return;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
form.addEventListener('formData', () => resolve(), { once: true });
|
||||||
|
form.gatherAndDispatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await optionArg.action(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently-open confirmation modal (if any). Prevents double-stacking when
|
||||||
|
* the user clicks the backdrop or the Cancel button while a confirm modal
|
||||||
|
* is already visible. The reference may become stale (point to a destroyed
|
||||||
|
* modal) if the user dismisses the confirm modal by clicking its own
|
||||||
|
* backdrop — so handleCancelRequest() uses isConnected to detect that.
|
||||||
|
*/
|
||||||
|
private cancelConfirmModal?: DeesModal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click handler on .stepperContainer. Mirrors dees-modal.handleOutsideClick:
|
||||||
|
* when the user clicks the empty backdrop area (target === stepperContainer,
|
||||||
|
* not any descendant tile), trigger the cancel confirmation flow. Clicks
|
||||||
|
* that originate inside a step tile have a different event.target and are
|
||||||
|
* ignored here.
|
||||||
|
*/
|
||||||
|
private handleOutsideClick(eventArg: MouseEvent) {
|
||||||
|
if (!this.overlay) return;
|
||||||
|
if (!this.cancelable) return;
|
||||||
|
eventArg.stopPropagation();
|
||||||
|
const stepperContainer = this.shadowRoot!.querySelector('.stepperContainer');
|
||||||
|
if (eventArg.target === stepperContainer) {
|
||||||
|
this.handleCancelRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown by both the backdrop click and the Cancel button in the footer.
|
||||||
|
* Presents a dees-modal asking the user to confirm cancellation. If they
|
||||||
|
* confirm, the stepper and window layer are destroyed; otherwise the
|
||||||
|
* confirm modal is dismissed and the stepper stays open.
|
||||||
|
*
|
||||||
|
* The isConnected check on the cached reference handles the case where the
|
||||||
|
* user dismissed the previous confirm modal by clicking ITS OWN backdrop —
|
||||||
|
* dees-modal.handleOutsideClick calls destroy() directly, bypassing our
|
||||||
|
* action callbacks, so our cached reference would be stale without this
|
||||||
|
* fallback check.
|
||||||
|
*/
|
||||||
|
public async handleCancelRequest() {
|
||||||
|
if (!this.cancelable) return;
|
||||||
|
if (this.cancelConfirmModal && this.cancelConfirmModal.isConnected) return;
|
||||||
|
this.cancelConfirmModal = undefined;
|
||||||
|
this.cancelConfirmModal = await DeesModal.createAndShow({
|
||||||
|
heading: 'Cancel setup?',
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Are you sure you want to cancel? Any progress on the current step will be lost.
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Continue setup',
|
||||||
|
action: async (modal) => {
|
||||||
|
this.cancelConfirmModal = undefined;
|
||||||
|
await modal!.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Yes, cancel',
|
||||||
|
action: async (modal) => {
|
||||||
|
this.cancelConfirmModal = undefined;
|
||||||
|
modal!.destroy(); // fire-and-forget — starts the confirm modal fade
|
||||||
|
const domtools = await this.domtoolsPromise;
|
||||||
|
await domtools.convenience.smartdelay.delayFor(100);
|
||||||
|
await this.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroy() {
|
||||||
|
const domtools = await this.domtoolsPromise;
|
||||||
|
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
||||||
|
container?.classList.add('predestroy');
|
||||||
|
await domtools.convenience.smartdelay.delayFor(250);
|
||||||
|
if (this.parentElement) {
|
||||||
|
this.parentElement.removeChild(this);
|
||||||
|
}
|
||||||
|
if (this.windowLayer) {
|
||||||
|
await this.windowLayer.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tear down form subscription to avoid leaks when the overlay closes.
|
||||||
|
this.activeFormSubscription?.unsubscribe();
|
||||||
|
this.activeFormSubscription = undefined;
|
||||||
|
this.activeForm = null;
|
||||||
|
|
||||||
|
// Unregister from z-index registry
|
||||||
|
zIndexRegistry.unregister(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export class DeesTile extends DeesElement {
|
|||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor heading: string = '';
|
accessor heading: string = '';
|
||||||
|
|
||||||
|
@property({ type: String, reflect: true })
|
||||||
|
accessor overscroll: 'contain' | 'auto' | 'none' = 'auto';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor hasFooter: boolean = false;
|
accessor hasFooter: boolean = false;
|
||||||
|
|
||||||
@@ -87,14 +90,27 @@ export class DeesTile extends DeesElement {
|
|||||||
color: var(--dees-color-text-secondary);
|
color: var(--dees-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Content: the rounded inset --- */
|
/* --- Content: the rounded inset ---
|
||||||
|
Uses overflow-y: auto so that when a consumer (e.g. dees-modal) caps
|
||||||
|
the tile with max-height, long content scrolls inside the tile
|
||||||
|
instead of being clipped. For consumers without max-height
|
||||||
|
(e.g. dees-stepper), the tile grows with content and the scroll
|
||||||
|
never activates. Horizontal overflow stays clipped to preserve the
|
||||||
|
rounded corners. */
|
||||||
.tile-content {
|
.tile-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-top: 1px solid var(--dees-color-border-subtle);
|
border-top: 1px solid var(--dees-color-border-subtle);
|
||||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([overscroll="contain"]) .tile-content {
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-content.no-footer {
|
.tile-content.no-footer {
|
||||||
|
|||||||
@@ -352,5 +352,80 @@ export const demoFunc = () => html`
|
|||||||
});
|
});
|
||||||
}}>Test Responsive</dees-button>
|
}}>Test Responsive</dees-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-section">
|
||||||
|
<h3>Scrollable Content</h3>
|
||||||
|
<p>When content exceeds the modal's max-height (<code>calc(100vh - 80px)</code>), the tile caps at that height and the content area scrolls inside. The heading and bottom buttons stay pinned.</p>
|
||||||
|
<div class="button-grid">
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Long Article',
|
||||||
|
width: 'medium',
|
||||||
|
content: html`
|
||||||
|
<h4 style="margin-top: 0;">Lorem ipsum dolor sit amet</h4>
|
||||||
|
${Array.from({ length: 40 }, (_, i) => html`
|
||||||
|
<p>
|
||||||
|
<strong>§ ${i + 1}.</strong>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
|
||||||
|
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
|
||||||
|
enim ad minim veniam, quis nostrud exercitation ullamco laboris
|
||||||
|
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
|
||||||
|
in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||||
|
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||||
|
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
</p>
|
||||||
|
`)}
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Accept',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Long Article</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Long List',
|
||||||
|
width: 'small',
|
||||||
|
content: html`
|
||||||
|
<p>Selected items:</p>
|
||||||
|
<ul style="padding-left: 20px; margin: 0;">
|
||||||
|
${Array.from({ length: 80 }, (_, i) => html`
|
||||||
|
<li style="padding: 4px 0;">Item ${i + 1} — option label</li>
|
||||||
|
`)}
|
||||||
|
</ul>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Done',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Long List</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => {
|
||||||
|
DeesModal.createAndShow({
|
||||||
|
heading: 'Tall Form',
|
||||||
|
width: 'medium',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
${Array.from({ length: 25 }, (_, i) => html`
|
||||||
|
<dees-input-text .label=${`Field ${i + 1}`}></dees-input-text>
|
||||||
|
`)}
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}, {
|
||||||
|
name: 'Submit',
|
||||||
|
action: async (modal) => modal!.destroy()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
}}>Tall Form</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -268,18 +268,9 @@ export class DeesModal extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heading .header-button dees-icon {
|
.heading .header-button dees-icon {
|
||||||
width: 14px;
|
font-size: 14px;
|
||||||
height: 14px;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
|
||||||
}
|
|
||||||
.bottomButtons {
|
.bottomButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -352,7 +343,7 @@ export class DeesModal extends DeesElement {
|
|||||||
${minWidthStyle ? `dees-tile { min-width: ${minWidthStyle}; }` : ''}
|
${minWidthStyle ? `dees-tile { min-width: ${minWidthStyle}; }` : ''}
|
||||||
</style>
|
</style>
|
||||||
<div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
|
<div class="modalContainer" @click=${this.handleOutsideClick} style="z-index: ${this.modalZIndex}">
|
||||||
<dees-tile class="${widthClass} ${mobileFullscreenClass}">
|
<dees-tile class="${widthClass} ${mobileFullscreenClass}" .overscroll=${'contain'}>
|
||||||
<div slot="header" class="heading">
|
<div slot="header" class="heading">
|
||||||
<div class="heading-text">${this.heading}</div>
|
<div class="heading-text">${this.heading}</div>
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
|
|||||||
@@ -353,12 +353,35 @@ export const demoFunc = () => html`
|
|||||||
name: 'Analytics',
|
name: 'Analytics',
|
||||||
iconName: 'lucide:lineChart',
|
iconName: 'lucide:lineChart',
|
||||||
element: DemoViewAnalytics,
|
element: DemoViewAnalytics,
|
||||||
|
subViews: [
|
||||||
|
{
|
||||||
|
name: 'Overview',
|
||||||
|
iconName: 'lucide:activity',
|
||||||
|
element: DemoViewAnalytics,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Reports',
|
||||||
|
iconName: 'lucide:fileText',
|
||||||
|
element: DemoViewDashboard,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
iconName: 'lucide:settings',
|
iconName: 'lucide:settings',
|
||||||
|
subViews: [
|
||||||
|
{
|
||||||
|
name: 'Profile',
|
||||||
|
iconName: 'lucide:user',
|
||||||
element: DemoViewSettings,
|
element: DemoViewSettings,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: 'Billing',
|
||||||
|
iconName: 'lucide:creditCard',
|
||||||
|
element: DemoViewSettings,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
] as IView[]}
|
] as IView[]}
|
||||||
@logout=${() => {
|
@logout=${() => {
|
||||||
console.log('Logout event triggered');
|
console.log('Logout event triggered');
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ declare global {
|
|||||||
export interface IView {
|
export interface IView {
|
||||||
name: string;
|
name: string;
|
||||||
iconName?: string;
|
iconName?: string;
|
||||||
element: DeesElement['constructor']['prototype'];
|
element?: DeesElement['constructor']['prototype'];
|
||||||
|
subViews?: IView[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TGlobalMessageType = 'info' | 'success' | 'warning' | 'error';
|
export type TGlobalMessageType = 'info' | 'success' | 'warning' | 'error';
|
||||||
@@ -250,6 +251,78 @@ export class DeesSimpleAppDash extends DeesElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.viewTab .chevron {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: transform 0.2s ease, opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewTab.hasSubs:hover .chevron {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewTab.hasSubs.groupActive .chevron {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subViews {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
margin-left: 12px;
|
||||||
|
position: relative;
|
||||||
|
transition:
|
||||||
|
grid-template-rows 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
margin-top 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
margin-bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subViews.expanded {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subViews::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
width: 1px;
|
||||||
|
background: var(--dees-color-border-default);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subViews.expanded::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subViews-inner {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewTab.sub {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewTab.sub dees-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewTab.sub.selected::before {
|
||||||
|
left: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
.appActions {
|
.appActions {
|
||||||
padding: 12px 8px;
|
padding: 12px 8px;
|
||||||
border-top: 1px solid var(--dees-color-border-default);
|
border-top: 1px solid var(--dees-color-border-default);
|
||||||
@@ -563,10 +636,12 @@ export class DeesSimpleAppDash extends DeesElement {
|
|||||||
<div class="viewTabs-container">
|
<div class="viewTabs-container">
|
||||||
<div class="section-label">Navigation</div>
|
<div class="section-label">Navigation</div>
|
||||||
<div class="viewTabs">
|
<div class="viewTabs">
|
||||||
${this.viewTabs.map(
|
${this.viewTabs.map((view) => {
|
||||||
(view) => html`
|
const hasSubs = !!view.subViews?.length;
|
||||||
|
const groupActive = hasSubs && this.isGroupActive(view);
|
||||||
|
return html`
|
||||||
<div
|
<div
|
||||||
class="viewTab ${this.selectedView === view ? 'selected' : ''}"
|
class="viewTab ${this.selectedView === view ? 'selected' : ''} ${hasSubs ? 'hasSubs' : ''} ${groupActive ? 'groupActive' : ''}"
|
||||||
@click=${() => this.loadView(view)}
|
@click=${() => this.loadView(view)}
|
||||||
>
|
>
|
||||||
${view.iconName ? html`
|
${view.iconName ? html`
|
||||||
@@ -575,11 +650,41 @@ export class DeesSimpleAppDash extends DeesElement {
|
|||||||
<dees-icon .icon="${'lucide:file'}"></dees-icon>
|
<dees-icon .icon="${'lucide:file'}"></dees-icon>
|
||||||
`}
|
`}
|
||||||
<span>${view.name}</span>
|
<span>${view.name}</span>
|
||||||
|
${hasSubs ? html`
|
||||||
|
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
${hasSubs ? html`
|
||||||
|
<div
|
||||||
|
class="subViews ${groupActive ? 'expanded' : ''}"
|
||||||
|
?inert=${!groupActive}
|
||||||
|
>
|
||||||
|
<div class="subViews-inner">
|
||||||
|
${view.subViews!.map(
|
||||||
|
(sub) => html`
|
||||||
|
<div
|
||||||
|
class="viewTab sub ${this.selectedView === sub ? 'selected' : ''}"
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.loadView(sub);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${sub.iconName ? html`
|
||||||
|
<dees-icon .icon="${sub.iconName.includes(':') ? sub.iconName : `lucide:${sub.iconName}`}"></dees-icon>
|
||||||
|
` : html`
|
||||||
|
<dees-icon .icon="${'lucide:dot'}"></dees-icon>
|
||||||
|
`}
|
||||||
|
<span>${sub.name}</span>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="appActions">
|
<div class="appActions">
|
||||||
<div class="action" @click=${() => {
|
<div class="action" @click=${() => {
|
||||||
this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
|
this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
|
||||||
@@ -771,8 +876,23 @@ export class DeesSimpleAppDash extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private isGroupActive(view: IView): boolean {
|
||||||
|
if (this.selectedView === view) return true;
|
||||||
|
return view.subViews?.some((sv) => sv === this.selectedView) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
private currentView!: DeesElement;
|
private currentView!: DeesElement;
|
||||||
public async loadView(viewArg: IView) {
|
public async loadView(viewArg: IView) {
|
||||||
|
// Group-only parent: resolve to first sub view with an element
|
||||||
|
if (!viewArg.element && viewArg.subViews?.length) {
|
||||||
|
const firstNavigable = viewArg.subViews.find((sv) => sv.element);
|
||||||
|
if (firstNavigable) {
|
||||||
|
return this.loadView(firstNavigable);
|
||||||
|
}
|
||||||
|
return; // nothing navigable — ignore click
|
||||||
|
}
|
||||||
|
if (!viewArg.element) return; // safety: no element and no subs → no-op
|
||||||
|
|
||||||
const appcontent = this.shadowRoot!.querySelector('.appcontent')!;
|
const appcontent = this.shadowRoot!.querySelector('.appcontent')!;
|
||||||
const view = new viewArg.element();
|
const view = new viewArg.element();
|
||||||
if (this.currentView) {
|
if (this.currentView) {
|
||||||
|
|||||||
Reference in New Issue
Block a user