Compare commits
200 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ef3613e91 | |||
| 940eebe29f | |||
| 8ecaffe165 | |||
| e5cb31ffb1 | |||
| 7c2635fc13 | |||
| 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 | |||
| 8d954b17ad | |||
| ac9cc8cfed | |||
| a1e808345e | |||
| efea2d62d9 | |||
| 2f95979cc6 | |||
| b3f098b41e | |||
| a0d5462ff1 | |||
| f1c204f790 | |||
| e806c9bce6 | |||
| fa56c7cce8 | |||
| 3601651e10 | |||
| b5055b0696 | |||
| a5f7a7ecee | |||
| dede3216fb | |||
| 5ee89b31b1 | |||
| 2d354ace55 | |||
| 34f5239607 | |||
| d17bdcbaad | |||
| dc8a3b620b | |||
| 9b96949a76 | |||
| 931797466a | |||
| 9bfb6446af | |||
| 976039798a | |||
| 0e2176ec7d | |||
| cada1a4234 | |||
| 465f7585ac | |||
| a7a710b320 | |||
| b1c174a4e2 | |||
| 395e0fa3da | |||
| f52b9d8b72 | |||
| 561d1b15d9 | |||
| 0722f362f3 | |||
| 2104b3bdce | |||
| e2d03107df | |||
| 54a87a5cc0 | |||
| 9bfbfcbb95 | |||
| 3505c390d8 | |||
| ff32470d8a | |||
| 4dba14060e | |||
| 31d728ec49 | |||
| ca290d1267 | |||
| dcef6faa66 | |||
| fe9eb21fe0 | |||
| f352314971 | |||
| 130ca68471 | |||
| cdde25d0b4 | |||
| 231c57b596 | |||
| 167dcb2b0a | |||
| fdccdcdf73 | |||
| bee1cafdb4 | |||
| 42b40da67c | |||
| 10cd1e2755 | |||
| 68ed024aaa | |||
| 6b6ccd0e3c | |||
| d933c47b49 | |||
| 3defbba5fd | |||
| 02522c9a15 | |||
| 4370efe6fb | |||
| cde2a833ef | |||
| 31fbe22f55 | |||
| e6f501e804 | |||
| f052fb9c9f | |||
| 77130ffb5e | |||
| 236b83d0a0 | |||
| a2e0760cc6 | |||
| 2e24d77f6a | |||
| 10b67adfe1 | |||
| 0d7f68086d | |||
| 9d0f6da905 | |||
| 42fd0b276e | |||
| 23d672040c | |||
| 24f96788d5 | |||
| 6e5def5708 | |||
| 472132e8cf | |||
| e062f5046e | |||
| f2d3fc28f8 | |||
| 7295bfcf92 | |||
| ad732a3e68 | |||
| b38bd28360 | |||
| 99a531ee74 | |||
| 1a3a5e5454 | |||
| 5cf8161735 | |||
| 46d9cdc741 | |||
| c13f319474 | |||
| c0ac8f593a | |||
| c52854f902 | |||
| c3b0f0df1f | |||
| cbc0bbcad4 | |||
| 8d3a1783fd | |||
| af1f660486 | |||
| b1c8a7446e | |||
| 7e991396e9 | |||
| 25cbf9bfdd | |||
| 4d8ba1fefc | |||
| 42317459ff | |||
| 932db338c6 | |||
| bc4b87b83a | |||
| eb055e7214 | |||
| c55eb948fe | |||
| 35779209ea | |||
| 8c6738ea15 | |||
| e7da1d8b44 | |||
| 358d82e7fa | |||
| 6452e05e1d | |||
| 07b536ea9a | |||
| 3fcb0cbf89 | |||
| 3285cbf0e7 | |||
| a2d750b2f6 | |||
| d4276710e6 | |||
| 66d64bf476 | |||
| 2504251707 | |||
| fed130f291 | |||
| 4f05b5907b | |||
| e517320dcd | |||
| ade5a25b3a | |||
| a396dfea12 | |||
| d0105e1b80 | |||
| 1eeebb35e6 | |||
| 14e8b8c533 | |||
| eaf327ea75 | |||
| 09741e0b37 | |||
| 5cadd1fc7f | |||
| 1795235c6d | |||
| ba7d387acb | |||
| 26ca16a284 | |||
| 3ab3eb5e5e | |||
| da5dbc70e2 | |||
| 19b7981542 | |||
| bad105074e | |||
| f124091784 | |||
| 68790a26ed | |||
| 2abf7e356d | |||
| ecd35683e6 | |||
| 93cb632448 | |||
| 047e42c6a3 | |||
| 59efa8cff0 | |||
| 09f0aa97dd | |||
| 7c62f45d77 | |||
| b123768474 | |||
| f292e7a7f4 | |||
| d82e5603a7 | |||
| 7e2386bcdf | |||
| eba2a03355 | |||
| 06c01f0690 | |||
| 91e03eb9c4 | |||
| b7f3f47c61 | |||
| 0a83f0e136 | |||
| 2b048cf34f | |||
| 7e50b8cb3f | |||
| b97601a876 | |||
| 5ddeb8fe7c | |||
| 25f46162c5 | |||
| b84b0e7ce6 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
607
changelog.md
607
changelog.md
@@ -1,5 +1,612 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-12 - 3.77.0 - feat(dees-table)
|
||||
add configurable cell flash comparison and border highlight mode
|
||||
|
||||
- adds column-level flashCompare support so update highlighting can detect meaningful changes for custom cell values
|
||||
- adds flashBorder styling for cells with badges, icons, or custom rendered content where text-color flashing is not visible
|
||||
- avoids false-positive flash animations for non-primitive cell values unless a custom comparator is provided
|
||||
|
||||
## 2026-04-12 - 3.76.1 - fix(demo-inputs)
|
||||
wrap input component demos in dees-form containers for consistent form integration
|
||||
|
||||
- Adds dees-form wrappers across multiple input demo pages including checkbox, dropdown, file upload, iban, list, multitoggle, phone, quantity selector, radio group, tags, text, toggle, and typelist demos
|
||||
- Keeps horizontal and grid example layouts intact while nesting them inside form containers
|
||||
- Cleans up file upload and text demo markup to better match expected dees-form structure
|
||||
|
||||
## 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)
|
||||
add virtualized row rendering for large tables and optimize table rendering performance
|
||||
|
||||
- add a virtualized mode with configurable overscan to render only visible rows while preserving scroll height
|
||||
- improve table render performance with memoized column and view-data computation plus deferred floating header rendering
|
||||
- update the dees-table demo to showcase virtualized scrolling in the fixed-height example
|
||||
|
||||
## 2026-04-07 - 3.65.0 - feat(dees-table)
|
||||
add schema-based in-cell editing with keyboard navigation and cell edit events
|
||||
|
||||
- replace editableFields with per-column editor configuration for text, number, checkbox, dropdown, date, and tags inputs
|
||||
- add focused/editing cell state with arrow key navigation plus Enter, Tab, Shift+Tab, F2, and Escape editing controls
|
||||
- dispatch cellEdit and cellEditError events with typed payloads and support column-level format, parse, validate, and editorOptions hooks
|
||||
- update table styles and demos to reflect editable cell behavior and rename sticky header usage to fixedHeight
|
||||
|
||||
## 2026-04-07 - 3.64.0 - feat(dees-table)
|
||||
add file-manager style row selection and JSON copy support
|
||||
|
||||
- adds optional selection checkbox rendering via the show-selection-checkbox property
|
||||
- supports plain, ctrl/cmd, and shift-click row selection with range selection behavior
|
||||
- adds Ctrl/Cmd+C and context menu actions to copy selected rows as formatted JSON
|
||||
- updates row selection styling to prevent native text selection during range selection
|
||||
|
||||
## 2026-04-07 - 3.63.0 - feat(dees-table)
|
||||
add floating header support with fixed-height table mode
|
||||
|
||||
- replace the sticky-header option with a fixed-height mode for internal scrolling
|
||||
- add a JS-managed floating header so column headers remain visible when tables scroll inside ancestor containers
|
||||
- sync floating header column widths and filter rows with the rendered table
|
||||
|
||||
## 2026-04-07 - 3.62.0 - feat(dees-table)
|
||||
add multi-column sorting with header menu controls and priority indicators
|
||||
|
||||
- replace single-column sort state with ordered sort descriptors for cascading client-side sorting
|
||||
- add Shift+click header sorting, context menu actions, and confirmation before replacing an active sort cascade
|
||||
- show multi-sort direction and priority badges in table headers and add a demo showcasing the new behavior
|
||||
|
||||
## 2026-04-06 - 3.61.2 - fix(dees-input-list,dees-icon)
|
||||
preserve input focus after list updates and make icons ignore pointer events
|
||||
|
||||
- Delays refocusing the add input in dees-input-list until after Lit re-renders complete when adding or removing entries.
|
||||
- Adds pointer-events: none to dees-icon so icons do not block click interactions on surrounding controls.
|
||||
|
||||
## 2026-04-05 - 3.61.1 - fix(dees-input-list)
|
||||
align list input with dees-tile styling and improve item affordances
|
||||
|
||||
- wrap the list in dees-tile with a dynamic item count heading and move the add-item controls into the tile footer
|
||||
- replace custom container styling with shared tile and theme tokens for hover, focus, row, and disabled states
|
||||
- show a bullet icon for non-sortable or disabled items when no candidate state icon is present
|
||||
|
||||
## 2026-04-05 - 3.61.0 - feat(dees-input-list)
|
||||
allow freeform entries alongside candidate suggestions in dees-input-list
|
||||
|
||||
- adds an allowFreeform option so Enter can add values that do not exactly match the candidate list
|
||||
- shows check and question-mark indicators to distinguish known candidates from custom freeform items
|
||||
- updates the component demo with a freeform-plus-candidates example
|
||||
|
||||
## 2026-04-05 - 3.60.0 - feat(dees-input-list)
|
||||
add candidate autocomplete with tab completion and payload retrieval
|
||||
|
||||
- Adds terminal-style inline autocomplete with ghost text, Tab accept, Shift+Tab cycling, and Escape clearing for candidate-based input.
|
||||
- Introduces candidate payload support with APIs to retrieve selected candidate objects after items are added.
|
||||
- Updates the dees-input-list demo with candidate selection examples for team members and technology stacks.
|
||||
|
||||
## 2026-04-05 - 3.59.1 - fix(project)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-04-05 - 3.59.0 - feat(input)
|
||||
extract datepicker popup into a window-layer overlay and enhance the code editor modal status UI
|
||||
|
||||
- move the datepicker calendar, time, timezone, and event rendering into a dedicated popup component exported from the input module
|
||||
- render the datepicker popup in a window-layer overlay with reposition and cleanup handling for scroll, resize, and close events
|
||||
- preserve timezone-aware value formatting for selected dates
|
||||
- add a footer to the code editor modal showing cursor position, line count, and selected language
|
||||
- apply modal-specific Monaco background themes that react to light and dark mode
|
||||
|
||||
## 2026-04-05 - 3.58.0 - feat(dees-input-code)
|
||||
add editor status footer with cursor position, line count, and language display
|
||||
|
||||
- Tracks and displays the current cursor line and column in the code editor footer
|
||||
- Shows dynamic line count updates as editor content changes
|
||||
- Aligns the Monaco editor background with the surrounding tile theme, including light and dark mode updates
|
||||
|
||||
## 2026-04-05 - 3.57.0 - feat(dees-input-fileupload)
|
||||
redesign the file upload dropzone with dees-tile integration and themed file list styling
|
||||
|
||||
- Replace the custom dropzone container with dees-tile and move actions and metadata into header and footer slots
|
||||
- Add an explicit empty state for the file list and simplify file list layout and interaction handling
|
||||
- Adopt shared theme tokens in the file upload styles and introduce a reusable row hover color token
|
||||
|
||||
## 2026-04-04 - 3.56.1 - fix(dees-input-dropdown)
|
||||
improve dropdown popup lifecycle with window layer overlay and animated visibility transitions
|
||||
|
||||
- use a window layer to handle outside-click closing instead of document-level mousedown listeners
|
||||
- await popup show and search focus to keep popup initialization and overlay setup in sync
|
||||
- add guarded async hide logic with animated teardown and cleanup of scroll/resize listeners
|
||||
|
||||
## 2026-04-04 - 3.56.0 - feat(dees-input-dropdown)
|
||||
extract dropdown popup into a floating overlay component with search, keyboard navigation, and viewport repositioning
|
||||
|
||||
- adds a new dees-input-dropdown-popup export for rendering the menu as a fixed overlay attached to document.body
|
||||
- keeps the dropdown aligned to its trigger on scroll and resize, and closes it when the trigger moves off-screen
|
||||
- moves option filtering and keyboard selection handling into the popup component while preserving selection events
|
||||
|
||||
## 2026-04-04 - 3.55.6 - fix(dees-heading)
|
||||
adjust heading hr text color to use muted theme values
|
||||
|
||||
- Updates the dees-heading horizontal rule variant to use softer light and dark theme text colors instead of pure black and white.
|
||||
|
||||
## 2026-04-04 - 3.55.5 - fix(chart)
|
||||
refine ECharts series styling and legend color handling across bar, donut, and radar charts
|
||||
|
||||
- switch chart series palettes to hex colors and add rgba conversion to prevent black flashes during ECharts hover and emphasis animations
|
||||
- explicitly provide legend item colors and solid tooltip markers so translucent fills render consistently across chart types
|
||||
- deep-merge legend theme options in the shared ECharts base component to preserve nested legend text styling
|
||||
- adjust donut chart spacing and shared chart container styling for improved layout
|
||||
|
||||
## 2026-04-04 - 3.55.4 - fix(chart)
|
||||
align ECharts components with theme tokens and load the full ECharts ESM bundle
|
||||
|
||||
- replace hardcoded chart colors with centralized themeDefaults-based ECharts theme helpers across bar, donut, gauge, and radar components
|
||||
- improve donut label styling to use theme-aware text colors
|
||||
- switch CDN loading to the pre-built echarts.esm.min.js bundle so all chart types and components are available
|
||||
|
||||
## 2026-04-04 - 3.55.3 - fix(theme)
|
||||
align component styles with shared theme CSS variables
|
||||
|
||||
- replace hardcoded bdTheme color usages across chart, dataview, input, layout, modal, media, simple, and workspace components with shared --dees-* theme tokens
|
||||
- add themeDefaultStyles to components and style modules that were not yet inheriting the shared theme defaults
|
||||
- standardize hover, border, background, text, and scrollbar colors for more consistent theming across the catalog
|
||||
|
||||
## 2026-04-04 - 3.55.2 - fix(dees-simple-appdash,dees-simple-login)
|
||||
migrate app dashboard and login styles to shared theme CSS variables
|
||||
|
||||
- Replaces hardcoded bdTheme color values with --dees-* design tokens across dashboard and login components
|
||||
- Aligns backgrounds, borders, text, hover, active, and scrollbar colors with the shared theming system
|
||||
|
||||
## 2026-04-04 - 3.55.1 - fix(appui)
|
||||
replace hardcoded app UI theme colors with shared dees CSS variables
|
||||
|
||||
- Standardizes app UI component styling on shared --dees-* theme tokens across app bar, menus, tabs, main content, and bottom bar
|
||||
- Removes remaining hardcoded light/dark color values in favor of centralized background, border, text, badge, tooltip, scrollbar, and accent variables
|
||||
|
||||
## 2026-04-04 - 3.55.0 - feat(theme)
|
||||
centralize theme CSS tokens in theme defaults and add missing interactive color variables
|
||||
|
||||
- Refactors theme CSS variables to derive light and dark values from the shared themeDefaults token map instead of hardcoded color literals.
|
||||
- Adjusts dark background token values so secondary and tertiary surfaces align more consistently with the dark UI palette.
|
||||
- Adds new theme variables for interactive states, focus ring, tooltip, link, code, selection, and scrollbar styling.
|
||||
|
||||
## 2026-04-04 - 3.54.0 - feat(media)
|
||||
rename media tile components to thumbnail components and add shared thumbnail base exports
|
||||
|
||||
- Replaces dees-tile-* media component exports and implementations with dees-thumbnail-* counterparts for audio, image, video, note, folder, and pdf previews.
|
||||
- Introduces a shared DeesThumbnailBase and shared thumbnail styles for consistent sizing, hover overlays, loading states, error states, and lazy-loading behavior.
|
||||
- Updates the media index to export the new thumbnail component modules.
|
||||
- Includes a small layout cleanup in dees-dataview-codebox by removing forced full-height and absolute grid positioning.
|
||||
|
||||
## 2026-04-03 - 3.53.0 - feat(dees-pdf-viewer)
|
||||
add configurable sidebar position support
|
||||
|
||||
- introduces a sidebarPosition property with left and right options
|
||||
- updates viewer layout and footer alignment when the sidebar is displayed on the right
|
||||
|
||||
## 2026-04-03 - 3.52.5 - fix(dees-pdf-viewer)
|
||||
add top scroll offset when navigating to a page in the PDF viewer
|
||||
|
||||
- Subtracts 16px from the calculated scroll target so the selected page is not flush against the top edge of the viewer.
|
||||
- Improves page navigation positioning in the dees-pdf-viewer component.
|
||||
|
||||
## 2026-04-03 - 3.52.4 - fix(appui-maincontent)
|
||||
adjust main content background theme colors
|
||||
|
||||
- Update the main content background from pure white/near-black to softer light and dark theme values.
|
||||
|
||||
## 2026-04-03 - 3.52.3 - fix(input-richtext)
|
||||
resolve rich text editor initialization and layout issues by bundling Tiptap locally and anchoring editor containers
|
||||
|
||||
- Switch Tiptap imports from CDN URLs to bundled npm packages to avoid duplicate ProseMirror instances
|
||||
- Update rich text, code, dataview, and terminal preview containers to use absolute inset positioning for stable full-size layouts
|
||||
- Trigger a component update after rich text editor initialization and improve ProseMirror wrapping behavior
|
||||
|
||||
## 2026-04-03 - 3.52.2 - fix(chart-log, simple-appdash)
|
||||
align terminal and dashboard theming with brightness mode and improve app dashboard scroll presentation
|
||||
|
||||
- Update dees-chart-log to refresh the terminal theme when goBright changes and derive dark mode directly from the brightness setting.
|
||||
- Refine dees-simple-appdash control bar colors, borders, and shadow gradients for better light and dark theme consistency.
|
||||
- Expand the app dashboard demo with recent activity content to showcase scrollable layout behavior.
|
||||
|
||||
## 2026-04-03 - 3.52.1 - fix(dees-modal)
|
||||
refine modal styling, spacing, and animations for a cleaner overlay presentation
|
||||
|
||||
- Adjust modal entrance and exit transitions with updated transform, opacity, and timing values
|
||||
- Refresh heading and action button styling with tighter spacing, smaller controls, and improved theme-aware colors
|
||||
- Update tile shadow, margins, and content scrollbar styling to improve modal visual polish and readability
|
||||
|
||||
## 2026-04-03 - 3.52.0 - feat(dees-chart-area)
|
||||
add full page toggle control for chart area
|
||||
|
||||
- adds a header action to expand and collapse the chart area into a full page view
|
||||
- updates chart area styling for the custom header, label, and expand button
|
||||
- resizes the chart after toggling full page mode by fitting the visible time scale
|
||||
|
||||
## 2026-04-03 - 3.51.2 - fix(ui)
|
||||
standardize tile-based layouts across input, product card, and terminal preview components
|
||||
|
||||
- Replace custom card containers with dees-tile in code input, richtext editor, shopping product card, and terminal preview components
|
||||
- Move toolbars and footers into dees-tile header/footer slots for more consistent structure and spacing
|
||||
- Update hover, focus, and selected state styling to target dees-tile parts while preserving existing component behavior
|
||||
|
||||
## 2026-04-03 - 3.51.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-04-03 - 3.51.0 - feat(chart-area)
|
||||
replace ApexCharts with Lightweight Charts for area chart rendering
|
||||
|
||||
- switch chart dependency and CDN loader from apexcharts to lightweight-charts
|
||||
- update dees-chart-area styling and tooltip support for the new chart engine
|
||||
- adjust appdash terminal overlay sizing with automatic height
|
||||
|
||||
## 2026-04-02 - 3.50.2 - fix(chart,dashboardgrid demos)
|
||||
use dees-button text property consistently in demo interactions and update dataset button highlighting reliably
|
||||
|
||||
- Replaces textContent reads with the component text property when wiring demo button handlers.
|
||||
- Updates the chart area demo to refresh dataset button highlight states directly on click without redundant listener replacement logic.
|
||||
- Uses the button text property when toggling dashboard edit mode labels.
|
||||
|
||||
## 2026-04-02 - 3.50.1 - fix(appdash)
|
||||
use banner height CSS variable for terminal layout and expose terminal resize handling
|
||||
|
||||
- Removes manual terminal top offset updates in the app dashboard and relies on the shared --banner-area-height CSS variable instead.
|
||||
- Drops fixed max-width and max-height calculations so the terminal can size more reliably within its container.
|
||||
- Makes the workspace terminal resize handler public to support external resize coordination.
|
||||
|
||||
## 2026-04-02 - 3.50.0 - feat(dees-simple-appdash)
|
||||
add global message banners with actions and dismissal support
|
||||
|
||||
- introduces typed global message APIs and public methods to add, remove, and clear banners
|
||||
- renders info, success, warning, and error banners with optional icons and action buttons
|
||||
- adjusts app content and terminal positioning to account for banner height dynamically
|
||||
- updates the demo to showcase dismissible and actionable global messages
|
||||
|
||||
## 2026-04-02 - 3.49.2 - fix(dees-input-list)
|
||||
refine dees-input-list spacing and simplify the add item action button
|
||||
|
||||
- reduce list item, input, helper text, and empty state sizing for a more compact layout
|
||||
- replace the add action from dees-button to a native icon-only button and remove the unused button import
|
||||
- simplify add-input styling by removing bordered focus treatment and using a minimal inline input appearance
|
||||
|
||||
## 2026-04-01 - 3.49.1 - fix(ts_web)
|
||||
resolve TypeScript nullability and event typing issues across web components
|
||||
|
||||
- adds explicit non-null assertions and nullable types to component properties, shadowRoot queries, and modal references
|
||||
- normalizes demo and component event listeners by casting generic Event objects to CustomEvent where needed
|
||||
- guards optional form, dropdown, menu, and DOM interactions to prevent invalid access under stricter TypeScript checks
|
||||
- updates tsconfig to include Node types for the web build environment
|
||||
|
||||
## 2026-03-18 - 3.49.0 - feat(dataview-statusobject)
|
||||
add last updated footer to status object and refresh demo data
|
||||
|
||||
- Render a bottom bar that shows the status object's lastUpdated timestamp when available.
|
||||
- Adjust detail row padding to keep spacing consistent with the new footer layout.
|
||||
- Update demo status objects to include lastUpdated examples for current, hourly, and daily timestamps.
|
||||
- Bump @tsclass/tsclass from ^9.3.0 to ^9.5.0.
|
||||
|
||||
## 2026-03-14 - 3.48.5 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-14 - 3.48.4 - fix(storage-browser)
|
||||
rename S3-specific storage browser interfaces to generic storage types
|
||||
|
||||
- Replaces IS3DataProvider, IS3Object, and IS3ChangeEvent with generic storage interface names across storage browser components
|
||||
- Updates demo provider naming and user-facing demo text from S3 browser to Storage browser
|
||||
- Aligns interface and utility comments with storage-agnostic terminology
|
||||
|
||||
## 2026-03-14 - 3.48.3 - fix(dataview)
|
||||
rename dees-s3-browser exports and custom elements to dees-storage-browser
|
||||
|
||||
- Replaces the dees-s3-browser module path with dees-storage-browser in dataview exports
|
||||
- Renames the main custom element from dees-s3-browser to dees-storage-browser
|
||||
- Renames related columns, keys, preview, demo, interfaces, and utility entry points under the new storage-browser module
|
||||
|
||||
## 2026-03-12 - 3.48.2 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-12 - 3.48.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-12 - 3.48.0 - feat(dataview)
|
||||
add an S3 browser component with column and list views, file preview, editing, and object management
|
||||
|
||||
- introduces a new dees-s3-browser module with shared interfaces, utilities, demo, and exports
|
||||
- supports browsing S3-style prefixes in both column and list layouts with breadcrumb navigation
|
||||
- adds file preview with text editing, download, and delete actions
|
||||
- includes create, rename, move, delete, upload, and drag-and-drop handling for files and folders
|
||||
- adds optional live change stream integration with refresh indicators
|
||||
|
||||
## 2026-03-11 - 3.47.2 - fix(deps)
|
||||
bump @design.estate/dees-domtools and @design.estate/dees-element dependencies
|
||||
|
||||
- update @design.estate/dees-domtools from ^2.3.9 to ^2.5.1
|
||||
- update @design.estate/dees-element from ^2.2.1 to ^2.2.2
|
||||
|
||||
## 2026-03-11 - 3.47.1 - fix(dees-statsgrid)
|
||||
add tablet breakpoint to render stats grid as three columns
|
||||
|
||||
- Added cssManager.cssForTablet rule to set .stats-grid grid-template-columns: repeat(3, 1fr).
|
||||
- Improves responsive layout on tablet devices for dees-statsgrid tiles.
|
||||
- Change made in ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
|
||||
|
||||
## 2026-03-11 - 3.47.0 - feat(dees-statsgrid)
|
||||
add container-responsive behavior and responsive CSS to dees-statsgrid; bump @design.estate/dees-element dependency to ^2.2.1
|
||||
|
||||
- Added @containerResponsive decorator and import to dees-statsgrid
|
||||
- Added cssManager.cssForPhablet and cssManager.cssForPhone responsive style blocks to adjust layout, spacing and font sizes on smaller viewports
|
||||
- Bumped dependency @design.estate/dees-element from ^2.1.6 to ^2.2.1
|
||||
|
||||
## 2026-03-10 - 3.46.1 - fix(dees-appui)
|
||||
add min-height: 0 to mainmenu and secondarymenu to prevent unintended container height and fix layout stacking
|
||||
|
||||
- Modified ts_web/elements/00group-appui/dees-appui/dees-appui.ts: added min-height: 0 to .maingrid > dees-appui-mainmenu and .maingrid > dees-appui-secondarymenu
|
||||
- Fixes layout issues where children or flexbox-derived min-height could cause menu containers to expand and interfere with z-index stacking
|
||||
|
||||
## 2026-03-10 - 3.46.0 - feat(dees-tile)
|
||||
unify tile metadata into a consistent bottom info bar and add PDF file-size display
|
||||
|
||||
- Introduce renderBottomBar() hook in DeesTileBase and remove per-component bottom badges/labels in favor of a unified info bar.
|
||||
- Implement renderBottomBar in audio, video, image, folder, note and pdf tiles to show label, counts, dimensions, duration, language/line info and page counts.
|
||||
- PDF tile: add fileSize state, attempt to read download info and display formatted file size in the info bar; show currentPreviewPage/pageCount when hovering.
|
||||
- Styling changes: replace legacy badges/labels with .tile-info-bar (.info-label, .info-detail, .info-spacer); adjust padding, font sizing, z-index, and remove hover translate for clickable tiles.
|
||||
- PDF demo and styles: use cssManager theming for demo colors and adjust preview padding.
|
||||
- Bump devDependencies: @git.zone/tswatch -> ^3.3.0 and @types/node -> ^25.4.0
|
||||
|
||||
## 2026-03-10 - 3.45.1 - fix(dees-appui)
|
||||
substitute route params into URL hash when navigating
|
||||
|
||||
- Replaces :param placeholders in view.route with provided params before updating the URL hash.
|
||||
- Ensures window.history.pushState is called with the resolved route so URLs do not contain literal parameter tokens.
|
||||
|
||||
## 2026-03-10 - 3.45.0 - feat(dees-form)
|
||||
register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates
|
||||
|
||||
- Added imports and registration of DeesInputTags, DeesInputList, DeesInputWysiwyg, and DeesInputRichtext in dees-form
|
||||
- Extended TFormInputElement union type to include the new input components
|
||||
- DeesInputRichtext now calls changeSubject.next(this.value) in the editor onUpdate handler to propagate changes
|
||||
|
||||
## 2026-03-10 - 3.44.0 - feat(appui-tabs)
|
||||
add support for left/right tab action buttons and content tab action APIs
|
||||
|
||||
- Introduce ITabAction interface and add actionsLeft/actionsRight properties to dees-appui-tabs, dees-appui-maincontent, and dees-appui.
|
||||
- Render action buttons with new styles and renderActions() helper, including disabled state and click handlers; wire actions into tab components.
|
||||
- Add public clear() on dees-appui-tabs and improve tab selection logic to reset selection when tabs become empty or when the selected tab is removed.
|
||||
- Expose setContentTabActionsLeft and setContentTabActionsRight on the DeesAppui programmatic API and update interfaces/appconfig accordingly.
|
||||
- Update demos to showcase action buttons, add clear-all behavior, and adjust layout/styling for action areas.
|
||||
|
||||
## 2026-03-09 - 3.43.4 - fix(media)
|
||||
remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies
|
||||
|
||||
- Removed deprecated PDF components and related demos/styles: ts_web/elements/00group-media/dees-pdf/* and ts_web/elements/00group-media/dees-pdf-preview/*
|
||||
- Removed exports for dees-pdf and dees-pdf-preview from ts_web/elements/00group-media/index.ts (public API removal)
|
||||
- Dependency upgrades: @design.estate/dees-domtools → ^2.3.9, apexcharts → ^5.10.3, lucide → ^0.577.0, @fortawesome/* → ^7.2.0
|
||||
- DevDependency upgrades: @git.zone/tsbuild → ^4.3.0, @git.zone/tsbundle → ^2.9.1, @git.zone/tstest → ^3.3.2, @git.zone/tswatch → ^3.2.5, @types/node → ^25.3.5
|
||||
- Updated ts_web/services/versions.ts to align CDN/version constants (apexcharts, tiptap → 2.27.2, fontawesome)
|
||||
|
||||
## 2026-02-24 - 3.43.3 - fix(dees-table)
|
||||
use lucide icon identifier for Search action in dees-table
|
||||
|
||||
- Replaced iconName 'magnifyingGlass' with 'lucide:Search' in ts_web/elements/00group-dataview/dees-table/dees-table.ts
|
||||
- Updates the icon identifier for the header 'Search' action; no functional behavior changed
|
||||
|
||||
## 2026-02-21 - 3.43.2 - fix(dees-chart-log)
|
||||
avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs
|
||||
|
||||
- Prevent property-bound logEntries from duplicating entries already present in logBuffer by deduplicating on timestamp|message
|
||||
- Call updateMetrics() when replaying or appending log entries so metrics stay accurate
|
||||
- Skip processing entirely when the incoming logEntries array is unchanged
|
||||
- Optimize append-only updates by writing only the new tail entries instead of full re-render
|
||||
- Enforce maxEntries when appending to the logBuffer to maintain buffer size
|
||||
- Respect filterMode and searchQuery when deciding whether to write appended entries to the terminal
|
||||
|
||||
## 2026-02-21 - 3.43.1 - fix(dees-chart-log)
|
||||
replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs
|
||||
|
||||
- Replay entries stored in logBuffer after terminal initialization to avoid losing entries that arrived early
|
||||
- Add updated() lifecycle hook to copy logEntries into logBuffer and call reRenderFilteredLogs when terminalReady
|
||||
- Call super.updated(changedProperties) to preserve base class behavior
|
||||
|
||||
## 2026-02-17 - 3.43.0 - feat(dees-form)
|
||||
add layout styles to dees-form and standardize demo input grouping
|
||||
|
||||
- Add static CSS to dees-form: default column layout with gap and support for [horizontal-layout] (row wrapping, alignment and gap).
|
||||
- Remove inline <style> from dees-form render to centralize styling.
|
||||
- Simplify dees-input-base styles by removing host margins and making spacing container-driven.
|
||||
- Update multiple demo files to wrap related inputs in a new .input-group container and include .input-group CSS for consistent vertical spacing.
|
||||
|
||||
## 2026-02-16 - 3.42.2 - fix(dees-chart-area)
|
||||
add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data
|
||||
|
||||
- Introduces ApexAxisChartSeries type alias with support for number arrays, [x,y] tuples, and object {x,y,...} series entries
|
||||
- Type-only change — no runtime or API behavior modified
|
||||
- File changed: ts_web/elements/00group-chart/dees-chart-area/component.ts
|
||||
|
||||
## 2026-02-16 - 3.42.1 - fix(dees-table)
|
||||
Guard against undefined action.type in dees-table by using optional chaining and update several dependencies
|
||||
|
||||
- Use optional chaining (action.type?.includes(...)) in ts_web/elements/.../dees-table.ts to prevent runtime errors when action.type is undefined
|
||||
- Bump dependency apexcharts from ^5.3.6 to ^5.5.0
|
||||
- Bump dependency lucide from ^0.563.0 to ^0.564.0
|
||||
- Bump devDependency @git.zone/tswatch from ^3.0.1 to ^3.1.0
|
||||
- Bump devDependency @types/node from ^25.0.10 to ^25.2.3
|
||||
|
||||
## 2026-02-02 - 3.42.0 - feat(dees-form-submit)
|
||||
forward button properties to internal dees-button, use property bindings, add demo and styles
|
||||
|
||||
- Added forwarded properties: type, size, icon, iconPosition (with defaults) and preserved text/status/disabled
|
||||
- Changed template to use property bindings (.prop) for dees-button instead of string attributes
|
||||
- Switched internal event handler to listen for dees-button's @clicked event (was @click)
|
||||
- Added component styles (:host display and dees-button width:100%) and improved layout
|
||||
- Expanded demo with multiple usage examples (basic, icons, types, sizes, states, and a form context)
|
||||
|
||||
## 2026-02-02 - 3.41.6 - fix(dees-simple-appdash)
|
||||
respect selectedView when loading initial view, falling back to the first tab
|
||||
|
||||
- firstUpdated now loads this.selectedView if set, otherwise loads the first view tab
|
||||
- Prevents always loading the first tab and preserves a previously selected view on initial render
|
||||
|
||||
## 2026-02-01 - 3.41.5 - fix(dees-service-lib-loader)
|
||||
prevent horizontal scrollbar by offsetting xterm WidthCache measurement container
|
||||
|
||||
- Injects additional CSS into DeesServiceLibLoader to move xterm.js WidthCache measurement div off-screen horizontally (selector: body > div[style*="top: -50000px"][style*="width: 50000px"])
|
||||
- Fixes root cause where xterm creates a large-width measurement container (width: 50000px) on document.body that expands scrollWidth and causes a horizontal scrollbar
|
||||
- Change applied in ts_web/services/DeesServiceLibLoader.ts by concatenating the fix CSS into the injected stylesheet
|
||||
|
||||
## 2026-01-29 - 3.41.4 - fix()
|
||||
no changes
|
||||
|
||||
- No files changed in this commit; no code or documentation modified
|
||||
- No release required
|
||||
|
||||
## 2026-01-29 - 3.41.3 - fix(dees-pdf-viewer)
|
||||
use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling
|
||||
|
||||
- Download and Print now use pdfDocument.getData() to create Blob URLs so in-memory PDFs (pdf.js) can be saved/printed.
|
||||
- Print flow now opens an HTML wrapper with an iframe to allow onafterprint handling, auto-close, popup-fallback and timed cleanup of Blob URLs.
|
||||
- Added try/catch logging, URL.revokeObjectURL calls and safety timeouts to avoid resource leaks.
|
||||
- Removed context menu items that relied on the raw PDF URL (Open in New Tab, Copy PDF URL); Download/Print actions now await the async handlers.
|
||||
|
||||
## 2026-01-28 - 3.41.2 - fix(dees-pdf-viewer)
|
||||
account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays
|
||||
|
||||
- Multiply canvas.width and canvas.height by window.devicePixelRatio (dpr) and use Math.floor to set the actual pixel buffer size
|
||||
- Call ctx.scale(dpr, dpr) so drawing is rendered at device pixels while keeping CSS display size unchanged
|
||||
- Apply the same high-DPI adjustments to both main page rendering and thumbnail generation
|
||||
- Keep canvas.style.width and canvas.style.height set to viewport dimensions to preserve layout
|
||||
|
||||
## 2026-01-27 - 3.41.1 - fix(dataview-codebox)
|
||||
fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies
|
||||
|
||||
- Updated CSS in ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts: added height:100%, box-sizing, display:flex and flex-direction:column on container, set flex-shrink on header elements, made code grid overflow:auto with flex:1 and min-height:0 to prevent overflow issues.
|
||||
- Bumped dependencies in package.json: @design.estate/dees-domtools from ^2.3.7 to ^2.3.8 and @design.estate/dees-element from ^2.1.5 to ^2.1.6.
|
||||
- Non-breaking visual/layout fix — suitable for a patch release.
|
||||
|
||||
## 2026-01-27 - 3.41.0 - feat(docs)
|
||||
document new media & tile components and expand Workspace/IDE component docs; update component count to 90+
|
||||
|
||||
|
||||
9
license
9
license
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020 Lossless GmbH (hello@lossless.com)
|
||||
Copyright (c) 2020 Task Venture Capital GmbH (hello@task.vc)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -8,10 +8,7 @@ copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software. You agree to being mentioned
|
||||
as reference by Lossless GmbH. This includes the use of your entity logos
|
||||
or profile picture by Lossless GmbH on websites and readme's, also on third party
|
||||
pages like gitlab.com or github.com.
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -19,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
37
package.json
37
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.41.0",
|
||||
"version": "3.77.0",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
@@ -16,13 +16,13 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.3.7",
|
||||
"@design.estate/dees-element": "^2.1.5",
|
||||
"@design.estate/dees-wcctools": "^3.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@design.estate/dees-domtools": "^2.5.4",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@design.estate/dees-wcctools": "^3.8.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||
"@push.rocks/smarti18n": "^1.0.4",
|
||||
"@push.rocks/smartpromise": "^4.2.0",
|
||||
"@push.rocks/smartstring": "^4.1.0",
|
||||
@@ -33,23 +33,24 @@
|
||||
"@tiptap/extension-typography": "^2.23.0",
|
||||
"@tiptap/extension-underline": "^2.23.0",
|
||||
"@tiptap/starter-kit": "^2.23.0",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"apexcharts": "^5.3.6",
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
"echarts": "^5.6.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"lucide": "^0.563.0",
|
||||
"lightweight-charts": "^5.1.0",
|
||||
"lucide": "^1.8.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tswatch": "^3.0.1",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.0.10"
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -60,7 +61,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
5764
pnpm-lock.yaml
generated
5764
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
33
readme.md
33
readme.md
@@ -58,13 +58,13 @@ For developers working on this library, please refer to the [UI Components Playb
|
||||
| **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols), [`DeesActionbar`](#deesactionbar) |
|
||||
| **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputToggle`](#deesinputtoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesInputCode`](#deesinputcode), [`DeesFormSubmit`](#deesformsubmit) |
|
||||
| **App Shell (Layout)** | [`DeesAppui`](#deesappui-️), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiBottombar`](#deesappuibottombar), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
|
||||
| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) |
|
||||
| **Media & Tiles** | [`DeesTilePdf`](#deestilepdf), [`DeesTileImage`](#deestileimage), [`DeesTileAudio`](#deestileaudio), [`DeesTileVideo`](#deestilevideo), [`DeesTileNote`](#deestilenote), [`DeesTileFolder`](#deestilefolder), [`DeesPreview`](#deespreview), [`DeesPdfViewer`](#deespdfviewer), [`DeesPdfPreview`](#deespdfpreview), [`DeesImageViewer`](#deesimageviewer), [`DeesAudioViewer`](#deesaudioviewer), [`DeesVideoViewer`](#deesvideoviewer) |
|
||||
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
|
||||
| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination), [`DeesStorageBrowser`](#deesstorgebrowser) |
|
||||
| **Media & Thumbnails** | [`DeesThumbnailPdf`](#deesthumbnailpdf), [`DeesThumbnailImage`](#deesthumbnailimage), [`DeesThumbnailAudio`](#deesthumbnailaudio), [`DeesThumbnailVideo`](#deesthumbnalvideo), [`DeesThumbnailNote`](#deesthumbnailnote), [`DeesThumbnailFolder`](#deesthumbnailfolder), [`DeesPreview`](#deespreview), [`DeesPdfViewer`](#deespdfviewer), [`DeesImageViewer`](#deesimageviewer), [`DeesAudioViewer`](#deesaudioviewer), [`DeesVideoViewer`](#deesvideoviewer) |
|
||||
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartBar`](#deeschartbar), [`DeesChartDonut`](#deeschartdonut), [`DeesChartGauge`](#deeschartgauge), [`DeesChartRadar`](#deeschartradar), [`DeesChartLog`](#deeschartlog) |
|
||||
| **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
|
||||
| **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
|
||||
| **Workspace / IDE** | [`DeesWorkspace`](#deesworkspace), [`DeesWorkspaceMonaco`](#deesworkspacemonaco), [`DeesWorkspaceDiffEditor`](#deesworkspacediffeditor), [`DeesWorkspaceFiletree`](#deesworkspacefiletree), [`DeesWorkspaceTerminal`](#deesworkspaceterminal), [`DeesWorkspaceTerminalPreview`](#deesworkspaceterminalpreview), [`DeesWorkspaceMarkdown`](#deesworkspacemarkdown), [`DeesWorkspaceMarkdownoutlet`](#deesworkspacemarkdownoutlet), [`DeesWorkspaceBottombar`](#deesworkspacebottombar) |
|
||||
| **Theming** | [`DeesTheme`](#deestheme) |
|
||||
| **Theming** | [`DeesTheme`](#deestheme), [`DeesUpdater`](#deesupdater) |
|
||||
| **Pre-built Templates** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) |
|
||||
| **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) |
|
||||
|
||||
@@ -143,14 +143,13 @@ Display icons from FontAwesome and Lucide icon libraries with library prefixes.
|
||||
```
|
||||
|
||||
#### `DeesLabel`
|
||||
Text label component with optional icon and status indicators.
|
||||
Text label component with optional required indicator and info tooltip. Used internally by all input components.
|
||||
|
||||
```typescript
|
||||
<dees-label
|
||||
text="Status" // Label text
|
||||
icon="info-circle" // Optional: icon name
|
||||
type="info" // Options: default, info, success, warning, error
|
||||
size="medium" // Options: small, medium, large
|
||||
.label=${'Email Address'} // Label text
|
||||
.required=${true} // Optional: shows red asterisk
|
||||
.infoText=${'We will never share your email'} // Optional: shows hover info icon with tooltip
|
||||
></dees-label>
|
||||
```
|
||||
|
||||
@@ -321,7 +320,7 @@ Container component for form elements with built-in validation and data handling
|
||||
```
|
||||
|
||||
#### `DeesInputText`
|
||||
Text input field with validation and formatting options.
|
||||
Text input field with validation, info tooltips, description text, and context menu (Cut/Copy/Paste/Select All).
|
||||
|
||||
```typescript
|
||||
<dees-input-text
|
||||
@@ -330,10 +329,20 @@ Text input field with validation and formatting options.
|
||||
value="initial@value.com" // Initial value
|
||||
required // Makes the field required
|
||||
disabled // Disables the input
|
||||
placeholder="Enter your email"
|
||||
.infoText=${'Hover icon tooltip text'} // Shows ⓘ icon on label with hover tooltip
|
||||
.description=${'Permanent help text below the input'} // Small text below the input
|
||||
.validationFunction=${(value) => { // Auto-validates on every keystroke
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailRegex.test(value)) {
|
||||
return { valid: true, message: 'Email is valid' };
|
||||
}
|
||||
return { valid: false, message: 'Please enter a valid email' };
|
||||
}}
|
||||
></dees-input-text>
|
||||
```
|
||||
|
||||
> 💡 **All input components** share these common properties from `DeesInputBase`: `key`, `label`, `required`, `disabled`, `infoText`, `description`, `layoutMode`, `labelPosition`.
|
||||
|
||||
#### `DeesInputCheckbox`
|
||||
Checkbox input component for boolean values.
|
||||
|
||||
@@ -1792,7 +1801,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
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).
|
||||
79
test/test.pdf-text-selection.chromium.ts
Normal file
79
test/test.pdf-text-selection.chromium.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
|
||||
import * as deesCatalog from '../ts_web/index.js';
|
||||
|
||||
tap.test('PDF viewer should render text layer', async () => {
|
||||
const viewer = await webhelpers.fixture(
|
||||
webhelpers.html`
|
||||
<dees-pdf-viewer
|
||||
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
|
||||
initialZoom="page-fit"
|
||||
style="height: 600px; width: 100%;"
|
||||
></dees-pdf-viewer>
|
||||
`
|
||||
) as deesCatalog.DeesPdfViewer;
|
||||
|
||||
// Wait for PDF to load and render
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
await viewer.updateComplete;
|
||||
|
||||
expect(viewer.totalPages).toBeGreaterThan(0);
|
||||
|
||||
const textLayer = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"]');
|
||||
expect(textLayer).toBeTruthy();
|
||||
|
||||
const textSpans = textLayer?.querySelectorAll('span');
|
||||
expect(textSpans?.length).toBeGreaterThan(0);
|
||||
console.log(`Text layer has ${textSpans?.length} spans`);
|
||||
});
|
||||
|
||||
tap.test('Text should be selectable', async () => {
|
||||
const viewer = await webhelpers.fixture(
|
||||
webhelpers.html`
|
||||
<dees-pdf-viewer
|
||||
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
|
||||
initialZoom="page-fit"
|
||||
style="height: 600px; width: 100%;"
|
||||
></dees-pdf-viewer>
|
||||
`
|
||||
) as deesCatalog.DeesPdfViewer;
|
||||
|
||||
// Wait for PDF to load and render
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
const textLayer = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"]');
|
||||
const firstSpan = textLayer?.querySelector('span') as HTMLElement;
|
||||
|
||||
if (firstSpan?.textContent) {
|
||||
const range = document.createRange();
|
||||
const textNode = firstSpan.firstChild;
|
||||
if (textNode) {
|
||||
range.setStart(textNode, 0);
|
||||
range.setEnd(textNode, Math.min(5, textNode.textContent?.length || 0));
|
||||
const selection = window.getSelection();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
expect(selection?.toString().length).toBeGreaterThan(0);
|
||||
console.log('Selected text:', selection?.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('endOfContent element exists for selection boundary', async () => {
|
||||
const viewer = await webhelpers.fixture(
|
||||
webhelpers.html`
|
||||
<dees-pdf-viewer
|
||||
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
|
||||
initialZoom="page-fit"
|
||||
style="height: 600px; width: 100%;"
|
||||
></dees-pdf-viewer>
|
||||
`
|
||||
) as deesCatalog.DeesPdfViewer;
|
||||
|
||||
// Wait for PDF to load and render
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
const endOfContent = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"] .endOfContent');
|
||||
expect(endOfContent).toBeTruthy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.41.0',
|
||||
version: '3.77.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export class DeesAppuiBar extends DeesElement {
|
||||
>
|
||||
${menuItem.iconName ? html`<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>` : ''}
|
||||
${menuItem.name}
|
||||
${hasSubmenu ? this.renderDropdown(menuItem.submenu, itemId, isActive) : ''}
|
||||
${hasSubmenu ? this.renderDropdown(menuItem.submenu!, itemId, isActive) : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -75,21 +75,21 @@ export const demoFunc = () => {
|
||||
// Set up status toggle
|
||||
const statusButtons = elementArg.querySelectorAll('.status-toggle dees-button');
|
||||
statusButtons[0].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'online' };
|
||||
appbar.user = { ...appbar.user!, status: 'online' };
|
||||
});
|
||||
statusButtons[1].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'busy' };
|
||||
appbar.user = { ...appbar.user!, status: 'busy' };
|
||||
});
|
||||
statusButtons[2].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'away' };
|
||||
appbar.user = { ...appbar.user!, status: 'away' };
|
||||
});
|
||||
statusButtons[3].addEventListener('click', () => {
|
||||
appbar.user = { ...appbar.user, status: 'offline' };
|
||||
appbar.user = { ...appbar.user!, status: 'offline' };
|
||||
});
|
||||
|
||||
// Set up window controls toggle
|
||||
const windowControlsButton = elementArg.querySelector('.window-controls-toggle dees-button');
|
||||
windowControlsButton.addEventListener('click', () => {
|
||||
windowControlsButton!.addEventListener('click', () => {
|
||||
appbar.showWindowControls = !appbar.showWindowControls;
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ export const appuiAppbarStyles = [
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: var(--appbar-height);
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
border-bottom: 1px solid var(--dees-color-border-default);
|
||||
background: var(--dees-color-bg-primary);
|
||||
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
||||
font-size: var(--appbar-font-size);
|
||||
display: grid;
|
||||
@@ -78,8 +78,8 @@ export const appuiAppbarStyles = [
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 4px;
|
||||
box-shadow: ${cssManager.bdTheme('0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.3)')};
|
||||
margin-top: 4px;
|
||||
@@ -112,7 +112,7 @@ export const appuiAppbarStyles = [
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
background: var(--dees-color-border-default);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export const appuiAppbarStyles = [
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
border: 2px solid var(--dees-color-bg-primary);
|
||||
}
|
||||
|
||||
.user-status.online {
|
||||
|
||||
@@ -52,10 +52,10 @@ export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
gap: 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 6%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
border-top: 1px solid var(--dees-color-border-default);
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.widget {
|
||||
@@ -70,8 +70,8 @@ export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
|
||||
}
|
||||
|
||||
.widget:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 12%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
.widget dees-icon {
|
||||
@@ -81,7 +81,7 @@ export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
|
||||
.widget-separator {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(0 0% 20%)')};
|
||||
background: var(--dees-color-border-strong);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
@@ -124,12 +124,12 @@ export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 12%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
.action-button.disabled {
|
||||
@@ -139,7 +139,7 @@ export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
|
||||
|
||||
.action-button.disabled:hover {
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -53,18 +53,23 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
@property({ type: Number })
|
||||
accessor tabsAutoHideThreshold: number = 0;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor tabActionsLeft: interfaces.ITabAction[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor tabActionsRight: interfaces.ITabAction[] = [];
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
background: var(--dees-color-bg-secondary);
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
@@ -106,6 +111,8 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
.tabStyle=${'horizontal'}
|
||||
.autoHide=${this.tabsAutoHide}
|
||||
.autoHideThreshold=${this.tabsAutoHideThreshold}
|
||||
.actionsLeft=${this.tabActionsLeft}
|
||||
.actionsRight=${this.tabActionsRight}
|
||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||
@tab-close=${(e: CustomEvent) => this.handleTabClose(e)}
|
||||
></dees-appui-tabs>
|
||||
@@ -157,7 +164,7 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
}
|
||||
// Tab selection is now handled by the dees-appui-tabs component
|
||||
// But we need to ensure the tabs component is ready
|
||||
const tabsComponent = this.shadowRoot.querySelector('dees-appui-tabs') as DeesAppuiTabs;
|
||||
const tabsComponent = this.shadowRoot!.querySelector('dees-appui-tabs') as DeesAppuiTabs;
|
||||
if (tabsComponent) {
|
||||
await tabsComponent.updateComplete;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
accessor tabs: interfaces.IMenuItem[] = [];
|
||||
|
||||
@property()
|
||||
accessor selectedTab: interfaces.IMenuItem;
|
||||
accessor selectedTab!: interfaces.IMenuItem;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
accessor collapsed: boolean = false;
|
||||
@@ -55,28 +55,27 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
--menu-width-expanded: 200px;
|
||||
--menu-width-collapsed: 56px;
|
||||
--tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||
--tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
||||
--tooltip-bg: var(--dees-color-tooltip-bg);
|
||||
--tooltip-fg: var(--dees-color-tooltip-fg);
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
color: ${cssManager.bdTheme('#666', '#ccc')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
z-index: ${zIndexLayers.fixed.appBar};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: var(--menu-width-expanded);
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
background: var(--dees-color-bg-secondary);
|
||||
user-select: none;
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
border-right: 1px solid var(--dees-color-border-subtle);
|
||||
font-family: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
@@ -94,23 +93,23 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#3f3f46')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-strong);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#737373', '#a1a1aa')};
|
||||
color: var(--dees-color-text-muted);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, background 0.15s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.collapse-toggle:hover {
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
:host(:hover) .collapse-toggle {
|
||||
@@ -128,14 +127,14 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
gap: 10px;
|
||||
height: 48px;
|
||||
padding: 0 14px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logoSection .logoIcon {
|
||||
font-size: 22px;
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
color: var(--dees-color-text-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -143,7 +142,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
color: var(--dees-color-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -178,12 +177,12 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
}
|
||||
|
||||
.menuSection::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.15)', 'rgba(255, 255, 255, 0.15)')};
|
||||
background: var(--dees-color-scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.menuSection::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.25)', 'rgba(255, 255, 255, 0.25)')};
|
||||
background: var(--dees-color-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Menu Group */
|
||||
@@ -200,7 +199,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
padding: 8px 12px 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#737373', '#737373')};
|
||||
color: var(--dees-color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
@@ -238,21 +237,21 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
color: ${cssManager.bdTheme('#525252', '#a3a3a3')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: ${cssManager.bdTheme('#262626', '#e5e5e5')};
|
||||
}
|
||||
|
||||
.tab:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
|
||||
background: var(--dees-color-active);
|
||||
}
|
||||
|
||||
.tab.selectedTab {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
background: var(--dees-color-active);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.tab.selectedTab::before {
|
||||
@@ -263,7 +262,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
background: var(--dees-color-text-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
@@ -353,23 +352,23 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
}
|
||||
|
||||
.badge.default {
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#a1a1aa')};
|
||||
background: var(--dees-color-badge-default-bg);
|
||||
color: var(--dees-color-badge-default-fg);
|
||||
}
|
||||
|
||||
.badge.success {
|
||||
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
||||
background: var(--dees-color-badge-success-bg);
|
||||
color: var(--dees-color-badge-success-fg);
|
||||
}
|
||||
|
||||
.badge.warning {
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#451a03')};
|
||||
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
|
||||
background: var(--dees-color-badge-warning-bg);
|
||||
color: var(--dees-color-badge-warning-fg);
|
||||
}
|
||||
|
||||
.badge.error {
|
||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||
background: var(--dees-color-badge-error-bg);
|
||||
color: var(--dees-color-badge-error-fg);
|
||||
}
|
||||
|
||||
:host([collapsed]) .badge {
|
||||
@@ -380,7 +379,7 @@ export class DeesAppuiMainmenu extends DeesElement {
|
||||
.bottomSection {
|
||||
flex-shrink: 0;
|
||||
padding: 8px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
border-top: 1px solid var(--dees-color-border-subtle);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
@@ -73,30 +73,29 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
--sidebar-width-expanded: 240px;
|
||||
--sidebar-width-collapsed: 56px;
|
||||
--sidebar-bg: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
--sidebar-fg: ${cssManager.bdTheme('#525252', '#a3a3a3')};
|
||||
--sidebar-fg-muted: ${cssManager.bdTheme('#737373', '#737373')};
|
||||
--sidebar-fg-active: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
--sidebar-border: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
--sidebar-hover: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
|
||||
--sidebar-active: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
|
||||
--sidebar-accent: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
--tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')};
|
||||
--tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')};
|
||||
--sidebar-bg: var(--dees-color-bg-secondary);
|
||||
--sidebar-fg: var(--dees-color-text-secondary);
|
||||
--sidebar-fg-muted: var(--dees-color-text-muted);
|
||||
--sidebar-fg-active: var(--dees-color-text-primary);
|
||||
--sidebar-border: var(--dees-color-border-subtle);
|
||||
--sidebar-hover: var(--dees-color-hover);
|
||||
--sidebar-active: var(--dees-color-active);
|
||||
--sidebar-accent: var(--dees-color-text-primary);
|
||||
--tooltip-bg: var(--dees-color-tooltip-bg);
|
||||
--tooltip-fg: var(--dees-color-tooltip-fg);
|
||||
|
||||
/* Badge colors */
|
||||
--badge-default-bg: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
--badge-default-fg: ${cssManager.bdTheme('#3f3f46', '#a1a1aa')};
|
||||
--badge-success-bg: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||
--badge-success-fg: ${cssManager.bdTheme('#166534', '#4ade80')};
|
||||
--badge-warning-bg: ${cssManager.bdTheme('#fef3c7', '#451a03')};
|
||||
--badge-warning-fg: ${cssManager.bdTheme('#92400e', '#fbbf24')};
|
||||
--badge-error-bg: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
--badge-error-fg: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||
--badge-default-bg: var(--dees-color-badge-default-bg);
|
||||
--badge-default-fg: var(--dees-color-badge-default-fg);
|
||||
--badge-success-bg: var(--dees-color-badge-success-bg);
|
||||
--badge-success-fg: var(--dees-color-badge-success-fg);
|
||||
--badge-warning-bg: var(--dees-color-badge-warning-bg);
|
||||
--badge-warning-fg: var(--dees-color-badge-warning-fg);
|
||||
--badge-error-bg: var(--dees-color-badge-error-bg);
|
||||
--badge-error-fg: var(--dees-color-badge-error-fg);
|
||||
|
||||
/* Action colors */
|
||||
--action-primary: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
|
||||
@@ -136,23 +135,23 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#3f3f46')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-strong);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('#737373', '#a1a1aa')};
|
||||
color: var(--dees-color-text-muted);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, background 0.15s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.collapse-toggle:hover {
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
:host(:hover) .collapse-toggle {
|
||||
@@ -215,12 +214,12 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
}
|
||||
|
||||
.menuSection::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.15)', 'rgba(255, 255, 255, 0.15)')};
|
||||
background: var(--dees-color-scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.menuSection::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.25)', 'rgba(255, 255, 255, 0.25)')};
|
||||
background: var(--dees-color-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Menu Group */
|
||||
@@ -261,7 +260,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
color: var(--dees-color-text-warm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
@@ -270,13 +269,13 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
|
||||
.groupHeader .groupTitle dees-icon {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
color: var(--dees-color-text-warm);
|
||||
}
|
||||
|
||||
.groupHeader .chevron {
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
color: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
color: var(--dees-color-text-warm);
|
||||
}
|
||||
|
||||
.groupHeader.collapsed .chevron {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { html, cssManager, css, DeesElement, customElement, state } from '@desig
|
||||
import * as interfaces from '../../interfaces/index.js';
|
||||
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
|
||||
|
||||
// Interactive demo component for closeable tabs
|
||||
// Interactive demo component for closeable tabs with action buttons
|
||||
@customElement('demo-closeable-tabs')
|
||||
class DemoCloseableTabs extends DeesElement {
|
||||
@state()
|
||||
@@ -18,24 +18,6 @@ class DemoCloseableTabs extends DeesElement {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
button {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.3)', 'rgba(59, 130, 246, 0.3)')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
button:hover {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
|
||||
}
|
||||
.info {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
@@ -66,17 +48,27 @@ class DemoCloseableTabs extends DeesElement {
|
||||
this.tabs = this.tabs.filter(t => t.key !== tabKey);
|
||||
}
|
||||
|
||||
private clearAll() {
|
||||
const tabsEl = this.shadowRoot!.querySelector('dees-appui-tabs') as DeesAppuiTabs;
|
||||
tabsEl?.clear();
|
||||
this.tabs = [];
|
||||
this.tabCounter = 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
const rightActions: interfaces.ITabAction[] = [
|
||||
{ id: 'add', iconName: 'lucide:plus', action: () => this.addTab(), tooltip: 'New Tab' },
|
||||
{ id: 'clear', iconName: 'lucide:trash2', action: () => this.clearAll(), tooltip: 'Clear All Tabs' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-appui-tabs
|
||||
.tabs=${this.tabs}
|
||||
.actionsRight=${rightActions}
|
||||
@tab-close=${(e: CustomEvent) => this.removeTab(e.detail.tab.key)}
|
||||
></dees-appui-tabs>
|
||||
<div class="controls">
|
||||
<button @click=${() => this.addTab()}>+ Add New Tab</button>
|
||||
</div>
|
||||
<div class="info">
|
||||
Click the X button on tabs to close them. The "Main" tab is not closeable.
|
||||
Click the X button on tabs to close them. Use the + button to add tabs and the trash button to clear all.
|
||||
<br>Current tabs: ${this.tabs.length}
|
||||
</div>
|
||||
`;
|
||||
@@ -232,6 +224,16 @@ export const demoFunc = () => {
|
||||
{ key: 'Archived', action: () => console.log('Archived clicked') },
|
||||
];
|
||||
|
||||
const actionsLeft: interfaces.ITabAction[] = [
|
||||
{ id: 'back', iconName: 'lucide:arrowLeft', action: () => console.log('Back'), tooltip: 'Go Back' },
|
||||
];
|
||||
|
||||
const actionsRight: interfaces.ITabAction[] = [
|
||||
{ id: 'add', iconName: 'lucide:plus', action: () => console.log('Add tab'), tooltip: 'New Tab' },
|
||||
{ id: 'search', iconName: 'lucide:search', action: () => console.log('Search'), tooltip: 'Search Tabs' },
|
||||
{ id: 'disabled', iconName: 'lucide:lock', action: () => {}, tooltip: 'Disabled Action', disabled: true },
|
||||
];
|
||||
|
||||
const demoContent = (text: string) => html`
|
||||
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
|
||||
${text}
|
||||
@@ -279,7 +281,17 @@ export const demoFunc = () => {
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Closeable Tabs (Browser-style)</div>
|
||||
<div class="section-title">Tabs with Action Buttons</div>
|
||||
<dees-appui-tabs
|
||||
.tabs=${horizontalTabs}
|
||||
.actionsLeft=${actionsLeft}
|
||||
.actionsRight=${actionsRight}
|
||||
></dees-appui-tabs>
|
||||
${demoContent('Action buttons can be placed on either side of the tab bar. They remain fixed while tabs scroll. The lock icon shows a disabled action.')}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Closeable Tabs with Actions</div>
|
||||
<demo-closeable-tabs></demo-closeable-tabs>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
@property({ type: Number })
|
||||
accessor autoHideThreshold: number = 0;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor actionsLeft: interfaces.ITabAction[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor actionsRight: interfaces.ITabAction[] = [];
|
||||
|
||||
// Scroll state for fade indicators
|
||||
@state()
|
||||
private accessor canScrollLeft: boolean = false;
|
||||
@@ -54,7 +60,6 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
@@ -70,9 +75,11 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
|
||||
.tabs-wrapper.horizontal-wrapper {
|
||||
height: 48px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-bottom: 1px solid var(--dees-color-border-default);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Scroll fade indicators */
|
||||
@@ -105,6 +112,72 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Tab action buttons */
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.tab-actions.left {
|
||||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
.tab-actions.right {
|
||||
padding-right: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
.tab-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
background: transparent;
|
||||
color: var(--dees-color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-action-button:hover {
|
||||
background: var(--dees-color-active);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.tab-action-button:active {
|
||||
background: var(--dees-color-pressed);
|
||||
}
|
||||
|
||||
.tab-action-button.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tab-action-button.disabled:hover {
|
||||
background: transparent;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.tab-action-button dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
@@ -121,12 +194,14 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Show scrollbar on hover */
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal {
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal,
|
||||
.scroll-area:hover .tabsContainer.horizontal {
|
||||
scrollbar-color: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')} transparent;
|
||||
}
|
||||
|
||||
@@ -144,11 +219,13 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb,
|
||||
.scroll-area:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')};
|
||||
}
|
||||
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover,
|
||||
.scroll-area:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0,0,0,0.35)', 'rgba(255,255,255,0.35)')};
|
||||
}
|
||||
|
||||
@@ -159,12 +236,12 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
font-size: 14px;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
color: var(--dees-color-text-muted);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
@@ -192,7 +269,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
transform: translateY(-50%);
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
background: var(--dees-color-border-default);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -213,11 +290,11 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.horizontal .tab:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.03)')};
|
||||
background: var(--dees-color-hover);
|
||||
}
|
||||
|
||||
.horizontal .tab:hover::after,
|
||||
@@ -230,7 +307,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
.horizontal .tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.horizontal .tab.selectedTab::after,
|
||||
@@ -239,7 +316,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
.vertical .tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.tab dees-icon {
|
||||
@@ -259,7 +336,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
.tabs-wrapper .tabIndicator {
|
||||
height: 3px;
|
||||
bottom: 0;
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
background: var(--dees-color-accent-primary);
|
||||
border-radius: 3px 3px 0 0;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -272,7 +349,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
z-index: 1;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
@@ -288,7 +365,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
margin-left: 8px;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.tab:hover .tab-close {
|
||||
@@ -297,8 +374,8 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
background: ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
|
||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
||||
background: var(--dees-color-pressed);
|
||||
color: var(--dees-color-accent-error);
|
||||
}
|
||||
|
||||
.tab.selectedTab .tab-close {
|
||||
@@ -331,13 +408,20 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
const containerClass = `tabsContainer ${this.tabStyle}`;
|
||||
|
||||
if (isHorizontal) {
|
||||
const hasLeftActions = this.actionsLeft && this.actionsLeft.length > 0;
|
||||
const hasRightActions = this.actionsRight && this.actionsRight.length > 0;
|
||||
|
||||
return html`
|
||||
<div class="${wrapperClass}">
|
||||
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
|
||||
<div class="${containerClass}" @scroll=${this.handleScroll}>
|
||||
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
|
||||
${hasLeftActions ? this.renderActions(this.actionsLeft, 'left') : ''}
|
||||
<div class="scroll-area">
|
||||
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
|
||||
<div class="${containerClass}" @scroll=${this.handleScroll}>
|
||||
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
|
||||
</div>
|
||||
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
|
||||
</div>
|
||||
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
|
||||
${hasRightActions ? this.renderActions(this.actionsRight, 'right') : ''}
|
||||
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
@@ -353,6 +437,22 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActions(actions: interfaces.ITabAction[], position: 'left' | 'right'): TemplateResult {
|
||||
return html`
|
||||
<div class="tab-actions ${position}">
|
||||
${actions.map(action => html`
|
||||
<div
|
||||
class="tab-action-button ${action.disabled ? 'disabled' : ''}"
|
||||
title="${action.tooltip || action.id}"
|
||||
@click=${() => !action.disabled && action.action()}
|
||||
>
|
||||
<dees-icon .icon=${action.iconName}></dees-icon>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
|
||||
const isSelected = tab === this.selectedTab;
|
||||
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
|
||||
@@ -406,6 +506,14 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tabs and reset selection.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.tabs = [];
|
||||
this.selectedTab = null;
|
||||
}
|
||||
|
||||
private closeTab(e: Event, tab: interfaces.IMenuItem) {
|
||||
e.stopPropagation(); // Don't select tab when closing
|
||||
|
||||
@@ -423,14 +531,9 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.tabs && this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
|
||||
// Set up ResizeObserver for scroll state updates
|
||||
// Tab selection is handled by updated() lifecycle
|
||||
this.setupResizeObserver();
|
||||
|
||||
// Initial scroll state check
|
||||
requestAnimationFrame(() => {
|
||||
this.updateScrollState();
|
||||
});
|
||||
@@ -503,8 +606,24 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
if (changedProperties.has('tabs')) {
|
||||
if (!this.tabs || this.tabs.length === 0) {
|
||||
// Tabs are empty => reset selection
|
||||
if (this.selectedTab !== null) {
|
||||
this.selectedTab = null;
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: { tab: null },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
} else if (this.selectedTab && !this.tabs.includes(this.selectedTab)) {
|
||||
// Selected tab was removed => select first available
|
||||
this.selectTab(this.tabs[0]);
|
||||
} else if (!this.selectedTab) {
|
||||
// Tabs exist but nothing selected => select first
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
||||
@@ -543,21 +662,21 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
private shouldShowIndicator(): boolean {
|
||||
return this.selectedTab && this.showTabIndicator && this.tabs.includes(this.selectedTab);
|
||||
return !!this.selectedTab && this.showTabIndicator && this.tabs.includes(this.selectedTab);
|
||||
}
|
||||
|
||||
private getSelectedTabElement(): HTMLElement | null {
|
||||
const selectedIndex = this.tabs.indexOf(this.selectedTab);
|
||||
const selectedIndex = this.tabs.indexOf(this.selectedTab!);
|
||||
const isHorizontal = this.tabStyle === 'horizontal';
|
||||
const selector = isHorizontal
|
||||
const selector = isHorizontal
|
||||
? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`
|
||||
: `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`;
|
||||
|
||||
return this.shadowRoot.querySelector(selector);
|
||||
|
||||
return this.shadowRoot!.querySelector(selector);
|
||||
}
|
||||
|
||||
private getIndicatorElement(): HTMLElement | null {
|
||||
return this.shadowRoot.querySelector('.tabIndicator');
|
||||
return this.shadowRoot!.querySelector('.tabIndicator');
|
||||
}
|
||||
|
||||
private handleInitialTransition(indicator: HTMLElement): void {
|
||||
@@ -575,7 +694,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
const tabContent = tabElement.querySelector('.tab-content') as HTMLElement;
|
||||
if (!tabContent) return;
|
||||
|
||||
const wrapperRect = indicator.parentElement.getBoundingClientRect();
|
||||
const wrapperRect = indicator.parentElement!.getBoundingClientRect();
|
||||
const contentRect = tabContent.getBoundingClientRect();
|
||||
|
||||
const contentLeft = contentRect.left - wrapperRect.left;
|
||||
@@ -587,7 +706,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
private updateVerticalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void {
|
||||
const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement;
|
||||
const tabsContainer = this.shadowRoot!.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement;
|
||||
if (!tabsContainer) return;
|
||||
|
||||
indicator.style.top = `${tabElement.offsetTop + tabsContainer.offsetTop}px`;
|
||||
|
||||
@@ -12,7 +12,7 @@ class DemoDashboardView extends DeesElement {
|
||||
@state()
|
||||
accessor activated: boolean = false;
|
||||
|
||||
private ctx: IViewActivationContext;
|
||||
private ctx!: IViewActivationContext;
|
||||
|
||||
private statsTiles: IStatsTile[] = [
|
||||
{
|
||||
@@ -267,7 +267,7 @@ class DemoSettingsView extends DeesElement {
|
||||
@state()
|
||||
accessor hasChanges: boolean = false;
|
||||
|
||||
private appui: DeesAppui;
|
||||
private appui!: DeesAppui;
|
||||
|
||||
onActivate(context: IViewActivationContext) {
|
||||
this.appui = context.appui as any;
|
||||
|
||||
@@ -143,6 +143,12 @@ export class DeesAppui extends DeesElement {
|
||||
@property({ type: Object })
|
||||
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor contentTabActionsLeft: interfaces.ITabAction[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor contentTabActionsRight: interfaces.ITabAction[] = [];
|
||||
|
||||
// References to child components
|
||||
@state()
|
||||
accessor appbar: DeesAppuiBar | undefined = undefined;
|
||||
@@ -178,12 +184,11 @@ export class DeesAppui extends DeesElement {
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
}
|
||||
.maingrid {
|
||||
position: absolute;
|
||||
@@ -213,11 +218,13 @@ export class DeesAppui extends DeesElement {
|
||||
.maingrid > dees-appui-mainmenu {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.maingrid > dees-appui-secondarymenu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.maingrid > dees-appui-maincontent {
|
||||
@@ -306,6 +313,8 @@ export class DeesAppui extends DeesElement {
|
||||
.showTabs=${this.maincontentTabsVisible}
|
||||
.tabsAutoHide=${this.contentTabsAutoHide}
|
||||
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
|
||||
.tabActionsLeft=${this.contentTabActionsLeft}
|
||||
.tabActionsRight=${this.contentTabActionsRight}
|
||||
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
||||
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
|
||||
>
|
||||
@@ -699,6 +708,20 @@ export class DeesAppui extends DeesElement {
|
||||
return this.maincontentSelectedTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content tab action buttons on the left side
|
||||
*/
|
||||
public setContentTabActionsLeft(actions: interfaces.ITabAction[]): void {
|
||||
this.contentTabActionsLeft = [...actions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content tab action buttons on the right side
|
||||
*/
|
||||
public setContentTabActionsRight(actions: interfaces.ITabAction[]): void {
|
||||
this.contentTabActionsRight = [...actions];
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PROGRAMMATIC API: ACTIVITY LOG
|
||||
// ==========================================
|
||||
@@ -853,8 +876,13 @@ export class DeesAppui extends DeesElement {
|
||||
try {
|
||||
await this.loadView(view, params);
|
||||
|
||||
// Update URL hash
|
||||
const route = view.route || viewId;
|
||||
// Update URL hash (substitute params into route pattern)
|
||||
let route = view.route || viewId;
|
||||
if (params) {
|
||||
for (const [key, val] of Object.entries(params)) {
|
||||
route = route.replace(`:${key}`, val);
|
||||
}
|
||||
}
|
||||
const newHash = `#${route}`;
|
||||
if (window.location.hash !== newHash) {
|
||||
window.history.pushState({ viewId }, '', newHash);
|
||||
@@ -1008,7 +1036,7 @@ export class DeesAppui extends DeesElement {
|
||||
if (!config.mainMenu?.sections) return [];
|
||||
|
||||
return config.mainMenu.sections.map((section) => ({
|
||||
name: section.name,
|
||||
name: section.name || '',
|
||||
items: section.views
|
||||
.map((viewId) => {
|
||||
const view = this.viewRegistry.get(viewId);
|
||||
|
||||
@@ -173,7 +173,7 @@ export class ViewRegistry {
|
||||
}
|
||||
|
||||
// Check for cached instance
|
||||
let element = shouldCache ? this.instances.get(viewId) : undefined;
|
||||
let element: HTMLElement | null | undefined = shouldCache ? this.instances.get(viewId) : undefined;
|
||||
|
||||
if (element) {
|
||||
// Reuse cached instance - just show it
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { html, css } from '@design.estate/dees-element';
|
||||
import '../00group-button/dees-button/dees-button.js';
|
||||
import '../../00group-button/dees-button/dees-button.js';
|
||||
import '../../00group-layout/dees-panel/dees-panel.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
|
||||
@@ -301,15 +301,15 @@ export class DeesMobilenavigation extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private windowLayer: DeesWindowLayer;
|
||||
private windowLayer!: DeesWindowLayer;
|
||||
|
||||
/**
|
||||
* inits the show
|
||||
*/
|
||||
public async show() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const main = this.shadowRoot.querySelector('.main');
|
||||
|
||||
const main = this.shadowRoot!.querySelector('.main');
|
||||
|
||||
// Create window layer first (it will get its own z-index)
|
||||
if (!this.windowLayer) {
|
||||
this.windowLayer = await DeesWindowLayer.createAndShow({
|
||||
@@ -328,7 +328,7 @@ export class DeesMobilenavigation extends DeesElement {
|
||||
zIndexRegistry.register(this, this.mobileNavZIndex);
|
||||
|
||||
await domtools.convenience.smartdelay.delayFor(10);
|
||||
main.classList.add('show');
|
||||
main!.classList.add('show');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,8 +336,8 @@ export class DeesMobilenavigation extends DeesElement {
|
||||
*/
|
||||
public async hide() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const main = this.shadowRoot.querySelector('.main');
|
||||
main.classList.remove('show');
|
||||
const main = this.shadowRoot!.querySelector('.main');
|
||||
main!.classList.remove('show');
|
||||
|
||||
// Unregister from z-index registry
|
||||
zIndexRegistry.unregister(this);
|
||||
|
||||
@@ -288,8 +288,8 @@ export const demoFunc = () => html`
|
||||
}
|
||||
|
||||
if (dataBtn && output) {
|
||||
dataBtn.addEventListener('clicked', (e: CustomEvent) => {
|
||||
output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`;
|
||||
dataBtn.addEventListener('clicked', (e: Event) => {
|
||||
output.textContent = `Clicked: Secondary button with data: ${(e as CustomEvent).detail.data}`;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -322,9 +322,9 @@ export const demoFunc = () => html`
|
||||
const output = elementArg.querySelector('#form-output');
|
||||
|
||||
if (form && output) {
|
||||
form.addEventListener('formData', (e: CustomEvent) => {
|
||||
output.innerHTML = '<strong>Form submitted with data:</strong><br>' +
|
||||
JSON.stringify(e.detail.data, null, 2);
|
||||
form.addEventListener('formData', (e: Event) => {
|
||||
output.innerHTML = '<strong>Form submitted with data:</strong><br>' +
|
||||
JSON.stringify((e as CustomEvent).detail.data, null, 2);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ export class DeesButton extends DeesElement {
|
||||
return true;
|
||||
}
|
||||
})
|
||||
accessor text: string;
|
||||
accessor text!: string;
|
||||
|
||||
@property()
|
||||
accessor eventDetailData: string;
|
||||
accessor eventDetailData!: string;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
@@ -69,7 +69,7 @@ export class DeesButton extends DeesElement {
|
||||
accessor insideForm: boolean = false;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
accessor icon: string;
|
||||
accessor icon!: string;
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
accessor iconPosition: 'left' | 'right' = 'left';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,8 +44,8 @@ export const demoFunc = () => {
|
||||
// Get the chart elements
|
||||
const chartElement = elementArg.querySelector('#main-chart') as DeesChartArea;
|
||||
const connectionsChartElement = elementArg.querySelector('#connections-chart') as DeesChartArea;
|
||||
let intervalId: number;
|
||||
let connectionsIntervalId: number;
|
||||
let intervalId: number | null;
|
||||
let connectionsIntervalId: number | null;
|
||||
let currentDataset = 'system';
|
||||
|
||||
// Y-axis formatters for different datasets
|
||||
@@ -71,7 +71,7 @@ export const demoFunc = () => {
|
||||
|
||||
// Generate initial data points for time window
|
||||
const generateInitialData = (baseValue: number, variance: number, interval: number = DATA_POINT_INTERVAL) => {
|
||||
const data = [];
|
||||
const data: Array<{ x: string; y: number }> = [];
|
||||
const now = Date.now();
|
||||
const pointCount = Math.floor(TIME_WINDOW / interval);
|
||||
|
||||
@@ -240,10 +240,10 @@ export const demoFunc = () => {
|
||||
// Switch dataset
|
||||
const switchDataset = (name: string) => {
|
||||
currentDataset = name;
|
||||
const dataset = datasets[name];
|
||||
const dataset = (datasets as Record<string, any>)[name];
|
||||
chartElement.label = dataset.label;
|
||||
chartElement.series = dataset.series;
|
||||
chartElement.yAxisFormatter = formatters[name];
|
||||
chartElement.yAxisFormatter = (formatters as Record<string, any>)[name];
|
||||
|
||||
// Set appropriate y-axis scaling
|
||||
if (name === 'system') {
|
||||
@@ -310,30 +310,11 @@ export const demoFunc = () => {
|
||||
connectionsLastUpdate = 0;
|
||||
};
|
||||
|
||||
// Wire up button click handlers
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.addEventListener('click', () => switchDataset('system'));
|
||||
} else if (text === 'Network Traffic') {
|
||||
button.addEventListener('click', () => switchDataset('network'));
|
||||
} else if (text === 'Sales Data') {
|
||||
button.addEventListener('click', () => switchDataset('sales'));
|
||||
} else if (text === 'Start Live') {
|
||||
button.addEventListener('click', () => startRealtime());
|
||||
} else if (text === 'Stop Live') {
|
||||
button.addEventListener('click', () => stopRealtime());
|
||||
} else if (text === 'Spike Values') {
|
||||
button.addEventListener('click', () => randomizeData());
|
||||
}
|
||||
});
|
||||
|
||||
// Update button states based on current dataset
|
||||
const updateButtonStates = () => {
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
const allButtons = elementArg.querySelectorAll('dees-button');
|
||||
allButtons.forEach((button: any) => {
|
||||
const text = button.text?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.type = currentDataset === 'system' ? 'highlighted' : 'normal';
|
||||
} else if (text === 'Network Traffic') {
|
||||
@@ -343,41 +324,38 @@ export const demoFunc = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Wire up button click handlers
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach((button: any) => {
|
||||
const text = button.text?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.addEventListener('click', () => { switchDataset('system'); updateButtonStates(); });
|
||||
} else if (text === 'Network Traffic') {
|
||||
button.addEventListener('click', () => { switchDataset('network'); updateButtonStates(); });
|
||||
} else if (text === 'Sales Data') {
|
||||
button.addEventListener('click', () => { switchDataset('sales'); updateButtonStates(); });
|
||||
} else if (text === 'Start Live') {
|
||||
button.addEventListener('click', () => startRealtime());
|
||||
} else if (text === 'Stop Live') {
|
||||
button.addEventListener('click', () => stopRealtime());
|
||||
} else if (text === 'Spike Values') {
|
||||
button.addEventListener('click', () => randomizeData());
|
||||
}
|
||||
});
|
||||
|
||||
// Configure main chart with rolling window
|
||||
chartElement.rollingWindow = TIME_WINDOW;
|
||||
chartElement.realtimeMode = false; // Will be enabled when starting live updates
|
||||
chartElement.yAxisScaling = 'percentage'; // Initial system dataset uses percentage
|
||||
chartElement.yAxisMax = 100;
|
||||
chartElement.autoScrollInterval = 1000; // Auto-scroll every second
|
||||
|
||||
|
||||
// Set initial time window
|
||||
setTimeout(() => {
|
||||
chartElement.updateTimeWindow();
|
||||
}, 100);
|
||||
|
||||
// Update button states when dataset changes
|
||||
const originalSwitchDataset = switchDataset;
|
||||
const switchDatasetWithButtonUpdate = (name: string) => {
|
||||
originalSwitchDataset(name);
|
||||
updateButtonStates();
|
||||
};
|
||||
|
||||
// Replace switchDataset with the one that updates buttons
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
if (text === 'System Usage') {
|
||||
button.removeEventListener('click', () => switchDataset('system'));
|
||||
button.addEventListener('click', () => switchDatasetWithButtonUpdate('system'));
|
||||
} else if (text === 'Network Traffic') {
|
||||
button.removeEventListener('click', () => switchDataset('network'));
|
||||
button.addEventListener('click', () => switchDatasetWithButtonUpdate('network'));
|
||||
} else if (text === 'Sales Data') {
|
||||
button.removeEventListener('click', () => switchDataset('sales'));
|
||||
button.addEventListener('click', () => switchDatasetWithButtonUpdate('sales'));
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize connections chart with data
|
||||
if (connectionsChartElement) {
|
||||
const initialConnectionsData = generateInitialData(previousValues.connections, 30, UPDATE_INTERVAL);
|
||||
|
||||
@@ -1,60 +1,103 @@
|
||||
import { css, cssManager } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
export const chartAreaStyles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 400px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-weight: 400;
|
||||
color: var(--dees-color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
dees-tile {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 16px 24px;
|
||||
z-index: 10;
|
||||
.chartHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 8px 0 16px;
|
||||
}
|
||||
.chartLabel {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
.expandBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--dees-color-text-muted);
|
||||
transition: all 0.15s ease;
|
||||
padding: 0;
|
||||
}
|
||||
.expandBtn:hover {
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
.chartContainer {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
padding: 44px 16px 16px 0px;
|
||||
overflow: hidden;
|
||||
background: transparent; /* Ensure container doesn't override chart background */
|
||||
inset: 0 0 4px 0;
|
||||
}
|
||||
|
||||
/* ApexCharts theme overrides */
|
||||
.apexcharts-canvas {
|
||||
background: transparent !important;
|
||||
.statsBar {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.apexcharts-inner {
|
||||
background: transparent !important;
|
||||
.statsSeries {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.apexcharts-graphical {
|
||||
background: transparent !important;
|
||||
.statsSeries + .statsSeries {
|
||||
padding-left: 24px;
|
||||
border-left: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
.statsColor {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.statsName {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-secondary);
|
||||
margin-right: 4px;
|
||||
}
|
||||
.statsItem {
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
.statsItem strong {
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
.lw-tooltip {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
|
||||
min-width: 140px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesChartArea } from './component.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
|
||||
export const renderChartArea = (component: DeesChartArea): TemplateResult => {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="chartTitle">${component.label}</div>
|
||||
<dees-tile>
|
||||
<div slot="header" class="chartHeader">
|
||||
<span class="chartLabel">${component.label}</span>
|
||||
<button class="expandBtn" @click=${() => component.toggleFullPage()} title="${component.isFullPage ? 'Exit full page' : 'Full page'}">
|
||||
<dees-icon .icon=${component.isFullPage ? 'lucide:Minimize2' : 'lucide:Maximize2'} .iconSize=${14}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chartContainer"></div>
|
||||
</div>
|
||||
${component.seriesStats.length > 0 ? html`
|
||||
<div slot="footer" class="statsBar">
|
||||
${component.seriesStats.map(s => html`
|
||||
<div class="statsSeries">
|
||||
<span class="statsColor" style="background:${s.color}"></span>
|
||||
<span class="statsName">${s.name}</span>
|
||||
<span class="statsItem">latest <strong>${component.yAxisFormatter(s.latest)}</strong></span>
|
||||
<span class="statsItem">min <strong>${component.yAxisFormatter(s.min)}</strong></span>
|
||||
<span class="statsItem">max <strong>${component.yAxisFormatter(s.max)}</strong></span>
|
||||
<span class="statsItem">avg <strong>${component.yAxisFormatter(s.avg)}</strong></span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</dees-tile>
|
||||
`;
|
||||
|
||||
};
|
||||
|
||||
158
ts_web/elements/00group-chart/dees-chart-bar/component.ts
Normal file
158
ts_web/elements/00group-chart/dees-chart-bar/component.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
||||
import { demoFunc } from './demo.js';
|
||||
import { barStyles } from './styles.js';
|
||||
import { renderChartBar } from './template.js';
|
||||
import { getEchartsSeriesColors, getThemeColors, hexToRgba } from '../dees-chart-echarts-theme.js';
|
||||
|
||||
export interface IBarSeriesItem {
|
||||
name: string;
|
||||
data: number[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-chart-bar': DeesChartBar;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-chart-bar')
|
||||
export class DeesChartBar extends DeesChartEchartsBase {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Chart'];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor categories: string[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor series: IBarSeriesItem[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor horizontal: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor stacked: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showLegend: boolean = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor valueFormatter: (value: number) => string = (val) => `${val}`;
|
||||
|
||||
public static styles = barStyles;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return renderChartBar(this);
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
this.chartInstance &&
|
||||
(changedProperties.has('categories') ||
|
||||
changedProperties.has('series') ||
|
||||
changedProperties.has('horizontal') ||
|
||||
changedProperties.has('stacked') ||
|
||||
changedProperties.has('showLegend'))
|
||||
) {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected buildOption(): Record<string, any> {
|
||||
const colors = getThemeColors(this.goBright);
|
||||
const seriesColors = getEchartsSeriesColors(this.goBright);
|
||||
const formatter = this.valueFormatter;
|
||||
|
||||
const categoryAxis: Record<string, any> = {
|
||||
type: 'category',
|
||||
data: this.categories,
|
||||
axisLine: { lineStyle: { color: colors.borderStrong } },
|
||||
axisLabel: { color: colors.textMuted },
|
||||
};
|
||||
|
||||
const valueAxis: Record<string, any> = {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisLabel: {
|
||||
color: colors.textMuted,
|
||||
formatter: (val: number) => formatter(val),
|
||||
},
|
||||
splitLine: { lineStyle: { color: colors.borderSubtle } },
|
||||
};
|
||||
|
||||
const fillAlpha = this.goBright ? 0.15 : 0.25;
|
||||
const borderRadius = this.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0];
|
||||
const noBorderRadius = [0, 0, 0, 0];
|
||||
|
||||
const legendData: Array<{ name: string; itemStyle: { color: string } }> = [];
|
||||
|
||||
const seriesData = this.series.map((s, index) => {
|
||||
const color = s.color || seriesColors[index % seriesColors.length];
|
||||
legendData.push({ name: s.name, itemStyle: { color } });
|
||||
return {
|
||||
name: s.name,
|
||||
type: 'bar' as const,
|
||||
data: s.data,
|
||||
stack: this.stacked ? 'total' : undefined,
|
||||
itemStyle: {
|
||||
color: hexToRgba(color, fillAlpha),
|
||||
borderColor: color,
|
||||
borderWidth: 1,
|
||||
borderRadius: this.stacked ? noBorderRadius : borderRadius,
|
||||
},
|
||||
barMaxWidth: 40,
|
||||
barGap: '20%',
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: hexToRgba(color, fillAlpha + 0.15),
|
||||
borderColor: color,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// For stacked bars, round the top corners of the last visible series
|
||||
if (this.stacked && seriesData.length > 0) {
|
||||
const last = seriesData[seriesData.length - 1];
|
||||
last.itemStyle.borderRadius = borderRadius;
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: (params: any) => {
|
||||
const items = Array.isArray(params) ? params : [params];
|
||||
let result = `<strong>${items[0].axisValueLabel}</strong><br/>`;
|
||||
for (const p of items) {
|
||||
const solidColor = p.borderColor || p.color;
|
||||
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${solidColor};"></span>`;
|
||||
result += `${marker}${p.seriesName}: <strong>${formatter(p.value)}</strong><br/>`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
legend: this.showLegend && this.series.length > 1
|
||||
? { bottom: 8, itemWidth: 10, itemHeight: 10, data: legendData }
|
||||
: { show: false },
|
||||
grid: {
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: this.showLegend && this.series.length > 1 ? 40 : 16,
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: this.horizontal ? valueAxis : categoryAxis,
|
||||
yAxis: this.horizontal ? categoryAxis : valueAxis,
|
||||
series: seriesData,
|
||||
};
|
||||
}
|
||||
}
|
||||
120
ts_web/elements/00group-chart/dees-chart-bar/demo.ts
Normal file
120
ts_web/elements/00group-chart/dees-chart-bar/demo.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { DeesChartBar } from './component.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import './component.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const endpointCategories = ['/api/users', '/api/orders', '/api/products', '/api/auth', '/api/search'];
|
||||
const endpointSeries = [
|
||||
{ name: 'GET', data: [1240, 890, 720, 2100, 560] },
|
||||
{ name: 'POST', data: [320, 450, 180, 890, 40] },
|
||||
{ name: 'PUT', data: [90, 210, 150, 30, 10] },
|
||||
];
|
||||
|
||||
const regionCategories = ['US-East', 'US-West', 'EU', 'Asia', 'Other'];
|
||||
const regionSeries = [
|
||||
{ name: 'Requests', data: [4500, 3200, 2800, 1900, 600] },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const vertChart = elementArg.querySelector('#vert-chart') as DeesChartBar;
|
||||
const horizChart = elementArg.querySelector('#horiz-chart') as DeesChartBar;
|
||||
const stackChart = elementArg.querySelector('#stack-chart') as DeesChartBar;
|
||||
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach((button: any) => {
|
||||
const text = button.text?.trim();
|
||||
if (text === 'Randomize') {
|
||||
button.addEventListener('click', () => {
|
||||
vertChart.series = endpointSeries.map((s) => ({
|
||||
...s,
|
||||
data: s.data.map((v) => Math.round(v * (0.5 + Math.random()))),
|
||||
}));
|
||||
horizChart.series = regionSeries.map((s) => ({
|
||||
...s,
|
||||
data: s.data.map((v) => Math.round(v * (0.5 + Math.random()))),
|
||||
}));
|
||||
stackChart.series = endpointSeries.map((s) => ({
|
||||
...s,
|
||||
data: s.data.map((v) => Math.round(v * (0.5 + Math.random()))),
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.chartRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info {
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="controls">
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Randomize</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="chartRow">
|
||||
<dees-chart-bar
|
||||
id="vert-chart"
|
||||
.label=${'Requests by Endpoint'}
|
||||
.categories=${endpointCategories}
|
||||
.series=${endpointSeries}
|
||||
.valueFormatter=${(val: number) => `${val} req`}
|
||||
></dees-chart-bar>
|
||||
|
||||
<dees-chart-bar
|
||||
id="horiz-chart"
|
||||
.label=${'Traffic by Region'}
|
||||
.categories=${regionCategories}
|
||||
.series=${regionSeries}
|
||||
.horizontal=${true}
|
||||
.valueFormatter=${(val: number) => `${(val / 1000).toFixed(1)}k`}
|
||||
></dees-chart-bar>
|
||||
</div>
|
||||
|
||||
<dees-chart-bar
|
||||
id="stack-chart"
|
||||
.label=${'Stacked: Requests by Endpoint'}
|
||||
.categories=${endpointCategories}
|
||||
.series=${endpointSeries}
|
||||
.stacked=${true}
|
||||
.valueFormatter=${(val: number) => `${val} req`}
|
||||
></dees-chart-bar>
|
||||
|
||||
<div class="info">
|
||||
Bar chart with vertical, horizontal, and stacked modes •
|
||||
Click 'Randomize' to update data with animation
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
7
ts_web/elements/00group-chart/dees-chart-bar/styles.ts
Normal file
7
ts_web/elements/00group-chart/dees-chart-bar/styles.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
|
||||
|
||||
export const barStyles = [
|
||||
...echartsBaseStyles,
|
||||
css``,
|
||||
];
|
||||
13
ts_web/elements/00group-chart/dees-chart-bar/template.ts
Normal file
13
ts_web/elements/00group-chart/dees-chart-bar/template.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesChartBar } from './component.js';
|
||||
|
||||
export const renderChartBar = (component: DeesChartBar): TemplateResult => {
|
||||
return html`
|
||||
<dees-tile>
|
||||
<div slot="header" class="chartHeader">
|
||||
<span class="chartLabel">${component.label}</span>
|
||||
</div>
|
||||
<div class="chartContainer"></div>
|
||||
</dees-tile>
|
||||
`;
|
||||
};
|
||||
142
ts_web/elements/00group-chart/dees-chart-donut/component.ts
Normal file
142
ts_web/elements/00group-chart/dees-chart-donut/component.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
||||
import { demoFunc } from './demo.js';
|
||||
import { donutStyles } from './styles.js';
|
||||
import { renderChartDonut } from './template.js';
|
||||
import { getEchartsSeriesColors, getThemeColors, hexToRgba } from '../dees-chart-echarts-theme.js';
|
||||
|
||||
export interface IDonutDataItem {
|
||||
name: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-chart-donut': DeesChartDonut;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-chart-donut')
|
||||
export class DeesChartDonut extends DeesChartEchartsBase {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Chart'];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor data: IDonutDataItem[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showLegend: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showLabels: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
accessor innerRadiusPercent: string = '55%';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor valueFormatter: (value: number) => string = (val) => `${val}`;
|
||||
|
||||
public static styles = donutStyles;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return renderChartDonut(this);
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
this.chartInstance &&
|
||||
(changedProperties.has('data') ||
|
||||
changedProperties.has('showLegend') ||
|
||||
changedProperties.has('showLabels') ||
|
||||
changedProperties.has('innerRadiusPercent'))
|
||||
) {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected buildOption(): Record<string, any> {
|
||||
const themeColors = getThemeColors(this.goBright);
|
||||
const seriesColors = getEchartsSeriesColors(this.goBright);
|
||||
const fillAlpha = this.goBright ? 0.15 : 0.2;
|
||||
|
||||
const legendData: Array<{ name: string; itemStyle: { color: string } }> = [];
|
||||
|
||||
const data = this.data.map((item, index) => {
|
||||
const color = item.color || seriesColors[index % seriesColors.length];
|
||||
legendData.push({ name: item.name, itemStyle: { color } });
|
||||
return {
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
itemStyle: {
|
||||
color: hexToRgba(color, fillAlpha),
|
||||
borderColor: color,
|
||||
borderWidth: 1,
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: hexToRgba(color, fillAlpha + 0.15),
|
||||
borderColor: color,
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const formatter = this.valueFormatter;
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
const solidColor = params.data?.itemStyle?.borderColor || params.color;
|
||||
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${solidColor};"></span>`;
|
||||
return `${marker}${params.name}: <strong>${formatter(params.value)}</strong> (${params.percent}%)`;
|
||||
},
|
||||
},
|
||||
legend: this.showLegend
|
||||
? {
|
||||
orient: 'vertical',
|
||||
right: 16,
|
||||
top: 'center',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 12,
|
||||
data: legendData,
|
||||
formatter: (name: string) => {
|
||||
const item = this.data.find((d) => d.name === name);
|
||||
return item ? `${name} ${formatter(item.value)}` : name;
|
||||
},
|
||||
}
|
||||
: { show: false },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: [this.innerRadiusPercent, '85%'],
|
||||
center: this.showLegend ? ['35%', '50%'] : ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
padAngle: 2,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
label: this.showLabels
|
||||
? {
|
||||
show: true,
|
||||
formatter: '{b}: {d}%',
|
||||
fontSize: 11,
|
||||
color: themeColors.textSecondary,
|
||||
textBorderColor: 'transparent',
|
||||
}
|
||||
: { show: false },
|
||||
data,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
127
ts_web/elements/00group-chart/dees-chart-donut/demo.ts
Normal file
127
ts_web/elements/00group-chart/dees-chart-donut/demo.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { DeesChartDonut } from './component.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import './component.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const diskData = [
|
||||
{ name: 'Documents', value: 42 },
|
||||
{ name: 'Media', value: 28 },
|
||||
{ name: 'Applications', value: 15 },
|
||||
{ name: 'System', value: 10 },
|
||||
{ name: 'Other', value: 5 },
|
||||
];
|
||||
|
||||
const statusData = [
|
||||
{ name: 'Healthy', value: 156 },
|
||||
{ name: 'Warning', value: 23 },
|
||||
{ name: 'Critical', value: 8 },
|
||||
{ name: 'Unknown', value: 3 },
|
||||
];
|
||||
|
||||
const trafficData = [
|
||||
{ name: 'API', value: 45200 },
|
||||
{ name: 'Static Assets', value: 23100 },
|
||||
{ name: 'WebSocket', value: 12800 },
|
||||
{ name: 'GraphQL', value: 8900 },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const diskChart = elementArg.querySelector('#disk-chart') as DeesChartDonut;
|
||||
const statusChart = elementArg.querySelector('#status-chart') as DeesChartDonut;
|
||||
const trafficChart = elementArg.querySelector('#traffic-chart') as DeesChartDonut;
|
||||
|
||||
// Wire up buttons
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach((button: any) => {
|
||||
const text = button.text?.trim();
|
||||
if (text === 'Randomize') {
|
||||
button.addEventListener('click', () => {
|
||||
diskChart.data = diskData.map((d) => ({
|
||||
...d,
|
||||
value: Math.round(d.value * (0.5 + Math.random())),
|
||||
}));
|
||||
statusChart.data = statusData.map((d) => ({
|
||||
...d,
|
||||
value: Math.round(d.value * (0.3 + Math.random() * 1.4)),
|
||||
}));
|
||||
trafficChart.data = trafficData.map((d) => ({
|
||||
...d,
|
||||
value: Math.round(d.value * (0.5 + Math.random())),
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.chartRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info {
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="controls">
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Randomize</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="chartRow">
|
||||
<dees-chart-donut
|
||||
id="disk-chart"
|
||||
.label=${'Disk Usage (GB)'}
|
||||
.data=${diskData}
|
||||
.valueFormatter=${(val: number) => `${val} GB`}
|
||||
></dees-chart-donut>
|
||||
|
||||
<dees-chart-donut
|
||||
id="status-chart"
|
||||
.label=${'Service Status'}
|
||||
.data=${statusData}
|
||||
.valueFormatter=${(val: number) => `${val} services`}
|
||||
.innerRadiusPercent=${'0%'}
|
||||
></dees-chart-donut>
|
||||
</div>
|
||||
|
||||
<dees-chart-donut
|
||||
id="traffic-chart"
|
||||
.label=${'Traffic Distribution'}
|
||||
.data=${trafficData}
|
||||
.valueFormatter=${(val: number) => `${(val / 1000).toFixed(1)}k req`}
|
||||
></dees-chart-donut>
|
||||
|
||||
<div class="info">
|
||||
Donut chart with configurable inner radius (set to 0% for full pie) •
|
||||
Click 'Randomize' to update data with animation
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
14
ts_web/elements/00group-chart/dees-chart-donut/styles.ts
Normal file
14
ts_web/elements/00group-chart/dees-chart-donut/styles.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { css, cssManager } from '@design.estate/dees-element';
|
||||
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
|
||||
|
||||
export const donutStyles = [
|
||||
...echartsBaseStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 360px;
|
||||
}
|
||||
.chartContainer {
|
||||
inset: 12px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
13
ts_web/elements/00group-chart/dees-chart-donut/template.ts
Normal file
13
ts_web/elements/00group-chart/dees-chart-donut/template.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesChartDonut } from './component.js';
|
||||
|
||||
export const renderChartDonut = (component: DeesChartDonut): TemplateResult => {
|
||||
return html`
|
||||
<dees-tile>
|
||||
<div slot="header" class="chartHeader">
|
||||
<span class="chartLabel">${component.label}</span>
|
||||
</div>
|
||||
<div class="chartContainer"></div>
|
||||
</dees-tile>
|
||||
`;
|
||||
};
|
||||
112
ts_web/elements/00group-chart/dees-chart-echarts-base.ts
Normal file
112
ts_web/elements/00group-chart/dees-chart-echarts-base.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesServiceLibLoader, type IEchartsBundle, type IEchartsInstance } from '../../services/index.js';
|
||||
import { getEchartsThemeOptions } from './dees-chart-echarts-theme.js';
|
||||
import '../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
/**
|
||||
* Abstract base class for ECharts-based chart components.
|
||||
* Handles library loading, chart lifecycle, resize observation, and theme switching.
|
||||
* Subclasses implement `buildOption()` to define their chart configuration.
|
||||
*/
|
||||
export abstract class DeesChartEchartsBase extends DeesElement {
|
||||
@property()
|
||||
accessor label: string = 'Untitled Chart';
|
||||
|
||||
protected chartInstance: IEchartsInstance | null = null;
|
||||
protected echartsBundle: IEchartsBundle | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
this.registerGarbageFunction(async () => {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
if (this.chartInstance) {
|
||||
try {
|
||||
this.chartInstance.dispose();
|
||||
this.chartInstance = null;
|
||||
} catch (e) {
|
||||
console.error('Error disposing ECharts instance:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<dees-tile>
|
||||
<div slot="header" class="chartHeader">
|
||||
<span class="chartLabel">${this.label}</span>
|
||||
</div>
|
||||
<div class="chartContainer"></div>
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
await this.domtoolsPromise;
|
||||
this.echartsBundle = await DeesServiceLibLoader.getInstance().loadEcharts();
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
const chartContainer = this.shadowRoot!.querySelector('.chartContainer') as HTMLDivElement;
|
||||
if (!chartContainer) return;
|
||||
|
||||
try {
|
||||
this.chartInstance = this.echartsBundle.init(chartContainer, null, { renderer: 'svg' });
|
||||
this.updateChart();
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.chartInstance?.resize();
|
||||
});
|
||||
this.resizeObserver.observe(chartContainer);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize ECharts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('goBright') && this.chartInstance) {
|
||||
this.applyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract buildOption(): Record<string, any>;
|
||||
|
||||
protected updateChart(): void {
|
||||
if (!this.chartInstance) return;
|
||||
const themeOptions = getEchartsThemeOptions(this.goBright);
|
||||
const chartOption = this.buildOption();
|
||||
// Deep-merge theme defaults with chart-specific options for nested objects
|
||||
const merged = {
|
||||
...themeOptions,
|
||||
...chartOption,
|
||||
textStyle: { ...themeOptions.textStyle, ...(chartOption.textStyle || {}) },
|
||||
tooltip: { ...themeOptions.tooltip, ...(chartOption.tooltip || {}) },
|
||||
legend: {
|
||||
...themeOptions.legend,
|
||||
...(chartOption.legend || {}),
|
||||
textStyle: { ...(themeOptions.legend?.textStyle || {}), ...(chartOption.legend?.textStyle || {}) },
|
||||
},
|
||||
};
|
||||
this.chartInstance.setOption(merged, true);
|
||||
}
|
||||
|
||||
protected applyTheme(): void {
|
||||
this.updateChart();
|
||||
}
|
||||
|
||||
public async forceResize(): Promise<void> {
|
||||
this.chartInstance?.resize();
|
||||
}
|
||||
}
|
||||
36
ts_web/elements/00group-chart/dees-chart-echarts-styles.ts
Normal file
36
ts_web/elements/00group-chart/dees-chart-echarts-styles.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { css, cssManager } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../00theme.js';
|
||||
|
||||
export const echartsBaseStyles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 400px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
color: var(--dees-color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
dees-tile {
|
||||
height: 100%;
|
||||
}
|
||||
.chartHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 0 8px 0 16px;
|
||||
}
|
||||
.chartLabel {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
.chartContainer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
91
ts_web/elements/00group-chart/dees-chart-echarts-theme.ts
Normal file
91
ts_web/elements/00group-chart/dees-chart-echarts-theme.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Shared theme utilities for ECharts-based chart components.
|
||||
* Uses the centralized themeDefaults tokens so chart colors stay in sync
|
||||
* with the rest of the dees-catalog design system.
|
||||
*
|
||||
* ECharts renders on <svg> and cannot read CSS custom properties,
|
||||
* so we reference the TypeScript source-of-truth (themeDefaults) directly.
|
||||
*
|
||||
* IMPORTANT: All colors passed to ECharts for data series must be hex or rgb/rgba.
|
||||
* ECharts cannot interpolate HSL strings during hover/emphasis animations,
|
||||
* causing them to flash black.
|
||||
*/
|
||||
|
||||
import { themeDefaults } from '../00theme.js';
|
||||
|
||||
const light = themeDefaults.colors.light;
|
||||
const dark = themeDefaults.colors.dark;
|
||||
|
||||
/**
|
||||
* Series color palette for ECharts charts.
|
||||
* Aligned with the Tailwind/shadcn-inspired palette used throughout the codebase.
|
||||
* All values are hex — ECharts requires this for animation interpolation.
|
||||
*/
|
||||
const SERIES_COLORS = {
|
||||
dark: [
|
||||
'#60a5fa', // blue-400 — softer in dark mode
|
||||
'#2dd4bf', // teal-400
|
||||
'#a78bfa', // violet-400
|
||||
'#fbbf24', // amber-400
|
||||
'#34d399', // emerald-400
|
||||
'#fb7185', // rose-400
|
||||
],
|
||||
light: [
|
||||
'#3b82f6', // blue-500
|
||||
'#14b8a6', // teal-500
|
||||
'#8b5cf6', // violet-500
|
||||
'#f59e0b', // amber-500
|
||||
'#10b981', // emerald-500
|
||||
'#f43f5e', // rose-500
|
||||
],
|
||||
};
|
||||
|
||||
export function getEchartsSeriesColors(goBright: boolean): string[] {
|
||||
return goBright ? SERIES_COLORS.light : SERIES_COLORS.dark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hex color to an rgba string with the given alpha.
|
||||
*/
|
||||
export function hexToRgba(hex: string, alpha: number): string {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
export function getEchartsThemeOptions(goBright: boolean): Record<string, any> {
|
||||
const colors = goBright ? light : dark;
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
textStyle: {
|
||||
color: colors.textSecondary,
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
fontSize: 12,
|
||||
},
|
||||
// No global `color` array — each component sets per-item/per-series
|
||||
// colors explicitly to avoid conflicts during emphasis animations.
|
||||
tooltip: {
|
||||
backgroundColor: colors.bgPrimary,
|
||||
borderColor: colors.borderDefault,
|
||||
textStyle: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: 12,
|
||||
},
|
||||
confine: true,
|
||||
},
|
||||
legend: {
|
||||
textStyle: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the resolved theme colors object for use in buildOption().
|
||||
*/
|
||||
export function getThemeColors(goBright: boolean) {
|
||||
return goBright ? light : dark;
|
||||
}
|
||||
161
ts_web/elements/00group-chart/dees-chart-gauge/component.ts
Normal file
161
ts_web/elements/00group-chart/dees-chart-gauge/component.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
||||
import { demoFunc } from './demo.js';
|
||||
import { gaugeStyles } from './styles.js';
|
||||
import { renderChartGauge } from './template.js';
|
||||
import { getEchartsSeriesColors, getThemeColors } from '../dees-chart-echarts-theme.js';
|
||||
|
||||
export interface IGaugeThreshold {
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-chart-gauge': DeesChartGauge;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-chart-gauge')
|
||||
export class DeesChartGauge extends DeesChartEchartsBase {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Chart'];
|
||||
|
||||
@property({ type: Number })
|
||||
accessor value: number = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor min: number = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor max: number = 100;
|
||||
|
||||
@property({ type: String })
|
||||
accessor unit: string = '%';
|
||||
|
||||
@property({ type: Array })
|
||||
accessor thresholds: IGaugeThreshold[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showTicks: boolean = true;
|
||||
|
||||
public static styles = gaugeStyles;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return renderChartGauge(this);
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
this.chartInstance &&
|
||||
(changedProperties.has('value') ||
|
||||
changedProperties.has('min') ||
|
||||
changedProperties.has('max') ||
|
||||
changedProperties.has('unit') ||
|
||||
changedProperties.has('thresholds') ||
|
||||
changedProperties.has('showTicks'))
|
||||
) {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected buildOption(): Record<string, any> {
|
||||
const colors = getThemeColors(this.goBright);
|
||||
const seriesColors = getEchartsSeriesColors(this.goBright);
|
||||
const primaryColor = seriesColors[0];
|
||||
|
||||
// Build axis line color stops from thresholds
|
||||
let axisLineColors: Array<[number, string]>;
|
||||
if (this.thresholds.length > 0) {
|
||||
const sorted = [...this.thresholds].sort((a, b) => a.value - b.value);
|
||||
axisLineColors = sorted.map((t) => [
|
||||
(t.value - this.min) / (this.max - this.min),
|
||||
t.color,
|
||||
]);
|
||||
// Ensure we end at 1
|
||||
if (axisLineColors[axisLineColors.length - 1][0] < 1) {
|
||||
axisLineColors.push([1, sorted[sorted.length - 1].color]);
|
||||
}
|
||||
} else {
|
||||
axisLineColors = [[1, primaryColor]];
|
||||
}
|
||||
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
min: this.min,
|
||||
max: this.max,
|
||||
startAngle: 220,
|
||||
endAngle: -40,
|
||||
progress: {
|
||||
show: true,
|
||||
width: 14,
|
||||
roundCap: true,
|
||||
},
|
||||
pointer: {
|
||||
show: true,
|
||||
length: '60%',
|
||||
width: 5,
|
||||
itemStyle: {
|
||||
color: 'auto',
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
width: 14,
|
||||
color: axisLineColors,
|
||||
opacity: 0.3,
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: this.showTicks,
|
||||
distance: -20,
|
||||
length: 6,
|
||||
lineStyle: {
|
||||
color: colors.borderStrong,
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
show: this.showTicks,
|
||||
distance: -24,
|
||||
length: 10,
|
||||
lineStyle: {
|
||||
color: colors.textMuted,
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
show: this.showTicks,
|
||||
distance: 30,
|
||||
color: colors.textMuted,
|
||||
fontSize: 11,
|
||||
},
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
offsetCenter: [0, '65%'],
|
||||
color: colors.textPrimary,
|
||||
formatter: `{value}${this.unit}`,
|
||||
},
|
||||
title: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: this.value,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
125
ts_web/elements/00group-chart/dees-chart-gauge/demo.ts
Normal file
125
ts_web/elements/00group-chart/dees-chart-gauge/demo.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { DeesChartGauge } from './component.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import './component.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const defaultThresholds = [
|
||||
{ value: 60, color: 'hsl(142 76% 36%)' },
|
||||
{ value: 80, color: 'hsl(38 92% 50%)' },
|
||||
{ value: 100, color: 'hsl(0 72% 50%)' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const cpuGauge = elementArg.querySelector('#cpu-gauge') as DeesChartGauge;
|
||||
const memGauge = elementArg.querySelector('#mem-gauge') as DeesChartGauge;
|
||||
const slaGauge = elementArg.querySelector('#sla-gauge') as DeesChartGauge;
|
||||
|
||||
let animInterval: number | null = null;
|
||||
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach((button: any) => {
|
||||
const text = button.text?.trim();
|
||||
if (text === 'Animate') {
|
||||
button.addEventListener('click', () => {
|
||||
if (animInterval) return;
|
||||
animInterval = window.setInterval(() => {
|
||||
cpuGauge.value = Math.round(30 + Math.random() * 60);
|
||||
memGauge.value = Math.round(40 + Math.random() * 50);
|
||||
slaGauge.value = Math.round((95 + Math.random() * 5) * 100) / 100;
|
||||
}, 2000);
|
||||
});
|
||||
} else if (text === 'Stop') {
|
||||
button.addEventListener('click', () => {
|
||||
if (animInterval) {
|
||||
window.clearInterval(animInterval);
|
||||
animInterval = null;
|
||||
}
|
||||
});
|
||||
} else if (text === 'Spike') {
|
||||
button.addEventListener('click', () => {
|
||||
cpuGauge.value = 95;
|
||||
memGauge.value = 88;
|
||||
slaGauge.value = 96.5;
|
||||
});
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.gaugeRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info {
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="controls">
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Animate</dees-button>
|
||||
<dees-button>Stop</dees-button>
|
||||
<dees-button>Spike</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="gaugeRow">
|
||||
<dees-chart-gauge
|
||||
id="cpu-gauge"
|
||||
.label=${'CPU Usage'}
|
||||
.value=${42}
|
||||
.unit=${'%'}
|
||||
.thresholds=${defaultThresholds}
|
||||
></dees-chart-gauge>
|
||||
|
||||
<dees-chart-gauge
|
||||
id="mem-gauge"
|
||||
.label=${'Memory Usage'}
|
||||
.value=${67}
|
||||
.unit=${'%'}
|
||||
.thresholds=${defaultThresholds}
|
||||
></dees-chart-gauge>
|
||||
|
||||
<dees-chart-gauge
|
||||
id="sla-gauge"
|
||||
.label=${'SLA Uptime'}
|
||||
.value=${99.8}
|
||||
.min=${95}
|
||||
.max=${100}
|
||||
.unit=${'%'}
|
||||
.showTicks=${true}
|
||||
></dees-chart-gauge>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Gauge chart with animated value transitions and threshold coloring •
|
||||
Click 'Animate' for live updates, 'Spike' to simulate high load
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
11
ts_web/elements/00group-chart/dees-chart-gauge/styles.ts
Normal file
11
ts_web/elements/00group-chart/dees-chart-gauge/styles.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
|
||||
|
||||
export const gaugeStyles = [
|
||||
...echartsBaseStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 320px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
13
ts_web/elements/00group-chart/dees-chart-gauge/template.ts
Normal file
13
ts_web/elements/00group-chart/dees-chart-gauge/template.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesChartGauge } from './component.js';
|
||||
|
||||
export const renderChartGauge = (component: DeesChartGauge): TemplateResult => {
|
||||
return html`
|
||||
<dees-tile>
|
||||
<div slot="header" class="chartHeader">
|
||||
<span class="chartLabel">${component.label}</span>
|
||||
</div>
|
||||
<div class="chartContainer"></div>
|
||||
</dees-tile>
|
||||
`;
|
||||
};
|
||||
@@ -8,8 +8,8 @@ export const demoFunc = () => {
|
||||
// Get the log elements
|
||||
const structuredLog = elementArg.querySelector('#structured-log') as DeesChartLog;
|
||||
const rawLog = elementArg.querySelector('#raw-log') as DeesChartLog;
|
||||
let structuredIntervalId: number;
|
||||
let rawIntervalId: number;
|
||||
let structuredIntervalId: number | null;
|
||||
let rawIntervalId: number | null;
|
||||
|
||||
const serverSources = ['Server', 'Database', 'API', 'Auth', 'Cache', 'Queue', 'WebSocket', 'Scheduler'];
|
||||
|
||||
@@ -174,7 +174,7 @@ export const demoFunc = () => {
|
||||
// Wire up button click handlers
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach(button => {
|
||||
const text = button.textContent?.trim();
|
||||
const text = (button as any).text?.trim();
|
||||
switch (text) {
|
||||
case 'Add Structured Log':
|
||||
button.addEventListener('click', () => generateRandomLog());
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-chart-log.demo.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { DeesServiceLibLoader, type IXtermSearchAddon, CDN_BASE, CDN_VERSIONS } from '../../../services/index.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
// Type imports (no runtime overhead)
|
||||
import type { Terminal } from 'xterm';
|
||||
@@ -103,37 +104,27 @@ export class DeesChartLog extends DeesElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 400px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
dees-tile {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -150,10 +141,10 @@ export class DeesChartLog extends DeesElement {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
color: var(--dees-color-text-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -162,7 +153,7 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.search-nav {
|
||||
@@ -173,35 +164,35 @@ export class DeesChartLog extends DeesElement {
|
||||
.search-nav button {
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 3px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.search-nav button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 4px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-toggle:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.filter-toggle.active {
|
||||
@@ -217,11 +208,11 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.control-button {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
@@ -229,9 +220,9 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 25%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
background: var(--dees-color-hover);
|
||||
border-color: var(--dees-color-border-strong);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.control-button.active {
|
||||
@@ -241,10 +232,10 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
}
|
||||
|
||||
.terminal-container .xterm {
|
||||
@@ -256,20 +247,21 @@ export class DeesChartLog extends DeesElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metrics-bar {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
padding: 6px 12px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.metric {
|
||||
@@ -307,7 +299,7 @@ export class DeesChartLog extends DeesElement {
|
||||
|
||||
.metric.rate {
|
||||
margin-left: auto;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.metric.rate::before {
|
||||
@@ -323,8 +315,8 @@ export class DeesChartLog extends DeesElement {
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="header">
|
||||
<dees-tile .heading=${this.label}>
|
||||
<div slot="header" class="header">
|
||||
<div class="title">${this.label}</div>
|
||||
<div class="search-box">
|
||||
<input
|
||||
@@ -367,7 +359,7 @@ export class DeesChartLog extends DeesElement {
|
||||
|
||||
${this.showMetrics
|
||||
? html`
|
||||
<div class="metrics-bar">
|
||||
<div slot="footer" class="metrics-bar">
|
||||
<span class="metric error">errors: ${this.metrics.error}</span>
|
||||
<span class="metric warn">warns: ${this.metrics.warn}</span>
|
||||
<span class="metric info">info: ${this.metrics.info}</span>
|
||||
@@ -377,7 +369,7 @@ export class DeesChartLog extends DeesElement {
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -385,11 +377,23 @@ export class DeesChartLog extends DeesElement {
|
||||
this.domtoolsInstance = await this.domtoolsPromise;
|
||||
await this.initializeTerminal();
|
||||
|
||||
// Process any initial log entries
|
||||
if (this.logEntries.length > 0) {
|
||||
// initializeTerminal() already replayed logBuffer (from addLog/updateLog).
|
||||
// Now handle logEntries set via property binding before terminal was ready.
|
||||
if (this.logEntries.length > 0 && this.logBuffer.length === 0) {
|
||||
this.logBuffer = [...this.logEntries];
|
||||
for (const entry of this.logEntries) {
|
||||
this.updateMetrics(entry.level);
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
} else if (this.logEntries.length > 0 && this.logBuffer.length > 0) {
|
||||
const bufferSet = new Set(this.logBuffer.map(e => `${e.timestamp}|${e.message}`));
|
||||
for (const entry of this.logEntries) {
|
||||
if (!bufferSet.has(`${entry.timestamp}|${entry.message}`)) {
|
||||
this.logBuffer.push(entry);
|
||||
this.updateMetrics(entry.level);
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,10 +449,65 @@ export class DeesChartLog extends DeesElement {
|
||||
this.rateInterval = setInterval(() => this.calculateRate(), 1000);
|
||||
|
||||
this.terminalReady = true;
|
||||
|
||||
// Replay any entries that arrived via updateLog()/addLog() before terminal was ready
|
||||
for (const entry of this.logBuffer) {
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('goBright') && this.terminal) {
|
||||
this.terminal.options.theme = this.getTerminalTheme();
|
||||
}
|
||||
if (changedProperties.has('logEntries') && this.terminalReady && this.logEntries.length > 0) {
|
||||
const oldEntries: ILogEntry[] = changedProperties.get('logEntries') || [];
|
||||
const newEntries = this.logEntries;
|
||||
|
||||
// Same content? Skip entirely.
|
||||
if (
|
||||
oldEntries.length === newEntries.length &&
|
||||
oldEntries.length > 0 &&
|
||||
oldEntries[oldEntries.length - 1].timestamp === newEntries[newEntries.length - 1].timestamp &&
|
||||
oldEntries[oldEntries.length - 1].message === newEntries[newEntries.length - 1].message
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Append-only? Write only the new tail entries incrementally.
|
||||
if (
|
||||
newEntries.length > oldEntries.length &&
|
||||
oldEntries.length > 0 &&
|
||||
oldEntries[oldEntries.length - 1].timestamp === newEntries[oldEntries.length - 1].timestamp &&
|
||||
oldEntries[oldEntries.length - 1].message === newEntries[oldEntries.length - 1].message
|
||||
) {
|
||||
const tailEntries = newEntries.slice(oldEntries.length);
|
||||
for (const entry of tailEntries) {
|
||||
this.logBuffer.push(entry);
|
||||
this.updateMetrics(entry.level);
|
||||
|
||||
// Enforce maxEntries
|
||||
if (this.logBuffer.length > this.maxEntries) {
|
||||
this.logBuffer.shift();
|
||||
}
|
||||
|
||||
// Respect filter mode
|
||||
if (!this.filterMode || !this.searchQuery || this.entryMatchesFilter(entry)) {
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Different content — full re-render
|
||||
this.logBuffer = [...newEntries];
|
||||
this.reRenderFilteredLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private getTerminalTheme() {
|
||||
const isDark = this.domtoolsInstance?.themeManager?.isDarkMode ?? true;
|
||||
const isDark = !this.goBright;
|
||||
return isDark
|
||||
? {
|
||||
background: '#0a0a0a',
|
||||
|
||||
132
ts_web/elements/00group-chart/dees-chart-radar/component.ts
Normal file
132
ts_web/elements/00group-chart/dees-chart-radar/component.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
||||
import { demoFunc } from './demo.js';
|
||||
import { radarStyles } from './styles.js';
|
||||
import { renderChartRadar } from './template.js';
|
||||
import { getEchartsSeriesColors, getThemeColors, hexToRgba } from '../dees-chart-echarts-theme.js';
|
||||
|
||||
export interface IRadarIndicator {
|
||||
name: string;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface IRadarSeriesItem {
|
||||
name: string;
|
||||
values: number[];
|
||||
color?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-chart-radar': DeesChartRadar;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-chart-radar')
|
||||
export class DeesChartRadar extends DeesChartEchartsBase {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Chart'];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor indicators: IRadarIndicator[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor series: IRadarSeriesItem[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showLegend: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor fillArea: boolean = true;
|
||||
|
||||
public static styles = radarStyles;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return renderChartRadar(this);
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
this.chartInstance &&
|
||||
(changedProperties.has('indicators') ||
|
||||
changedProperties.has('series') ||
|
||||
changedProperties.has('showLegend') ||
|
||||
changedProperties.has('fillArea'))
|
||||
) {
|
||||
this.updateChart();
|
||||
}
|
||||
}
|
||||
|
||||
protected buildOption(): Record<string, any> {
|
||||
const colors = getThemeColors(this.goBright);
|
||||
const seriesColors = getEchartsSeriesColors(this.goBright);
|
||||
|
||||
const fillAlpha = this.goBright ? 0.1 : 0.15;
|
||||
|
||||
const seriesData = this.series.map((s, index) => {
|
||||
const color = s.color || seriesColors[index % seriesColors.length];
|
||||
return {
|
||||
name: s.name,
|
||||
value: s.values,
|
||||
itemStyle: { color, borderColor: color, borderWidth: 1 },
|
||||
lineStyle: { color, width: 1.5 },
|
||||
areaStyle: this.fillArea ? { color: hexToRgba(color, fillAlpha) } : undefined,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: this.showLegend && this.series.length > 1
|
||||
? { bottom: 8, itemWidth: 10, itemHeight: 10 }
|
||||
: { show: false },
|
||||
radar: {
|
||||
indicator: this.indicators.map((ind) => ({
|
||||
name: ind.name,
|
||||
max: ind.max,
|
||||
})),
|
||||
shape: 'polygon',
|
||||
splitNumber: 4,
|
||||
axisName: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: 11,
|
||||
},
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
color: [colors.bgTertiary, colors.bgSecondary],
|
||||
},
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: colors.borderDefault,
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: colors.borderDefault,
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
data: seriesData,
|
||||
emphasis: {
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
119
ts_web/elements/00group-chart/dees-chart-radar/demo.ts
Normal file
119
ts_web/elements/00group-chart/dees-chart-radar/demo.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { DeesChartRadar } from './component.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import './component.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const indicators = [
|
||||
{ name: 'Latency', max: 100 },
|
||||
{ name: 'Throughput', max: 100 },
|
||||
{ name: 'Availability', max: 100 },
|
||||
{ name: 'Error Rate', max: 100 },
|
||||
{ name: 'Saturation', max: 100 },
|
||||
{ name: 'Security', max: 100 },
|
||||
];
|
||||
|
||||
const series = [
|
||||
{ name: 'Service A', values: [85, 90, 99, 12, 45, 78] },
|
||||
{ name: 'Service B', values: [70, 65, 95, 28, 60, 90] },
|
||||
];
|
||||
|
||||
const singleIndicators = [
|
||||
{ name: 'Speed', max: 10 },
|
||||
{ name: 'Reliability', max: 10 },
|
||||
{ name: 'Comfort', max: 10 },
|
||||
{ name: 'Safety', max: 10 },
|
||||
{ name: 'Cost', max: 10 },
|
||||
];
|
||||
|
||||
const singleSeries = [
|
||||
{ name: 'Rating', values: [8.5, 9.2, 7.0, 9.5, 6.0] },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
const compChart = elementArg.querySelector('#comparison-chart') as DeesChartRadar;
|
||||
const singleChart = elementArg.querySelector('#single-chart') as DeesChartRadar;
|
||||
|
||||
const buttons = elementArg.querySelectorAll('dees-button');
|
||||
buttons.forEach((button: any) => {
|
||||
const text = button.text?.trim();
|
||||
if (text === 'Randomize') {
|
||||
button.addEventListener('click', () => {
|
||||
compChart.series = series.map((s) => ({
|
||||
...s,
|
||||
values: s.values.map(() => Math.round(20 + Math.random() * 80)),
|
||||
}));
|
||||
singleChart.series = singleSeries.map((s) => ({
|
||||
...s,
|
||||
values: s.values.map(() => Math.round((2 + Math.random() * 8) * 10) / 10),
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
.chartRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info {
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<div class="controls">
|
||||
<dees-button-group label="Actions:">
|
||||
<dees-button>Randomize</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="chartRow">
|
||||
<dees-chart-radar
|
||||
id="comparison-chart"
|
||||
.label=${'Service Health Comparison'}
|
||||
.indicators=${indicators}
|
||||
.series=${series}
|
||||
></dees-chart-radar>
|
||||
|
||||
<dees-chart-radar
|
||||
id="single-chart"
|
||||
.label=${'Product Rating'}
|
||||
.indicators=${singleIndicators}
|
||||
.series=${singleSeries}
|
||||
.fillArea=${true}
|
||||
></dees-chart-radar>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Radar chart for multi-dimensional comparison •
|
||||
Supports multiple overlay series and configurable fill •
|
||||
Click 'Randomize' to update data
|
||||
</div>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
7
ts_web/elements/00group-chart/dees-chart-radar/styles.ts
Normal file
7
ts_web/elements/00group-chart/dees-chart-radar/styles.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
|
||||
|
||||
export const radarStyles = [
|
||||
...echartsBaseStyles,
|
||||
css``,
|
||||
];
|
||||
13
ts_web/elements/00group-chart/dees-chart-radar/template.ts
Normal file
13
ts_web/elements/00group-chart/dees-chart-radar/template.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesChartRadar } from './component.js';
|
||||
|
||||
export const renderChartRadar = (component: DeesChartRadar): TemplateResult => {
|
||||
return html`
|
||||
<dees-tile>
|
||||
<div slot="header" class="chartHeader">
|
||||
<span class="chartLabel">${component.label}</span>
|
||||
</div>
|
||||
<div class="chartContainer"></div>
|
||||
</dees-tile>
|
||||
`;
|
||||
};
|
||||
@@ -1,3 +1,7 @@
|
||||
// Chart Components
|
||||
export * from './dees-chart-area/index.js';
|
||||
export * from './dees-chart-bar/index.js';
|
||||
export * from './dees-chart-donut/index.js';
|
||||
export * from './dees-chart-gauge/index.js';
|
||||
export * from './dees-chart-log/index.js';
|
||||
export * from './dees-chart-radar/index.js';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { demoFunc } from './dees-dataview-codebox.demo.js';
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
css,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { cssGeistFontFamily, cssMonoFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
import type { HLJSApi } from 'highlight.js';
|
||||
|
||||
@@ -17,6 +19,7 @@ import * as smartstring from '@push.rocks/smartstring';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -38,6 +41,11 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
})
|
||||
accessor codeToDisplay: string = '';
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
@@ -52,28 +60,21 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mainbox {
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
dees-tile {
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.appbar {
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
color: var(--dees-color-text-muted);
|
||||
height: 32px;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.appbar .fileName {
|
||||
@@ -84,17 +85,16 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
color: var(--dees-color-text-muted);
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.spacesLabel {
|
||||
@@ -121,14 +121,14 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
.codegrid {
|
||||
display: grid;
|
||||
grid-template-columns: 50px auto;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.lineNumbers {
|
||||
color: ${cssManager.bdTheme('#71717a', '#52525b')};
|
||||
color: var(--dees-color-text-muted);
|
||||
padding: 24px 16px 0px 0px;
|
||||
text-align: right;
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-right: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
.lineCounter:last-child {
|
||||
@@ -191,9 +191,8 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="mainbox"
|
||||
@contextmenu="${(eventArg) => {
|
||||
<dees-tile
|
||||
@contextmenu="${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
name: 'About',
|
||||
@@ -205,7 +204,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
]);
|
||||
}}"
|
||||
>
|
||||
<div class="appbar">
|
||||
<div slot="header" class="appbar">
|
||||
<div class="fileName">index.ts</div>
|
||||
</div>
|
||||
<div class="codegrid">
|
||||
@@ -220,18 +219,18 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
</div>
|
||||
<pre><code></code></pre>
|
||||
</div>
|
||||
<div class="bottomBar">
|
||||
<div slot="footer" class="bottomBar">
|
||||
<div class="spacesLabel">Spaces: 2</div>
|
||||
<div class="languageLabel">${this.progLang}</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
|
||||
private codeToDisplayStore = '';
|
||||
private highlightJs: HLJSApi | null = null;
|
||||
|
||||
public async updated(_changedProperties) {
|
||||
public async updated(_changedProperties: Map<string, any>) {
|
||||
super.updated(_changedProperties);
|
||||
console.log('highlighting now');
|
||||
console.log(this.childNodes);
|
||||
@@ -257,11 +256,11 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
this.highlightJs = await DeesServiceLibLoader.getInstance().loadHighlightJs();
|
||||
}
|
||||
|
||||
const localCodeNode = this.shadowRoot.querySelector('code');
|
||||
const localCodeNode = this.shadowRoot!.querySelector('code');
|
||||
const highlightedHtml = this.highlightJs.highlight(this.codeToDisplayStore, {
|
||||
language: this.progLang,
|
||||
ignoreIllegals: true,
|
||||
});
|
||||
localCodeNode.innerHTML = highlightedHtml.value;
|
||||
localCodeNode!.innerHTML = highlightedHtml.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export const demoFunc = () => html` <style>
|
||||
.statusObject=${{
|
||||
id: '1',
|
||||
name: 'API Gateway Service',
|
||||
lastUpdated: Date.now(),
|
||||
combinedStatus: 'ok',
|
||||
combinedStatusText: 'All systems operational',
|
||||
details: [
|
||||
@@ -89,6 +90,7 @@ export const demoFunc = () => html` <style>
|
||||
.statusObject=${{
|
||||
id: '2',
|
||||
name: 'PostgreSQL Cluster',
|
||||
lastUpdated: Date.now() - 3600000,
|
||||
combinedStatus: 'partly_ok',
|
||||
combinedStatusText: 'Minor issues detected',
|
||||
details: [
|
||||
@@ -128,6 +130,7 @@ export const demoFunc = () => html` <style>
|
||||
.statusObject=${{
|
||||
id: '3',
|
||||
name: 'CI/CD Pipeline',
|
||||
lastUpdated: Date.now() - 86400000,
|
||||
combinedStatus: 'not_ok',
|
||||
combinedStatusText: 'Build failure',
|
||||
details: [
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -29,7 +30,7 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Data View'];
|
||||
|
||||
@property({ type: Object }) accessor statusObject: tsclass.code.IStatusObject;
|
||||
@property({ type: Object }) accessor statusObject!: tsclass.code.IStatusObject;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
@@ -40,35 +41,28 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
box-shadow: 0 1px 3px 0 hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
|
||||
min-height: 48px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
|
||||
dees-tile {
|
||||
color: var(--dees-color-text-primary);
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: grid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-template-columns: 48px auto 100px;
|
||||
height: 56px;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
h1 {
|
||||
display: block;
|
||||
margin: 0px;
|
||||
padding: 0px 12px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statusdot {
|
||||
@@ -82,23 +76,23 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
}
|
||||
|
||||
.copyMain {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
text-align: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
color: var(--dees-color-text-muted);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.copyMain:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
background: var(--dees-color-hover);
|
||||
border-color: var(--dees-color-border-strong);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.copyMain:active {
|
||||
@@ -122,57 +116,86 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
}
|
||||
|
||||
.detail {
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: 48px auto;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 14.9%)')};
|
||||
gap: 10px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||
transition: background-color 0.15s ease;
|
||||
padding-right: 16px;
|
||||
cursor: context-menu;
|
||||
}
|
||||
|
||||
.detail:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
.detail:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail:active {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
.detail:hover {
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.04)', 'hsl(217.2 91.2% 59.8% / 0.06)')};
|
||||
}
|
||||
|
||||
.detail .statusdot {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail .detailsText {
|
||||
padding: 12px;
|
||||
word-break: break-all;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail .detailsText .label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}
|
||||
margin-bottom: 2px;
|
||||
color: var(--dees-color-text-muted);
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail .detailsText .value {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-family: 'Intel One Mono', 'Geist Mono', monospace;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
line-height: 1.5;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
color: var(--dees-color-text-muted);
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bottomBar .statusLabel {
|
||||
font-weight: 500;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="heading">
|
||||
<dees-tile>
|
||||
<div slot="header" class="heading">
|
||||
<div class="statusdot ${this.statusObject?.combinedStatus}"></div>
|
||||
<h1>${this.statusObject?.name || 'No status object assigned'}</h1>
|
||||
<div class="copyMain" @click=${this.handleCopyAsJson}>Copy JSON</div>
|
||||
</div>
|
||||
${this.statusObject?.details?.map((detailArg) => {
|
||||
return html`
|
||||
<div
|
||||
<div
|
||||
class="detail"
|
||||
@contextmenu=${(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -202,14 +225,19 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
}}
|
||||
>
|
||||
<div class="statusdot ${detailArg.status}"></div>
|
||||
<div class="detailsText">
|
||||
<div class="label">${detailArg.name}</div>
|
||||
<div class="value">${detailArg.value}</div>
|
||||
</div>
|
||||
<span class="detailsText">
|
||||
<span class="label">${detailArg.name}</span>
|
||||
<span class="value">${detailArg.value}</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div slot="footer" class="bottomBar">
|
||||
<div class="statusLabel">${this.statusObject?.lastUpdated
|
||||
? `Last updated: ${new Date(this.statusObject.lastUpdated).toLocaleString()}`
|
||||
: ''}</div>
|
||||
</div>
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -232,7 +260,7 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
await navigator.clipboard.writeText(JSON.stringify(this.statusObject, null, 2));
|
||||
|
||||
// Show feedback
|
||||
const button = this.shadowRoot.querySelector('.copyMain') as HTMLElement;
|
||||
const button = this.shadowRoot!.querySelector('.copyMain') as HTMLElement;
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Copied!';
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
css,
|
||||
unsafeCSS,
|
||||
cssManager,
|
||||
containerResponsive,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { TemplateResult } from '@design.estate/dees-element';
|
||||
|
||||
@@ -17,6 +18,7 @@ import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
import '../../00group-button/dees-button/dees-button.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -93,6 +95,7 @@ export interface IStatsTile {
|
||||
actions?: plugins.tsclass.website.IMenuItem[];
|
||||
}
|
||||
|
||||
@containerResponsive()
|
||||
@customElement('dees-statsgrid')
|
||||
export class DeesStatsGrid extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
@@ -157,7 +160,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.grid-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
color: var(--dees-color-text-primary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
@@ -177,40 +180,31 @@ export class DeesStatsGrid extends DeesElement {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Tile Base Styles */
|
||||
.stats-tile {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--tile-padding);
|
||||
/* Tile Base Styles — frame provided by dees-tile, hover via ::part */
|
||||
.stats-tile::part(outer) {
|
||||
transition: all var(--transition-duration) ease;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats-tile:hover {
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0d0d0d')};
|
||||
border-color: ${cssManager.bdTheme('#d0d0d0', '#2a2a2a')};
|
||||
.stats-tile:hover::part(outer) {
|
||||
border-color: var(--dees-color-border-strong);
|
||||
}
|
||||
|
||||
.stats-tile.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stats-tile.clickable:hover {
|
||||
.stats-tile.clickable:hover::part(outer) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px ${cssManager.bdTheme('rgba(0,0,0,0.03)', 'rgba(0,0,0,0.15)')};
|
||||
}
|
||||
|
||||
/* Tile Header */
|
||||
/* Tile Header — slotted into dees-tile header */
|
||||
.tile-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--header-spacing);
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -230,12 +224,12 @@ export class DeesStatsGrid extends DeesElement {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tile Content */
|
||||
/* Tile Content — slotted into dees-tile content area */
|
||||
.tile-content {
|
||||
min-height: var(--content-min-height);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
min-height: var(--content-min-height);
|
||||
}
|
||||
|
||||
.tile-value {
|
||||
@@ -259,9 +253,10 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.tile-description {
|
||||
font-size: var(--label-font-size);
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
margin-top: var(--description-spacing);
|
||||
letter-spacing: -0.01em;
|
||||
flex-shrink: 0;
|
||||
padding: 0 12px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/* Gauge Styles */
|
||||
@@ -286,7 +281,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
|
||||
.gauge-background {
|
||||
fill: none;
|
||||
stroke: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
stroke: var(--dees-color-border-default);
|
||||
stroke-width: 6;
|
||||
}
|
||||
|
||||
@@ -334,7 +329,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.percentage-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
background: var(--dees-color-border-default);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-top: auto;
|
||||
@@ -342,7 +337,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
|
||||
.percentage-fill {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
|
||||
background: var(--dees-color-text-muted);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -391,14 +386,14 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.multi-percentage-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
background: var(--dees-color-border-default);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.multi-percentage-fill {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
|
||||
background: var(--dees-color-text-muted);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -469,7 +464,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.cpu-core-bar-wrapper {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
background: var(--dees-color-border-default);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -482,21 +477,21 @@ export class DeesStatsGrid extends DeesElement {
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('#666666', '#888888')};
|
||||
background: var(--dees-color-text-muted);
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s ease;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.cpu-core-bar-fill.low {
|
||||
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
background: var(--dees-color-accent-success);
|
||||
}
|
||||
|
||||
.cpu-core-bar-fill.medium {
|
||||
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
|
||||
background: var(--dees-color-accent-warning);
|
||||
}
|
||||
|
||||
.cpu-core-bar-fill.high {
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
|
||||
background: var(--dees-color-accent-error);
|
||||
}
|
||||
|
||||
.cpu-core-label {
|
||||
@@ -536,24 +531,24 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.partition-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
background: var(--dees-color-border-default);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.partition-bar-fill {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
|
||||
background: var(--dees-color-text-muted);
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.partition-bar-fill.warning {
|
||||
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
|
||||
background: var(--dees-color-accent-warning);
|
||||
}
|
||||
|
||||
.partition-bar-fill.critical {
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
|
||||
background: var(--dees-color-accent-error);
|
||||
}
|
||||
|
||||
.partition-stats {
|
||||
@@ -700,7 +695,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
.disk-health-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
background: var(--dees-color-border-default);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -776,7 +771,7 @@ export class DeesStatsGrid extends DeesElement {
|
||||
|
||||
.trend-line {
|
||||
fill: none;
|
||||
stroke: ${cssManager.bdTheme('#999999', '#666666')};
|
||||
stroke: var(--dees-color-text-muted);
|
||||
stroke-width: 1.5;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
@@ -801,6 +796,38 @@ export class DeesStatsGrid extends DeesElement {
|
||||
z-index: 1000;
|
||||
}
|
||||
`,
|
||||
// Container-responsive: when this statsgrid is narrow
|
||||
cssManager.cssForTablet(css`
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
`, this),
|
||||
cssManager.cssForPhablet(css`
|
||||
:host {
|
||||
--tile-padding: 12px;
|
||||
--value-font-size: 22px;
|
||||
--title-font-size: 12px;
|
||||
--grid-gap: 8px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.stats-tile {
|
||||
grid-column: span 1 !important;
|
||||
}
|
||||
`, this),
|
||||
cssManager.cssForPhone(css`
|
||||
:host {
|
||||
--tile-padding: 10px;
|
||||
--value-font-size: 20px;
|
||||
--header-spacing: 8px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
`, this),
|
||||
];
|
||||
|
||||
constructor() {
|
||||
@@ -844,31 +871,31 @@ export class DeesStatsGrid extends DeesElement {
|
||||
|
||||
private renderTile(tile: IStatsTile): TemplateResult {
|
||||
const hasActions = tile.actions && tile.actions.length > 0;
|
||||
const clickable = hasActions && tile.actions.length === 1;
|
||||
const clickable = hasActions && tile.actions!.length === 1;
|
||||
const columnSpan = tile.columnSpan && tile.columnSpan > 1 ? tile.columnSpan : undefined;
|
||||
|
||||
return html`
|
||||
<div
|
||||
<dees-tile
|
||||
class="stats-tile ${clickable ? 'clickable' : ''}"
|
||||
style="${columnSpan ? `grid-column: span ${columnSpan}` : ''}"
|
||||
@click=${clickable ? () => this.handleTileAction(tile.actions![0], tile) : undefined}
|
||||
@contextmenu=${hasActions ? (e: MouseEvent) => this.showContextMenu(e, tile) : undefined}
|
||||
>
|
||||
<div class="tile-header">
|
||||
<div slot="header" class="tile-header">
|
||||
<h3 class="tile-title">${tile.title}</h3>
|
||||
${tile.icon ? html`
|
||||
<dees-icon class="tile-icon" .icon=${tile.icon} size="small"></dees-icon>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tile-content">
|
||||
${this.renderTileContent(tile)}
|
||||
</div>
|
||||
|
||||
|
||||
${tile.description && tile.type !== 'trend' ? html`
|
||||
<div class="tile-description">${tile.description}</div>
|
||||
<div slot="footer" class="tile-description">${tile.description}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { IStorageDataProvider, IStorageObject } from './interfaces.js';
|
||||
import './dees-storage-browser.js';
|
||||
|
||||
// Mock in-memory storage data provider for demo purposes
|
||||
class MockStorageDataProvider implements IStorageDataProvider {
|
||||
private objects: Map<string, { content: string; contentType: string; size: number; lastModified: string }> = new Map();
|
||||
|
||||
constructor() {
|
||||
const now = new Date().toISOString();
|
||||
// Seed with sample data
|
||||
this.objects.set('documents/readme.md', {
|
||||
content: btoa('# Welcome\n\nThis is a demo Storage browser.\n'),
|
||||
contentType: 'text/markdown',
|
||||
size: 42,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('documents/config.json', {
|
||||
content: btoa('{\n "name": "demo",\n "version": "1.0.0"\n}'),
|
||||
contentType: 'application/json',
|
||||
size: 48,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('documents/notes/todo.txt', {
|
||||
content: btoa('Buy milk\nFix bug #42\nDeploy to production'),
|
||||
contentType: 'text/plain',
|
||||
size: 45,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('images/logo.png', {
|
||||
content: btoa('fake-png-data'),
|
||||
contentType: 'image/png',
|
||||
size: 24500,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('images/banner.jpg', {
|
||||
content: btoa('fake-jpg-data'),
|
||||
contentType: 'image/jpeg',
|
||||
size: 156000,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('scripts/deploy.sh', {
|
||||
content: btoa('#!/bin/bash\necho "Deploying..."\n'),
|
||||
contentType: 'text/plain',
|
||||
size: 34,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('index.html', {
|
||||
content: btoa('<!DOCTYPE html>\n<html>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>'),
|
||||
contentType: 'text/html',
|
||||
size: 72,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('styles.css', {
|
||||
content: btoa('body { margin: 0; font-family: sans-serif; }'),
|
||||
contentType: 'text/css',
|
||||
size: 44,
|
||||
lastModified: now,
|
||||
});
|
||||
}
|
||||
|
||||
async listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IStorageObject[]; prefixes: string[] }> {
|
||||
const pfx = prefix || '';
|
||||
const objects: IStorageObject[] = [];
|
||||
const prefixes = new Set<string>();
|
||||
|
||||
for (const [key, data] of this.objects) {
|
||||
if (!key.startsWith(pfx)) continue;
|
||||
const rest = key.slice(pfx.length);
|
||||
|
||||
if (delimiter) {
|
||||
const slashIndex = rest.indexOf(delimiter);
|
||||
if (slashIndex >= 0) {
|
||||
prefixes.add(pfx + rest.slice(0, slashIndex + 1));
|
||||
} else {
|
||||
objects.push({ key, size: data.size, lastModified: data.lastModified });
|
||||
}
|
||||
} else {
|
||||
objects.push({ key, size: data.size, lastModified: data.lastModified });
|
||||
}
|
||||
}
|
||||
|
||||
return { objects, prefixes: Array.from(prefixes).sort() };
|
||||
}
|
||||
|
||||
async getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }> {
|
||||
const obj = this.objects.get(key);
|
||||
if (!obj) throw new Error('Not found');
|
||||
return { ...obj };
|
||||
}
|
||||
|
||||
async putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean> {
|
||||
this.objects.set(key, {
|
||||
content: base64Content,
|
||||
contentType,
|
||||
size: atob(base64Content).length,
|
||||
lastModified: new Date().toISOString(),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteObject(bucket: string, key: string): Promise<boolean> {
|
||||
return this.objects.delete(key);
|
||||
}
|
||||
|
||||
async deletePrefix(bucket: string, prefix: string): Promise<boolean> {
|
||||
for (const key of this.objects.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.objects.delete(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async getObjectUrl(bucket: string, key: string): Promise<string> {
|
||||
const obj = this.objects.get(key);
|
||||
if (!obj) return '';
|
||||
const blob = new Blob([Uint8Array.from(atob(obj.content), c => c.charCodeAt(0))], { type: obj.contentType });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }> {
|
||||
const obj = this.objects.get(sourceKey);
|
||||
if (!obj) return { success: false, error: 'Source not found' };
|
||||
this.objects.set(destKey, { ...obj, lastModified: new Date().toISOString() });
|
||||
this.objects.delete(sourceKey);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }> {
|
||||
let count = 0;
|
||||
const toMove = Array.from(this.objects.entries()).filter(([k]) => k.startsWith(sourcePrefix));
|
||||
for (const [key, data] of toMove) {
|
||||
const newKey = destPrefix + key.slice(sourcePrefix.length);
|
||||
this.objects.set(newKey, { ...data, lastModified: new Date().toISOString() });
|
||||
this.objects.delete(key);
|
||||
count++;
|
||||
}
|
||||
return { success: true, movedCount: count };
|
||||
}
|
||||
}
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
height: 600px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-storage-browser
|
||||
.dataProvider=${new MockStorageDataProvider()}
|
||||
.bucketName=${'demo-bucket'}
|
||||
></dees-storage-browser>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,439 @@
|
||||
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { demoFunc } from './dees-storage-browser.demo.js';
|
||||
import type { IStorageDataProvider, IStorageChangeEvent } from './interfaces.js';
|
||||
import './dees-storage-columns.js';
|
||||
import './dees-storage-keys.js';
|
||||
import './dees-storage-preview.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-storage-browser': DeesStorageBrowser;
|
||||
}
|
||||
}
|
||||
|
||||
type TViewType = 'columns' | 'keys';
|
||||
|
||||
@customElement('dees-storage-browser')
|
||||
export class DeesStorageBrowser extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Data View'];
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor dataProvider: IStorageDataProvider | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor bucketName: string = '';
|
||||
|
||||
/**
|
||||
* Optional change stream subscription.
|
||||
* Pass a function that takes a callback and returns an unsubscribe function.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
public accessor onChangeEvent: ((callback: (event: IStorageChangeEvent) => void) => (() => void)) | null = null;
|
||||
|
||||
@state()
|
||||
private accessor viewType: TViewType = 'columns';
|
||||
|
||||
@state()
|
||||
private accessor currentPrefix: string = '';
|
||||
|
||||
@state()
|
||||
private accessor selectedKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor refreshKey: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor previewWidth: number = 700;
|
||||
|
||||
@state()
|
||||
private accessor isResizingPreview: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor recentChangeCount: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor isStreamConnected: boolean = false;
|
||||
|
||||
private changeUnsubscribe: (() => void) | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.browser-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#999')};
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#fff')};
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: ${cssManager.bdTheme('#d4d4d8', '#555')};
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
border-color: ${cssManager.bdTheme('#a1a1aa', '#666')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#aaa')};
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border-color: ${cssManager.bdTheme('#a1a1aa', '#404040')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#e0e0e0')};
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr 4px var(--preview-width, 700px);
|
||||
}
|
||||
|
||||
.resize-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resize-divider:hover,
|
||||
.resize-divider.active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')};
|
||||
}
|
||||
|
||||
.main-view {
|
||||
overflow: auto;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content,
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel,
|
||||
.resize-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
margin-left: auto;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.stream-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#a1a1aa', '#888')};
|
||||
}
|
||||
|
||||
.stream-dot.connected {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.change-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: #f59e0b;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.change-indicator.pulse {
|
||||
animation: pulse-orange 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-orange {
|
||||
0% { background: rgba(245, 158, 11, 0.4); }
|
||||
100% { background: rgba(245, 158, 11, 0.2); }
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
this.unsubscribeFromChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to trigger a refresh of child components
|
||||
*/
|
||||
public refresh() {
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
private setViewType(type: TViewType) {
|
||||
this.viewType = type;
|
||||
}
|
||||
|
||||
private navigateToPrefix(prefix: string) {
|
||||
this.currentPrefix = prefix;
|
||||
this.selectedKey = '';
|
||||
}
|
||||
|
||||
private handleKeySelected(e: CustomEvent) {
|
||||
this.selectedKey = e.detail.key;
|
||||
}
|
||||
|
||||
private handleNavigate(e: CustomEvent) {
|
||||
this.navigateToPrefix(e.detail.prefix);
|
||||
}
|
||||
|
||||
private handleObjectDeleted(e: CustomEvent) {
|
||||
this.selectedKey = '';
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('bucketName')) {
|
||||
this.selectedKey = '';
|
||||
this.currentPrefix = '';
|
||||
this.recentChangeCount = 0;
|
||||
this.unsubscribeFromChanges();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
if (changedProperties.has('onChangeEvent')) {
|
||||
this.unsubscribeFromChanges();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToChanges() {
|
||||
if (!this.onChangeEvent) {
|
||||
this.isStreamConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.changeUnsubscribe = this.onChangeEvent((event: IStorageChangeEvent) => {
|
||||
this.handleChange(event);
|
||||
});
|
||||
this.isStreamConnected = true;
|
||||
} catch (error) {
|
||||
console.warn('[StorageBrowser] Failed to subscribe to changes:', error);
|
||||
this.isStreamConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsubscribeFromChanges() {
|
||||
if (this.changeUnsubscribe) {
|
||||
this.changeUnsubscribe();
|
||||
this.changeUnsubscribe = null;
|
||||
}
|
||||
this.isStreamConnected = false;
|
||||
}
|
||||
|
||||
private handleChange(event: IStorageChangeEvent) {
|
||||
this.recentChangeCount++;
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
private startPreviewResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizingPreview = true;
|
||||
document.addEventListener('mousemove', this.handlePreviewResize);
|
||||
document.addEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
private handlePreviewResize = (e: MouseEvent) => {
|
||||
if (!this.isResizingPreview) return;
|
||||
const contentEl = this.shadowRoot?.querySelector('.content');
|
||||
if (!contentEl) return;
|
||||
const containerRect = contentEl.getBoundingClientRect();
|
||||
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000);
|
||||
this.previewWidth = newWidth;
|
||||
};
|
||||
|
||||
private endPreviewResize = () => {
|
||||
this.isResizingPreview = false;
|
||||
document.removeEventListener('mousemove', this.handlePreviewResize);
|
||||
document.removeEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
render() {
|
||||
const breadcrumbParts = this.currentPrefix
|
||||
? this.currentPrefix.split('/').filter(Boolean)
|
||||
: [];
|
||||
|
||||
return html`
|
||||
<div class="browser-container">
|
||||
<div class="toolbar">
|
||||
<div class="breadcrumb">
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.navigateToPrefix('')}
|
||||
>
|
||||
${this.bucketName}
|
||||
</span>
|
||||
${breadcrumbParts.map((part, index) => {
|
||||
const prefix = breadcrumbParts.slice(0, index + 1).join('/') + '/';
|
||||
return html`
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.navigateToPrefix(prefix)}
|
||||
>
|
||||
${part}
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
${this.onChangeEvent ? html`
|
||||
<div class="stream-status">
|
||||
<span class="stream-dot ${this.isStreamConnected ? 'connected' : ''}"></span>
|
||||
${this.isStreamConnected ? 'Live' : 'Offline'}
|
||||
</div>
|
||||
` : ''}
|
||||
${this.recentChangeCount > 0
|
||||
? html`
|
||||
<div class="change-indicator pulse">
|
||||
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn ${this.viewType === 'columns' ? 'active' : ''}"
|
||||
@click=${() => this.setViewType('columns')}
|
||||
>
|
||||
Columns
|
||||
</button>
|
||||
<button
|
||||
class="view-btn ${this.viewType === 'keys' ? 'active' : ''}"
|
||||
@click=${() => this.setViewType('keys')}
|
||||
>
|
||||
List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content ${this.selectedKey ? 'has-preview' : ''}" style="--preview-width: ${this.previewWidth}px">
|
||||
<div class="main-view">
|
||||
${this.viewType === 'columns'
|
||||
? html`
|
||||
<dees-storage-columns
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></dees-storage-columns>
|
||||
`
|
||||
: html`
|
||||
<dees-storage-keys
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></dees-storage-keys>
|
||||
`}
|
||||
</div>
|
||||
|
||||
${this.selectedKey
|
||||
? html`
|
||||
<div
|
||||
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
|
||||
@mousedown=${this.startPreviewResize}
|
||||
></div>
|
||||
<div class="preview-panel">
|
||||
<dees-storage-preview
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.objectKey=${this.selectedKey}
|
||||
@object-deleted=${this.handleObjectDeleted}
|
||||
></dees-storage-preview>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,540 @@
|
||||
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import type { IStorageDataProvider } from './interfaces.js';
|
||||
import { formatSize, getFileName } from './utilities.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-storage-preview': DeesStoragePreview;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-storage-preview')
|
||||
export class DeesStoragePreview extends DeesElement {
|
||||
@property({ type: Object })
|
||||
public accessor dataProvider: IStorageDataProvider | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor bucketName: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public accessor objectKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor saving: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor content: string = '';
|
||||
|
||||
@state()
|
||||
private accessor originalTextContent: string = '';
|
||||
|
||||
@state()
|
||||
private accessor hasChanges: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor editing: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor contentType: string = '';
|
||||
|
||||
@state()
|
||||
private accessor fileSize: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor lastModified: string = '';
|
||||
|
||||
@state()
|
||||
private accessor error: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content dees-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-content.code-editor {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content.code-editor dees-input-code {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
padding: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#404040')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#e0e0e0')};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.15)')};
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: #ef4444;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
border-color: #3b82f6;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.action-btn.primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
|
||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#555')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#aaa')};
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#fff')};
|
||||
}
|
||||
|
||||
.unsaved-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.unsaved-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fbbf24;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#a1a1aa', '#666')};
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
}
|
||||
|
||||
.error-state {
|
||||
padding: 16px;
|
||||
color: #f87171;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('objectKey') || changedProperties.has('bucketName')) {
|
||||
if (this.objectKey) {
|
||||
this.loadObject();
|
||||
} else {
|
||||
this.content = '';
|
||||
this.contentType = '';
|
||||
this.error = '';
|
||||
this.originalTextContent = '';
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadObject() {
|
||||
if (!this.objectKey || !this.bucketName || !this.dataProvider) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
|
||||
try {
|
||||
const result = await this.dataProvider.getObject(this.bucketName, this.objectKey);
|
||||
if (!result) {
|
||||
this.error = 'Object not found';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.content = result.content || '';
|
||||
this.contentType = result.contentType || '';
|
||||
this.fileSize = result.size || 0;
|
||||
this.lastModified = result.lastModified || '';
|
||||
|
||||
if (this.isText()) {
|
||||
this.originalTextContent = this.getTextContent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading object:', err);
|
||||
this.error = 'Failed to load object';
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
private isImage(): boolean {
|
||||
return this.contentType.startsWith('image/');
|
||||
}
|
||||
|
||||
private isText(): boolean {
|
||||
return (
|
||||
this.contentType.startsWith('text/') ||
|
||||
this.contentType === 'application/json' ||
|
||||
this.contentType === 'application/xml' ||
|
||||
this.contentType === 'application/javascript'
|
||||
);
|
||||
}
|
||||
|
||||
private getTextContent(): string {
|
||||
try {
|
||||
const binaryString = atob(this.content);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
} catch {
|
||||
return 'Unable to decode content';
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownload() {
|
||||
try {
|
||||
const blob = new Blob([Uint8Array.from(atob(this.content), (c) => c.charCodeAt(0))], {
|
||||
type: this.contentType,
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = getFileName(this.objectKey);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Error downloading:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDelete() {
|
||||
if (!this.dataProvider) return;
|
||||
if (!confirm(`Delete "${getFileName(this.objectKey)}"?`)) return;
|
||||
|
||||
try {
|
||||
await this.dataProvider.deleteObject(this.bucketName, this.objectKey);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('object-deleted', {
|
||||
detail: { key: this.objectKey },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error deleting object:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private getLanguage(): string {
|
||||
const ext = this.objectKey.split('.').pop()?.toLowerCase() || '';
|
||||
const languageMap: Record<string, string> = {
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
js: 'javascript',
|
||||
jsx: 'javascript',
|
||||
mjs: 'javascript',
|
||||
cjs: 'javascript',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
htm: 'html',
|
||||
css: 'css',
|
||||
scss: 'scss',
|
||||
sass: 'scss',
|
||||
less: 'less',
|
||||
md: 'markdown',
|
||||
markdown: 'markdown',
|
||||
xml: 'xml',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
py: 'python',
|
||||
rb: 'ruby',
|
||||
go: 'go',
|
||||
rs: 'rust',
|
||||
java: 'java',
|
||||
c: 'c',
|
||||
cpp: 'cpp',
|
||||
h: 'c',
|
||||
hpp: 'cpp',
|
||||
cs: 'csharp',
|
||||
php: 'php',
|
||||
sh: 'shell',
|
||||
bash: 'shell',
|
||||
zsh: 'shell',
|
||||
sql: 'sql',
|
||||
graphql: 'graphql',
|
||||
gql: 'graphql',
|
||||
dockerfile: 'dockerfile',
|
||||
txt: 'plaintext',
|
||||
};
|
||||
return languageMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
private handleContentChange(event: CustomEvent) {
|
||||
const newValue = event.detail as string;
|
||||
this.hasChanges = newValue !== this.originalTextContent;
|
||||
}
|
||||
|
||||
private handleEdit() {
|
||||
this.editing = true;
|
||||
}
|
||||
|
||||
private handleDiscard() {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
if (codeEditor) {
|
||||
codeEditor.value = this.originalTextContent;
|
||||
}
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
}
|
||||
|
||||
private async handleSave() {
|
||||
if (!this.hasChanges || this.saving || !this.dataProvider) return;
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
const currentContent = codeEditor?.value ?? '';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(currentContent);
|
||||
const base64Content = btoa(String.fromCharCode(...bytes));
|
||||
|
||||
const success = await this.dataProvider.putObject(
|
||||
this.bucketName,
|
||||
this.objectKey,
|
||||
base64Content,
|
||||
this.contentType
|
||||
);
|
||||
|
||||
if (success) {
|
||||
this.originalTextContent = currentContent;
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
this.content = base64Content;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving object:', err);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.objectKey) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<p>Select a file to preview</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="loading-state">Loading...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="error-state">${this.error}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">${getFileName(this.objectKey)}</div>
|
||||
<div class="preview-meta">
|
||||
<span class="meta-item">${this.contentType}</span>
|
||||
<span class="meta-item">${formatSize(this.fileSize)}</span>
|
||||
<span class="meta-item">${this.formatDate(this.lastModified)}</span>
|
||||
${this.hasChanges ? html`
|
||||
<span class="unsaved-indicator">
|
||||
<span class="unsaved-dot"></span>
|
||||
Unsaved changes
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-content ${this.editing ? 'code-editor' : ''}">
|
||||
${this.editing
|
||||
? html`
|
||||
<dees-input-code
|
||||
.value=${this.originalTextContent}
|
||||
.language=${this.getLanguage()}
|
||||
height="100%"
|
||||
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
|
||||
></dees-input-code>
|
||||
`
|
||||
: this.isText()
|
||||
? html`
|
||||
<dees-preview
|
||||
.textContent=${this.originalTextContent}
|
||||
.filename=${getFileName(this.objectKey)}
|
||||
.language=${this.getLanguage()}
|
||||
.showToolbar=${true}
|
||||
.showFilename=${false}
|
||||
></dees-preview>
|
||||
`
|
||||
: html`
|
||||
<dees-preview
|
||||
.base64=${this.content}
|
||||
.mimeType=${this.contentType}
|
||||
.filename=${getFileName(this.objectKey)}
|
||||
.showToolbar=${true}
|
||||
.showFilename=${false}
|
||||
></dees-preview>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="preview-actions">
|
||||
${this.editing
|
||||
? html`
|
||||
<button class="action-btn secondary" @click=${this.handleDiscard}>
|
||||
${this.hasChanges ? 'Discard' : 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click=${this.handleSave}
|
||||
?disabled=${this.saving || !this.hasChanges}
|
||||
>
|
||||
${this.saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${this.isText()
|
||||
? html`<button class="action-btn" @click=${this.handleEdit}>Edit</button>`
|
||||
: ''}
|
||||
<button class="action-btn" @click=${this.handleDownload}>Download</button>
|
||||
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './dees-storage-browser.js';
|
||||
export * from './dees-storage-columns.js';
|
||||
export * from './dees-storage-keys.js';
|
||||
export * from './dees-storage-preview.js';
|
||||
export * from './interfaces.js';
|
||||
export { formatSize, formatCount, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Storage Data Provider interface - implement this to connect the storage browser to your backend
|
||||
*/
|
||||
|
||||
export interface IStorageObject {
|
||||
key: string;
|
||||
size?: number;
|
||||
lastModified?: string;
|
||||
isPrefix?: boolean;
|
||||
}
|
||||
|
||||
export interface IStorageChangeEvent {
|
||||
type: 'add' | 'modify' | 'delete';
|
||||
key: string;
|
||||
bucket: string;
|
||||
size?: number;
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export interface IStorageDataProvider {
|
||||
listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IStorageObject[]; prefixes: string[] }>;
|
||||
getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }>;
|
||||
putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean>;
|
||||
deleteObject(bucket: string, key: string): Promise<boolean>;
|
||||
deletePrefix(bucket: string, prefix: string): Promise<boolean>;
|
||||
getObjectUrl(bucket: string, key: string): Promise<string>;
|
||||
moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }>;
|
||||
movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }>;
|
||||
}
|
||||
|
||||
export interface IColumn {
|
||||
prefix: string;
|
||||
objects: IStorageObject[];
|
||||
prefixes: string[];
|
||||
selectedItem: string | null;
|
||||
width: number;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Shared utilities for Storage browser components
|
||||
*/
|
||||
|
||||
export interface IMoveValidation {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a byte size into a human-readable string
|
||||
*/
|
||||
export function formatSize(bytes?: number): string {
|
||||
if (bytes === undefined || bytes === null) return '-';
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a count into a compact human-readable string
|
||||
*/
|
||||
export function formatCount(count?: number): string {
|
||||
if (count === undefined || count === null) return '';
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file name from a path
|
||||
*/
|
||||
export function getFileName(path: string): string {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a move operation is allowed
|
||||
*/
|
||||
export function validateMove(sourceKey: string, destPrefix: string): IMoveValidation {
|
||||
if (sourceKey.endsWith('/')) {
|
||||
if (destPrefix.startsWith(sourceKey)) {
|
||||
return { valid: false, error: 'Cannot move a folder into itself' };
|
||||
}
|
||||
}
|
||||
|
||||
const sourceParent = getParentPrefix(sourceKey);
|
||||
if (sourceParent === destPrefix) {
|
||||
return { valid: false, error: 'Item is already in this location' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent prefix (directory) of a given key
|
||||
*/
|
||||
export function getParentPrefix(key: string): string {
|
||||
const trimmed = key.endsWith('/') ? key.slice(0, -1) : key;
|
||||
const lastSlash = trimmed.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? trimmed.substring(0, lastSlash + 1) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type from file extension
|
||||
*/
|
||||
export function getContentType(ext: string): string {
|
||||
const contentTypes: Record<string, string> = {
|
||||
json: 'application/json',
|
||||
txt: 'text/plain',
|
||||
html: 'text/html',
|
||||
css: 'text/css',
|
||||
js: 'application/javascript',
|
||||
ts: 'text/typescript',
|
||||
md: 'text/markdown',
|
||||
xml: 'application/xml',
|
||||
yaml: 'text/yaml',
|
||||
yml: 'text/yaml',
|
||||
csv: 'text/csv',
|
||||
};
|
||||
return contentTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default content for a new file based on extension
|
||||
*/
|
||||
export function getDefaultContent(ext: string): string {
|
||||
const defaults: Record<string, string> = {
|
||||
json: '{\n \n}',
|
||||
html: '<!DOCTYPE html>\n<html>\n<head>\n <title></title>\n</head>\n<body>\n \n</body>\n</html>',
|
||||
md: '# Title\n\n',
|
||||
txt: '',
|
||||
};
|
||||
return defaults[ext] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a prefix into cumulative path segments
|
||||
*/
|
||||
export function getPathSegments(prefix: string): string[] {
|
||||
if (!prefix) return [];
|
||||
const parts = prefix.split('/').filter(p => p);
|
||||
const segments: string[] = [];
|
||||
let cumulative = '';
|
||||
for (const part of parts) {
|
||||
cumulative += part + '/';
|
||||
segments.push(cumulative);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Column, TDisplayFunction } from './types.js';
|
||||
import type { Column, ISortDescriptor, TDisplayFunction } from './types.js';
|
||||
|
||||
export function computeColumnsFromDisplayFunction<T>(
|
||||
displayFunction: TDisplayFunction<T>,
|
||||
@@ -36,11 +36,31 @@ export function getCellValue<T>(row: T, col: Column<T>, displayFunction?: TDispl
|
||||
return col.value ? col.value(row) : (row as any)[col.key as any];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two cell values in ascending order. Returns -1, 0, or 1.
|
||||
* Null/undefined values sort before defined values. Numbers compare numerically;
|
||||
* everything else compares as case-insensitive strings.
|
||||
*/
|
||||
export function compareCellValues(va: any, vb: any): number {
|
||||
if (va == null && vb == null) return 0;
|
||||
if (va == null) return -1;
|
||||
if (vb == null) return 1;
|
||||
if (typeof va === 'number' && typeof vb === 'number') {
|
||||
if (va < vb) return -1;
|
||||
if (va > vb) return 1;
|
||||
return 0;
|
||||
}
|
||||
const sa = String(va).toLowerCase();
|
||||
const sb = String(vb).toLowerCase();
|
||||
if (sa < sb) return -1;
|
||||
if (sa > sb) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getViewData<T>(
|
||||
data: T[],
|
||||
effectiveColumns: Column<T>[],
|
||||
sortKey?: string,
|
||||
sortDir?: 'asc' | 'desc' | null,
|
||||
sortBy: ISortDescriptor[],
|
||||
filterText?: string,
|
||||
columnFilters?: Record<string, string>,
|
||||
filterMode: 'table' | 'data' = 'table',
|
||||
@@ -94,21 +114,17 @@ export function getViewData<T>(
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (!sortKey || !sortDir) return arr;
|
||||
const col = effectiveColumns.find((c) => String(c.key) === sortKey);
|
||||
if (!col) return arr;
|
||||
const dir = sortDir === 'asc' ? 1 : -1;
|
||||
if (!sortBy || sortBy.length === 0) return arr;
|
||||
// Pre-resolve descriptors -> columns once for performance.
|
||||
const resolved = sortBy
|
||||
.map((desc) => ({ desc, col: effectiveColumns.find((c) => String(c.key) === desc.key) }))
|
||||
.filter((entry): entry is { desc: ISortDescriptor; col: Column<T> } => !!entry.col);
|
||||
if (resolved.length === 0) return arr;
|
||||
arr.sort((a, b) => {
|
||||
const va = getCellValue(a, col);
|
||||
const vb = getCellValue(b, col);
|
||||
if (va == null && vb == null) return 0;
|
||||
if (va == null) return -1 * dir;
|
||||
if (vb == null) return 1 * dir;
|
||||
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
||||
const sa = String(va).toLowerCase();
|
||||
const sb = String(vb).toLowerCase();
|
||||
if (sa < sb) return -1 * dir;
|
||||
if (sa > sb) return 1 * dir;
|
||||
for (const { desc, col } of resolved) {
|
||||
const cmp = compareCellValues(getCellValue(a, col), getCellValue(b, col));
|
||||
if (cmp !== 0) return desc.dir === 'asc' ? cmp : -cmp;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return arr;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ITableAction } from './dees-table.js';
|
||||
import * as plugins from '../../00plugins.js';
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
interface ITableDemoData {
|
||||
date: string;
|
||||
@@ -55,36 +56,66 @@ export const demoFunc = () => html`
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Basic Table with Actions</h2>
|
||||
<p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
|
||||
<p class="demo-description">A standard table with row actions, editable cells, and context menu support. Double-click any cell to edit. Tab moves to the next editable cell, Enter to the row below, Esc cancels.</p>
|
||||
<dees-table
|
||||
heading1="Current Account Statement"
|
||||
heading2="Bunq - Payment Account 2 - April 2021"
|
||||
.editableFields="${['description']}"
|
||||
.columns=${[
|
||||
{ key: 'date', header: 'Date', sortable: true, editable: true, editor: 'date' },
|
||||
{ key: 'amount', header: 'Amount', editable: true, editor: 'text' },
|
||||
{
|
||||
key: 'category',
|
||||
header: 'Category',
|
||||
editable: true,
|
||||
editor: 'dropdown',
|
||||
editorOptions: {
|
||||
options: [
|
||||
{ option: 'Office Supplies', key: 'office' },
|
||||
{ option: 'Hardware', key: 'hardware' },
|
||||
{ option: 'Software', key: 'software' },
|
||||
{ option: 'Travel', key: 'travel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ key: 'description', header: 'Description', editable: true },
|
||||
{ key: 'reconciled', header: 'OK', editable: true, editor: 'checkbox' },
|
||||
]}
|
||||
@cellEdit=${(e: CustomEvent) => console.log('cellEdit', e.detail)}
|
||||
.data=${[
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
|
||||
category: 'office',
|
||||
description: 'Printing Paper - STAPLES BREMEN',
|
||||
reconciled: true,
|
||||
},
|
||||
{
|
||||
date: '2021-04-02',
|
||||
amount: '165.65 €',
|
||||
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
|
||||
category: 'hardware',
|
||||
description: 'Logitech Mouse - logi.com OnlineShop',
|
||||
reconciled: false,
|
||||
},
|
||||
{
|
||||
date: '2021-04-03',
|
||||
amount: '2999,00 €',
|
||||
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
|
||||
category: 'hardware',
|
||||
description: 'Macbook Pro 16inch - Apple.de OnlineShop',
|
||||
reconciled: false,
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
category: 'office',
|
||||
description: 'Office-Supplies - STAPLES BREMEN',
|
||||
reconciled: true,
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
category: 'office',
|
||||
description: 'Office-Supplies - STAPLES BREMEN',
|
||||
reconciled: true,
|
||||
},
|
||||
]}
|
||||
dataName="transactions"
|
||||
@@ -510,13 +541,13 @@ export const demoFunc = () => html`
|
||||
<h2 class="demo-title">Column Filters + Sticky Header (New)</h2>
|
||||
<p class="demo-description">Per-column quick filters and sticky header with internal scroll. Try filtering the Name column. Uses --table-max-height var.</p>
|
||||
<style>
|
||||
dees-table[sticky-header] { --table-max-height: 220px; }
|
||||
dees-table[fixed-height] { --table-max-height: 220px; }
|
||||
</style>
|
||||
<dees-table
|
||||
heading1="Employees"
|
||||
heading2="Quick filter per column + sticky header"
|
||||
.showColumnFilters=${true}
|
||||
.stickyHeader=${true}
|
||||
.fixedHeight=${true}
|
||||
.columns=${[
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'email', header: 'Email', sortable: true },
|
||||
@@ -580,6 +611,44 @@ export const demoFunc = () => html`
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Multi-Column Sort</h2>
|
||||
<p class="demo-description">
|
||||
Click any column header for a single-column sort. Hold Shift while clicking to add the
|
||||
column to a multi-sort cascade (or cycle its direction). Right-click any sortable header
|
||||
to open a menu where you can pin a column to a specific priority slot, remove it, or
|
||||
clear the cascade.
|
||||
</p>
|
||||
<dees-table
|
||||
heading1="People Directory"
|
||||
heading2="Pre-seeded with department ▲ 1, name ▲ 2"
|
||||
.sortBy=${[
|
||||
{ key: 'department', dir: 'asc' },
|
||||
{ key: 'name', dir: 'asc' },
|
||||
]}
|
||||
.columns=${[
|
||||
{ key: 'department', header: 'Department', sortable: true },
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'role', header: 'Role', sortable: true },
|
||||
{ key: 'createdAt', header: 'Created', sortable: true },
|
||||
{ key: 'location', header: 'Location', sortable: true },
|
||||
{ key: 'status', header: 'Status', sortable: true },
|
||||
]}
|
||||
.data=${[
|
||||
{ department: 'R&D', name: 'Alice Johnson', role: 'Engineer', createdAt: '2023-01-12', location: 'Berlin', status: 'Active' },
|
||||
{ department: 'R&D', name: 'Diana Martinez', role: 'Engineer', createdAt: '2020-06-30', location: 'Madrid', status: 'Active' },
|
||||
{ department: 'R&D', name: 'Mark Lee', role: 'Engineer', createdAt: '2024-03-04', location: 'Berlin', status: 'Active' },
|
||||
{ department: 'Design', name: 'Bob Smith', role: 'Designer', createdAt: '2022-11-05', location: 'Paris', status: 'Active' },
|
||||
{ department: 'Design', name: 'Sara Kim', role: 'Designer', createdAt: '2021-08-19', location: 'Paris', status: 'On Leave' },
|
||||
{ department: 'Ops', name: 'Charlie Davis', role: 'Manager', createdAt: '2021-04-21', location: 'London', status: 'On Leave' },
|
||||
{ department: 'Ops', name: 'Helena Voss', role: 'SRE', createdAt: '2023-07-22', location: 'London', status: 'Active' },
|
||||
{ department: 'QA', name: 'Fiona Clark', role: 'QA', createdAt: '2022-03-14', location: 'Vienna', status: 'Active' },
|
||||
{ department: 'QA', name: 'Tomás Rivera', role: 'QA', createdAt: '2024-01-09', location: 'Madrid', status: 'Active' },
|
||||
{ department: 'CS', name: 'Ethan Brown', role: 'Support', createdAt: '2019-09-18', location: 'Rome', status: 'Inactive' },
|
||||
]}
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Wide Properties + Many Actions</h2>
|
||||
<p class="demo-description">A table with many columns and rich actions to stress test layout and sticky Actions.</p>
|
||||
@@ -631,8 +700,9 @@ export const demoFunc = () => html`
|
||||
</style>
|
||||
<dees-table
|
||||
id="scrollSmallHeight"
|
||||
.stickyHeader=${true}
|
||||
heading1="People Directory (Scrollable)"
|
||||
.fixedHeight=${true}
|
||||
.virtualized=${true}
|
||||
heading1="People Directory (Scrollable, Virtualized)"
|
||||
heading2="Forced scrolling with many items"
|
||||
.columns=${[
|
||||
{ key: 'id', header: 'ID', sortable: true },
|
||||
@@ -673,6 +743,71 @@ export const demoFunc = () => html`
|
||||
] as ITableAction[]}
|
||||
></dees-table>
|
||||
</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>
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,17 +12,11 @@ export const tableStyles: CSSResult[] = [
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
dees-tile {
|
||||
color: var(--dees-color-text-primary);
|
||||
font-family: ${cssGeistFontFamily};
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -30,30 +24,31 @@ export const tableStyles: CSSResult[] = [
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
min-height: 64px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
padding: 0 16px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.headingContainer {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.heading {
|
||||
line-height: 1.5;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.heading1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--dees-color-text-secondary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
margin-top: 2px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.headingSeparation {
|
||||
@@ -70,22 +65,22 @@ export const tableStyles: CSSResult[] = [
|
||||
.headerAction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.headerAction:hover {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
background: var(--dees-color-hover);
|
||||
border-color: var(--dees-color-border-strong);
|
||||
}
|
||||
|
||||
.headerAction dees-icon {
|
||||
@@ -98,8 +93,8 @@ export const tableStyles: CSSResult[] = [
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: 1fr max-content;
|
||||
padding: 16px 24px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(0 0% 3.9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-bg-secondary);
|
||||
border-bottom: 1px solid var(--dees-color-border-default);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -119,29 +114,48 @@ export const tableStyles: CSSResult[] = [
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
/* Default mode (Mode B, page sticky): horizontal scroll lives on
|
||||
.tableScroll (so wide tables don't get clipped by an ancestor
|
||||
overflow:hidden such as dees-tile). Vertical sticky is handled by
|
||||
a JS-managed floating header (.floatingHeader, position:fixed),
|
||||
which is unaffected by ancestor overflow. */
|
||||
.tableScroll {
|
||||
/* enable horizontal scroll only when content exceeds width */
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
/* prevent vertical scroll inside the table container */
|
||||
overflow-y: hidden;
|
||||
/* avoid reserving extra space for classic scrollbars where possible */
|
||||
scrollbar-gutter: stable both-edges;
|
||||
overflow-y: visible;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
/* Hide horizontal scrollbar entirely when not using sticky header */
|
||||
:host(:not([sticky-header])) .tableScroll {
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
scrollbar-width: none; /* Firefox (hides both axes) */
|
||||
}
|
||||
:host(:not([sticky-header])) .tableScroll::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
/* In sticky-header mode, hide only the horizontal scrollbar in WebKit/Blink */
|
||||
:host([sticky-header]) .tableScroll::-webkit-scrollbar:horizontal {
|
||||
height: 0px;
|
||||
}
|
||||
:host([sticky-header]) .tableScroll {
|
||||
/* Mode A, internal scroll: opt-in via fixed-height attribute.
|
||||
The table scrolls inside its own box and the header sticks via plain CSS sticky. */
|
||||
:host([fixed-height]) .tableScroll {
|
||||
max-height: var(--table-max-height, 360px);
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
:host([fixed-height]) .tableScroll::-webkit-scrollbar:horizontal {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
/* Floating header overlay (Mode B). Position is managed by JS so it
|
||||
escapes any ancestor overflow:hidden (position:fixed is not clipped
|
||||
by overflow ancestors). */
|
||||
.floatingHeader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.floatingHeader.active {
|
||||
visibility: visible;
|
||||
}
|
||||
.floatingHeader table {
|
||||
margin: 0;
|
||||
}
|
||||
.floatingHeader th {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -162,22 +176,32 @@ export const tableStyles: CSSResult[] = [
|
||||
|
||||
thead {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
border-bottom: 1px solid var(--dees-color-border-strong);
|
||||
}
|
||||
:host([sticky-header]) thead th {
|
||||
/* th needs its own background so sticky cells paint over scrolled rows
|
||||
(browsers don't paint the <thead> box behind a sticky <th>). */
|
||||
th {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
}
|
||||
/* Mode A — internal scroll sticky */
|
||||
:host([fixed-height]) thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
:host([fixed-height]) thead tr.filtersRow th {
|
||||
top: 36px; /* matches th { height: 36px } below */
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
transition: background-color 0.15s ease;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Default horizontal lines (bottom border only) */
|
||||
tbody tr {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-bottom: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
tbody tr:last-child {
|
||||
@@ -186,8 +210,8 @@ export const tableStyles: CSSResult[] = [
|
||||
|
||||
/* Full horizontal lines when enabled */
|
||||
:host([show-horizontal-lines]) tbody tr {
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-top: 1px solid var(--dees-color-border-default);
|
||||
border-bottom: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
:host([show-horizontal-lines]) tbody tr:first-child {
|
||||
@@ -195,11 +219,11 @@ export const tableStyles: CSSResult[] = [
|
||||
}
|
||||
|
||||
:host([show-horizontal-lines]) tbody tr:last-child {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-bottom: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')};
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.06)', 'hsl(217.2 91.2% 59.8% / 0.08)')};
|
||||
}
|
||||
|
||||
/* Column hover effect for better traceability */
|
||||
@@ -214,7 +238,7 @@ export const tableStyles: CSSResult[] = [
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.3)', 'hsl(0 0% 14.9% / 0.3)')};
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.04)', 'hsl(217.2 91.2% 59.8% / 0.05)')};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
@@ -227,41 +251,45 @@ export const tableStyles: CSSResult[] = [
|
||||
|
||||
/* Grid mode - shows both vertical and horizontal lines */
|
||||
:host([show-grid]) th {
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:host([show-grid]) td {
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:host([show-grid]) th:first-child,
|
||||
:host([show-grid]) td:first-child {
|
||||
border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
:host([show-grid]) tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Remove edge borders that would double with tile frame */
|
||||
:host([show-grid]) th:last-child,
|
||||
:host([show-grid]) td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
:host([show-grid]) tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Sticky Actions column (right pinned) */
|
||||
thead th.actionsCol,
|
||||
tbody td.actionsCol {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
}
|
||||
thead th.actionsCol { z-index: 3; }
|
||||
tbody td.actionsCol {
|
||||
z-index: 1;
|
||||
box-shadow: -1px 0 0 0 ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
box-shadow: -1px 0 0 0 var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
tbody tr.selected {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.12)')};
|
||||
}
|
||||
|
||||
tbody tr.hasAttachment {
|
||||
@@ -269,74 +297,211 @@ export const tableStyles: CSSResult[] = [
|
||||
}
|
||||
|
||||
th {
|
||||
height: 48px;
|
||||
padding: 12px 24px;
|
||||
height: 36px;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
th[role='columnheader']:hover {
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
th .sortArrow {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
opacity: 0.7;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
th .sortBadge {
|
||||
display: inline-block;
|
||||
margin-left: 3px;
|
||||
padding: 1px 5px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: ${cssManager.bdTheme('hsl(222.2 47.4% 30%)', 'hsl(217.2 91.2% 75%)')};
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.12)', 'hsl(217.2 91.2% 59.8% / 0.18)')};
|
||||
border-radius: 999px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:host([show-vertical-lines]) th {
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-right: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 24px;
|
||||
padding: 8px 16px;
|
||||
vertical-align: middle;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-size: 13px;
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
:host([show-vertical-lines]) td {
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-right: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 24px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 24px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
:host([show-vertical-lines]) th:last-child,
|
||||
:host([show-vertical-lines]) td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Default bottom border on last row removed — tile frame handles it */
|
||||
:host(:not([show-horizontal-lines])) tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.innerCellContainer {
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
td input {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 8px);
|
||||
padding: 0 12px;
|
||||
outline: none;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
|
||||
td input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')};
|
||||
|
||||
.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); }
|
||||
}
|
||||
|
||||
/* Border/background flash variant for cells with styled content
|
||||
(badges, icons, custom components) where a text-color animation
|
||||
would be invisible. Activated via flashBorder on Column. */
|
||||
.innerCellContainer.flashing-border {
|
||||
animation: dees-table-cell-flash-border
|
||||
var(--dees-table-flash-duration, 900ms)
|
||||
var(--dees-table-flash-easing);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@keyframes dees-table-cell-flash-border {
|
||||
0%,
|
||||
35% {
|
||||
box-shadow: inset 0 0 0 1.5px var(--dees-table-flash-color);
|
||||
background: ${cssManager.bdTheme(
|
||||
'hsl(45 93% 62% / 0.10)',
|
||||
'hsl(45 93% 62% / 0.08)'
|
||||
)};
|
||||
}
|
||||
100% {
|
||||
box-shadow: inset 0 0 0 1.5px transparent;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.innerCellContainer.flashing {
|
||||
animation: none;
|
||||
color: var(--dees-table-flash-color);
|
||||
}
|
||||
.innerCellContainer.flashing-border {
|
||||
animation: none;
|
||||
box-shadow: inset 0 0 0 1.5px var(--dees-table-flash-color);
|
||||
background: ${cssManager.bdTheme(
|
||||
'hsl(45 93% 62% / 0.10)',
|
||||
'hsl(45 93% 62% / 0.08)'
|
||||
)};
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
td.editable {
|
||||
cursor: text;
|
||||
}
|
||||
td.focused {
|
||||
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 {
|
||||
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 {
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
td.editingCell dees-input-text,
|
||||
td.editingCell dees-input-checkbox,
|
||||
td.editingCell dees-input-dropdown,
|
||||
td.editingCell dees-input-datepicker,
|
||||
td.editingCell dees-input-tags {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* filter row */
|
||||
@@ -349,9 +514,9 @@ export const tableStyles: CSSResult[] = [
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
.actionsContainer {
|
||||
display: flex;
|
||||
@@ -389,12 +554,12 @@ export const tableStyles: CSSResult[] = [
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-muted);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tableStatistics {
|
||||
@@ -409,9 +574,10 @@ export const tableStyles: CSSResult[] = [
|
||||
.footerActions .footerAction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -15,15 +15,84 @@ export interface ITableAction<T = any> {
|
||||
actionFunc: (actionDataArg: ITableActionDataArg<T>) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Available cell editor types. Each maps to a dees-input-* component.
|
||||
* Use `editor` on `Column<T>` to opt a column into in-cell editing.
|
||||
*/
|
||||
export type TCellEditorType =
|
||||
| 'text'
|
||||
| 'number'
|
||||
| 'checkbox'
|
||||
| 'dropdown'
|
||||
| 'date'
|
||||
| 'tags';
|
||||
|
||||
/** Detail payload for the `cellEdit` CustomEvent dispatched on commit. */
|
||||
export interface ICellEditDetail<T = any> {
|
||||
row: T;
|
||||
key: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
}
|
||||
|
||||
/** Detail payload for the `cellEditError` CustomEvent dispatched on validation failure. */
|
||||
export interface ICellEditErrorDetail<T = any> {
|
||||
row: T;
|
||||
key: string;
|
||||
value: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Column<T = any> {
|
||||
key: keyof T | string;
|
||||
header?: string | TemplateResult;
|
||||
value?: (row: T) => any;
|
||||
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;
|
||||
/** whether this column participates in per-column quick filtering (default: true) */
|
||||
filterable?: boolean;
|
||||
hidden?: boolean;
|
||||
/** Marks the column as editable. Shorthand for `editor: 'text'` if no editor is specified. */
|
||||
editable?: boolean;
|
||||
/** Editor type — picks the dees-input-* component used for in-cell editing. */
|
||||
editor?: TCellEditorType;
|
||||
/** Editor-specific options forwarded to the editor (e.g. `{ options: [...] }` for dropdowns). */
|
||||
editorOptions?: Record<string, any>;
|
||||
/** Convert raw row value -> editor value. Defaults to identity. */
|
||||
format?: (raw: any, row: T) => any;
|
||||
/** Convert editor value -> raw row value. Defaults to identity. */
|
||||
parse?: (editorValue: any, row: T) => any;
|
||||
/** Validate the parsed value before commit. Return string for error, true/void for ok. */
|
||||
validate?: (value: any, row: T) => true | string | void;
|
||||
|
||||
// ─── Flash highlight options ───
|
||||
|
||||
/**
|
||||
* Custom comparison for flash-on-update diffing.
|
||||
* Return `true` if the cell should flash (i.e. the values differ).
|
||||
* When absent, non-primitive cell values are skipped entirely
|
||||
* (only strings, numbers, booleans, null, and undefined are diffed).
|
||||
*/
|
||||
flashCompare?: (prevVal: any, nextVal: any) => boolean;
|
||||
|
||||
/**
|
||||
* When `true`, flash this cell with a border/background pulse instead of
|
||||
* the default text-color animation. Useful for cells containing styled
|
||||
* badges, icons, or custom web-component renderings where a text-color
|
||||
* change would be invisible.
|
||||
* @default false
|
||||
*/
|
||||
flashBorder?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry in a multi-column sort cascade. Order in the array reflects priority:
|
||||
* index 0 is the primary sort key, index 1 the secondary tiebreaker, and so on.
|
||||
*/
|
||||
export interface ISortDescriptor {
|
||||
key: string;
|
||||
dir: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export type TDisplayFunction<T = any> = (itemArg: T) => Record<string, any>;
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './dees-dataview-codebox/index.js';
|
||||
export * from './dees-dataview-statusobject/index.js';
|
||||
export * from './dees-table/index.js';
|
||||
export * from './dees-statsgrid/index.js';
|
||||
export * from './dees-storage-browser/index.js';
|
||||
|
||||
@@ -89,7 +89,7 @@ export class DeesProgressbar extends DeesElement {
|
||||
}
|
||||
|
||||
public async updatePercentage() {
|
||||
const progressBarFill = this.shadowRoot.querySelector('.progressBarFill') as HTMLElement;
|
||||
const progressBarFill = this.shadowRoot!.querySelector('.progressBarFill') as HTMLElement;
|
||||
progressBarFill.style.width = `${this.percentage}%`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,85 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`<dees-form-submit>Submit Form</dees-form-submit>`;
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
.demo-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.demo-section h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
}
|
||||
.demo-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Usage</h3>
|
||||
<div class="demo-row">
|
||||
<dees-form-submit>Submit Form</dees-form-submit>
|
||||
<dees-form-submit text="With Text Property"></dees-form-submit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>With Icons (inherited from DeesButton)</h3>
|
||||
<div class="demo-row">
|
||||
<dees-form-submit icon="lucide:send">Submit</dees-form-submit>
|
||||
<dees-form-submit icon="lucide:save" iconPosition="left">Save Form</dees-form-submit>
|
||||
<dees-form-submit icon="lucide:arrow-right" iconPosition="right">Continue</dees-form-submit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Button Types</h3>
|
||||
<div class="demo-row">
|
||||
<dees-form-submit type="highlighted" icon="lucide:send">Highlighted</dees-form-submit>
|
||||
<dees-form-submit type="normal" icon="lucide:send">Normal</dees-form-submit>
|
||||
<dees-form-submit type="discreet" icon="lucide:send">Discreet</dees-form-submit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Sizes</h3>
|
||||
<div class="demo-row">
|
||||
<dees-form-submit size="small" icon="lucide:send">Small</dees-form-submit>
|
||||
<dees-form-submit size="normal" icon="lucide:send">Normal</dees-form-submit>
|
||||
<dees-form-submit size="large" icon="lucide:send">Large</dees-form-submit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>States</h3>
|
||||
<div class="demo-row">
|
||||
<dees-form-submit status="normal" icon="lucide:send">Normal</dees-form-submit>
|
||||
<dees-form-submit status="pending" icon="lucide:send">Pending</dees-form-submit>
|
||||
<dees-form-submit status="success" icon="lucide:check">Success</dees-form-submit>
|
||||
<dees-form-submit status="error" icon="lucide:x">Error</dees-form-submit>
|
||||
<dees-form-submit disabled icon="lucide:send">Disabled</dees-form-submit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>In a Form Context</h3>
|
||||
<dees-form>
|
||||
<dees-input-text label="Name" key="name"></dees-input-text>
|
||||
<dees-input-text label="Email" key="email"></dees-input-text>
|
||||
<dees-form-submit icon="lucide:send" type="highlighted">Submit Form</dees-form-submit>
|
||||
</dees-form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { DeesForm } from '../dees-form/dees-form.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
@@ -21,42 +22,76 @@ export class DeesFormSubmit extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Form', 'Button'];
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
// =============================================
|
||||
// Properties forwarded to internal dees-button
|
||||
// =============================================
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
accessor disabled = false;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor text: string;
|
||||
@property({ type: String })
|
||||
accessor text!: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
@property({ type: String })
|
||||
accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
accessor type: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default';
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
accessor size: 'sm' | 'default' | 'lg' | 'icon' | 'small' | 'normal' | 'large' = 'default';
|
||||
|
||||
@property({ type: String })
|
||||
accessor icon!: string;
|
||||
|
||||
@property({ type: String })
|
||||
accessor iconPosition: 'left' | 'right' = 'left';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [themeDefaultStyles, cssManager.defaultStyles, css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
`];
|
||||
|
||||
public render() {
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
dees-button {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<dees-button
|
||||
status="${this.status}"
|
||||
@click="${this.submit}"
|
||||
?disabled="${this.disabled}"
|
||||
>
|
||||
${this.text || html`<slot></slot>`}
|
||||
</dees-button>
|
||||
.status=${this.status}
|
||||
.type=${this.type}
|
||||
.size=${this.size}
|
||||
.icon=${this.icon}
|
||||
.iconPosition=${this.iconPosition}
|
||||
.text=${this.text}
|
||||
?disabled=${this.disabled}
|
||||
@clicked=${this.submit}
|
||||
></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() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
|
||||
@@ -57,10 +57,10 @@ export const demoFunc = () => html`
|
||||
const outputDiv = elementArg.querySelector('.form-output');
|
||||
|
||||
if (form && outputDiv) {
|
||||
form.addEventListener('formData', async (eventArg: CustomEvent) => {
|
||||
form.addEventListener('formData', (async (eventArg: CustomEvent) => {
|
||||
const data = eventArg.detail.data;
|
||||
console.log('Form submitted with data:', data);
|
||||
|
||||
|
||||
// Show processing state
|
||||
form.setStatus('pending', 'Processing your registration...');
|
||||
outputDiv.innerHTML = `<strong>Submitted Data:</strong>\n${JSON.stringify(data, null, 2)}`;
|
||||
@@ -75,7 +75,7 @@ export const demoFunc = () => html`
|
||||
await domtools.plugins.smartdelay.delayFor(2000);
|
||||
form.reset();
|
||||
outputDiv.innerHTML = '<em>Form has been reset</em>';
|
||||
});
|
||||
}) as unknown as EventListener);
|
||||
|
||||
// Track individual field changes
|
||||
const inputs = form.querySelectorAll('dees-input-text, dees-input-dropdown, dees-input-checkbox');
|
||||
@@ -92,7 +92,7 @@ export const demoFunc = () => html`
|
||||
.required=${true}
|
||||
key="firstName"
|
||||
label="First Name"
|
||||
.description=${'Your given name'}
|
||||
.infoText=${'Your given name'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
@@ -105,7 +105,7 @@ export const demoFunc = () => html`
|
||||
.required=${true}
|
||||
key="email"
|
||||
label="Email Address"
|
||||
.description=${'We will use this to contact you'}
|
||||
.infoText=${'We will use this to contact you'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-dropdown
|
||||
@@ -126,7 +126,7 @@ export const demoFunc = () => html`
|
||||
key="password"
|
||||
label="Password"
|
||||
isPasswordBool
|
||||
.description=${'Minimum 8 characters'}
|
||||
.infoText=${'Minimum 8 characters'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-checkbox
|
||||
@@ -158,14 +158,14 @@ export const demoFunc = () => html`
|
||||
console.log('Horizontal form layout active');
|
||||
|
||||
// Monitor filter changes
|
||||
form.addEventListener('formData', (event: CustomEvent) => {
|
||||
form.addEventListener('formData', ((event: CustomEvent) => {
|
||||
const filters = event.detail.data;
|
||||
console.log('Filter applied:', filters);
|
||||
|
||||
|
||||
// Simulate search
|
||||
const resultsCount = Math.floor(Math.random() * 100) + 1;
|
||||
console.log(`Found ${resultsCount} results with filters:`, filters);
|
||||
});
|
||||
}) as unknown as EventListener);
|
||||
|
||||
// Setup real-time filter updates
|
||||
const inputs = form.querySelectorAll('[key]');
|
||||
@@ -224,7 +224,7 @@ export const demoFunc = () => html`
|
||||
const statusDiv = elementArg.querySelector('#status-display');
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('formData', async (eventArg: CustomEvent) => {
|
||||
form.addEventListener('formData', (async (eventArg: CustomEvent) => {
|
||||
const data = eventArg.detail.data;
|
||||
console.log('Advanced form data:', data);
|
||||
|
||||
@@ -252,7 +252,7 @@ export const demoFunc = () => html`
|
||||
}
|
||||
|
||||
console.log('Form data logged:', data);
|
||||
});
|
||||
}) as unknown as EventListener);
|
||||
|
||||
// Monitor file uploads
|
||||
const fileUpload = form.querySelector('dees-input-fileupload');
|
||||
@@ -300,7 +300,7 @@ export const demoFunc = () => html`
|
||||
<dees-input-fileupload
|
||||
key="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-form-submit>Submit Application</dees-form-submit>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
type TemplateResult,
|
||||
DeesElement,
|
||||
type CSSResult,
|
||||
@@ -21,6 +22,10 @@ import { DeesInputMultitoggle } from '../../00group-input/dees-input-multitoggle
|
||||
import { DeesInputPhone } from '../../00group-input/dees-input-phone/dees-input-phone.js';
|
||||
import { DeesInputToggle } from '../../00group-input/dees-input-toggle/dees-input-toggle.js';
|
||||
import { DeesInputTypelist } from '../../00group-input/dees-input-typelist/dees-input-typelist.js';
|
||||
import { DeesInputTags } from '../../00group-input/dees-input-tags/dees-input-tags.js';
|
||||
import { DeesInputList } from '../../00group-input/dees-input-list/dees-input-list.js';
|
||||
import { DeesInputWysiwyg } from '../../00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
|
||||
import { DeesInputRichtext } from '../../00group-input/dees-input-richtext/component.js';
|
||||
import { DeesFormSubmit } from '../dees-form-submit/dees-form-submit.js';
|
||||
import { DeesTable } from '../../00group-dataview/dees-table/index.js';
|
||||
import { demoFunc } from './dees-form.demo.js';
|
||||
@@ -40,6 +45,10 @@ const FORM_INPUT_TYPES = [
|
||||
DeesInputText,
|
||||
DeesInputToggle,
|
||||
DeesInputTypelist,
|
||||
DeesInputTags,
|
||||
DeesInputList,
|
||||
DeesInputWysiwyg,
|
||||
DeesInputRichtext,
|
||||
DeesTable,
|
||||
];
|
||||
|
||||
@@ -57,6 +66,10 @@ export type TFormInputElement =
|
||||
| DeesInputText
|
||||
| DeesInputToggle
|
||||
| DeesInputTypelist
|
||||
| DeesInputTags
|
||||
| DeesInputList
|
||||
| DeesInputWysiwyg
|
||||
| DeesInputRichtext
|
||||
| DeesTable<any>;
|
||||
|
||||
declare global {
|
||||
@@ -81,13 +94,25 @@ export class DeesForm extends DeesElement {
|
||||
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
|
||||
accessor horizontalLayout: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
:host([horizontal-layout]) {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
@@ -110,13 +135,14 @@ export class DeesForm extends DeesElement {
|
||||
}
|
||||
|
||||
public getFormElements(): Array<TFormInputElement> {
|
||||
return Array.from(this.children).filter((child) =>
|
||||
// Use querySelectorAll('*') to find form inputs nested inside wrapper elements (e.g. <div>)
|
||||
return Array.from(this.querySelectorAll('*')).filter((child) =>
|
||||
FORM_INPUT_TYPES.includes(child.constructor as any)
|
||||
) as unknown as TFormInputElement[];
|
||||
}
|
||||
|
||||
public getSubmitButton(): DeesFormSubmit | undefined {
|
||||
return Array.from(this.children).find(
|
||||
return Array.from(this.querySelectorAll('*')).find(
|
||||
(child) => child instanceof DeesFormSubmit
|
||||
) as DeesFormSubmit;
|
||||
}
|
||||
@@ -130,8 +156,9 @@ export class DeesForm extends DeesElement {
|
||||
requiredOK = false;
|
||||
}
|
||||
}
|
||||
if (this.getSubmitButton()) {
|
||||
this.getSubmitButton().disabled = !requiredOK;
|
||||
const submitButton = this.getSubmitButton();
|
||||
if (submitButton) {
|
||||
submitButton.disabled = !requiredOK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +201,7 @@ export class DeesForm extends DeesElement {
|
||||
) {
|
||||
const inputChildren = this.getFormElements();
|
||||
const submitButton = this.getSubmitButton();
|
||||
if (!submitButton) return;
|
||||
|
||||
switch (visualStateArg) {
|
||||
case 'normal':
|
||||
@@ -214,7 +242,6 @@ export class DeesForm extends DeesElement {
|
||||
*/
|
||||
reset() {
|
||||
const inputChildren = this.getFormElements();
|
||||
const submitButton = this.getSubmitButton();
|
||||
|
||||
for (const inputChild of inputChildren) {
|
||||
inputChild.value = null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
css,
|
||||
type CSSResult,
|
||||
cssManager,
|
||||
@@ -31,10 +32,10 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
* Common properties for all inputs
|
||||
*/
|
||||
@property({ type: String })
|
||||
accessor key: string;
|
||||
accessor key!: string;
|
||||
|
||||
@property({ type: String })
|
||||
accessor label: string;
|
||||
accessor label!: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor required: boolean = false;
|
||||
@@ -43,7 +44,10 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
accessor disabled: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor description: string;
|
||||
accessor infoText!: string;
|
||||
|
||||
@property({ type: String })
|
||||
accessor description!: string;
|
||||
|
||||
/**
|
||||
* Common styles for all input components
|
||||
@@ -54,37 +58,20 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
/* CSS Variables for consistent spacing */
|
||||
:host {
|
||||
--dees-input-spacing-unit: 8px;
|
||||
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
|
||||
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
|
||||
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
|
||||
}
|
||||
|
||||
/* Default vertical stacking mode (for forms) */
|
||||
/* Default block display with no margins - spacing is container-driven */
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-bottom: var(--dees-input-vertical-gap);
|
||||
}
|
||||
|
||||
/* Last child in container should have no bottom margin */
|
||||
:host(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal layout mode - activated by attribute */
|
||||
:host([layout-mode="horizontal"]) {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
margin-right: var(--dees-input-horizontal-gap);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:host([layout-mode="horizontal"]:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Auto mode - inherit from parent dees-form if present */
|
||||
|
||||
/* Label position variations */
|
||||
:host([label-position="left"]) .input-wrapper {
|
||||
display: grid;
|
||||
@@ -107,6 +94,14 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
:host([label-position="none"]) dees-label {
|
||||
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%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -172,6 +167,14 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -111,90 +111,92 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Checkboxes'} .subtitle=${'Simple checkbox examples with various labels'}>
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'I agree to the Terms and Conditions'}
|
||||
<dees-form>
|
||||
<dees-input-checkbox
|
||||
.label=${'I agree to the Terms and Conditions'}
|
||||
.value=${true}
|
||||
.key=${'terms'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Subscribe to newsletter'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Subscribe to newsletter'}
|
||||
.value=${false}
|
||||
.key=${'newsletter'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Enable notifications'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Enable notifications'}
|
||||
.value=${false}
|
||||
.description=${'Receive email updates about your account'}
|
||||
.key=${'notifications'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Checkbox States'} .subtitle=${'Different checkbox states and configurations'}>
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Default state'}
|
||||
<dees-form>
|
||||
<dees-input-checkbox
|
||||
.label=${'Default state'}
|
||||
.value=${false}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Checked state'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Checked state'}
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled unchecked'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled unchecked'}
|
||||
.value=${false}
|
||||
.disabled=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled checked'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Disabled checked'}
|
||||
.value=${true}
|
||||
.disabled=${true}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Required checkbox'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Required checkbox'}
|
||||
.required=${true}
|
||||
.key=${'required'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Checkboxes arranged horizontally for compact forms'}>
|
||||
<div class="horizontal-checkboxes">
|
||||
<dees-input-checkbox
|
||||
.label=${'Option A'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionA'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option B'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionB'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option C'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionC'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option D'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionD'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="horizontal-checkboxes">
|
||||
<dees-input-checkbox
|
||||
.label=${'Option A'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionA'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option B'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionB'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option C'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionC'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Option D'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'optionD'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Feature Selection Example'} .subtitle=${'Common use case for feature toggles with batch operations'}>
|
||||
@@ -204,76 +206,76 @@ export const demoFunc = () => html`
|
||||
</div>
|
||||
|
||||
<div class="feature-list">
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Dark Mode Support'}
|
||||
<dees-form>
|
||||
<dees-input-checkbox
|
||||
.label=${'Dark Mode Support'}
|
||||
.value=${true}
|
||||
.key=${'feature1'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Email Notifications'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Email Notifications'}
|
||||
.value=${true}
|
||||
.key=${'feature2'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Two-Factor Authentication'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Two-Factor Authentication'}
|
||||
.value=${false}
|
||||
.key=${'feature3'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'API Access'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'API Access'}
|
||||
.value=${true}
|
||||
.key=${'feature4'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Advanced Analytics'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Advanced Analytics'}
|
||||
.value=${false}
|
||||
.key=${'feature5'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Privacy Settings Example'} .subtitle=${'Checkboxes in a typical form context'}>
|
||||
<div class="form-section">
|
||||
<h4 class="section-title">Privacy Preferences</h4>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Share analytics data'}
|
||||
|
||||
<dees-form>
|
||||
<dees-input-checkbox
|
||||
.label=${'Share analytics data'}
|
||||
.value=${true}
|
||||
.description=${'Help us improve by sharing anonymous usage data'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Personalized recommendations'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Personalized recommendations'}
|
||||
.value=${true}
|
||||
.description=${'Get suggestions based on your activity'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Marketing communications'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Marketing communications'}
|
||||
.value=${false}
|
||||
.description=${'Receive promotional emails and special offers'}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Third-party integrations'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Third-party integrations'}
|
||||
.value=${false}
|
||||
.description=${'Allow approved partners to access your data'}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Interactive Example'} .subtitle=${'Click checkboxes to see value changes'}>
|
||||
<div class="checkbox-group">
|
||||
<dees-input-checkbox
|
||||
.label=${'Feature toggle'}
|
||||
<dees-form>
|
||||
<dees-input-checkbox
|
||||
.label=${'Feature toggle'}
|
||||
.value=${false}
|
||||
@changeSubject=${(event: CustomEvent) => {
|
||||
const output = document.querySelector('#checkbox-output');
|
||||
@@ -283,9 +285,9 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}
|
||||
></dees-input-checkbox>
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Debug mode'}
|
||||
|
||||
<dees-input-checkbox
|
||||
.label=${'Debug mode'}
|
||||
.value=${false}
|
||||
@changeSubject=${(event: CustomEvent) => {
|
||||
const output = document.querySelector('#debug-output');
|
||||
@@ -295,8 +297,8 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
|
||||
</dees-form>
|
||||
|
||||
<div class="interactive-section">
|
||||
<div id="checkbox-output" class="output-text">Feature is disabled</div>
|
||||
<div id="debug-output" class="output-text" style="margin-top: 8px;">Debug mode: OFF</div>
|
||||
|
||||
@@ -147,12 +147,6 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
||||
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 class="label-container">
|
||||
${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>
|
||||
@@ -215,7 +209,7 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
const checkboxDiv = this.shadowRoot.querySelector('.checkbox');
|
||||
const checkboxDiv = this.shadowRoot!.querySelector('.checkbox');
|
||||
if (checkboxDiv) {
|
||||
(checkboxDiv as any).focus();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { DeesModal } from '../../00group-overlay/dees-modal/dees-modal.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import '../../00group-layout/dees-label/dees-label.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
import '../../00group-workspace/dees-workspace-monaco/dees-workspace-monaco.js';
|
||||
import { DeesWorkspaceMonaco } from '../../00group-workspace/dees-workspace-monaco/dees-workspace-monaco.js';
|
||||
|
||||
@@ -78,6 +79,12 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
@state()
|
||||
accessor copySuccess: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor lineCount: number = 0;
|
||||
|
||||
@state()
|
||||
accessor cursorPosition: { line: number; column: number } = { line: 1, column: 1 };
|
||||
|
||||
private editorElement: DeesWorkspaceMonaco | null = null;
|
||||
|
||||
public static styles = [
|
||||
@@ -105,24 +112,16 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.code-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
dees-tile {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
padding: 4px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -149,16 +148,16 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 12%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 20%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.language-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
|
||||
background: var(--dees-color-hover);
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
@@ -166,8 +165,8 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 20%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
@@ -180,16 +179,16 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
color: var(--dees-color-text-secondary);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 15%)')};
|
||||
background: var(--dees-color-hover);
|
||||
}
|
||||
|
||||
.language-option.selected {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 20%)')};
|
||||
background: var(--dees-color-active);
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
@@ -202,18 +201,18 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 60%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.toolbar-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(0 0% 15%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
.toolbar-button.active {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
background: var(--dees-color-active);
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
.toolbar-button.success {
|
||||
@@ -221,9 +220,8 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
dees-workspace-monaco {
|
||||
@@ -231,13 +229,44 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Match Monaco background to tile */
|
||||
dees-workspace-monaco::part(container) {
|
||||
background: var(--dees-color-bg-primary);
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
|
||||
background: var(--dees-color-border-default);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.editor-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-muted);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
:host([disabled]) .code-container {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@@ -255,9 +284,9 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
}
|
||||
</style>
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
|
||||
<div class="code-container">
|
||||
<div class="toolbar">
|
||||
<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>
|
||||
<dees-tile>
|
||||
<div slot="header" class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<div class="language-selector">
|
||||
<button
|
||||
@@ -322,7 +351,18 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
@content-change=${this.handleContentChange}
|
||||
></dees-workspace-monaco>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer" class="editor-footer">
|
||||
<div class="footer-left">
|
||||
<span>Ln ${this.cursorPosition.line}, Col ${this.cursorPosition.column}</span>
|
||||
<div class="footer-separator"></div>
|
||||
<span>${this.lineCount} line${this.lineCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span>${(LANGUAGES.find(l => l.key === this.language) || LANGUAGES[0]).label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</dees-tile>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -337,6 +377,49 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
this.changeSubject.next(this as any);
|
||||
}
|
||||
});
|
||||
|
||||
// Track cursor position and line count
|
||||
const editor = await this.editorElement.editorDeferred.promise;
|
||||
|
||||
// Set initial line count
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
this.lineCount = model.getLineCount();
|
||||
model.onDidChangeContent(() => {
|
||||
this.lineCount = model.getLineCount();
|
||||
});
|
||||
}
|
||||
|
||||
// Track cursor position
|
||||
editor.onDidChangeCursorPosition((e) => {
|
||||
this.cursorPosition = { line: e.position.lineNumber, column: e.position.column };
|
||||
});
|
||||
|
||||
// Override Monaco editor background to match tile
|
||||
const domtoolsInstance = await this.editorElement.domtoolsPromise;
|
||||
const updateEditorBg = (isBright: boolean) => {
|
||||
const bg = isBright ? '#ffffff' : '#0a0a0a';
|
||||
editor.updateOptions({});
|
||||
// Override via Monaco's theme API
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
editor.updateOptions({ theme: isBright ? 'dees-light' : 'dees-dark' });
|
||||
};
|
||||
|
||||
updateEditorBg(domtoolsInstance.themeManager.goBrightBoolean);
|
||||
domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
|
||||
updateEditorBg(goBright);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,23 +509,15 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
||||
if (!toolbar) return;
|
||||
|
||||
// Update language button text
|
||||
const langBtn = toolbar.querySelector('.language-button span');
|
||||
if (langBtn) langBtn.textContent = getLanguageLabel();
|
||||
|
||||
// Update word wrap button
|
||||
const wrapBtn = toolbar.querySelector('.wrap-btn') as HTMLElement;
|
||||
if (wrapBtn) {
|
||||
wrapBtn.classList.toggle('active', modalWordWrap === 'on');
|
||||
}
|
||||
if (wrapBtn) wrapBtn.classList.toggle('active', modalWordWrap === 'on');
|
||||
|
||||
// Update line numbers button
|
||||
const linesBtn = toolbar.querySelector('.lines-btn') as HTMLElement;
|
||||
if (linesBtn) {
|
||||
linesBtn.classList.toggle('active', modalShowLineNumbers);
|
||||
}
|
||||
if (linesBtn) linesBtn.classList.toggle('active', modalShowLineNumbers);
|
||||
|
||||
// Update copy button
|
||||
const copyBtn = toolbar.querySelector('.copy-btn') as HTMLElement;
|
||||
const copyIcon = copyBtn?.querySelector('dees-icon') as any;
|
||||
if (copyBtn && copyIcon) {
|
||||
@@ -450,13 +525,28 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
copyIcon.icon = modalCopySuccess ? 'lucide:Check' : 'lucide:Copy';
|
||||
}
|
||||
|
||||
// Update dropdown visibility
|
||||
const dropdown = toolbar.querySelector('.language-dropdown') as HTMLElement;
|
||||
if (dropdown) {
|
||||
dropdown.style.display = modalLanguageDropdownOpen ? 'block' : 'none';
|
||||
}
|
||||
if (dropdown) dropdown.style.display = modalLanguageDropdownOpen ? 'block' : 'none';
|
||||
};
|
||||
|
||||
// Helper to update footer UI
|
||||
const updateFooterUI = (modal: DeesModal) => {
|
||||
const footer = modal.shadowRoot?.querySelector('.modal-footer');
|
||||
if (!footer) return;
|
||||
|
||||
const cursorEl = footer.querySelector('.footer-cursor');
|
||||
const linesEl = footer.querySelector('.footer-lines');
|
||||
const langEl = footer.querySelector('.footer-lang');
|
||||
|
||||
if (cursorEl) cursorEl.textContent = `Ln ${modalCursorLine}, Col ${modalCursorCol}`;
|
||||
if (linesEl) linesEl.textContent = `${modalLineCount} line${modalLineCount !== 1 ? 's' : ''}`;
|
||||
if (langEl) langEl.textContent = getLanguageLabel();
|
||||
};
|
||||
|
||||
let modalCursorLine = 1;
|
||||
let modalCursorCol = 1;
|
||||
let modalLineCount = currentValue.split('\n').length;
|
||||
|
||||
const modal = await DeesModal.createAndShow({
|
||||
heading: this.label || 'Code Editor',
|
||||
width: 'fullscreen',
|
||||
@@ -467,9 +557,7 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
padding: 4px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
.modal-toolbar .toolbar-left {
|
||||
@@ -562,9 +650,30 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
}
|
||||
.modal-editor-wrapper {
|
||||
position: relative;
|
||||
height: calc(100vh - 175px);
|
||||
height: calc(100vh - 200px);
|
||||
width: 100%;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
.modal-footer .footer-left,
|
||||
.modal-footer .footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.modal-footer .footer-separator {
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
|
||||
}
|
||||
</style>
|
||||
<div class="modal-toolbar">
|
||||
<div class="toolbar-left">
|
||||
@@ -605,25 +714,34 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
.wordWrap=${modalWordWrap}
|
||||
></dees-workspace-monaco>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="footer-left">
|
||||
<span class="footer-cursor">Ln ${modalCursorLine}, Col ${modalCursorCol}</span>
|
||||
<div class="footer-separator"></div>
|
||||
<span class="footer-lines">${modalLineCount} line${modalLineCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="footer-lang">${getLanguageLabel()}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modalRef) => {
|
||||
await modalRef.destroy();
|
||||
await modalRef!.destroy();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Save & Close',
|
||||
action: async (modalRef) => {
|
||||
// Get the editor content from the modal
|
||||
modalEditorElement = modalRef.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||
modalEditorElement = modalRef!.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||
if (modalEditorElement) {
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
const newValue = editor.getValue();
|
||||
this.setValue(newValue);
|
||||
}
|
||||
await modalRef.destroy();
|
||||
await modalRef!.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -633,17 +751,61 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
modalEditorElement = modal.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||
|
||||
// Apply custom Monaco theme for matching background
|
||||
if (modalEditorElement) {
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
const domtoolsInstance = await modalEditorElement.domtoolsPromise;
|
||||
|
||||
const applyModalTheme = (isBright: boolean) => {
|
||||
const bg = isBright ? '#ffffff' : '#0a0a0a';
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
(window as any).monaco?.editor?.defineTheme?.('dees-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: { 'editor.background': bg },
|
||||
});
|
||||
editor.updateOptions({ theme: isBright ? 'dees-light' : 'dees-dark' });
|
||||
};
|
||||
|
||||
applyModalTheme(domtoolsInstance.themeManager.goBrightBoolean);
|
||||
domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
|
||||
applyModalTheme(goBright);
|
||||
});
|
||||
|
||||
// Track cursor position
|
||||
editor.onDidChangeCursorPosition((e) => {
|
||||
modalCursorLine = e.position.lineNumber;
|
||||
modalCursorCol = e.position.column;
|
||||
updateFooterUI(modal);
|
||||
});
|
||||
|
||||
// Track line count
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
modalLineCount = model.getLineCount();
|
||||
updateFooterUI(modal);
|
||||
model.onDidChangeContent(() => {
|
||||
modalLineCount = model.getLineCount();
|
||||
updateFooterUI(modal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up toolbar event handlers
|
||||
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');
|
||||
if (toolbar) {
|
||||
// Language button click
|
||||
const langBtn = toolbar.querySelector('.language-button');
|
||||
langBtn?.addEventListener('click', () => {
|
||||
modalLanguageDropdownOpen = !modalLanguageDropdownOpen;
|
||||
updateToolbarUI(modal);
|
||||
});
|
||||
|
||||
// Language option clicks
|
||||
const langOptions = toolbar.querySelectorAll('.language-option');
|
||||
langOptions.forEach((option) => {
|
||||
option.addEventListener('click', async () => {
|
||||
@@ -652,23 +814,21 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
modalLanguage = newLang;
|
||||
modalLanguageDropdownOpen = false;
|
||||
|
||||
// Update editor language
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
(window as any).monaco.editor.setModelLanguage(model, newLang);
|
||||
const editorModel = editor.getModel();
|
||||
if (editorModel) {
|
||||
(window as any).monaco.editor.setModelLanguage(editorModel, newLang);
|
||||
}
|
||||
|
||||
// Update selected state
|
||||
langOptions.forEach(opt => opt.classList.remove('selected'));
|
||||
option.classList.add('selected');
|
||||
|
||||
updateToolbarUI(modal);
|
||||
updateFooterUI(modal);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Word wrap button
|
||||
const wrapBtn = toolbar.querySelector('.wrap-btn');
|
||||
wrapBtn?.addEventListener('click', async () => {
|
||||
modalWordWrap = modalWordWrap === 'on' ? 'off' : 'on';
|
||||
@@ -679,7 +839,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
updateToolbarUI(modal);
|
||||
});
|
||||
|
||||
// Line numbers button
|
||||
const linesBtn = toolbar.querySelector('.lines-btn');
|
||||
linesBtn?.addEventListener('click', async () => {
|
||||
modalShowLineNumbers = !modalShowLineNumbers;
|
||||
@@ -690,7 +849,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
updateToolbarUI(modal);
|
||||
});
|
||||
|
||||
// Copy button
|
||||
const copyBtn = toolbar.querySelector('.copy-btn');
|
||||
copyBtn?.addEventListener('click', async () => {
|
||||
if (modalEditorElement) {
|
||||
@@ -710,7 +868,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
}
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (modalLanguageDropdownOpen && !langBtn?.contains(e.target as Node)) {
|
||||
modalLanguageDropdownOpen = false;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
||||
import { demoFunc } from './demo.js';
|
||||
import { datepickerStyles } from './styles.js';
|
||||
import { renderDatepicker } from './template.js';
|
||||
import type { IDateEvent } from './types.js';
|
||||
import { DeesInputDatepickerPopup } from './datepicker-popup.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import '../../00group-layout/dees-label/dees-label.js';
|
||||
|
||||
@@ -49,7 +49,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
accessor disabledDates: string[] = [];
|
||||
|
||||
@property({ type: Number })
|
||||
accessor weekStartsOn: 0 | 1 = 1; // Default to Monday
|
||||
accessor weekStartsOn: 0 | 1 = 1;
|
||||
|
||||
@property({ type: String })
|
||||
accessor placeholder: string = 'YYYY-MM-DD';
|
||||
@@ -61,14 +61,11 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor events: IDateEvent[] = [];
|
||||
accessor events: import('./types.js').IDateEvent[] = [];
|
||||
|
||||
@state()
|
||||
accessor isOpened: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor opensToTop: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor selectedDate: Date | null = null;
|
||||
|
||||
@@ -81,57 +78,19 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
@state()
|
||||
accessor selectedMinute: number = 0;
|
||||
|
||||
private popupInstance: DeesInputDatepickerPopup | null = null;
|
||||
|
||||
public static styles = datepickerStyles;
|
||||
|
||||
|
||||
|
||||
public getTimezones(): { value: string; label: string }[] {
|
||||
// Common timezones with their display names
|
||||
return [
|
||||
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
||||
{ value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (US & Canada)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
|
||||
{ value: 'America/Phoenix', label: 'Arizona' },
|
||||
{ value: 'America/Anchorage', label: 'Alaska' },
|
||||
{ value: 'Pacific/Honolulu', label: 'Hawaii' },
|
||||
{ value: 'Europe/London', label: 'London' },
|
||||
{ value: 'Europe/Paris', label: 'Paris' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Europe/Moscow', label: 'Moscow' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai' },
|
||||
{ value: 'Asia/Kolkata', label: 'India Standard Time' },
|
||||
{ value: 'Asia/Shanghai', label: 'China Standard Time' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney' },
|
||||
{ value: 'Pacific/Auckland', label: 'Auckland' },
|
||||
];
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return renderDatepicker(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
// Initialize with empty value if not set
|
||||
if (!this.value) {
|
||||
this.value = '';
|
||||
}
|
||||
|
||||
// Initialize view date and selected time
|
||||
if (this.value) {
|
||||
try {
|
||||
const date = new Date(this.value);
|
||||
@@ -160,19 +119,15 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
let formatted = this.dateFormat;
|
||||
|
||||
// Basic date formatting
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear().toString();
|
||||
|
||||
// Replace in correct order to avoid conflicts
|
||||
|
||||
formatted = formatted.replace('YYYY', year);
|
||||
formatted = formatted.replace('YY', year.slice(-2));
|
||||
formatted = formatted.replace('MM', month);
|
||||
formatted = formatted.replace('DD', day);
|
||||
|
||||
// Time formatting if enabled
|
||||
if (this.enableTime) {
|
||||
const hours24 = date.getHours();
|
||||
const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24;
|
||||
@@ -186,17 +141,11 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
// Timezone formatting if enabled
|
||||
if (this.enableTimezone) {
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZoneName: 'short',
|
||||
timeZone: this.timezone
|
||||
});
|
||||
const formatter = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short', timeZone: this.timezone });
|
||||
const parts = formatter.formatToParts(date);
|
||||
const tzPart = parts.find(part => part.type === 'timeZoneName');
|
||||
if (tzPart) {
|
||||
formatted += ` ${tzPart.value}`;
|
||||
}
|
||||
if (tzPart) formatted += ` ${tzPart.value}`;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
@@ -205,274 +154,101 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
private handleClickOutside = (event: MouseEvent) => {
|
||||
const path = event.composedPath();
|
||||
if (!path.includes(this)) {
|
||||
this.isOpened = false;
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
};
|
||||
|
||||
public async toggleCalendar(): Promise<void> {
|
||||
if (this.disabled) return;
|
||||
|
||||
this.isOpened = !this.isOpened;
|
||||
|
||||
if (this.isOpened) {
|
||||
// Check available space and set position
|
||||
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||
const rect = inputContainer.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
|
||||
// Determine if we should open upwards (approximate height of 400px)
|
||||
this.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
||||
this.closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add click outside listener
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
}, 0);
|
||||
} else {
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
this.isOpened = true;
|
||||
|
||||
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||
const rect = inputContainer.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
const opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
||||
|
||||
if (!this.popupInstance) {
|
||||
this.popupInstance = new DeesInputDatepickerPopup();
|
||||
}
|
||||
|
||||
// Configure popup
|
||||
this.popupInstance.triggerRect = rect;
|
||||
this.popupInstance.ownerComponent = this;
|
||||
this.popupInstance.opensToTop = opensToTop;
|
||||
this.popupInstance.enableTime = this.enableTime;
|
||||
this.popupInstance.timeFormat = this.timeFormat;
|
||||
this.popupInstance.minuteIncrement = this.minuteIncrement;
|
||||
this.popupInstance.weekStartsOn = this.weekStartsOn;
|
||||
this.popupInstance.minDate = this.minDate;
|
||||
this.popupInstance.maxDate = this.maxDate;
|
||||
this.popupInstance.disabledDates = this.disabledDates;
|
||||
this.popupInstance.enableTimezone = this.enableTimezone;
|
||||
this.popupInstance.timezone = this.timezone;
|
||||
this.popupInstance.events = this.events;
|
||||
this.popupInstance.selectedDate = this.selectedDate;
|
||||
this.popupInstance.viewDate = new Date(this.viewDate);
|
||||
this.popupInstance.selectedHour = this.selectedHour;
|
||||
this.popupInstance.selectedMinute = this.selectedMinute;
|
||||
|
||||
// Listen for popup events
|
||||
this.popupInstance.addEventListener('date-selected', this.handleDateSelected);
|
||||
this.popupInstance.addEventListener('date-cleared', this.handleDateCleared);
|
||||
this.popupInstance.addEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest);
|
||||
|
||||
await this.popupInstance.show();
|
||||
}
|
||||
|
||||
private closePopup(): void {
|
||||
this.isOpened = false;
|
||||
if (this.popupInstance) {
|
||||
this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
|
||||
this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
|
||||
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
||||
this.popupInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
public getDaysInMonth(): Date[] {
|
||||
const year = this.viewDate.getFullYear();
|
||||
const month = this.viewDate.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const days: Date[] = [];
|
||||
|
||||
// Adjust for week start
|
||||
const startOffset = this.weekStartsOn === 1
|
||||
? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
|
||||
: firstDay.getDay();
|
||||
|
||||
// Add days from previous month
|
||||
for (let i = startOffset; i > 0; i--) {
|
||||
days.push(new Date(year, month, 1 - i));
|
||||
}
|
||||
|
||||
// Add days of current month
|
||||
for (let i = 1; i <= lastDay.getDate(); i++) {
|
||||
days.push(new Date(year, month, i));
|
||||
}
|
||||
|
||||
// Add days from next month to complete the grid (6 rows)
|
||||
const remainingDays = 42 - days.length;
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
days.push(new Date(year, month + 1, i));
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
public isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear();
|
||||
}
|
||||
|
||||
public isSelected(date: Date): boolean {
|
||||
if (!this.selectedDate) return false;
|
||||
return date.getDate() === this.selectedDate.getDate() &&
|
||||
date.getMonth() === this.selectedDate.getMonth() &&
|
||||
date.getFullYear() === this.selectedDate.getFullYear();
|
||||
}
|
||||
|
||||
public isDisabled(date: Date): boolean {
|
||||
// Check min date
|
||||
if (this.minDate) {
|
||||
const min = new Date(this.minDate);
|
||||
if (date < min) return true;
|
||||
}
|
||||
|
||||
// Check max date
|
||||
if (this.maxDate) {
|
||||
const max = new Date(this.maxDate);
|
||||
if (date > max) return true;
|
||||
}
|
||||
|
||||
// Check disabled dates
|
||||
if (this.disabledDates && this.disabledDates.length > 0) {
|
||||
return this.disabledDates.some(disabledStr => {
|
||||
try {
|
||||
const disabled = new Date(disabledStr);
|
||||
return date.getDate() === disabled.getDate() &&
|
||||
date.getMonth() === disabled.getMonth() &&
|
||||
date.getFullYear() === disabled.getFullYear();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getEventsForDate(date: Date): IDateEvent[] {
|
||||
if (!this.events || this.events.length === 0) return [];
|
||||
|
||||
const dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
return this.events.filter(event => event.date === dateStr);
|
||||
}
|
||||
|
||||
public selectDate(date: Date): void {
|
||||
this.selectedDate = new Date(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
this.selectedHour,
|
||||
this.selectedMinute
|
||||
);
|
||||
|
||||
this.value = this.formatValueWithTimezone(this.selectedDate);
|
||||
private handleDateSelected = (event: Event): void => {
|
||||
const date = (event as CustomEvent).detail as Date;
|
||||
this.selectedDate = date;
|
||||
this.selectedHour = date.getHours();
|
||||
this.selectedMinute = date.getMinutes();
|
||||
this.viewDate = new Date(date);
|
||||
this.value = this.formatValueWithTimezone(date);
|
||||
this.changeSubject.next(this);
|
||||
|
||||
if (!this.enableTime) {
|
||||
this.isOpened = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public selectToday(): void {
|
||||
const today = new Date();
|
||||
this.selectedDate = today;
|
||||
this.viewDate = new Date(today);
|
||||
this.selectedHour = today.getHours();
|
||||
this.selectedMinute = today.getMinutes();
|
||||
|
||||
this.value = this.formatValueWithTimezone(this.selectedDate);
|
||||
this.changeSubject.next(this);
|
||||
|
||||
if (!this.enableTime) {
|
||||
this.isOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
private handleDateCleared = (): void => {
|
||||
this.value = '';
|
||||
this.selectedDate = null;
|
||||
this.changeSubject.next(this);
|
||||
this.isOpened = false;
|
||||
}
|
||||
};
|
||||
|
||||
public previousMonth(): void {
|
||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
|
||||
}
|
||||
private handleCloseRequest = (): void => {
|
||||
this.closePopup();
|
||||
};
|
||||
|
||||
public nextMonth(): void {
|
||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
|
||||
}
|
||||
private handleRepositionRequest = (): void => {
|
||||
if (!this.popupInstance || !this.isOpened) return;
|
||||
const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement;
|
||||
if (!inputContainer) return;
|
||||
|
||||
public handleHourInput(e: InputEvent): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
let value = parseInt(input.value) || 0;
|
||||
|
||||
if (this.timeFormat === '12h') {
|
||||
value = Math.max(1, Math.min(12, value));
|
||||
// Convert to 24h format
|
||||
if (this.selectedHour >= 12 && value !== 12) {
|
||||
this.selectedHour = value + 12;
|
||||
} else if (this.selectedHour < 12 && value === 12) {
|
||||
this.selectedHour = 0;
|
||||
} else {
|
||||
this.selectedHour = value;
|
||||
}
|
||||
} else {
|
||||
this.selectedHour = Math.max(0, Math.min(23, value));
|
||||
const rect = inputContainer.getBoundingClientRect();
|
||||
if (rect.bottom < 0 || rect.top > window.innerHeight) {
|
||||
this.closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSelectedDateTime();
|
||||
}
|
||||
|
||||
public handleMinuteInput(e: InputEvent): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
let value = parseInt(input.value) || 0;
|
||||
value = Math.max(0, Math.min(59, value));
|
||||
|
||||
if (this.minuteIncrement && this.minuteIncrement > 1) {
|
||||
value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
|
||||
}
|
||||
|
||||
this.selectedMinute = value;
|
||||
this.updateSelectedDateTime();
|
||||
}
|
||||
|
||||
public setAMPM(period: 'am' | 'pm'): void {
|
||||
if (period === 'am' && this.selectedHour >= 12) {
|
||||
this.selectedHour -= 12;
|
||||
} else if (period === 'pm' && this.selectedHour < 12) {
|
||||
this.selectedHour += 12;
|
||||
}
|
||||
this.updateSelectedDateTime();
|
||||
}
|
||||
|
||||
private updateSelectedDateTime(): void {
|
||||
if (this.selectedDate) {
|
||||
this.selectedDate = new Date(
|
||||
this.selectedDate.getFullYear(),
|
||||
this.selectedDate.getMonth(),
|
||||
this.selectedDate.getDate(),
|
||||
this.selectedHour,
|
||||
this.selectedMinute
|
||||
);
|
||||
this.value = this.formatValueWithTimezone(this.selectedDate);
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
}
|
||||
|
||||
public handleTimezoneChange(e: Event): void {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this.timezone = select.value;
|
||||
this.updateSelectedDateTime();
|
||||
}
|
||||
|
||||
private formatValueWithTimezone(date: Date): string {
|
||||
if (!this.enableTimezone) {
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
// Format the date with timezone offset
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: this.timezone,
|
||||
timeZoneName: 'short'
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const dateParts: any = {};
|
||||
parts.forEach(part => {
|
||||
dateParts[part.type] = part.value;
|
||||
});
|
||||
|
||||
// Create ISO-like format with timezone
|
||||
const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
||||
|
||||
// Get timezone offset
|
||||
const tzOffset = this.getTimezoneOffset(date, this.timezone);
|
||||
return `${isoString}${tzOffset}`;
|
||||
}
|
||||
|
||||
private getTimezoneOffset(date: Date, timezone: string): string {
|
||||
// Create a date in the target timezone
|
||||
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
|
||||
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
|
||||
const offsetMinutes = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60);
|
||||
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
|
||||
const minutes = Math.abs(offsetMinutes) % 60;
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
|
||||
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
this.popupInstance.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow;
|
||||
this.popupInstance.triggerRect = rect;
|
||||
};
|
||||
|
||||
public handleKeydown(e: KeyboardEvent): void {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -480,7 +256,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
this.toggleCalendar();
|
||||
} else if (e.key === 'Escape' && this.isOpened) {
|
||||
e.preventDefault();
|
||||
this.isOpened = false;
|
||||
this.closePopup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,9 +270,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
public handleManualInput(e: InputEvent): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const inputValue = input.value.trim();
|
||||
|
||||
|
||||
if (!inputValue) {
|
||||
// Clear the value if input is empty
|
||||
this.value = '';
|
||||
this.selectedDate = null;
|
||||
return;
|
||||
@@ -504,7 +279,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
|
||||
const parsedDate = this.parseManualDate(inputValue);
|
||||
if (parsedDate && !isNaN(parsedDate.getTime())) {
|
||||
// Update internal state without triggering re-render of input
|
||||
this.value = parsedDate.toISOString();
|
||||
this.selectedDate = parsedDate;
|
||||
this.viewDate = new Date(parsedDate);
|
||||
@@ -517,7 +291,7 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
public handleInputBlur(e: FocusEvent): void {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const inputValue = input.value.trim();
|
||||
|
||||
|
||||
if (!inputValue) {
|
||||
this.value = '';
|
||||
this.selectedDate = null;
|
||||
@@ -533,10 +307,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
this.selectedHour = parsedDate.getHours();
|
||||
this.selectedMinute = parsedDate.getMinutes();
|
||||
this.changeSubject.next(this);
|
||||
// Update the input with formatted date
|
||||
input.value = this.formatDate(this.value);
|
||||
} else {
|
||||
// Revert to previous valid value on blur if parsing failed
|
||||
input.value = this.formatDate(this.value);
|
||||
}
|
||||
}
|
||||
@@ -544,22 +316,17 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
private parseManualDate(input: string): Date | null {
|
||||
if (!input) return null;
|
||||
|
||||
// Split date and time parts if present
|
||||
const parts = input.split(' ');
|
||||
let datePart = parts[0];
|
||||
let timePart = parts[1] || '';
|
||||
|
||||
let parsedDate: Date | null = null;
|
||||
|
||||
// Try different date formats
|
||||
// Format 1: YYYY-MM-DD (ISO-like)
|
||||
const isoMatch = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||||
if (isoMatch) {
|
||||
const [_, year, month, day] = isoMatch;
|
||||
parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||
}
|
||||
|
||||
// Format 2: DD.MM.YYYY (European)
|
||||
if (!parsedDate) {
|
||||
const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (euMatch) {
|
||||
@@ -568,7 +335,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
// Format 3: MM/DD/YYYY (US)
|
||||
if (!parsedDate) {
|
||||
const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
||||
if (usMatch) {
|
||||
@@ -577,12 +343,8 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
|
||||
// If no date was parsed, return null
|
||||
if (!parsedDate || isNaN(parsedDate.getTime())) {
|
||||
return null;
|
||||
}
|
||||
if (!parsedDate || isNaN(parsedDate.getTime())) return null;
|
||||
|
||||
// Parse time if present (HH:MM format)
|
||||
if (timePart) {
|
||||
const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (timeMatch) {
|
||||
@@ -591,7 +353,6 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
parsedDate.setMinutes(parseInt(minutes));
|
||||
}
|
||||
} else if (!this.enableTime) {
|
||||
// If time is not enabled and not provided, use current time
|
||||
const now = new Date();
|
||||
parsedDate.setHours(now.getHours());
|
||||
parsedDate.setMinutes(now.getMinutes());
|
||||
@@ -602,6 +363,34 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
return parsedDate;
|
||||
}
|
||||
|
||||
private formatValueWithTimezone(date: Date): string {
|
||||
if (!this.enableTimezone) return date.toISOString();
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false, timeZone: this.timezone, timeZoneName: 'short',
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const dateParts: any = {};
|
||||
parts.forEach(part => { dateParts[part.type] = part.value; });
|
||||
|
||||
const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
||||
const tzOffset = this.getTimezoneOffset(date, this.timezone);
|
||||
return `${isoString}${tzOffset}`;
|
||||
}
|
||||
|
||||
private getTimezoneOffset(date: Date, timezone: string): string {
|
||||
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
|
||||
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
const offsetMinutes = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60);
|
||||
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
|
||||
const minutes = Math.abs(offsetMinutes) % 60;
|
||||
const sign = offsetMinutes >= 0 ? '+' : '-';
|
||||
return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
@@ -622,4 +411,16 @@ export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
if (this.popupInstance) {
|
||||
this.popupInstance.removeEventListener('date-selected', this.handleDateSelected);
|
||||
this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared);
|
||||
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
||||
this.popupInstance.hide();
|
||||
this.popupInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,758 @@
|
||||
import {
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
DeesElement,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { zIndexRegistry } from '../../00zindex.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import type { IDateEvent } from './types.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-input-datepicker-popup': DeesInputDatepickerPopup;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-input-datepicker-popup')
|
||||
export class DeesInputDatepickerPopup extends DeesElement {
|
||||
// Properties set by the parent
|
||||
@property({ attribute: false })
|
||||
accessor triggerRect: DOMRect | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor ownerComponent: HTMLElement | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableTime: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor timeFormat: '24h' | '12h' = '24h';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor minuteIncrement: number = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor weekStartsOn: 0 | 1 = 1;
|
||||
|
||||
@property({ type: String })
|
||||
accessor minDate: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor maxDate: string = '';
|
||||
|
||||
@property({ type: Array })
|
||||
accessor disabledDates: string[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableTimezone: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor events: IDateEvent[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor opensToTop: boolean = false;
|
||||
|
||||
// Internal state
|
||||
@state()
|
||||
accessor selectedDate: Date | null = null;
|
||||
|
||||
@state()
|
||||
accessor viewDate: Date = new Date();
|
||||
|
||||
@state()
|
||||
accessor selectedHour: number = 0;
|
||||
|
||||
@state()
|
||||
accessor selectedMinute: number = 0;
|
||||
|
||||
@state()
|
||||
accessor menuZIndex: number = 1000;
|
||||
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
private windowLayer: DeesWindowLayer | null = null;
|
||||
private isDestroying: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.calendar-popup {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
will-change: transform, opacity;
|
||||
transition: all 0.15s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 10px 15px -3px hsl(0 0% 0% / 0.1), 0 4px 6px -4px hsl(0 0% 0% / 0.1)',
|
||||
'0 10px 15px -3px hsl(0 0% 0% / 0.2), 0 4px 6px -4px hsl(0 0% 0% / 0.2)'
|
||||
)};
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
user-select: none;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.calendar-popup.top {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.calendar-popup.show {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Calendar Header */
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-year-display {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
/* Weekday headers */
|
||||
.weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
padding: 0 0 8px 0;
|
||||
}
|
||||
|
||||
/* Days grid */
|
||||
.days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
border: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.day:hover:not(.disabled) {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
}
|
||||
|
||||
.day.other-month {
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.day.today {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day.selected {
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day.disabled {
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Event indicators */
|
||||
.event-indicator {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.event-dot.info { background: ${cssManager.bdTheme('hsl(211 70% 52%)', 'hsl(211 70% 62%)')}; }
|
||||
.event-dot.warning { background: ${cssManager.bdTheme('hsl(45 90% 45%)', 'hsl(45 90% 55%)')}; }
|
||||
.event-dot.success { background: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')}; }
|
||||
.event-dot.error { background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')}; }
|
||||
|
||||
.event-count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.day.has-event:hover .event-tooltip { opacity: 1; }
|
||||
|
||||
/* Time selector */
|
||||
.time-selector {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.time-selector-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.time-inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-input {
|
||||
width: 65px;
|
||||
height: 36px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.time-input:focus {
|
||||
outline: none;
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.am-pm-selector { display: flex; gap: 4px; margin-left: 8px; }
|
||||
|
||||
.am-pm-button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.am-pm-button.selected {
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
.am-pm-button:hover:not(.selected) {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
}
|
||||
|
||||
/* Timezone selector */
|
||||
.timezone-selector {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.timezone-selector-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.timezone-select {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.calendar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.today-button {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
.today-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
}
|
||||
|
||||
.clear-action-button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.clear-action-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private static readonly MONTH_NAMES = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
];
|
||||
|
||||
private static readonly TIMEZONES = [
|
||||
{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' },
|
||||
{ value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (US & Canada)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
|
||||
{ value: 'Europe/London', label: 'London' },
|
||||
{ value: 'Europe/Paris', label: 'Paris' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Europe/Moscow', label: 'Moscow' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai' },
|
||||
{ value: 'Asia/Kolkata', label: 'India Standard Time' },
|
||||
{ value: 'Asia/Shanghai', label: 'China Standard Time' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney' },
|
||||
{ value: 'Pacific/Auckland', label: 'Auckland' },
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.triggerRect) return html``;
|
||||
|
||||
const posStyle = this.computePositionStyle();
|
||||
const weekDays = this.weekStartsOn === 1
|
||||
? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
||||
: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
const days = this.getDaysInMonth();
|
||||
const isAM = this.selectedHour < 12;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="calendar-popup ${this.visible ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}"
|
||||
style="${posStyle}; z-index: ${this.menuZIndex};"
|
||||
>
|
||||
<div class="calendar-header">
|
||||
<button class="nav-button" @click=${this.previousMonth}>
|
||||
<dees-icon icon="lucide:chevronLeft" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
<div class="month-year-display">
|
||||
${DeesInputDatepickerPopup.MONTH_NAMES[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()}
|
||||
</div>
|
||||
<button class="nav-button" @click=${this.nextMonth}>
|
||||
<dees-icon icon="lucide:chevronRight" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="weekdays">
|
||||
${weekDays.map(day => html`<div class="weekday">${day}</div>`)}
|
||||
</div>
|
||||
|
||||
<div class="days-grid">
|
||||
${days.map(day => {
|
||||
const isToday = this.isToday(day);
|
||||
const isSelected = this.isSelected(day);
|
||||
const isOtherMonth = day.getMonth() !== this.viewDate.getMonth();
|
||||
const isDayDisabled = this.isDayDisabled(day);
|
||||
const dayEvents = this.getEventsForDate(day);
|
||||
const hasEvents = dayEvents.length > 0;
|
||||
const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDayDisabled ? 'disabled' : ''} ${hasEvents ? 'has-event' : ''}"
|
||||
@click=${() => !isDayDisabled && this.handleSelectDate(day)}
|
||||
>
|
||||
${day.getDate()}
|
||||
${hasEvents ? html`
|
||||
${totalEventCount > 3 ? html`
|
||||
<div class="event-count">${totalEventCount}</div>
|
||||
` : html`
|
||||
<div class="event-indicator">
|
||||
${dayEvents.slice(0, 3).map(event => html`
|
||||
<div class="event-dot ${event.type || 'info'}"></div>
|
||||
`)}
|
||||
</div>
|
||||
`}
|
||||
${dayEvents[0].title ? html`
|
||||
<div class="event-tooltip">
|
||||
${dayEvents[0].title}
|
||||
${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
${this.enableTime ? html`
|
||||
<div class="time-selector">
|
||||
<div class="time-selector-title">Time</div>
|
||||
<div class="time-inputs">
|
||||
<input type="number" class="time-input"
|
||||
.value=${this.timeFormat === '12h'
|
||||
? (this.selectedHour === 0 ? 12 : this.selectedHour > 12 ? this.selectedHour - 12 : this.selectedHour).toString().padStart(2, '0')
|
||||
: this.selectedHour.toString().padStart(2, '0')}
|
||||
@input=${this.handleHourInput}
|
||||
min="${this.timeFormat === '12h' ? 1 : 0}"
|
||||
max="${this.timeFormat === '12h' ? 12 : 23}"
|
||||
/>
|
||||
<span class="time-separator">:</span>
|
||||
<input type="number" class="time-input"
|
||||
.value=${this.selectedMinute.toString().padStart(2, '0')}
|
||||
@input=${this.handleMinuteInput}
|
||||
min="0" max="59" step="${this.minuteIncrement || 1}"
|
||||
/>
|
||||
${this.timeFormat === '12h' ? html`
|
||||
<div class="am-pm-selector">
|
||||
<button class="am-pm-button ${isAM ? 'selected' : ''}" @click=${() => this.setAMPM('am')}>AM</button>
|
||||
<button class="am-pm-button ${!isAM ? 'selected' : ''}" @click=${() => this.setAMPM('pm')}>PM</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.enableTimezone ? html`
|
||||
<div class="timezone-selector">
|
||||
<div class="timezone-selector-title">Timezone</div>
|
||||
<select class="timezone-select" .value=${this.timezone} @change=${this.handleTimezoneChange}>
|
||||
${DeesInputDatepickerPopup.TIMEZONES.map(tz => html`
|
||||
<option value="${tz.value}" ?selected=${tz.value === this.timezone}>${tz.label}</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="calendar-actions">
|
||||
<button class="action-button today-button" @click=${this.handleSelectToday}>Today</button>
|
||||
<button class="action-button clear-action-button" @click=${this.handleClear}>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private computePositionStyle(): string {
|
||||
const rect = this.triggerRect!;
|
||||
const left = rect.left;
|
||||
|
||||
if (this.opensToTop) {
|
||||
const bottom = window.innerHeight - rect.top + 4;
|
||||
return `left: ${left}px; bottom: ${bottom}px; top: auto`;
|
||||
} else {
|
||||
const top = rect.bottom + 4;
|
||||
return `left: ${left}px; top: ${top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar logic
|
||||
private getDaysInMonth(): Date[] {
|
||||
const year = this.viewDate.getFullYear();
|
||||
const month = this.viewDate.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const days: Date[] = [];
|
||||
|
||||
const startOffset = this.weekStartsOn === 1
|
||||
? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1)
|
||||
: firstDay.getDay();
|
||||
|
||||
for (let i = startOffset; i > 0; i--) days.push(new Date(year, month, 1 - i));
|
||||
for (let i = 1; i <= lastDay.getDate(); i++) days.push(new Date(year, month, i));
|
||||
const remaining = 42 - days.length;
|
||||
for (let i = 1; i <= remaining; i++) days.push(new Date(year, month + 1, i));
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
private isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
|
||||
}
|
||||
|
||||
private isSelected(date: Date): boolean {
|
||||
if (!this.selectedDate) return false;
|
||||
return date.getDate() === this.selectedDate.getDate() && date.getMonth() === this.selectedDate.getMonth() && date.getFullYear() === this.selectedDate.getFullYear();
|
||||
}
|
||||
|
||||
private isDayDisabled(date: Date): boolean {
|
||||
if (this.minDate) { const min = new Date(this.minDate); if (date < min) return true; }
|
||||
if (this.maxDate) { const max = new Date(this.maxDate); if (date > max) return true; }
|
||||
if (this.disabledDates?.length) {
|
||||
return this.disabledDates.some(ds => {
|
||||
try { const d = new Date(ds); return date.getDate() === d.getDate() && date.getMonth() === d.getMonth() && date.getFullYear() === d.getFullYear(); }
|
||||
catch { return false; }
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getEventsForDate(date: Date): IDateEvent[] {
|
||||
if (!this.events?.length) return [];
|
||||
const dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
||||
return this.events.filter(e => e.date === dateStr);
|
||||
}
|
||||
|
||||
private previousMonth(): void {
|
||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1);
|
||||
}
|
||||
|
||||
private nextMonth(): void {
|
||||
this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1);
|
||||
}
|
||||
|
||||
// Event dispatching
|
||||
private handleSelectDate(day: Date): void {
|
||||
this.selectedDate = new Date(day.getFullYear(), day.getMonth(), day.getDate(), this.selectedHour, this.selectedMinute);
|
||||
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
||||
if (!this.enableTime) {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectToday(): void {
|
||||
const today = new Date();
|
||||
this.selectedDate = today;
|
||||
this.viewDate = new Date(today);
|
||||
this.selectedHour = today.getHours();
|
||||
this.selectedMinute = today.getMinutes();
|
||||
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
||||
if (!this.enableTime) {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
}
|
||||
|
||||
private handleClear(): void {
|
||||
this.dispatchEvent(new CustomEvent('date-cleared'));
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
|
||||
private handleHourInput = (e: InputEvent): void => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
let value = parseInt(input.value) || 0;
|
||||
if (this.timeFormat === '12h') {
|
||||
value = Math.max(1, Math.min(12, value));
|
||||
if (this.selectedHour >= 12 && value !== 12) this.selectedHour = value + 12;
|
||||
else if (this.selectedHour < 12 && value === 12) this.selectedHour = 0;
|
||||
else this.selectedHour = value;
|
||||
} else {
|
||||
this.selectedHour = Math.max(0, Math.min(23, value));
|
||||
}
|
||||
this.emitTimeUpdate();
|
||||
};
|
||||
|
||||
private handleMinuteInput = (e: InputEvent): void => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
let value = parseInt(input.value) || 0;
|
||||
value = Math.max(0, Math.min(59, value));
|
||||
if (this.minuteIncrement > 1) value = Math.round(value / this.minuteIncrement) * this.minuteIncrement;
|
||||
this.selectedMinute = value;
|
||||
this.emitTimeUpdate();
|
||||
};
|
||||
|
||||
private setAMPM(period: 'am' | 'pm'): void {
|
||||
if (period === 'am' && this.selectedHour >= 12) this.selectedHour -= 12;
|
||||
else if (period === 'pm' && this.selectedHour < 12) this.selectedHour += 12;
|
||||
this.emitTimeUpdate();
|
||||
}
|
||||
|
||||
private handleTimezoneChange = (e: Event): void => {
|
||||
this.timezone = (e.target as HTMLSelectElement).value;
|
||||
this.emitTimeUpdate();
|
||||
};
|
||||
|
||||
private emitTimeUpdate(): void {
|
||||
if (this.selectedDate) {
|
||||
this.selectedDate = new Date(this.selectedDate.getFullYear(), this.selectedDate.getMonth(), this.selectedDate.getDate(), this.selectedHour, this.selectedMinute);
|
||||
this.dispatchEvent(new CustomEvent('date-selected', { detail: this.selectedDate }));
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide lifecycle
|
||||
public async show(): Promise<void> {
|
||||
this.windowLayer = await DeesWindowLayer.createAndShow();
|
||||
this.windowLayer.addEventListener('click', () => {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
});
|
||||
|
||||
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(this, this.menuZIndex);
|
||||
this.style.zIndex = this.menuZIndex.toString();
|
||||
|
||||
document.body.appendChild(this);
|
||||
|
||||
await domtools.plugins.smartdelay.delayFor(0);
|
||||
this.visible = true;
|
||||
|
||||
window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
|
||||
window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
|
||||
}
|
||||
|
||||
public async hide(): Promise<void> {
|
||||
if (this.isDestroying) return;
|
||||
this.isDestroying = true;
|
||||
|
||||
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
||||
window.removeEventListener('resize', this.handleScrollOrResize);
|
||||
zIndexRegistry.unregister(this);
|
||||
|
||||
if (this.windowLayer) {
|
||||
this.windowLayer.destroy();
|
||||
this.windowLayer = null;
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
await domtools.plugins.smartdelay.delayFor(150);
|
||||
|
||||
if (this.parentElement) this.parentElement.removeChild(this);
|
||||
this.isDestroying = false;
|
||||
}
|
||||
|
||||
private handleScrollOrResize = (): void => {
|
||||
this.dispatchEvent(new CustomEvent('reposition-request'));
|
||||
};
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
||||
window.removeEventListener('resize', this.handleScrollOrResize);
|
||||
zIndexRegistry.unregister(this);
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,9 @@ export const demoFunc = () => html`
|
||||
const datePicker = elementArg.querySelector('dees-input-datepicker');
|
||||
|
||||
if (datePicker) {
|
||||
datePicker.addEventListener('change', (event: CustomEvent) => {
|
||||
datePicker.addEventListener('change', ((event: CustomEvent) => {
|
||||
console.log('Basic date selected:', (event.target as DeesInputDatepicker).value);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Basic Date Picker'} .subtitle=${'Simple date selection without time'}>
|
||||
@@ -66,17 +66,17 @@ export const demoFunc = () => html`
|
||||
const appointmentPicker = elementArg.querySelector('dees-input-datepicker[label="Appointment"]');
|
||||
|
||||
if (dateTimePicker) {
|
||||
dateTimePicker.addEventListener('change', (event: CustomEvent) => {
|
||||
dateTimePicker.addEventListener('change', ((event: CustomEvent) => {
|
||||
const value = (event.target as DeesInputDatepicker).value;
|
||||
console.log('24h format datetime:', value);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
if (appointmentPicker) {
|
||||
appointmentPicker.addEventListener('change', (event: CustomEvent) => {
|
||||
appointmentPicker.addEventListener('change', ((event: CustomEvent) => {
|
||||
const value = (event.target as DeesInputDatepicker).value;
|
||||
console.log('12h format datetime:', value);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Date and Time Selection'} .subtitle=${'Date pickers with time selection in different formats'}>
|
||||
@@ -102,14 +102,14 @@ export const demoFunc = () => html`
|
||||
const timezonePickers = elementArg.querySelectorAll('dees-input-datepicker');
|
||||
|
||||
timezonePickers.forEach((picker) => {
|
||||
picker.addEventListener('change', (event: CustomEvent) => {
|
||||
picker.addEventListener('change', ((event: CustomEvent) => {
|
||||
const target = event.target as DeesInputDatepicker;
|
||||
console.log(`${target.label} value:`, target.value);
|
||||
const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement;
|
||||
if (input) {
|
||||
console.log(`${target.label} formatted:`, input.value);
|
||||
}
|
||||
});
|
||||
}) as EventListener);
|
||||
});
|
||||
}}>
|
||||
<dees-panel .title=${'Timezone Support'} .subtitle=${'Date and time selection with timezone awareness'}>
|
||||
@@ -140,7 +140,7 @@ export const demoFunc = () => html`
|
||||
|
||||
if (futureDatePicker) {
|
||||
// Show the min/max constraints in action
|
||||
futureDatePicker.addEventListener('change', (event: CustomEvent) => {
|
||||
futureDatePicker.addEventListener('change', ((event: CustomEvent) => {
|
||||
const value = (event.target as DeesInputDatepicker).value;
|
||||
if (value) {
|
||||
const selectedDate = new Date(value);
|
||||
@@ -148,7 +148,7 @@ export const demoFunc = () => html`
|
||||
const daysDiff = Math.floor((selectedDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||
console.log(`Selected date is ${daysDiff} days from today`);
|
||||
}
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Date Range Constraints'} .subtitle=${'Limit selectable dates with min and max values'}>
|
||||
@@ -171,14 +171,14 @@ export const demoFunc = () => html`
|
||||
|
||||
const datePickers = elementArg.querySelectorAll('dees-input-datepicker');
|
||||
datePickers.forEach((picker) => {
|
||||
picker.addEventListener('change', (event: CustomEvent) => {
|
||||
picker.addEventListener('change', ((event: CustomEvent) => {
|
||||
const target = event.target as DeesInputDatepicker;
|
||||
// Log the formatted value that's displayed in the input
|
||||
const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement;
|
||||
if (input) {
|
||||
console.log(`${target.label} format:`, input.value);
|
||||
}
|
||||
});
|
||||
}) as EventListener);
|
||||
});
|
||||
}}>
|
||||
<dees-panel .title=${'Date Formats'} .subtitle=${'Different date display formats for various regions'}>
|
||||
@@ -268,7 +268,7 @@ export const demoFunc = () => html`
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
// Generate weekend dates for the current month
|
||||
const generateWeekends = () => {
|
||||
const weekends = [];
|
||||
const weekends: string[] = [];
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth();
|
||||
@@ -286,7 +286,7 @@ export const demoFunc = () => html`
|
||||
|
||||
const picker = elementArg.querySelector('dees-input-datepicker');
|
||||
if (picker) {
|
||||
picker.disabledDates = generateWeekends();
|
||||
(picker as DeesInputDatepicker).disabledDates = generateWeekends();
|
||||
console.log('Disabled weekend dates for current month');
|
||||
}
|
||||
}}>
|
||||
@@ -344,7 +344,7 @@ export const demoFunc = () => html`
|
||||
|
||||
const picker = elementArg.querySelector('dees-input-datepicker');
|
||||
if (picker) {
|
||||
picker.events = sampleEvents;
|
||||
(picker as DeesInputDatepicker).events = sampleEvents;
|
||||
console.log('Calendar events loaded:', sampleEvents);
|
||||
}
|
||||
}}>
|
||||
@@ -371,7 +371,7 @@ export const demoFunc = () => html`
|
||||
const output = elementArg.querySelector('#event-output');
|
||||
|
||||
if (picker && output) {
|
||||
picker.addEventListener('change', (event: CustomEvent) => {
|
||||
picker.addEventListener('change', ((event: CustomEvent) => {
|
||||
const target = event.target as DeesInputDatepicker;
|
||||
const value = target.value;
|
||||
if (value) {
|
||||
@@ -379,16 +379,16 @@ export const demoFunc = () => html`
|
||||
// Get the formatted value from the input element
|
||||
const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement;
|
||||
const formattedValue = input?.value || 'N/A';
|
||||
output.innerHTML = `
|
||||
(output as HTMLElement).innerHTML = `
|
||||
<strong>Event triggered!</strong><br>
|
||||
ISO Value: ${value}<br>
|
||||
Formatted: ${formattedValue}<br>
|
||||
Date object: ${date.toLocaleString()}
|
||||
`;
|
||||
} else {
|
||||
output.innerHTML = '<em>Date cleared</em>';
|
||||
(output as HTMLElement).innerHTML = '<em>Date cleared</em>';
|
||||
}
|
||||
});
|
||||
}) as EventListener);
|
||||
|
||||
picker.addEventListener('blur', () => {
|
||||
console.log('Datepicker lost focus');
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './component.js';
|
||||
export * from './datepicker-popup.js';
|
||||
|
||||
@@ -56,7 +56,6 @@ export const datepickerStyles = [
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Icon container using flexbox for better positioning */
|
||||
.icon-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -101,414 +100,5 @@ export const datepickerStyles = [
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
.clear-button:disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Calendar Popup Styles */
|
||||
.calendar-popup {
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'0 10px 15px -3px hsl(0 0% 0% / 0.1), 0 4px 6px -4px hsl(0 0% 0% / 0.1)',
|
||||
'0 10px 15px -3px hsl(0 0% 0% / 0.2), 0 4px 6px -4px hsl(0 0% 0% / 0.2)'
|
||||
)};
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
margin-top: 4px;
|
||||
z-index: 50;
|
||||
left: 0;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.calendar-popup.top {
|
||||
bottom: calc(100% + 4px);
|
||||
top: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 4px;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.calendar-popup.bottom {
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.calendar-popup.show {
|
||||
pointer-events: all;
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Calendar Header */
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-year-display {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
.nav-button:active {
|
||||
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
/* Weekday headers */
|
||||
.weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
padding: 0 0 8px 0;
|
||||
}
|
||||
|
||||
/* Days grid */
|
||||
.days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
border: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.day:hover:not(.disabled) {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
}
|
||||
|
||||
.day.other-month {
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.day.today {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day.selected {
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day.disabled {
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Event indicators */
|
||||
.day.has-event {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-indicator {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.event-dot.info {
|
||||
background: ${cssManager.bdTheme('hsl(211 70% 52%)', 'hsl(211 70% 62%)')};
|
||||
}
|
||||
|
||||
.event-dot.warning {
|
||||
background: ${cssManager.bdTheme('hsl(45 90% 45%)', 'hsl(45 90% 55%)')};
|
||||
}
|
||||
|
||||
.event-dot.success {
|
||||
background: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')};
|
||||
}
|
||||
|
||||
.event-dot.error {
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')};
|
||||
}
|
||||
|
||||
.event-count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')};
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Tooltip for event details */
|
||||
.event-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')};
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.event-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 4px solid transparent;
|
||||
border-top-color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.day.has-event:hover .event-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Time selector */
|
||||
.time-selector {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.time-selector-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.time-inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-input {
|
||||
width: 65px;
|
||||
height: 36px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.time-input:hover {
|
||||
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
}
|
||||
|
||||
.time-input:focus {
|
||||
outline: none;
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
|
||||
}
|
||||
|
||||
.time-separator {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.am-pm-selector {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.am-pm-button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.am-pm-button.selected {
|
||||
background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
.am-pm-button:hover:not(.selected) {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.calendar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.today-button {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
}
|
||||
|
||||
.today-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.today-button:active {
|
||||
background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
}
|
||||
|
||||
.clear-button:active {
|
||||
background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.2)', 'hsl(0 62.8% 30.6% / 0.2)')};
|
||||
}
|
||||
|
||||
/* Timezone selector */
|
||||
.timezone-selector {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
}
|
||||
|
||||
.timezone-selector-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')};
|
||||
}
|
||||
|
||||
.timezone-select {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')};
|
||||
color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.timezone-select:hover {
|
||||
border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')};
|
||||
}
|
||||
|
||||
.timezone-select:focus {
|
||||
outline: none;
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
];
|
||||
|
||||
@@ -2,22 +2,9 @@ import { html, type TemplateResult } from '@design.estate/dees-element';
|
||||
import type { DeesInputDatepicker } from './component.js';
|
||||
|
||||
export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult => {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
];
|
||||
|
||||
const weekDays = component.weekStartsOn === 1
|
||||
? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
||||
: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
|
||||
const days = component.getDaysInMonth();
|
||||
const isAM = component.selectedHour < 12;
|
||||
const timezones = component.getTimezones();
|
||||
|
||||
return html`
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
@@ -39,141 +26,9 @@ export const renderDatepicker = (component: DeesInputDatepicker): TemplateResult
|
||||
` : ''}
|
||||
<dees-icon class="calendar-icon" icon="lucide:calendar" iconSize="16"></dees-icon>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Popup -->
|
||||
<div class="calendar-popup ${component.isOpened ? 'show' : ''} ${component.opensToTop ? 'top' : 'bottom'}">
|
||||
<!-- Month/Year Navigation -->
|
||||
<div class="calendar-header">
|
||||
<button class="nav-button" @click=${component.previousMonth}>
|
||||
<dees-icon icon="lucide:chevronLeft" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
<div class="month-year-display">
|
||||
${monthNames[component.viewDate.getMonth()]} ${component.viewDate.getFullYear()}
|
||||
</div>
|
||||
<button class="nav-button" @click=${component.nextMonth}>
|
||||
<dees-icon icon="lucide:chevronRight" iconSize="16"></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Weekday Headers -->
|
||||
<div class="weekdays">
|
||||
${weekDays.map(day => html`<div class="weekday">${day}</div>`)}
|
||||
</div>
|
||||
|
||||
<!-- Days Grid -->
|
||||
<div class="days-grid">
|
||||
${days.map(day => {
|
||||
const isToday = component.isToday(day);
|
||||
const isSelected = component.isSelected(day);
|
||||
const isOtherMonth = day.getMonth() !== component.viewDate.getMonth();
|
||||
const isDisabled = component.isDisabled(day);
|
||||
const dayEvents = component.getEventsForDate(day);
|
||||
const hasEvents = dayEvents.length > 0;
|
||||
const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''} ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''} ${hasEvents ? 'has-event' : ''}"
|
||||
@click=${() => !isDisabled && component.selectDate(day)}
|
||||
>
|
||||
${day.getDate()}
|
||||
${hasEvents ? html`
|
||||
${totalEventCount > 3 ? html`
|
||||
<div class="event-count">${totalEventCount}</div>
|
||||
` : html`
|
||||
<div class="event-indicator">
|
||||
${dayEvents.slice(0, 3).map(event => html`
|
||||
<div class="event-dot ${event.type || 'info'}"></div>
|
||||
`)}
|
||||
</div>
|
||||
`}
|
||||
${dayEvents[0].title ? html`
|
||||
<div class="event-tooltip">
|
||||
${dayEvents[0].title}
|
||||
${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Time Selector -->
|
||||
${component.enableTime ? html`
|
||||
<div class="time-selector">
|
||||
<div class="time-selector-title">Time</div>
|
||||
<div class="time-inputs">
|
||||
<input
|
||||
type="number"
|
||||
class="time-input"
|
||||
.value=${component.timeFormat === '12h'
|
||||
? (component.selectedHour === 0 ? 12 : component.selectedHour > 12 ? component.selectedHour - 12 : component.selectedHour).toString().padStart(2, '0')
|
||||
: component.selectedHour.toString().padStart(2, '0')}
|
||||
@input=${(e: InputEvent) => component.handleHourInput(e)}
|
||||
min="${component.timeFormat === '12h' ? 1 : 0}"
|
||||
max="${component.timeFormat === '12h' ? 12 : 23}"
|
||||
/>
|
||||
<span class="time-separator">:</span>
|
||||
<input
|
||||
type="number"
|
||||
class="time-input"
|
||||
.value=${component.selectedMinute.toString().padStart(2, '0')}
|
||||
@input=${(e: InputEvent) => component.handleMinuteInput(e)}
|
||||
min="0"
|
||||
max="59"
|
||||
step="${component.minuteIncrement || 1}"
|
||||
/>
|
||||
${component.timeFormat === '12h' ? html`
|
||||
<div class="am-pm-selector">
|
||||
<button
|
||||
class="am-pm-button ${isAM ? 'selected' : ''}"
|
||||
@click=${() => component.setAMPM('am')}
|
||||
>
|
||||
AM
|
||||
</button>
|
||||
<button
|
||||
class="am-pm-button ${!isAM ? 'selected' : ''}"
|
||||
@click=${() => component.setAMPM('pm')}
|
||||
>
|
||||
PM
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Timezone Selector -->
|
||||
${component.enableTimezone ? html`
|
||||
<div class="timezone-selector">
|
||||
<div class="timezone-selector-title">Timezone</div>
|
||||
<select
|
||||
class="timezone-select"
|
||||
.value=${component.timezone}
|
||||
@change=${(e: Event) => component.handleTimezoneChange(e)}
|
||||
>
|
||||
${timezones.map(tz => html`
|
||||
<option value="${tz.value}" ?selected=${tz.value === component.timezone}>
|
||||
${tz.label}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="calendar-actions">
|
||||
<button class="action-button today-button" @click=${component.selectToday}>
|
||||
Today
|
||||
</button>
|
||||
<button class="action-button clear-button" @click=${component.clear}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${component.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
import {
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
DeesElement,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { zIndexRegistry } from '../../00zindex.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-input-dropdown-popup': DeesInputDropdownPopup;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-input-dropdown-popup')
|
||||
export class DeesInputDropdownPopup extends DeesElement {
|
||||
@property({ type: Array })
|
||||
accessor options: { option: string; key: string; payload?: any }[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableSearch: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor opensToTop: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor triggerRect: DOMRect | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor ownerComponent: HTMLElement | null = null;
|
||||
|
||||
@state()
|
||||
accessor filteredOptions: { option: string; key: string; payload?: any }[] = [];
|
||||
|
||||
@state()
|
||||
accessor highlightedIndex: number = 0;
|
||||
|
||||
@state()
|
||||
accessor searchValue: string = '';
|
||||
|
||||
@state()
|
||||
accessor menuZIndex: number = 1000;
|
||||
|
||||
@state()
|
||||
accessor visible: boolean = false;
|
||||
|
||||
private windowLayer: DeesWindowLayer | null = null;
|
||||
private isDestroying: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.selectionBox {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
will-change: transform, opacity;
|
||||
transition: all 0.15s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.98);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
|
||||
min-height: 40px;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.selectionBox.top {
|
||||
transform: translateY(8px) scale(0.98);
|
||||
}
|
||||
|
||||
.selectionBox.show {
|
||||
pointer-events: auto;
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.option {
|
||||
transition: all 0.15s ease;
|
||||
line-height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.option.highlighted {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.no-options {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.search input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.search input::placeholder {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
}
|
||||
|
||||
.search input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.triggerRect) return html``;
|
||||
|
||||
const posStyle = this.computePositionStyle();
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="selectionBox ${this.visible ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}"
|
||||
style="${posStyle}; z-index: ${this.menuZIndex};"
|
||||
>
|
||||
${this.enableSearch
|
||||
? html`
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search options..."
|
||||
.value="${this.searchValue}"
|
||||
@input="${this.handleSearch}"
|
||||
@click="${(e: Event) => e.stopPropagation()}"
|
||||
@keydown="${this.handleSearchKeydown}"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div class="options-container">
|
||||
${this.filteredOptions.length === 0
|
||||
? html`<div class="no-options">No options found</div>`
|
||||
: this.filteredOptions.map((option, index) => {
|
||||
const isHighlighted = this.highlightedIndex === index;
|
||||
return html`
|
||||
<div
|
||||
class="option ${isHighlighted ? 'highlighted' : ''}"
|
||||
@click="${() => this.selectOption(option)}"
|
||||
@mouseenter="${() => (this.highlightedIndex = index)}"
|
||||
>
|
||||
${option.option}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private computePositionStyle(): string {
|
||||
const rect = this.triggerRect!;
|
||||
const left = rect.left;
|
||||
const width = rect.width;
|
||||
|
||||
if (this.opensToTop) {
|
||||
const bottom = window.innerHeight - rect.top + 4;
|
||||
return `left: ${left}px; width: ${width}px; bottom: ${bottom}px; top: auto`;
|
||||
} else {
|
||||
const top = rect.bottom + 4;
|
||||
return `left: ${left}px; width: ${width}px; top: ${top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public async show(): Promise<void> {
|
||||
this.filteredOptions = this.options;
|
||||
this.highlightedIndex = 0;
|
||||
this.searchValue = '';
|
||||
|
||||
// Create window layer (transparent, no blur)
|
||||
this.windowLayer = await DeesWindowLayer.createAndShow();
|
||||
this.windowLayer.addEventListener('click', () => {
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
});
|
||||
|
||||
// Set z-index above the window layer
|
||||
this.menuZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(this, this.menuZIndex);
|
||||
this.style.zIndex = this.menuZIndex.toString();
|
||||
|
||||
document.body.appendChild(this);
|
||||
|
||||
// Animate in on next frame
|
||||
await domtools.plugins.smartdelay.delayFor(0);
|
||||
this.visible = true;
|
||||
|
||||
// Add scroll/resize listeners for repositioning
|
||||
window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
|
||||
window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
|
||||
}
|
||||
|
||||
public async hide(): Promise<void> {
|
||||
// Guard against double-destruction
|
||||
if (this.isDestroying) {
|
||||
return;
|
||||
}
|
||||
this.isDestroying = true;
|
||||
|
||||
// Remove scroll/resize listeners
|
||||
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
||||
window.removeEventListener('resize', this.handleScrollOrResize);
|
||||
|
||||
zIndexRegistry.unregister(this);
|
||||
|
||||
this.searchValue = '';
|
||||
this.filteredOptions = this.options;
|
||||
this.highlightedIndex = 0;
|
||||
|
||||
// Don't await - let window layer cleanup happen in background for instant visual feedback
|
||||
if (this.windowLayer) {
|
||||
this.windowLayer.destroy();
|
||||
this.windowLayer = null;
|
||||
}
|
||||
|
||||
// Animate out via CSS transition
|
||||
this.visible = false;
|
||||
await domtools.plugins.smartdelay.delayFor(150);
|
||||
|
||||
if (this.parentElement) {
|
||||
this.parentElement.removeChild(this);
|
||||
}
|
||||
|
||||
this.isDestroying = false;
|
||||
}
|
||||
|
||||
public async focusSearchInput(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
const input = this.shadowRoot!.querySelector('.search input') as HTMLInputElement;
|
||||
if (input) input.focus();
|
||||
}
|
||||
|
||||
public updateOptions(options: { option: string; key: string; payload?: any }[]): void {
|
||||
this.options = options;
|
||||
// Re-filter with current search value
|
||||
if (this.searchValue) {
|
||||
const searchLower = this.searchValue.toLowerCase();
|
||||
this.filteredOptions = this.options.filter((opt) =>
|
||||
opt.option.toLowerCase().includes(searchLower)
|
||||
);
|
||||
} else {
|
||||
this.filteredOptions = this.options;
|
||||
}
|
||||
this.highlightedIndex = 0;
|
||||
}
|
||||
|
||||
private selectOption(option: { option: string; key: string; payload?: any }): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('option-selected', {
|
||||
detail: option,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleSearch = (event: Event): void => {
|
||||
const searchTerm = (event.target as HTMLInputElement).value;
|
||||
this.searchValue = searchTerm;
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
this.filteredOptions = this.options.filter((option) =>
|
||||
option.option.toLowerCase().includes(searchLower)
|
||||
);
|
||||
this.highlightedIndex = 0;
|
||||
};
|
||||
|
||||
private handleSearchKeydown = (event: KeyboardEvent): void => {
|
||||
const key = event.key;
|
||||
const maxIndex = this.filteredOptions.length - 1;
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
|
||||
} else if (key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
|
||||
} else if (key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (this.filteredOptions[this.highlightedIndex]) {
|
||||
this.selectOption(this.filteredOptions[this.highlightedIndex]);
|
||||
}
|
||||
} else if (key === 'Escape') {
|
||||
event.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('close-request'));
|
||||
}
|
||||
};
|
||||
|
||||
private handleScrollOrResize = (): void => {
|
||||
this.dispatchEvent(new CustomEvent('reposition-request'));
|
||||
};
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
|
||||
window.removeEventListener('resize', this.handleScrollOrResize);
|
||||
zIndexRegistry.unregister(this);
|
||||
}
|
||||
}
|
||||
@@ -50,43 +50,46 @@ export const demoFunc = () => html`
|
||||
|
||||
// Log when country changes
|
||||
if (countryDropdown) {
|
||||
countryDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
|
||||
countryDropdown.addEventListener('selectedOption', ((event: CustomEvent) => {
|
||||
console.log('Country selected:', event.detail);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
// Log when role changes
|
||||
if (roleDropdown) {
|
||||
roleDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
|
||||
roleDropdown.addEventListener('selectedOption', ((event: CustomEvent) => {
|
||||
console.log('Role selected:', event.detail);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Country'}
|
||||
.options=${[
|
||||
{ option: 'United States', key: 'us' },
|
||||
{ option: 'Canada', key: 'ca' },
|
||||
{ option: 'Germany', key: 'de' },
|
||||
{ option: 'France', key: 'fr' },
|
||||
{ option: 'United Kingdom', key: 'uk' },
|
||||
{ option: 'Australia', key: 'au' },
|
||||
{ option: 'Japan', key: 'jp' },
|
||||
{ option: 'Brazil', key: 'br' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'United States', key: 'us' }}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Role'}
|
||||
.options=${[
|
||||
{ option: 'Administrator', key: 'admin' },
|
||||
{ option: 'Editor', key: 'editor' },
|
||||
{ option: 'Viewer', key: 'viewer' },
|
||||
{ option: 'Guest', key: 'guest' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Country'}
|
||||
.description=${'Choose the country where your business is registered'}
|
||||
.options=${[
|
||||
{ option: 'United States', key: 'us' },
|
||||
{ option: 'Canada', key: 'ca' },
|
||||
{ option: 'Germany', key: 'de' },
|
||||
{ option: 'France', key: 'fr' },
|
||||
{ option: 'United Kingdom', key: 'uk' },
|
||||
{ option: 'Australia', key: 'au' },
|
||||
{ option: 'Japan', key: 'jp' },
|
||||
{ option: 'Brazil', key: 'br' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'United States', key: 'us' }}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Role'}
|
||||
.options=${[
|
||||
{ option: 'Administrator', key: 'admin' },
|
||||
{ option: 'Editor', key: 'editor' },
|
||||
{ option: 'Viewer', key: 'viewer' },
|
||||
{ option: 'Guest', key: 'guest' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -95,9 +98,9 @@ export const demoFunc = () => html`
|
||||
const priorityDropdown = elementArg.querySelector('dees-input-dropdown');
|
||||
|
||||
if (priorityDropdown) {
|
||||
priorityDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
|
||||
priorityDropdown.addEventListener('selectedOption', ((event: CustomEvent) => {
|
||||
console.log(`Priority changed to: ${event.detail.option}`);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}>
|
||||
@@ -120,47 +123,49 @@ export const demoFunc = () => html`
|
||||
|
||||
// Log all changes from horizontal dropdowns
|
||||
dropdowns.forEach((dropdown) => {
|
||||
dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
|
||||
dropdown.addEventListener('selectedOption', ((event: CustomEvent) => {
|
||||
const label = dropdown.getAttribute('label');
|
||||
console.log(`${label}: ${event.detail.option}`);
|
||||
});
|
||||
}) as EventListener);
|
||||
});
|
||||
}}>
|
||||
<dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-dropdown
|
||||
.label=${'Department'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${[
|
||||
{ option: 'Engineering', key: 'eng' },
|
||||
{ option: 'Design', key: 'design' },
|
||||
{ option: 'Marketing', key: 'marketing' },
|
||||
{ option: 'Sales', key: 'sales' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Team Size'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: '1-5', key: 'small' },
|
||||
{ option: '6-20', key: 'medium' },
|
||||
{ option: '21-50', key: 'large' },
|
||||
{ option: '50+', key: 'xlarge' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Location'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${[
|
||||
{ option: 'Remote', key: 'remote' },
|
||||
{ option: 'On-site', key: 'onsite' },
|
||||
{ option: 'Hybrid', key: 'hybrid' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-dropdown
|
||||
.label=${'Department'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${[
|
||||
{ option: 'Engineering', key: 'eng' },
|
||||
{ option: 'Design', key: 'design' },
|
||||
{ option: 'Marketing', key: 'marketing' },
|
||||
{ option: 'Sales', key: 'sales' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Team Size'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.enableSearch=${false}
|
||||
.options=${[
|
||||
{ option: '1-5', key: 'small' },
|
||||
{ option: '6-20', key: 'medium' },
|
||||
{ option: '21-50', key: 'large' },
|
||||
{ option: '50+', key: 'xlarge' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Location'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.options=${[
|
||||
{ option: 'Remote', key: 'remote' },
|
||||
{ option: 'On-site', key: 'onsite' },
|
||||
{ option: 'Hybrid', key: 'hybrid' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -176,24 +181,26 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
|
||||
<dees-input-dropdown
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.options=${[
|
||||
{ option: 'Option A', key: 'a' },
|
||||
{ option: 'Option B', key: 'b' },
|
||||
{ option: 'Option C', key: 'c' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Disabled Dropdown'}
|
||||
.disabled=${true}
|
||||
.options=${[
|
||||
{ option: 'Cannot Select', key: 'disabled' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||
></dees-input-dropdown>
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.options=${[
|
||||
{ option: 'Option A', key: 'a' },
|
||||
{ option: 'Option B', key: 'b' },
|
||||
{ option: 'Option C', key: 'c' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Disabled Dropdown'}
|
||||
.disabled=${true}
|
||||
.options=${[
|
||||
{ option: 'Cannot Select', key: 'disabled' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||
></dees-input-dropdown>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -206,9 +213,9 @@ export const demoFunc = () => html`
|
||||
const dropdown = elementArg.querySelector('dees-input-dropdown');
|
||||
|
||||
if (dropdown) {
|
||||
dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
|
||||
dropdown.addEventListener('selectedOption', ((event: CustomEvent) => {
|
||||
console.log('Bottom dropdown selected:', event.detail);
|
||||
});
|
||||
}) as EventListener);
|
||||
|
||||
// Note: The dropdown automatically detects available space
|
||||
// and opens upward when near the bottom of the viewport
|
||||
@@ -238,16 +245,16 @@ export const demoFunc = () => html`
|
||||
output.innerHTML = '<em>Select a product to see details...</em>';
|
||||
|
||||
// Handle dropdown changes
|
||||
dropdown.addEventListener('change', (event: CustomEvent) => {
|
||||
dropdown.addEventListener('change', ((event: CustomEvent) => {
|
||||
if (event.detail.value) {
|
||||
output.innerHTML = `
|
||||
(output as HTMLElement).innerHTML = `
|
||||
<strong>Selected:</strong> ${event.detail.value.option}<br>
|
||||
<strong>Key:</strong> ${event.detail.value.key}<br>
|
||||
<strong>Price:</strong> $${event.detail.value.payload?.price || 'N/A'}<br>
|
||||
<strong>Features:</strong> ${event.detail.value.payload?.features?.join(', ') || 'N/A'}
|
||||
`;
|
||||
}
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}>
|
||||
@@ -271,20 +278,20 @@ export const demoFunc = () => html`
|
||||
const frameworkDropdown = elementArg.querySelector('dees-input-dropdown[key="framework"]');
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('formData', (event: CustomEvent) => {
|
||||
form.addEventListener('formData', ((event: CustomEvent) => {
|
||||
console.log('Form submitted with data:', event.detail.data);
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
if (projectTypeDropdown && frameworkDropdown) {
|
||||
// Filter frameworks based on project type
|
||||
projectTypeDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
|
||||
projectTypeDropdown.addEventListener('selectedOption', ((event: CustomEvent) => {
|
||||
const selectedType = event.detail.key;
|
||||
console.log(`Project type changed to: ${selectedType}`);
|
||||
|
||||
|
||||
// In a real app, you could filter the framework options based on project type
|
||||
// For demo purposes, we just log the change
|
||||
});
|
||||
}) as EventListener);
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}>
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { demoFunc } from './dees-input-dropdown.demo.js';
|
||||
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { DeesInputDropdownPopup } from './dees-input-dropdown-popup.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -30,14 +30,14 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
accessor options: { option: string; key: string; payload?: any }[] = [];
|
||||
|
||||
@property()
|
||||
accessor selectedOption: { option: string; key: string; payload?: any } = null;
|
||||
accessor selectedOption: { option: string; key: string; payload?: any } | null = null;
|
||||
|
||||
// Add value property for form compatibility
|
||||
public get value() {
|
||||
return this.selectedOption;
|
||||
}
|
||||
|
||||
public set value(val: { option: string; key: string; payload?: any }) {
|
||||
public set value(val: { option: string; key: string; payload?: any } | null) {
|
||||
this.selectedOption = val;
|
||||
}
|
||||
|
||||
@@ -46,27 +46,22 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
})
|
||||
accessor enableSearch: boolean = true;
|
||||
|
||||
@state()
|
||||
accessor opensToTop: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor filteredOptions: { option: string; key: string; payload?: any }[] = [];
|
||||
|
||||
@state()
|
||||
accessor highlightedIndex: number = 0;
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
accessor vintegrated: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor isOpened = false;
|
||||
|
||||
@state()
|
||||
accessor searchValue: string = '';
|
||||
private popupInstance: DeesInputDropdownPopup | null = null;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -138,135 +133,34 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
.selectionBox {
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
transition: all 0.15s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.98);
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
|
||||
min-height: 40px;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
margin-top: 4px;
|
||||
z-index: 50;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.selectionBox.top {
|
||||
bottom: calc(100% + 4px);
|
||||
top: auto;
|
||||
margin-top: 0;
|
||||
margin-bottom: 4px;
|
||||
transform: translateY(8px) scale(0.98);
|
||||
:host([vintegrated]) .maincontainer {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.selectionBox.bottom {
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.selectionBox.show {
|
||||
pointer-events: all;
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Options container */
|
||||
.options-container {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Options */
|
||||
.option {
|
||||
transition: all 0.15s ease;
|
||||
line-height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
.option.highlighted {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
/* No options message */
|
||||
.no-options {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.search {
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.search.bottom {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
margin-bottom: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.search input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
:host([vintegrated]) .selectedBox {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 32px 0 16px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.search input::placeholder {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
}
|
||||
|
||||
.search input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.options-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar-track {
|
||||
:host([vintegrated]) .selectedBox:hover:not(.disabled),
|
||||
:host([vintegrated]) .selectedBox:focus-visible {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.options-container::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
:host([vintegrated]) .selectedBox::after {
|
||||
right: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -274,7 +168,7 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<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="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}"
|
||||
@@ -284,68 +178,27 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
>
|
||||
${this.selectedOption?.option || 'Select an option'}
|
||||
</div>
|
||||
<div class="selectionBox ${this.isOpened ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}">
|
||||
${this.enableSearch
|
||||
? html`
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search options..."
|
||||
.value="${this.searchValue}"
|
||||
@input="${this.handleSearch}"
|
||||
@click="${(e: Event) => e.stopPropagation()}"
|
||||
@keydown="${this.handleSearchKeydown}"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div class="options-container">
|
||||
${this.filteredOptions.length === 0
|
||||
? html`<div class="no-options">No options found</div>`
|
||||
: this.filteredOptions.map((option, index) => {
|
||||
const isHighlighted = this.highlightedIndex === index;
|
||||
return html`
|
||||
<div
|
||||
class="option ${isHighlighted ? 'highlighted' : ''}"
|
||||
@click="${() => this.updateSelection(option)}"
|
||||
@mouseenter="${() => this.highlightedIndex = index}"
|
||||
>
|
||||
${option.option}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.selectedOption = this.selectedOption || null;
|
||||
this.filteredOptions = this.options;
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('options')) {
|
||||
this.filteredOptions = this.options;
|
||||
|
||||
if (changedProperties.has('options') && this.popupInstance && this.isOpened) {
|
||||
this.popupInstance.updateOptions(this.options);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) {
|
||||
this.selectedOption = selectedOption;
|
||||
this.isOpened = false;
|
||||
this.searchValue = '';
|
||||
this.filteredOptions = this.options;
|
||||
this.highlightedIndex = 0;
|
||||
this.closePopup();
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectedOption', {
|
||||
@@ -353,92 +206,95 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
private handleClickOutside = (event: MouseEvent) => {
|
||||
const path = event.composedPath();
|
||||
if (!path.includes(this)) {
|
||||
this.isOpened = false;
|
||||
this.searchValue = '';
|
||||
this.filteredOptions = this.options;
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
};
|
||||
|
||||
public async toggleSelectionBox() {
|
||||
this.isOpened = !this.isOpened;
|
||||
|
||||
if (this.isOpened) {
|
||||
// Check available space and set position
|
||||
const selectedBox = this.shadowRoot.querySelector('.selectedBox') as HTMLElement;
|
||||
const rect = selectedBox.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
|
||||
// Determine if we should open upwards
|
||||
this.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
|
||||
|
||||
// Focus search input if present
|
||||
await this.updateComplete;
|
||||
const searchInput = this.shadowRoot.querySelector('.search input') as HTMLInputElement;
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
// Add click outside listener
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.handleClickOutside);
|
||||
}, 0);
|
||||
} else {
|
||||
// Cleanup
|
||||
this.searchValue = '';
|
||||
this.filteredOptions = this.options;
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
this.closePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpened = true;
|
||||
|
||||
// Get trigger position
|
||||
const selectedBox = this.shadowRoot!.querySelector('.selectedBox') as HTMLElement;
|
||||
const rect = selectedBox.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
const opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
|
||||
|
||||
// Create popup if needed
|
||||
if (!this.popupInstance) {
|
||||
this.popupInstance = new DeesInputDropdownPopup();
|
||||
}
|
||||
|
||||
// Configure popup
|
||||
this.popupInstance.options = this.options;
|
||||
this.popupInstance.enableSearch = this.enableSearch;
|
||||
this.popupInstance.opensToTop = opensToTop;
|
||||
this.popupInstance.triggerRect = rect;
|
||||
this.popupInstance.ownerComponent = this;
|
||||
|
||||
// Listen for popup events
|
||||
this.popupInstance.addEventListener('option-selected', this.handleOptionSelected);
|
||||
this.popupInstance.addEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest);
|
||||
|
||||
// Show popup (creates window layer, appends to document.body)
|
||||
await this.popupInstance.show();
|
||||
|
||||
// Focus search input
|
||||
if (this.enableSearch) {
|
||||
await this.popupInstance.focusSearchInput();
|
||||
}
|
||||
}
|
||||
|
||||
private handleSearch(event: Event): void {
|
||||
const searchTerm = (event.target as HTMLInputElement).value;
|
||||
this.searchValue = searchTerm;
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
this.filteredOptions = this.options.filter((option) =>
|
||||
option.option.toLowerCase().includes(searchLower)
|
||||
);
|
||||
this.highlightedIndex = 0;
|
||||
}
|
||||
private closePopup(): void {
|
||||
this.isOpened = false;
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent): void {
|
||||
const key = event.key;
|
||||
const maxIndex = this.filteredOptions.length - 1;
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
|
||||
} else if (key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
|
||||
} else if (key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (this.filteredOptions[this.highlightedIndex]) {
|
||||
this.updateSelection(this.filteredOptions[this.highlightedIndex]);
|
||||
}
|
||||
} else if (key === 'Escape') {
|
||||
event.preventDefault();
|
||||
this.isOpened = false;
|
||||
if (this.popupInstance) {
|
||||
this.popupInstance.removeEventListener('option-selected', this.handleOptionSelected);
|
||||
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
||||
this.popupInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private handleSearchKeydown(event: KeyboardEvent): void {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
|
||||
this.handleKeyDown(event);
|
||||
private handleOptionSelected = (event: Event): void => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
this.updateSelection(detail);
|
||||
};
|
||||
|
||||
private handleCloseRequest = (): void => {
|
||||
this.closePopup();
|
||||
};
|
||||
|
||||
private handleRepositionRequest = (): void => {
|
||||
if (!this.popupInstance || !this.isOpened) return;
|
||||
|
||||
const selectedBox = this.shadowRoot!.querySelector('.selectedBox') as HTMLElement;
|
||||
if (!selectedBox) return;
|
||||
|
||||
const rect = selectedBox.getBoundingClientRect();
|
||||
|
||||
// Close if trigger scrolled off-screen
|
||||
if (rect.bottom < 0 || rect.top > window.innerHeight) {
|
||||
this.closePopup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update position
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
this.popupInstance.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
|
||||
this.popupInstance.triggerRect = rect;
|
||||
};
|
||||
|
||||
private handleSelectedBoxKeydown(event: KeyboardEvent) {
|
||||
if (this.disabled) return;
|
||||
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.toggleSelectionBox();
|
||||
@@ -450,21 +306,27 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
if (this.isOpened) {
|
||||
this.isOpened = false;
|
||||
this.closePopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getValue(): { option: string; key: string; payload?: any } {
|
||||
public getValue(): { option: string; key: string; payload?: any } | null {
|
||||
return this.selectedOption;
|
||||
}
|
||||
|
||||
public setValue(value: { option: string; key: string; payload?: any }): void {
|
||||
this.selectedOption = value;
|
||||
}
|
||||
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
if (this.popupInstance) {
|
||||
this.popupInstance.removeEventListener('option-selected', this.handleOptionSelected);
|
||||
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
|
||||
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
|
||||
this.popupInstance.hide();
|
||||
this.popupInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './dees-input-dropdown.js';
|
||||
export * from './dees-input-dropdown-popup.js';
|
||||
|
||||
@@ -3,6 +3,7 @@ import { demoFunc } from './demo.js';
|
||||
import { fileuploadStyles } from './styles.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import '../../00group-layout/dees-label/dees-label.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
import {
|
||||
customElement,
|
||||
@@ -48,7 +49,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
accessor maxFiles: number = 0; // 0 means no limit
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
accessor validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
|
||||
accessor validationState: 'valid' | 'invalid' | 'warn' | 'pending' | null = null;
|
||||
|
||||
accessor validationMessage: string = '';
|
||||
|
||||
@@ -72,17 +73,16 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
<div class="input-wrapper">
|
||||
<dees-label
|
||||
.label=${this.label}
|
||||
.description=${this.description}
|
||||
.infoText=${this.infoText}
|
||||
.required=${this.required}
|
||||
></dees-label>
|
||||
<div
|
||||
class="dropzone ${this.state === 'dragOver' ? 'dropzone--active' : ''} ${this.disabled ? 'dropzone--disabled' : ''} ${this.value.length > 0 ? 'dropzone--has-files' : ''}"
|
||||
role="button"
|
||||
<dees-tile
|
||||
class="${this.state === 'dragOver' ? 'dragover' : ''}"
|
||||
@click=${this.handleDropzoneClick}
|
||||
@keydown=${this.handleDropzoneKeydown}
|
||||
tabindex=${this.disabled ? -1 : 0}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${`Select files${acceptedSummary ? ` (${acceptedSummary})` : ''}`}
|
||||
@click=${this.handleDropzoneClick}
|
||||
@keydown=${this.handleDropzoneKeydown}
|
||||
>
|
||||
<input
|
||||
class="file-input"
|
||||
@@ -94,55 +94,48 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
@change=${this.handleFileInputChange}
|
||||
tabindex="-1"
|
||||
/>
|
||||
<div class="dropzone__body">
|
||||
<div class="dropzone__icon">
|
||||
${this.isLoading
|
||||
? html`<span class="dropzone__loader" aria-hidden="true"></span>`
|
||||
: html`<dees-icon icon="lucide:FolderOpen"></dees-icon>`}
|
||||
</div>
|
||||
<div class="dropzone__content">
|
||||
<span class="dropzone__headline">${this.buttonText || 'Select files'}</span>
|
||||
<span class="dropzone__subline">
|
||||
Drag and drop files here or
|
||||
<button
|
||||
type="button"
|
||||
class="dropzone__browse"
|
||||
@click=${this.handleBrowseClick}
|
||||
?disabled=${this.disabled}
|
||||
>
|
||||
browse
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropzone__meta">
|
||||
${metaEntries.map((entry) => html`<span>${entry}</span>`)}
|
||||
<div slot="header" class="dropzone-header">
|
||||
${this.isLoading
|
||||
? html`<span class="dropzone-loader" aria-hidden="true"></span>`
|
||||
: html`<dees-icon icon="lucide:Upload"></dees-icon>`}
|
||||
<span class="dropzone-title">Drop files here or</span>
|
||||
<button
|
||||
type="button"
|
||||
class="dropzone-browse"
|
||||
@click=${(e: MouseEvent) => { e.stopPropagation(); this.openFileSelector(); }}
|
||||
?disabled=${this.disabled}
|
||||
>browse</button>
|
||||
</div>
|
||||
${this.renderFileList()}
|
||||
</div>
|
||||
<div slot="footer" class="dropzone-footer">
|
||||
${metaEntries.map((entry) => html`<span class="meta-chip">${entry}</span>`)}
|
||||
</div>
|
||||
</dees-tile>
|
||||
${this.validationMessage
|
||||
? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>`
|
||||
: html``}
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderFileList(): TemplateResult {
|
||||
if (this.value.length === 0) {
|
||||
return html``;
|
||||
return html`
|
||||
<div class="file-list-empty">
|
||||
<dees-icon icon="lucide:FileStack"></dees-icon>
|
||||
<span>No files selected</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="file-list">
|
||||
<div class="file-list__header">
|
||||
<div class="file-list-header">
|
||||
<span>${this.value.length} file${this.value.length === 1 ? '' : 's'} selected</span>
|
||||
${this.value.length > 0
|
||||
? html`<button type="button" class="file-list__clear" @click=${this.handleClearAll}>Clear ${this.value.length > 1 ? 'all' : ''}</button>`
|
||||
: html``}
|
||||
</div>
|
||||
<div class="file-list__items">
|
||||
${this.value.map((file) => this.renderFileRow(file))}
|
||||
<button type="button" class="file-list-clear" @click=${this.handleClearAll}>Clear ${this.value.length > 1 ? 'all' : ''}</button>
|
||||
</div>
|
||||
${this.value.map((file) => this.renderFileRow(file))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -193,21 +186,14 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
// Don't open file selector if clicking on the browse button or file list
|
||||
if ((event.target as HTMLElement).closest('.dropzone__browse, .file-list')) {
|
||||
// Don't open file selector when clicking file list items or actions
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.closest('.file-list, .dropzone-header, .dropzone-footer')) {
|
||||
return;
|
||||
}
|
||||
this.openFileSelector();
|
||||
};
|
||||
|
||||
private handleBrowseClick = (event: MouseEvent) => {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation(); // Stop propagation to prevent double trigger
|
||||
this.openFileSelector();
|
||||
};
|
||||
|
||||
private handleDropzoneKeydown = (event: KeyboardEvent) => {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
@@ -266,7 +252,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
return;
|
||||
}
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
||||
this.dropArea!.addEventListener(eventName, this.handleDragEvent);
|
||||
this.dropArea!.addEventListener(eventName, this.handleDragEvent as unknown as EventListener);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -275,12 +261,12 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
return;
|
||||
}
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
||||
this.dropArea!.removeEventListener(eventName, this.handleDragEvent);
|
||||
this.dropArea!.removeEventListener(eventName, this.handleDragEvent as unknown as EventListener);
|
||||
});
|
||||
}
|
||||
|
||||
private rebindInteractiveElements(): void {
|
||||
const newDropArea = this.shadowRoot?.querySelector('.dropzone') as HTMLElement | null;
|
||||
const newDropArea = this.shadowRoot?.querySelector('dees-tile') as HTMLElement | null;
|
||||
|
||||
if (newDropArea !== this.dropArea) {
|
||||
this.detachDropListeners();
|
||||
|
||||
@@ -55,40 +55,43 @@ export const demoFunc = () => html`
|
||||
.title=${'Modern file uploader'}
|
||||
.subtitle=${'Shadcn-inspired layout with drag & drop, previews and validation'}
|
||||
>
|
||||
<div class="demo-grid demo-grid--two">
|
||||
<div class="demo-stack">
|
||||
<dees-input-fileupload
|
||||
.label=${'Attachments'}
|
||||
.description=${'Upload supporting documents for your request'}
|
||||
.accept=${'image/*,.pdf,.zip'}
|
||||
.maxSize=${10 * 1024 * 1024}
|
||||
></dees-input-fileupload>
|
||||
<dees-form>
|
||||
<div class="demo-grid demo-grid--two">
|
||||
<div class="demo-stack">
|
||||
<dees-input-fileupload
|
||||
.label=${'Attachments'}
|
||||
.infoText=${'Upload supporting documents for your request'}
|
||||
.description=${'Accepted formats: images, PDF, and ZIP archives up to 10MB'}
|
||||
.accept=${'image/*,.pdf,.zip'}
|
||||
.maxSize=${10 * 1024 * 1024}
|
||||
></dees-input-fileupload>
|
||||
|
||||
<dees-input-fileupload
|
||||
.label=${'Brand assets'}
|
||||
.description=${'Upload high-resolution imagery (JPG/PNG)'}
|
||||
.accept=${'image/jpeg,image/png'}
|
||||
.multiple=${false}
|
||||
.maxSize=${5 * 1024 * 1024}
|
||||
.buttonText=${'Select cover image'}
|
||||
></dees-input-fileupload>
|
||||
<dees-input-fileupload
|
||||
.label=${'Brand assets'}
|
||||
.infoText=${'Upload high-resolution imagery (JPG/PNG)'}
|
||||
.accept=${'image/jpeg,image/png'}
|
||||
.multiple=${false}
|
||||
.maxSize=${5 * 1024 * 1024}
|
||||
.buttonText=${'Select cover image'}
|
||||
></dees-input-fileupload>
|
||||
</div>
|
||||
|
||||
<div class="demo-stack">
|
||||
<dees-input-fileupload
|
||||
.label=${'Audio uploads'}
|
||||
.infoText=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
|
||||
.accept=${'audio/*'}
|
||||
.maxSize=${25 * 1024 * 1024}
|
||||
></dees-input-fileupload>
|
||||
|
||||
<dees-input-fileupload
|
||||
.label=${'Disabled example'}
|
||||
.infoText=${'Uploader is disabled while moderation is pending'}
|
||||
.disabled=${true}
|
||||
></dees-input-fileupload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-stack">
|
||||
<dees-input-fileupload
|
||||
.label=${'Audio uploads'}
|
||||
.description=${'Share podcast drafts (MP3/WAV, max 25MB each)'}
|
||||
.accept=${'audio/*'}
|
||||
.maxSize=${25 * 1024 * 1024}
|
||||
></dees-input-fileupload>
|
||||
|
||||
<dees-input-fileupload
|
||||
.label=${'Disabled example'}
|
||||
.description=${'Uploader is disabled while moderation is pending'}
|
||||
.disabled=${true}
|
||||
></dees-input-fileupload>
|
||||
</div>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel
|
||||
@@ -97,10 +100,9 @@ export const demoFunc = () => html`
|
||||
>
|
||||
<div class="demo-grid">
|
||||
<dees-form>
|
||||
<div class="demo-stack">
|
||||
<dees-input-text
|
||||
.label=${'Project name'}
|
||||
.description=${'How should we refer to this project internally?'}
|
||||
.infoText=${'How should we refer to this project internally?'}
|
||||
.required=${true}
|
||||
.key=${'projectName'}
|
||||
></dees-input-text>
|
||||
@@ -114,7 +116,7 @@ export const demoFunc = () => html`
|
||||
|
||||
<dees-input-fileupload
|
||||
.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}
|
||||
.accept=${'application/pdf'}
|
||||
.maxSize=${15 * 1024 * 1024}
|
||||
@@ -124,7 +126,7 @@ export const demoFunc = () => html`
|
||||
|
||||
<dees-input-fileupload
|
||||
.label=${'Creative references'}
|
||||
.description=${'Optional. Upload up to five visual references'}
|
||||
.infoText=${'Optional. Upload up to five visual references'}
|
||||
.accept=${'image/*'}
|
||||
.maxFiles=${5}
|
||||
.maxSize=${8 * 1024 * 1024}
|
||||
@@ -133,13 +135,12 @@ export const demoFunc = () => html`
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Notes'}
|
||||
.description=${'Add optional context for reviewers'}
|
||||
.infoText=${'Add optional context for reviewers'}
|
||||
.inputType=${'textarea'}
|
||||
.key=${'notes'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-form-submit .text=${'Submit briefing'}></dees-form-submit>
|
||||
</div>
|
||||
</dees-form>
|
||||
|
||||
<div class="demo-note">
|
||||
|
||||
@@ -1,201 +1,158 @@
|
||||
import { css, cssManager } from '@design.estate/dees-element';
|
||||
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
export const fileuploadStyles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px dashed ${cssManager.bdTheme('hsl(215 16% 80%)', 'hsl(217 20% 25%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')};
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
/* ── Tile integration ── */
|
||||
dees-tile {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dropzone:focus-visible {
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')},
|
||||
0 0 0 4px ${cssManager.bdTheme('hsl(217 91% 60% / 0.5)', 'hsl(213 93% 68% / 0.4)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
dees-tile:hover::part(outer) {
|
||||
border-color: var(--dees-color-border-strong);
|
||||
}
|
||||
|
||||
.dropzone--active {
|
||||
border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
box-shadow: 0 12px 32px ${cssManager.bdTheme('rgba(15, 23, 42, 0.12)', 'rgba(0, 0, 0, 0.35)')};
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.06)', 'hsl(213 93% 68% / 0.12)')};
|
||||
dees-tile.dragover::part(outer) {
|
||||
border-color: var(--dees-color-accent-primary);
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(217 91% 60% / 0.15)', 'hsl(213 93% 68% / 0.15)')};
|
||||
}
|
||||
|
||||
.dropzone--has-files {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(215 20% 11%)')};
|
||||
}
|
||||
|
||||
.dropzone--disabled {
|
||||
:host([disabled]) dees-tile {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropzone__body {
|
||||
/* ── Header slot: sleek toolbar ── */
|
||||
.dropzone-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.dropzone__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 60% / 0.12)', 'hsl(213 93% 68% / 0.12)')};
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
.dropzone-header dees-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.dropzone__icon dees-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.dropzone__loader {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
.dropzone-loader {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: var(--dees-radius-full);
|
||||
border: 2px solid ${cssManager.bdTheme('rgba(15, 23, 42, 0.15)', 'rgba(255, 255, 255, 0.15)')};
|
||||
border-top-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
border-top-color: var(--dees-color-accent-primary);
|
||||
animation: loader-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.dropzone__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dropzone__headline {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
|
||||
}
|
||||
|
||||
.dropzone__subline {
|
||||
.dropzone-title {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.dropzone__browse {
|
||||
.dropzone-browse {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin-left: 4px;
|
||||
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
color: var(--dees-color-accent-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropzone__browse:hover {
|
||||
.dropzone-browse:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dropzone__browse:disabled {
|
||||
.dropzone-browse:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dropzone__meta {
|
||||
margin-top: 14px;
|
||||
/* ── Content slot: file list in rounded inset ── */
|
||||
.file-list-empty {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 72%)')};
|
||||
padding: 24px 16px;
|
||||
color: var(--dees-color-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dropzone__meta span {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(213 93% 18%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(213 93% 24%)')};
|
||||
.file-list-empty dees-icon {
|
||||
font-size: 24px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(213 93% 24%)')};
|
||||
}
|
||||
|
||||
.file-list__header {
|
||||
.file-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 68%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.file-list__clear {
|
||||
.file-list-clear {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
color: var(--dees-color-accent-primary);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.file-list__clear:hover {
|
||||
.file-list-clear:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.file-list__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.5)', 'hsl(215 20% 16% / 0.5)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(213 27% 92%)', 'hsl(217 25% 26%)')};
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s ease;
|
||||
padding: 6px 12px;
|
||||
transition: background var(--dees-transition-fast) ease;
|
||||
}
|
||||
|
||||
.file-row:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.8)', 'hsl(215 20% 16% / 0.8)')};
|
||||
background: var(--dees-color-row-hover);
|
||||
}
|
||||
|
||||
.file-thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 32% 18%)')};
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--dees-radius-sm);
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -204,16 +161,15 @@ export const fileuploadStyles = [
|
||||
}
|
||||
|
||||
.file-thumb dees-icon {
|
||||
font-size: 18px;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 70%)')};
|
||||
font-size: 16px;
|
||||
color: var(--dees-color-text-muted);
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.thumb-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -223,14 +179,14 @@ export const fileuploadStyles = [
|
||||
.file-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')};
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--dees-color-text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -241,8 +197,8 @@ export const fileuploadStyles = [
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
@@ -250,39 +206,40 @@ export const fileuploadStyles = [
|
||||
}
|
||||
|
||||
.file-type {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 32% 28%)')};
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')};
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--dees-radius-full);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
color: var(--dees-color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--dees-radius-xs);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease, transform 0.15s ease, color 0.15s ease;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 52%)', 'hsl(215 16% 68%)')};
|
||||
transition: background var(--dees-transition-fast) ease,
|
||||
color var(--dees-transition-fast) ease;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.remove-button:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 72% 50% / 0.08)', 'hsl(0 62% 32% / 0.15)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 72% 46%)', 'hsl(0 70% 70%)')};
|
||||
color: var(--dees-color-accent-error);
|
||||
}
|
||||
|
||||
.remove-button:active {
|
||||
@@ -298,9 +255,28 @@ export const fileuploadStyles = [
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Footer slot: meta chips ── */
|
||||
.dropzone-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-chip {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--dees-radius-full);
|
||||
color: var(--dees-color-text-muted);
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
border: 1px solid var(--dees-color-border-subtle);
|
||||
}
|
||||
|
||||
/* ── Validation ── */
|
||||
.validation-message {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')};
|
||||
color: var(--dees-color-accent-error);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,54 +24,61 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
|
||||
<dees-input-iban
|
||||
.label=${'Bank Account IBAN'}
|
||||
.description=${'Enter your International Bank Account Number'}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Verified IBAN'}
|
||||
.description=${'This IBAN has been verified'}
|
||||
.value=${'DE89370400440532013000'}
|
||||
></dees-input-iban>
|
||||
<dees-form>
|
||||
<dees-input-iban
|
||||
.label=${'Bank Account IBAN'}
|
||||
.infoText=${'Enter your International Bank Account Number'}
|
||||
.description=${'Your IBAN can be found on your bank statement'}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Verified IBAN'}
|
||||
.infoText=${'This IBAN has been verified'}
|
||||
.value=${'DE89370400440532013000'}
|
||||
></dees-input-iban>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Payment Information'} .subtitle=${'IBAN input with horizontal layout for payment forms'}>
|
||||
<div class="payment-group">
|
||||
<dees-input-text
|
||||
.label=${'Account Holder'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'John Doe'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'IBAN'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'GB82WEST12345698765432'}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="payment-group">
|
||||
<dees-input-text
|
||||
.label=${'Account Holder'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'John Doe'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'IBAN'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'GB82WEST12345698765432'}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}>
|
||||
<dees-input-iban
|
||||
.label=${'Payment Account'}
|
||||
.description=${'Required for processing payments'}
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Locked IBAN'}
|
||||
.description=${'This IBAN cannot be changed'}
|
||||
.value=${'FR1420041010050500013M02606'}
|
||||
.disabled=${true}
|
||||
></dees-input-iban>
|
||||
<dees-form>
|
||||
<dees-input-iban
|
||||
.label=${'Payment Account'}
|
||||
.infoText=${'Required for processing payments'}
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Locked IBAN'}
|
||||
.infoText=${'This IBAN cannot be changed'}
|
||||
.value=${'FR1420041010050500013M02606'}
|
||||
.disabled=${true}
|
||||
></dees-input-iban>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Bank Transfer Form'} .subtitle=${'Complete form example with IBAN validation'}>
|
||||
<dees-form>
|
||||
<dees-input-text .label=${'Recipient Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-iban .label=${'Recipient IBAN'} .required=${true}></dees-input-iban>
|
||||
<dees-input-text .label=${'Transfer Reference'} .description=${'Optional reference for the transfer'}></dees-input-text>
|
||||
<dees-input-text .label=${'Transfer Reference'} .infoText=${'Optional reference for the transfer'}></dees-input-text>
|
||||
<dees-input-text .label=${'Amount'} .inputType=${'number'} .required=${true}></dees-input-text>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
@@ -44,23 +44,34 @@ export class DeesInputIban extends DeesInputBase<DeesInputIban> {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<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
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.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) => {
|
||||
this.validateIban(eventArg);
|
||||
}}
|
||||
></dees-input-text>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
const deesInputText = this.shadowRoot.querySelector('dees-input-text') as any;
|
||||
const deesInputText = this.shadowRoot!.querySelector('dees-input-text') as any;
|
||||
if (deesInputText && deesInputText.changeSubject) {
|
||||
deesInputText.changeSubject.subscribe(() => {
|
||||
this.changeSubject.next(this);
|
||||
@@ -81,8 +92,6 @@ export class DeesInputIban extends DeesInputBase<DeesInputIban> {
|
||||
}
|
||||
}
|
||||
this.enteredIbanIsValid = ibantools.isValidIBAN(this.enteredString.replace(/ /g, ''));
|
||||
const deesInputText = this.shadowRoot.querySelector('dees-input-text');
|
||||
deesInputText.validationText = `IBAN is valid: ${this.enteredIbanIsValid}`;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
|
||||
@@ -109,25 +109,27 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'3. Validation & Constraints'} .subtitle=${'Lists with minimum/maximum items and duplicate prevention'}>
|
||||
<div class="grid-layout">
|
||||
<dees-input-list
|
||||
.label=${'Team Members (Min 2, Max 5)'}
|
||||
.placeholder=${'Add team member...'}
|
||||
.minItems=${2}
|
||||
.maxItems=${5}
|
||||
.value=${['Alice', 'Bob']}
|
||||
.required=${true}
|
||||
.description=${'Add 2-5 team members'}
|
||||
></dees-input-list>
|
||||
|
||||
<dees-input-list
|
||||
.label=${'Unique Tags (No Duplicates)'}
|
||||
.placeholder=${'Add unique tag...'}
|
||||
.allowDuplicates=${false}
|
||||
.value=${['frontend', 'backend', 'database']}
|
||||
.description=${'Duplicate items are not allowed'}
|
||||
></dees-input-list>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="grid-layout">
|
||||
<dees-input-list
|
||||
.label=${'Team Members (Min 2, Max 5)'}
|
||||
.placeholder=${'Add team member...'}
|
||||
.minItems=${2}
|
||||
.maxItems=${5}
|
||||
.value=${['Alice', 'Bob']}
|
||||
.required=${true}
|
||||
.description=${'Add 2-5 team members'}
|
||||
></dees-input-list>
|
||||
|
||||
<dees-input-list
|
||||
.label=${'Unique Tags (No Duplicates)'}
|
||||
.placeholder=${'Add unique tag...'}
|
||||
.allowDuplicates=${false}
|
||||
.value=${['frontend', 'backend', 'database']}
|
||||
.description=${'Duplicate items are not allowed'}
|
||||
></dees-input-list>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'4. Delete Confirmation'} .subtitle=${'Require confirmation before deleting items'}>
|
||||
@@ -262,7 +264,85 @@ export const demoFunc = () => html`
|
||||
></dees-input-list>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'9. Empty State'} .subtitle=${'How the component looks with no items'}>
|
||||
<dees-panel .title=${'9. Candidates with Tab Completion'} .subtitle=${'Terminal-style autocomplete — Tab accepts, Shift+Tab cycles'}>
|
||||
<div class="grid-layout">
|
||||
<dees-input-list
|
||||
id="candidate-list"
|
||||
.label=${'Assign Team Members'}
|
||||
.placeholder=${'Type a name... (Tab to complete)'}
|
||||
.candidates=${[
|
||||
{ viewKey: 'Alice Smith', payload: { id: 1, role: 'Engineer', department: 'Frontend' } },
|
||||
{ viewKey: 'Bob Johnson', payload: { id: 2, role: 'Designer', department: 'UX' } },
|
||||
{ viewKey: 'Carol Williams', payload: { id: 3, role: 'Product Manager', department: 'Product' } },
|
||||
{ viewKey: 'David Brown', payload: { id: 4, role: 'Engineer', department: 'Backend' } },
|
||||
{ viewKey: 'Eve Davis', payload: { id: 5, role: 'QA Engineer', department: 'Quality' } },
|
||||
{ viewKey: 'Frank Miller', payload: { id: 6, role: 'DevOps', department: 'Infrastructure' } },
|
||||
{ viewKey: 'Grace Wilson', payload: { id: 7, role: 'Designer', department: 'UX' } },
|
||||
{ viewKey: 'Henry Moore', payload: { id: 8, role: 'Engineer', department: 'Frontend' } },
|
||||
]}
|
||||
.value=${['Alice Smith', 'Carol Williams']}
|
||||
.maxItems=${5}
|
||||
.description=${'Type to see ghost completion. Tab to accept, Shift+Tab to cycle, Enter to add.'}
|
||||
@change=${(e: CustomEvent) => {
|
||||
const preview = document.querySelector('#candidate-json');
|
||||
if (preview) {
|
||||
const list = (e.target as any);
|
||||
const candidates = list.getAddedCandidates();
|
||||
preview.textContent = JSON.stringify(candidates, null, 2);
|
||||
}
|
||||
}}
|
||||
></dees-input-list>
|
||||
|
||||
<div>
|
||||
<div style="font-size: 13px; font-weight: 500; margin-bottom: 8px; color: inherit;">Selected Candidates (with payloads)</div>
|
||||
<div class="output-preview" id="candidate-json">[]</div>
|
||||
<div class="feature-note">
|
||||
Try typing "D" — ghost text shows "avid Brown". Press Shift+Tab to cycle to other D-matches. Tab accepts, Enter adds.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'10. Technology Stack'} .subtitle=${'Larger candidate pool with Shift+Tab cycling'}>
|
||||
<dees-input-list
|
||||
.label=${'Select Technologies'}
|
||||
.placeholder=${'Type to autocomplete...'}
|
||||
.candidates=${[
|
||||
{ viewKey: 'TypeScript', payload: { category: 'language' } },
|
||||
{ viewKey: 'React', payload: { category: 'framework' } },
|
||||
{ viewKey: 'Vue.js', payload: { category: 'framework' } },
|
||||
{ viewKey: 'Angular', payload: { category: 'framework' } },
|
||||
{ viewKey: 'Node.js', payload: { category: 'runtime' } },
|
||||
{ viewKey: 'Deno', payload: { category: 'runtime' } },
|
||||
{ viewKey: 'Docker', payload: { category: 'devops' } },
|
||||
{ viewKey: 'PostgreSQL', payload: { category: 'database' } },
|
||||
{ viewKey: 'MongoDB', payload: { category: 'database' } },
|
||||
{ viewKey: 'Redis', payload: { category: 'database' } },
|
||||
{ viewKey: 'Kubernetes', payload: { category: 'devops' } },
|
||||
]}
|
||||
.description=${'Try "D" — cycles through Deno/Docker. "R" — cycles through React/Redis.'}
|
||||
></dees-input-list>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'11. Freeform + Candidates'} .subtitle=${'Allow adding items not in the candidate list (shown with a question mark)'}>
|
||||
<dees-input-list
|
||||
.label=${'Tags'}
|
||||
.placeholder=${'Type a tag... (freeform allowed)'}
|
||||
.allowFreeform=${true}
|
||||
.candidates=${[
|
||||
{ viewKey: 'bug', payload: { color: 'red' } },
|
||||
{ viewKey: 'feature', payload: { color: 'blue' } },
|
||||
{ viewKey: 'docs', payload: { color: 'green' } },
|
||||
{ viewKey: 'refactor', payload: { color: 'purple' } },
|
||||
{ viewKey: 'performance', payload: { color: 'orange' } },
|
||||
{ viewKey: 'security', payload: { color: 'red' } },
|
||||
]}
|
||||
.value=${['bug', 'my-custom-tag', 'feature']}
|
||||
.description=${'Known tags get a checkmark, custom tags get a question mark. Tab to complete, Enter to add freeform.'}
|
||||
></dees-input-list>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'12. Empty State'} .subtitle=${'How the component looks with no items'}>
|
||||
<dees-input-list
|
||||
.label=${'Your Ideas'}
|
||||
.placeholder=${'Share your ideas...'}
|
||||
|
||||
@@ -9,10 +9,15 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
|
||||
import '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import '../../00group-button/dees-button/dees-button.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
import { demoFunc } from './dees-input-list.demo.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
export interface IListCandidate {
|
||||
viewKey: string;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-input-list': DeesInputList;
|
||||
@@ -47,12 +52,27 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
@property({ type: Boolean })
|
||||
accessor confirmDelete: boolean = false;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor candidates: IListCandidate[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor allowFreeform: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor validationText: string = '';
|
||||
|
||||
private addedCandidatesMap: Map<string, IListCandidate> = new Map();
|
||||
private matchingCandidates: IListCandidate[] = [];
|
||||
|
||||
@state()
|
||||
accessor inputValue: string = '';
|
||||
|
||||
@state()
|
||||
accessor ghostText: string = '';
|
||||
|
||||
@state()
|
||||
accessor currentCandidateIndex: number = -1;
|
||||
|
||||
@state()
|
||||
accessor editingIndex: number = -1;
|
||||
|
||||
@@ -100,26 +120,19 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: all 0.15s ease;
|
||||
dees-tile:hover::part(outer) {
|
||||
border-color: var(--dees-color-border-strong);
|
||||
}
|
||||
|
||||
.list-container:hover:not(.disabled) {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
.list-container:focus-within {
|
||||
dees-tile:focus-within::part(outer) {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
|
||||
}
|
||||
|
||||
.list-container.disabled {
|
||||
:host([disabled]) dees-tile {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list-items {
|
||||
@@ -130,13 +143,13 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||
background: transparent;
|
||||
transition: transform 0.2s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||
position: relative;
|
||||
overflow: hidden; /* Prevent animation from affecting scroll bounds */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item:last-of-type {
|
||||
@@ -144,7 +157,7 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
}
|
||||
|
||||
.list-items:not(.is-dragging) .list-item:hover:not(.disabled) {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')};
|
||||
background: var(--dees-color-row-hover);
|
||||
}
|
||||
|
||||
/* Dragging item - follows cursor */
|
||||
@@ -168,6 +181,28 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
}
|
||||
|
||||
|
||||
.candidate-check {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.candidate-unknown {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(45 93% 47%)', 'hsl(45 93% 58%)')};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-bullet {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--dees-color-text-muted);
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -181,8 +216,8 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
}
|
||||
|
||||
.drag-handle dees-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
@@ -195,15 +230,15 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
.item-text {
|
||||
flex: 1;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.item-edit-input {
|
||||
flex: 1;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
padding: 3px 6px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
@@ -222,8 +257,8 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -262,34 +297,27 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
}
|
||||
|
||||
.action-button dees-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.add-item-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 97.5%)', 'hsl(0 0% 6.9%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.add-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-family: inherit;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.add-input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.add-input::placeholder {
|
||||
@@ -302,29 +330,47 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
}
|
||||
|
||||
.add-button {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
transition: all 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-button:hover:not(:disabled) {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.add-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.add-button dees-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 32px 16px;
|
||||
padding: 16px 10px;
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
@@ -349,6 +395,38 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
.list-items.dropping .list-item {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ── Terminal-style inline autocomplete ── */
|
||||
.autocomplete-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ghost-text {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ghost-typed {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ghost-completion {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
||||
opacity: 0.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -357,7 +435,7 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
<div class="input-wrapper">
|
||||
${this.label ? html`<dees-label .label=${this.label} .required=${this.required}></dees-label>` : ''}
|
||||
|
||||
<div class="list-container ${this.disabled ? 'disabled' : ''}">
|
||||
<dees-tile .heading="${this.value.length} item${this.value.length !== 1 ? 's' : ''}">
|
||||
<div class="list-items">
|
||||
${this.value.length > 0 ? this.value.map((item, index) => html`
|
||||
<div
|
||||
@@ -373,7 +451,17 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
<dees-icon .icon=${'lucide:gripVertical'}></dees-icon>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
|
||||
${this.candidates.length > 0 ? html`
|
||||
${this.candidates.some(c => c.viewKey === item) ? html`
|
||||
<dees-icon class="candidate-check" .icon=${'lucide:check'}></dees-icon>
|
||||
` : html`
|
||||
<dees-icon class="candidate-unknown" .icon=${'lucide:helpCircle'}></dees-icon>
|
||||
`}
|
||||
` : !this.sortable || this.disabled ? html`
|
||||
<dees-icon class="item-bullet" .icon=${'lucide:dot'}></dees-icon>
|
||||
` : ''}
|
||||
|
||||
<div class="item-content">
|
||||
${this.editingIndex === index ? html`
|
||||
<input
|
||||
@@ -419,45 +507,121 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
</div>
|
||||
|
||||
${!this.disabled && (!this.maxItems || this.value.length < this.maxItems) ? html`
|
||||
<div class="add-item-container">
|
||||
<input
|
||||
type="text"
|
||||
class="add-input"
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.inputValue}
|
||||
@input=${this.handleInput}
|
||||
@keydown=${this.handleAddKeyDown}
|
||||
?disabled=${this.disabled}
|
||||
/>
|
||||
<dees-button
|
||||
<div slot="footer" class="add-item-container">
|
||||
<div class="autocomplete-wrapper">
|
||||
${this.ghostText ? html`
|
||||
<span class="ghost-text">
|
||||
<span class="ghost-typed">${this.inputValue}</span><span class="ghost-completion">${this.ghostText}</span>
|
||||
</span>
|
||||
` : ''}
|
||||
<input
|
||||
type="text"
|
||||
class="add-input"
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.inputValue}
|
||||
@input=${this.handleInput}
|
||||
@keydown=${this.handleAddKeyDown}
|
||||
?disabled=${this.disabled}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="add-button"
|
||||
@click=${this.addItem}
|
||||
?disabled=${!this.inputValue.trim()}
|
||||
>
|
||||
<dees-icon .icon=${'lucide:plus'}></dees-icon> Add
|
||||
</dees-button>
|
||||
<dees-icon .icon=${'lucide:plus'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</dees-tile>
|
||||
|
||||
${this.validationText ? html`
|
||||
<div class="validation-message">${this.validationText}</div>
|
||||
` : ''}
|
||||
|
||||
${this.description ? html`
|
||||
<div class="description">${this.description}</div>
|
||||
` : ''}
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleInput(e: InputEvent) {
|
||||
this.inputValue = (e.target as HTMLInputElement).value;
|
||||
this.updateGhostText();
|
||||
}
|
||||
|
||||
private updateGhostText(): void {
|
||||
if (this.candidates.length === 0 || !this.inputValue) {
|
||||
this.ghostText = '';
|
||||
this.currentCandidateIndex = -1;
|
||||
this.matchingCandidates = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const search = this.inputValue.toLowerCase();
|
||||
this.matchingCandidates = this.candidates
|
||||
.filter(c => {
|
||||
if (this.value.includes(c.viewKey)) return false;
|
||||
return c.viewKey.toLowerCase().startsWith(search);
|
||||
})
|
||||
.sort((a, b) => a.viewKey.length - b.viewKey.length);
|
||||
|
||||
if (this.matchingCandidates.length > 0) {
|
||||
this.currentCandidateIndex = 0;
|
||||
this.ghostText = this.matchingCandidates[0].viewKey.slice(this.inputValue.length);
|
||||
} else {
|
||||
this.currentCandidateIndex = -1;
|
||||
this.ghostText = '';
|
||||
}
|
||||
}
|
||||
|
||||
private handleAddKeyDown(e: KeyboardEvent) {
|
||||
// Tab/Shift+Tab: autocomplete handling when candidates are active
|
||||
if (e.key === 'Tab' && this.candidates.length > 0 && this.inputValue) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey && this.matchingCandidates.length > 0) {
|
||||
// Shift+Tab: cycle to next candidate
|
||||
this.currentCandidateIndex = (this.currentCandidateIndex + 1) % this.matchingCandidates.length;
|
||||
const candidate = this.matchingCandidates[this.currentCandidateIndex];
|
||||
this.ghostText = candidate.viewKey.slice(this.inputValue.length);
|
||||
} else if (!e.shiftKey && this.ghostText && this.matchingCandidates.length > 0) {
|
||||
// Tab: accept the completion into the input
|
||||
const candidate = this.matchingCandidates[this.currentCandidateIndex];
|
||||
this.inputValue = candidate.viewKey;
|
||||
this.ghostText = '';
|
||||
const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement;
|
||||
if (input) input.value = candidate.viewKey;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape: clear ghost text
|
||||
if (e.key === 'Escape' && this.ghostText) {
|
||||
e.preventDefault();
|
||||
this.ghostText = '';
|
||||
this.currentCandidateIndex = -1;
|
||||
this.matchingCandidates = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter: add item
|
||||
if (e.key === 'Enter' && this.inputValue.trim()) {
|
||||
e.preventDefault();
|
||||
if (this.candidates.length > 0) {
|
||||
// Try exact candidate match first
|
||||
const match = this.candidates.find(
|
||||
c => c.viewKey.toLowerCase() === this.inputValue.trim().toLowerCase()
|
||||
);
|
||||
if (match) {
|
||||
this.selectCandidate(match);
|
||||
} else if (this.allowFreeform) {
|
||||
// Allow freeform entry (won't have a candidate checkmark)
|
||||
this.ghostText = '';
|
||||
this.currentCandidateIndex = -1;
|
||||
this.matchingCandidates = [];
|
||||
this.addItem();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.addItem();
|
||||
}
|
||||
}
|
||||
@@ -472,6 +636,52 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
}
|
||||
}
|
||||
|
||||
private selectCandidate(candidate: IListCandidate): void {
|
||||
if (this.maxItems && this.value.length >= this.maxItems) {
|
||||
this.validationText = `Maximum ${this.maxItems} items allowed`;
|
||||
setTimeout(() => this.validationText = '', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.allowDuplicates && this.value.includes(candidate.viewKey)) {
|
||||
this.validationText = 'This item already exists in the list';
|
||||
setTimeout(() => this.validationText = '', 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addedCandidatesMap.set(candidate.viewKey, candidate);
|
||||
this.value = [...this.value, candidate.viewKey];
|
||||
this.inputValue = '';
|
||||
this.ghostText = '';
|
||||
this.currentCandidateIndex = -1;
|
||||
this.matchingCandidates = [];
|
||||
this.validationText = '';
|
||||
this.emitChange();
|
||||
|
||||
// Re-focus input after Lit re-renders
|
||||
this.updateComplete.then(() => {
|
||||
const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement;
|
||||
if (input) { input.value = ''; input.focus(); }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full candidate object for an item by its viewKey.
|
||||
* Returns undefined if the item was added as a plain string.
|
||||
*/
|
||||
public getCandidateForItem(viewKey: string): IListCandidate | undefined {
|
||||
return this.addedCandidatesMap.get(viewKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all added candidates with their payloads.
|
||||
*/
|
||||
public getAddedCandidates(): IListCandidate[] {
|
||||
return this.value
|
||||
.map(v => this.addedCandidatesMap.get(v))
|
||||
.filter((c): c is IListCandidate => c !== undefined);
|
||||
}
|
||||
|
||||
private addItem() {
|
||||
const trimmedValue = this.inputValue.trim();
|
||||
if (!trimmedValue) return;
|
||||
@@ -491,15 +701,13 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
this.value = [...this.value, trimmedValue];
|
||||
this.inputValue = '';
|
||||
this.validationText = '';
|
||||
|
||||
// Clear the input
|
||||
const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement;
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.focus();
|
||||
}
|
||||
|
||||
this.emitChange();
|
||||
|
||||
// Re-focus input after Lit re-renders
|
||||
this.updateComplete.then(() => {
|
||||
const input = this.shadowRoot?.querySelector('.add-input') as HTMLInputElement;
|
||||
if (input) { input.value = ''; input.focus(); }
|
||||
});
|
||||
}
|
||||
|
||||
private startEdit(index: number) {
|
||||
@@ -551,6 +759,8 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
const removedKey = this.value[index];
|
||||
this.addedCandidatesMap.delete(removedKey);
|
||||
this.value = this.value.filter((_, i) => i !== index);
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user