Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6047705e7d | |||
| ab19b561c4 | |||
| 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 |
358
changelog.md
358
changelog.md
@@ -1,5 +1,363 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-12 - 3.78.0 - feat(dees-settings)
|
||||
add dees-settings layout component for displaying read-only settings with footer actions
|
||||
|
||||
- introduces a new dees-settings element with heading, description, settings field grid, and footer action support
|
||||
- exports dees-settings from the 00group-layout module index
|
||||
- adds demo examples covering populated, empty, and multi-action states
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.51.1",
|
||||
"version": "3.78.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",
|
||||
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.5.4",
|
||||
"@design.estate/dees-element": "^2.2.4",
|
||||
"@design.estate/dees-wcctools": "^3.8.0",
|
||||
"@design.estate/dees-wcctools": "^3.8.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
||||
@@ -34,10 +34,11 @@
|
||||
"@tiptap/extension-underline": "^2.23.0",
|
||||
"@tiptap/starter-kit": "^2.23.0",
|
||||
"@tsclass/tsclass": "^9.5.0",
|
||||
"lightweight-charts": "^5.1.0",
|
||||
"echarts": "^5.6.0",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"lucide": "^0.577.0",
|
||||
"lightweight-charts": "^5.1.0",
|
||||
"lucide": "^1.8.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"xterm": "^5.3.0",
|
||||
@@ -49,7 +50,7 @@
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@types/node": "^25.5.0"
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
88
pnpm-lock.yaml
generated
88
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
||||
specifier: ^2.2.4
|
||||
version: 2.2.4
|
||||
'@design.estate/dees-wcctools':
|
||||
specifier: ^3.8.0
|
||||
version: 3.8.0
|
||||
specifier: ^3.8.4
|
||||
version: 3.8.4
|
||||
'@fortawesome/fontawesome-svg-core':
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
@@ -62,6 +62,9 @@ importers:
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.5.0
|
||||
version: 9.5.0
|
||||
echarts:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
highlight.js:
|
||||
specifier: 11.11.1
|
||||
version: 11.11.1
|
||||
@@ -72,8 +75,8 @@ importers:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
lucide:
|
||||
specifier: ^0.577.0
|
||||
version: 0.577.0
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
monaco-editor:
|
||||
specifier: 0.55.1
|
||||
version: 0.55.1
|
||||
@@ -103,8 +106,8 @@ importers:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
'@types/node':
|
||||
specifier: ^25.5.0
|
||||
version: 25.5.0
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -320,8 +323,8 @@ packages:
|
||||
'@design.estate/dees-element@2.2.4':
|
||||
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.0':
|
||||
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
|
||||
'@design.estate/dees-wcctools@3.8.4':
|
||||
resolution: {integrity: sha512-KpFK/azK+a/Xpq33pXKcho+tdFKVHhKZM5ArvHqo9QMwTczgp5DZZgowTDUuqAofjZwnuVfCPHK/Pw9e64N46A==}
|
||||
|
||||
'@emnapi/core@1.8.1':
|
||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||
@@ -2118,11 +2121,11 @@ packages:
|
||||
'@types/node@16.9.1':
|
||||
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||
'@types/node@22.19.17':
|
||||
resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==}
|
||||
|
||||
'@types/node@25.5.0':
|
||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
||||
'@types/node@25.6.0':
|
||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||
|
||||
'@types/randomatic@3.1.5':
|
||||
resolution: {integrity: sha512-VCwCTw6qh1pRRw+5rNTAwqPmf6A+hdrkdM7dBpZVmhl7g+em3ONXlYK/bWPVKqVGMWgP0d1bog8Vc/X6zRwRRQ==}
|
||||
@@ -2534,6 +2537,9 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
echarts@5.6.0:
|
||||
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -3048,6 +3054,9 @@ packages:
|
||||
lucide@0.577.0:
|
||||
resolution: {integrity: sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==}
|
||||
|
||||
lucide@1.8.0:
|
||||
resolution: {integrity: sha512-JjV/QnadgFLj1Pyu9IKl0lknrolFEzo04B64QcYLLeRzZl/iEHpdbSrRRKbyXcv45SZNv+WGjIUCT33e7xHO6Q==}
|
||||
|
||||
make-dir@3.1.0:
|
||||
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3936,6 +3945,9 @@ packages:
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -3993,8 +4005,8 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
undici-types@7.19.2:
|
||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||
|
||||
unified@11.0.5:
|
||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||
@@ -4149,6 +4161,9 @@ packages:
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zrender@5.6.1:
|
||||
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -4696,7 +4711,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
'@design.estate/dees-wcctools': 3.8.0
|
||||
'@design.estate/dees-wcctools': 3.8.4
|
||||
'@fortawesome/fontawesome-svg-core': 7.2.0
|
||||
'@fortawesome/free-brands-svg-icons': 7.2.0
|
||||
'@fortawesome/free-regular-svg-icons': 7.2.0
|
||||
@@ -4772,7 +4787,7 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@design.estate/dees-wcctools@3.8.0':
|
||||
'@design.estate/dees-wcctools@3.8.4':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.5.4
|
||||
'@design.estate/dees-element': 2.2.4
|
||||
@@ -5245,7 +5260,7 @@ snapshots:
|
||||
'@inquirer/figures': 1.0.15
|
||||
'@inquirer/type': 2.0.0
|
||||
'@types/mute-stream': 0.0.4
|
||||
'@types/node': 22.19.15
|
||||
'@types/node': 22.19.17
|
||||
'@types/wrap-ansi': 3.0.0
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
@@ -7223,7 +7238,7 @@ snapshots:
|
||||
|
||||
'@types/clean-css@4.2.11':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
source-map: 0.6.1
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
@@ -7233,7 +7248,7 @@ snapshots:
|
||||
'@types/fs-extra@11.0.4':
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.4
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
@@ -7253,7 +7268,7 @@ snapshots:
|
||||
|
||||
'@types/jsonfile@6.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
@@ -7276,21 +7291,21 @@ snapshots:
|
||||
|
||||
'@types/mute-stream@0.0.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/node@16.9.1': {}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
'@types/node@22.19.17':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.5.0':
|
||||
'@types/node@25.6.0':
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@types/randomatic@3.1.5': {}
|
||||
|
||||
@@ -7302,11 +7317,11 @@ snapshots:
|
||||
|
||||
'@types/tar-stream@3.1.4':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/through2@2.0.41':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/trusted-types@2.0.7': {}
|
||||
|
||||
@@ -7332,11 +7347,11 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
optional: true
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
@@ -7671,6 +7686,11 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
echarts@5.6.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 5.6.1
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
@@ -8283,6 +8303,8 @@ snapshots:
|
||||
|
||||
lucide@0.577.0: {}
|
||||
|
||||
lucide@1.8.0: {}
|
||||
|
||||
make-dir@3.1.0:
|
||||
dependencies:
|
||||
semver: 6.3.1
|
||||
@@ -9510,6 +9532,8 @@ snapshots:
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
@@ -9551,7 +9575,7 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
undici-types@7.19.2: {}
|
||||
|
||||
unified@11.0.5:
|
||||
dependencies:
|
||||
@@ -9699,4 +9723,8 @@ snapshots:
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zrender@5.6.1:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
31
readme.md
31
readme.md
@@ -59,8 +59,8 @@ For developers working on this library, please refer to the [UI Components Playb
|
||||
| **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), [`DeesStorageBrowser`](#deesstorgebrowser) |
|
||||
| **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) |
|
||||
| **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) |
|
||||
@@ -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.
|
||||
|
||||
@@ -1780,7 +1789,7 @@ interface ITileFolderItem {
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
@@ -1798,5 +1807,3 @@ 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.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
352
readme.plan.md
Normal file
352
readme.plan.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# Plan: dees-stepper — adopt dees-tile + optional overlay window layer
|
||||
|
||||
> First line (per CLAUDE.md): Please reread `/home/philkunz/.claude/CLAUDE.md` before continuing.
|
||||
|
||||
## Context
|
||||
|
||||
Today `dees-stepper` is an inline-only layout component: it hard-codes each step as a custom `.step` `<div>` with its own border / background / box-shadow / border-radius, and its `:host` is `position: absolute; width: 100%; height: 100%;` so it can only live inside a bounded parent container.
|
||||
|
||||
The user wants it to behave more like `dees-modal`:
|
||||
|
||||
1. Each step should be wrapped in a `<dees-tile>` — the unified "rounded on rounded" frame used by modals and panels — rather than a bespoke `.step` div.
|
||||
2. A `DeesWindowLayer` should be added behind the stepper, the same way `DeesModal.createAndShow` does, so the stepper can appear as an overlay on top of the page.
|
||||
|
||||
User has confirmed (via AskUserQuestion in this session):
|
||||
- **API**: keep the current inline usage working AND add a static `createAndShow()` like dees-modal.
|
||||
- **Layout**: keep the current vertical stack + SweetScroll behavior inside the overlay (don't switch to single-tile swap).
|
||||
- **Nav placement**: split header/footer — goBack + step counter go into the `dees-tile` header slot; the title stays in the content area; the tile footer is used for optional next/submit buttons supplied per-step.
|
||||
|
||||
No external consumers of `dees-stepper` were found inside this package (`grep dees-stepper|DeesStepper` only matches its own source, demo, index, changelog, readme). External consumers in dependent projects may exist — the refactor is kept backward-compatible for the inline path.
|
||||
|
||||
## Current state (reference)
|
||||
|
||||
**File:** `ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts` (lines 20–299)
|
||||
|
||||
- `IStep` interface — `title`, `content: TemplateResult`, `validationFunc`, `onReturnToStepFunc`, internal flags (lines 20–27).
|
||||
- `:host { position: absolute; width: 100%; height: 100%; }` (lines 59–63).
|
||||
- `.stepperContainer` — absolute, 100% w/h, `overflow: hidden`, holds SweetScroll (lines 64–69).
|
||||
- `.step` — max-width 500, min-height 300, `border-radius: 12px`, theme background, theme border, `box-shadow: 0 8px 32px rgba(0,0,0,0.4)`, `filter: opacity(0.55) saturate(0.85)`, transform transition (lines 71–97). **These frame styles overlap with what `dees-tile` already provides.**
|
||||
- `.step.selected` — `filter: opacity(1) saturate(1)` (lines 89–93). **Scroll-through visual cue, keep.**
|
||||
- `.step.hiddenStep` — `filter: opacity(0)` (line 95). **Keep.**
|
||||
- `.step.entrance` — faster transition variant for first-render (lines 99–105). **Keep.**
|
||||
- `.step .stepCounter` — `position: absolute; top: 12px; right: 12px;` pill (lines 111–121). **Move into header slot as a flex child.**
|
||||
- `.step .goBack` — `position: absolute; top: 12px; left: 12px;` pill + icon + hover (lines 123–161). **Move into header slot as a flex child.**
|
||||
- `.step .title` — centered, 24px, 64px top padding (lines 163–171). **Keep inside the tile's content slot; remove the 64px top padding since goBack/counter no longer overlap it.**
|
||||
- `.step .content` — 32px padding (lines 173–175). **Keep.**
|
||||
- `render()` (lines 179–204) — maps `steps` to `.step` divs.
|
||||
- `setScrollStatus()` (lines 226–263) — SweetScroll container setup + step validation kick-off. **Keep mostly as-is; selectors still target `.step`/`.selected` so rename cautiously.**
|
||||
- `firstUpdated` (lines 210–218), `updated` (lines 220–222), `goBack` (lines 265–282), `goNext` (lines 284–298) — untouched in behavior, only DOM selectors may need adjusting.
|
||||
|
||||
**Reference files (read, do not modify):**
|
||||
- `ts_web/elements/00group-overlay/dees-modal/dees-modal.ts` — canonical `createAndShow` + `destroy` + window-layer coordination + z-index registry usage.
|
||||
- `ts_web/elements/00group-layout/dees-tile/dees-tile.ts` — slot API: `slot="header"`, default slot, `slot="footer"`. Auto-hides footer when slotted nodes are empty. Uses `part="outer"`, `part="header"`, `part="content"`, `part="footer"` for external shadow-part styling.
|
||||
- `ts_web/elements/00group-overlay/dees-windowlayer/dees-windowlayer.ts` — `createAndShow({ blur })`, `destroy()`, dispatches `clicked` event on backdrop click, uses `zIndexRegistry`.
|
||||
- `ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts` — existing inline demo.
|
||||
|
||||
## Target state
|
||||
|
||||
### 1. IStep interface — add one optional field
|
||||
|
||||
```ts
|
||||
export interface IStep {
|
||||
title: string;
|
||||
content: TemplateResult;
|
||||
footerContent?: TemplateResult; // NEW: optional, rendered in dees-tile footer slot
|
||||
validationFunc?: (stepper: DeesStepper, htmlElement: HTMLElement, signal?: AbortSignal) => Promise<any>;
|
||||
onReturnToStepFunc?: (stepper: DeesStepper, htmlElement: HTMLElement) => Promise<any>;
|
||||
validationFuncCalled?: boolean;
|
||||
abortController?: AbortController;
|
||||
}
|
||||
```
|
||||
|
||||
Form-based steps don't need `footerContent` — their `dees-form-submit` stays inside the form in the content slot (as today). `footerContent` is for non-form steps that need an explicit primary action, or for any step that wants buttons in the conventional tile footer location.
|
||||
|
||||
### 2. New overlay-mode state + API on DeesStepper
|
||||
|
||||
```ts
|
||||
@state() accessor overlay: boolean = false;
|
||||
@state() accessor stepperZIndex: number = 1000;
|
||||
private windowLayer?: DeesWindowLayer;
|
||||
|
||||
public static async createAndShow(optionsArg: {
|
||||
steps: IStep[];
|
||||
}): Promise<DeesStepper> {
|
||||
const body = document.body;
|
||||
const stepper = new DeesStepper();
|
||||
stepper.steps = optionsArg.steps;
|
||||
stepper.overlay = true;
|
||||
stepper.windowLayer = await DeesWindowLayer.createAndShow({ blur: true });
|
||||
stepper.windowLayer.addEventListener('click', async () => {
|
||||
await stepper.destroy();
|
||||
});
|
||||
body.append(stepper.windowLayer); // (already appended inside createAndShow, but mirror dees-modal's pattern; see note)
|
||||
body.append(stepper);
|
||||
stepper.stepperZIndex = zIndexRegistry.getNextZIndex();
|
||||
zIndexRegistry.register(stepper, stepper.stepperZIndex);
|
||||
return stepper;
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const container = this.shadowRoot!.querySelector('.stepperContainer');
|
||||
container?.classList.add('predestroy');
|
||||
await domtools.convenience.smartdelay.delayFor(200);
|
||||
if (this.parentElement) this.parentElement.removeChild(this);
|
||||
if (this.windowLayer) await this.windowLayer.destroy();
|
||||
zIndexRegistry.unregister(this);
|
||||
}
|
||||
```
|
||||
|
||||
**Note on `body.append(windowLayer)`:** `DeesWindowLayer.createAndShow` already appends the window layer to `document.body` (line 27 of `dees-windowlayer.ts`). `dees-modal.ts:71` still calls `body.append(modal.windowLayer)` — that's either a no-op (already-attached nodes) or a re-parent to keep ordering. I will match dees-modal's exact sequence verbatim to avoid introducing subtle differences; if it's a bug in dees-modal it is out of scope for this task.
|
||||
|
||||
**Minimum new scope for createAndShow:** just `steps` for now. No `onComplete`, no `showCloseButton`, no width options. Future-proofing via additional options is an explicit follow-up — this plan keeps scope razor-sharp (per CLAUDE.md). The caller can already wire completion via the last step's `validationFunc` calling back into their own code.
|
||||
|
||||
### 3. Render template — wrap each step in `<dees-tile>`
|
||||
|
||||
```ts
|
||||
public render() {
|
||||
return html`
|
||||
<div class="stepperContainer ${this.overlay ? 'overlay' : ''}" style="${this.overlay ? `z-index: ${this.stepperZIndex}` : ''}">
|
||||
${this.steps.map((stepArg, i) => {
|
||||
const isSelected = stepArg === this.selectedStep;
|
||||
const isHidden = this.getIndexOfStep(stepArg) > this.getIndexOfStep(this.selectedStep);
|
||||
const isFirst = i === 0;
|
||||
const stepNumber = i + 1;
|
||||
return html`
|
||||
<dees-tile
|
||||
class="step ${isSelected ? 'selected' : ''} ${isHidden ? 'hiddenStep' : ''} ${isFirst ? 'entrance' : ''}"
|
||||
>
|
||||
<div slot="header" class="step-header">
|
||||
${!isFirst
|
||||
? html`<div class="goBack" @click=${this.goBack}>
|
||||
<span>←</span> go to previous step
|
||||
</div>`
|
||||
: html`<div class="goBack-spacer"></div>`}
|
||||
<div class="stepCounter">Step ${stepNumber} of ${this.steps.length}</div>
|
||||
</div>
|
||||
<div class="step-body">
|
||||
<div class="title">${stepArg.title}</div>
|
||||
<div class="content">${stepArg.content}</div>
|
||||
</div>
|
||||
${stepArg.footerContent
|
||||
? html`<div slot="footer" class="step-footer">${stepArg.footerContent}</div>`
|
||||
: ''}
|
||||
</dees-tile>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
**Key detail:** on the first step, render a `.goBack-spacer` (empty div) in the header instead of nothing — so the `stepCounter` stays right-aligned via `justify-content: space-between`. Without a spacer, flex would left-align the counter on step 1.
|
||||
|
||||
### 4. CSS changes
|
||||
|
||||
**Remove from `.step`:**
|
||||
- `border-radius: 12px;`
|
||||
- `background: ${cssManager.bdTheme(...)};`
|
||||
- `border: 1px solid ${cssManager.bdTheme(...)};`
|
||||
- `color: ${cssManager.bdTheme(...)};`
|
||||
- `box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);`
|
||||
- `overflow: hidden;`
|
||||
|
||||
**Why:** `dees-tile` owns all of these now. The `.step` selector still exists (since `dees-tile` has `class="step ..."` on it), but it only controls the outer animation wrapper: `max-width`, `min-height`, `margin`, `filter`, `transform`, `transition`, `user-select`, `pointer-events`.
|
||||
|
||||
**Keep on `.step`:**
|
||||
- `position: relative;`
|
||||
- `pointer-events: none;` + `.step.selected { pointer-events: all; }`
|
||||
- `max-width: 500px;` / `min-height: 300px;`
|
||||
- `margin: auto; margin-bottom: 20px;`
|
||||
- `filter: opacity(0.55) saturate(0.85);` + `.selected { filter: opacity(1) saturate(1); }`
|
||||
- `.hiddenStep { filter: opacity(0); }`
|
||||
- All the cubic-bezier transitions (transform/filter/box-shadow — but box-shadow is now a no-op since dees-tile provides the shadow; leave the transition spec in so we don't have to re-check browser parsing; or just drop `box-shadow` from the transition list — I'll drop it for cleanliness).
|
||||
- `.step.entrance` + `.step.entrance.hiddenStep { transform: translateY(16px); }`
|
||||
- `.step:last-child { margin-bottom: 100vh; }`
|
||||
|
||||
**Add for dees-tile shadow enhancement:** use `::part(outer)` to apply the modal-style elevated shadow only when in overlay mode (optional polish — inline mode stays flat):
|
||||
```css
|
||||
.stepperContainer.overlay dees-tile.step::part(outer) {
|
||||
box-shadow:
|
||||
0 0 0 1px ${cssManager.bdTheme('hsl(0 0% 0% / 0.03)', 'hsl(0 0% 100% / 0.03)')},
|
||||
0 8px 40px ${cssManager.bdTheme('hsl(0 0% 0% / 0.12)', 'hsl(0 0% 0% / 0.5)')},
|
||||
0 2px 8px ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 0% / 0.25)')};
|
||||
}
|
||||
```
|
||||
This exactly mirrors the dees-modal::part(outer) shadow stack (dees-modal.ts:157–161) so the overlay stepper reads as "same visual language as modal."
|
||||
|
||||
**Restyle `.step-header` (NEW — the `<div slot="header">`):**
|
||||
```css
|
||||
.step-header {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
```
|
||||
|
||||
**Restyle `.step .stepCounter` → `.step-header .stepCounter` (move from absolute to flex child):**
|
||||
- Drop `position: absolute; top: 12px; right: 12px;`
|
||||
- Keep everything else (padding, font-size, border-radius, background, border).
|
||||
|
||||
**Restyle `.step .goBack` → `.step-header .goBack` (move from absolute to flex child):**
|
||||
- Drop `position: absolute; top: 12px; left: 12px;`
|
||||
- Keep everything else (padding, font-size, border-radius, background, border, hover/active states).
|
||||
|
||||
**Add `.goBack-spacer`:**
|
||||
```css
|
||||
.goBack-spacer { width: 1px; } /* placeholder so flex space-between works on step 1 */
|
||||
```
|
||||
|
||||
**Restyle `.step .title`:**
|
||||
- Drop `padding-top: 64px;` — no longer overlaps anything since header is in its own slot.
|
||||
- Keep `text-align: center; font-family: 'Geist Sans', sans-serif; font-size: 24px; font-weight: 600; letter-spacing: -0.01em; color: inherit;`
|
||||
- Add `padding-top: 32px;` (or similar) so there's consistent breathing room above the title inside the tile content.
|
||||
|
||||
**Add `.step-footer` (new container for `stepArg.footerContent`):**
|
||||
```css
|
||||
.step-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
```
|
||||
|
||||
**Add overlay-mode positioning:**
|
||||
```css
|
||||
.stepperContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stepperContainer.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.stepperContainer.predestroy {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
```
|
||||
|
||||
**Adjust `:host` for dual-mode:**
|
||||
```css
|
||||
:host {
|
||||
position: absolute; /* inline default */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
:host([overlay]) {
|
||||
position: fixed; /* overlay mode */
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
```
|
||||
|
||||
The `overlay` @state needs to reflect to an attribute for the `:host([overlay])` selector to work. Since `@state` doesn't reflect attributes, use `@property({ type: Boolean, reflect: true })` instead — change the decorator accordingly.
|
||||
|
||||
### 5. Imports to add in `dees-stepper.ts`
|
||||
|
||||
```ts
|
||||
import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
import { zIndexRegistry } from '../../00zindex.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
```
|
||||
|
||||
`dees-tile` side-effect import registers the custom element. `cssGeistFontFamily` is only needed if I add it to `:host` (which I want, to match modal).
|
||||
|
||||
### 6. SweetScroll selector stability
|
||||
|
||||
`setScrollStatus()` selectors target `.step` and `.selected` (lines 228–229). These continue to match since I'm keeping those class names on the `<dees-tile>` elements. **No selector changes needed.**
|
||||
|
||||
One subtlety: `offsetTop` / `offsetHeight` on `<dees-tile>` should still work — the tile's `:host` is `display: flex; flex-direction: column;` which participates in layout. I'll verify visually in the demo.
|
||||
|
||||
### 7. Demo update
|
||||
|
||||
**File:** `ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts`
|
||||
|
||||
Current demo renders one inline stepper directly. I'll keep that and add an **overlay launcher button** above it:
|
||||
|
||||
```ts
|
||||
export const stepperDemo = () => html`
|
||||
<div style="padding: 16px;">
|
||||
<dees-button @click=${async () => {
|
||||
const stepper = await DeesStepper.createAndShow({
|
||||
steps: [/* same steps as inline demo */],
|
||||
});
|
||||
}}>Open stepper as overlay</dees-button>
|
||||
</div>
|
||||
<dees-stepper .steps=${[/* ... existing inline demo steps ... */]}></dees-stepper>
|
||||
`;
|
||||
```
|
||||
|
||||
Extract the step definitions into a `const demoSteps = [...]` above the template so both the inline and overlay paths reuse them (DRY). Import `DeesStepper` at the top of the demo file.
|
||||
|
||||
## Files to modify
|
||||
|
||||
1. **`ts_web/elements/00group-layout/dees-stepper/dees-stepper.ts`** — main refactor (IStep, imports, render, styles, createAndShow, destroy, overlay state).
|
||||
2. **`ts_web/elements/00group-layout/dees-stepper/dees-stepper.demo.ts`** — add overlay launcher button, extract shared `demoSteps` const, import `DeesStepper`.
|
||||
|
||||
**Files explicitly NOT modified:**
|
||||
- `dees-tile.ts` — used as-is via its slot API.
|
||||
- `dees-windowlayer.ts` — used as-is via `createAndShow` / `destroy` / `click` event.
|
||||
- `dees-modal.ts` — reference only.
|
||||
- `00zindex.ts` — reference only.
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Build**: `pnpm run build` — must pass with no TS errors. Pure refactor, no new dependencies, no lib-check regressions expected.
|
||||
|
||||
2. **Inline demo (backward compat)**:
|
||||
- Start the demo server (port 8080 is already running) and navigate to the dees-stepper demo page.
|
||||
- Confirm the stepper renders inline exactly like before: first step centered, subsequent steps dimmed below, scroll-through animation on goNext / goBack.
|
||||
- Fill out the first form, submit → stepper scrolls to step 2. Click goBack → scrolls back.
|
||||
- Confirm the `dees-tile` frame is visible on each step (rounded, bordered, themed) and that the title + form are inside the tile's content area.
|
||||
- Confirm goBack button + step counter sit in the tile's header row, space-between, left/right respectively.
|
||||
|
||||
3. **Overlay demo (new path)**:
|
||||
- Click the "Open stepper as overlay" button.
|
||||
- Confirm a `dees-windowlayer` with blur appears behind the stepper.
|
||||
- Confirm the stepper fills the viewport (fixed, 100vw×100vh).
|
||||
- Confirm z-index stacking: stepper above window layer above page content.
|
||||
- Click the window layer (outside the tile) → stepper animates out, then destroys along with the window layer.
|
||||
- Re-open and step through forward & back — behavior identical to inline mode.
|
||||
|
||||
4. **Playwright visual check** (per CLAUDE.md: screenshots MUST go in `.playwright-mcp/`):
|
||||
- `.playwright-mcp/dees-stepper-inline.png` — inline mode, step 1 with form.
|
||||
- `.playwright-mcp/dees-stepper-overlay.png` — overlay mode, same step.
|
||||
- `.playwright-mcp/dees-stepper-overlay-step3.png` — overlay mode mid-flow, to verify scroll-stack visual.
|
||||
- Both light and dark themes if the demo has a theme toggle.
|
||||
|
||||
5. **Grep sanity**:
|
||||
- Confirm `dees-stepper` has no new unexpected match locations: `grep dees-stepper ts_web/` should still only match stepper's own files.
|
||||
- Confirm no `.step` class collisions elsewhere (unlikely — `.step` is a plain class name; all usages should be shadow-scoped to `dees-stepper`).
|
||||
|
||||
## Open assumptions & deferred scope
|
||||
|
||||
These are explicit defaults in this plan. If the user wants different behavior for any of them, they should flag it on review — each is a simple follow-up but not in scope right now (CLAUDE.md: stay focused, no "while we're at it"):
|
||||
|
||||
- **No close button on overlay stepper.** Clicking the window layer backdrop is the only way to dismiss. Matches how dees-modal with `showCloseButton: false` behaves. Can add a close button in a follow-up.
|
||||
- **No `onComplete` callback in `createAndShow`.** The last step doesn't auto-destroy the overlay — the app controls it via the step's `validationFunc`. Can add a callback option in a follow-up.
|
||||
- **No width/size options in `createAndShow`.** The step tile continues to use the stepper's existing `max-width: 500px`. Can parameterize in a follow-up.
|
||||
- **Box-shadow in the `.step` transition list** is dropped from the transition for cleanliness — the box-shadow is now on `dees-tile::part(outer)` and doesn't change between selected/hiddenStep, so transitioning it was already a no-op.
|
||||
- **`pnpm start` / dev server path**: I'll reuse the existing server on port 8080 that was already listening when this session began; if that server doesn't serve the stepper demo, I'll start wcctools manually.
|
||||
|
||||
## Risk
|
||||
|
||||
- **Low-medium.** The change is localized to one component and its demo. No API removal, only an additive `createAndShow` + an optional `footerContent` field. External consumers of the inline API continue to work if they only set `steps` + `selectedStep`.
|
||||
- **Biggest risk:** SweetScroll's `offsetTop` / `offsetHeight` measurements on `<dees-tile>` may compute differently than on the former `<div class="step">` because `dees-tile` has an internal `display: flex; flex-direction: column;` host and a `.tile-outer { flex: 1; min-height: 0; }` inner frame. If the scroll math drifts, the mitigation is to keep the `.step` wrapper as an outer `<div>` that **contains** a `<dees-tile>`, rather than putting the class directly on `<dees-tile>`. That preserves the exact box model SweetScroll was measuring. I'll try the direct-class approach first (simpler) and fall back to the wrapper approach if the scroll target looks off in the demo.
|
||||
- **Second risk:** The `:host([overlay])` attribute selector requires `overlay` to be a reflected `@property`, not `@state`. I've already accounted for this in the plan (decorator change).
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.51.1',
|
||||
version: '3.78.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -63,14 +63,13 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -60,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;
|
||||
@@ -76,7 +75,7 @@ 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;
|
||||
@@ -133,13 +132,13 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
.tab-actions.left {
|
||||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-right: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
.tab-actions.right {
|
||||
padding-right: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-left: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
.tab-action-button {
|
||||
@@ -152,17 +151,17 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
color: var(--dees-color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-action-button:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
background: var(--dees-color-active);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.tab-action-button:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
background: var(--dees-color-pressed);
|
||||
}
|
||||
|
||||
.tab-action-button.disabled {
|
||||
@@ -172,7 +171,7 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
|
||||
.tab-action-button.disabled:hover {
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.tab-action-button dees-icon {
|
||||
@@ -237,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;
|
||||
@@ -270,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;
|
||||
}
|
||||
|
||||
@@ -291,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,
|
||||
@@ -308,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,
|
||||
@@ -317,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 {
|
||||
@@ -337,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;
|
||||
}
|
||||
@@ -350,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);
|
||||
}
|
||||
@@ -366,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 {
|
||||
@@ -375,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 {
|
||||
|
||||
@@ -184,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;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { renderChartArea } from './template.js';
|
||||
|
||||
import type { IChartApi, ISeriesApi, UTCTimestamp, MouseEventParams } from 'lightweight-charts';
|
||||
import { DeesServiceLibLoader, type ILightweightChartsBundle } from '../../../services/index.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
export type ChartSeriesConfig = {
|
||||
name?: string;
|
||||
@@ -36,6 +37,9 @@ export class DeesChartArea extends DeesElement {
|
||||
@state()
|
||||
accessor seriesStats: Array<{ name: string; latest: number; min: number; max: number; avg: number; color: string }> = [];
|
||||
|
||||
@state()
|
||||
accessor isFullPage: boolean = false;
|
||||
|
||||
@property()
|
||||
accessor label: string = 'Untitled Chart';
|
||||
|
||||
@@ -576,6 +580,29 @@ export class DeesChartArea extends DeesElement {
|
||||
await this.resizeChart();
|
||||
}
|
||||
|
||||
public toggleFullPage() {
|
||||
this.isFullPage = !this.isFullPage;
|
||||
if (this.isFullPage) {
|
||||
this.style.position = 'fixed';
|
||||
this.style.inset = '0';
|
||||
this.style.zIndex = '10000';
|
||||
this.style.height = '100vh';
|
||||
this.style.padding = '0';
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
this.style.position = '';
|
||||
this.style.inset = '';
|
||||
this.style.zIndex = '';
|
||||
this.style.height = '';
|
||||
this.style.padding = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
// Give LC a tick to resize
|
||||
requestAnimationFrame(() => {
|
||||
this.chart?.timeScale().fitContent();
|
||||
});
|
||||
}
|
||||
|
||||
private startAutoScroll() {
|
||||
if (this.autoScrollTimer) return;
|
||||
this.autoScrollTimer = window.setInterval(() => {
|
||||
|
||||
@@ -1,55 +1,63 @@
|
||||
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%)')};
|
||||
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: 44px;
|
||||
left: 0;
|
||||
bottom: 32px;
|
||||
right: 0;
|
||||
inset: 0 0 4px 0;
|
||||
}
|
||||
.statsBar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 0 16px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.statsSeries {
|
||||
display: flex;
|
||||
@@ -58,7 +66,7 @@ export const chartAreaStyles = [
|
||||
}
|
||||
.statsSeries + .statsSeries {
|
||||
padding-left: 24px;
|
||||
border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-left: 1px solid var(--dees-color-border-default);
|
||||
}
|
||||
.statsColor {
|
||||
width: 8px;
|
||||
@@ -68,11 +76,16 @@ export const chartAreaStyles = [
|
||||
}
|
||||
.statsName {
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
|
||||
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: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
.lw-tooltip {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
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>
|
||||
${component.seriesStats.length > 0 ? html`
|
||||
<div class="statsBar">
|
||||
<div slot="footer" class="statsBar">
|
||||
${component.seriesStats.map(s => html`
|
||||
<div class="statsSeries">
|
||||
<span class="statsColor" style="background:${s.color}"></span>
|
||||
@@ -20,6 +26,6 @@ export const renderChartArea = (component: DeesChartArea): TemplateResult => {
|
||||
`)}
|
||||
</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>
|
||||
`;
|
||||
};
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -466,6 +458,9 @@ export class DeesChartLog extends DeesElement {
|
||||
|
||||
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;
|
||||
@@ -512,7 +507,7 @@ export class DeesChartLog extends DeesElement {
|
||||
}
|
||||
|
||||
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,35 +60,21 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
height: 100%;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
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;
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.appbar .fileName {
|
||||
@@ -91,18 +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;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.spacesLabel {
|
||||
@@ -130,15 +122,13 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
display: grid;
|
||||
grid-template-columns: 50px auto;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -201,8 +191,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="mainbox"
|
||||
<dees-tile
|
||||
@contextmenu="${(eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||
{
|
||||
@@ -215,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">
|
||||
@@ -230,11 +219,11 @@ 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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,63 +116,70 @@ 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%)')};
|
||||
transition: background-color 0.15s ease;
|
||||
gap: 10px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--dees-color-border-subtle);
|
||||
transition: background-color 0.15s ease;
|
||||
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;
|
||||
}
|
||||
|
||||
.detail .detailsText .value {
|
||||
font-size: 14px;
|
||||
font-family: 'Intel One Mono', 'Geist Mono', monospace;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
position: relative;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
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%)')};
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bottomBar .statusLabel {
|
||||
padding: 0 16px;
|
||||
.detail .detailsText .value {
|
||||
font-size: 13px;
|
||||
font-family: 'Intel One Mono', 'Geist Mono', monospace;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
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;
|
||||
}
|
||||
`,
|
||||
@@ -186,15 +187,15 @@ export class DeesDataviewStatusobject extends DeesElement {
|
||||
|
||||
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();
|
||||
@@ -224,19 +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 class="bottomBar">
|
||||
<div slot="footer" class="bottomBar">
|
||||
<div class="statusLabel">${this.statusObject?.lastUpdated
|
||||
? `Last updated: ${new Date(this.statusObject.lastUpdated).toLocaleString()}`
|
||||
: ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,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 {
|
||||
@@ -159,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;
|
||||
}
|
||||
|
||||
@@ -179,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;
|
||||
}
|
||||
|
||||
@@ -232,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 {
|
||||
@@ -261,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 */
|
||||
@@ -288,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;
|
||||
}
|
||||
|
||||
@@ -336,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;
|
||||
@@ -344,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;
|
||||
}
|
||||
@@ -393,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;
|
||||
}
|
||||
@@ -471,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;
|
||||
@@ -484,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 {
|
||||
@@ -538,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 {
|
||||
@@ -702,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;
|
||||
}
|
||||
@@ -778,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;
|
||||
@@ -882,27 +875,27 @@ export class DeesStatsGrid extends DeesElement {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -75,12 +75,23 @@ export class DeesFormSubmit extends DeesElement {
|
||||
.text=${this.text}
|
||||
?disabled=${this.disabled}
|
||||
@clicked=${this.submit}
|
||||
>
|
||||
<slot></slot>
|
||||
</dees-button>
|
||||
></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;
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
css,
|
||||
type CSSResult,
|
||||
cssManager,
|
||||
@@ -42,6 +43,9 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
@property({ type: Boolean })
|
||||
accessor disabled: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor infoText!: string;
|
||||
|
||||
@property({ type: String })
|
||||
accessor description!: string;
|
||||
|
||||
@@ -90,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%)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -155,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>
|
||||
|
||||
@@ -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,6 +714,16 @@ 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: [
|
||||
{
|
||||
@@ -616,7 +735,6 @@ export class DeesInputCode extends DeesInputBase<string> {
|
||||
{
|
||||
name: 'Save & Close',
|
||||
action: async (modalRef) => {
|
||||
// Get the editor content from the modal
|
||||
modalEditorElement = modalRef!.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
|
||||
if (modalEditorElement) {
|
||||
const editor = await modalEditorElement.editorDeferred.promise;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,6 @@ export const demoFunc = () => html`
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
@@ -69,9 +63,10 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
|
||||
<div class="input-group">
|
||||
<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' },
|
||||
@@ -94,7 +89,7 @@ export const demoFunc = () => html`
|
||||
{ option: 'Guest', key: 'guest' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -135,40 +130,42 @@ export const demoFunc = () => html`
|
||||
});
|
||||
}}>
|
||||
<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>
|
||||
|
||||
@@ -184,7 +181,7 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
@@ -203,7 +200,7 @@ export const demoFunc = () => html`
|
||||
]}
|
||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,7 +306,7 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
if (this.isOpened) {
|
||||
this.isOpened = false;
|
||||
this.closePopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,9 +318,15 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
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,
|
||||
@@ -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;
|
||||
@@ -280,7 +266,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.payment-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -30,58 +24,61 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-iban
|
||||
.label=${'Bank Account IBAN'}
|
||||
.description=${'Enter your International Bank Account Number'}
|
||||
.infoText=${'Enter your International Bank Account Number'}
|
||||
.description=${'Your IBAN can be found on your bank statement'}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Verified IBAN'}
|
||||
.description=${'This IBAN has been verified'}
|
||||
.infoText=${'This IBAN has been verified'}
|
||||
.value=${'DE89370400440532013000'}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
</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'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-iban
|
||||
.label=${'Payment Account'}
|
||||
.description=${'Required for processing payments'}
|
||||
.infoText=${'Required for processing payments'}
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Locked IBAN'}
|
||||
.description=${'This IBAN cannot be changed'}
|
||||
.infoText=${'This IBAN cannot be changed'}
|
||||
.value=${'FR1420041010050500013M02606'}
|
||||
.disabled=${true}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
</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,16 +44,27 @@ 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>
|
||||
`;
|
||||
}
|
||||
@@ -81,10 +92,6 @@ export class DeesInputIban extends DeesInputBase<DeesInputIban> {
|
||||
}
|
||||
}
|
||||
this.enteredIbanIsValid = ibantools.isValidIBAN(this.enteredString.replace(/ /g, ''));
|
||||
const deesInputText = this.shadowRoot!.querySelector('dees-input-text') as any;
|
||||
if (deesInputText) {
|
||||
deesInputText.validationText = `IBAN is valid: ${this.enteredIbanIsValid}`;
|
||||
}
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
|
||||
@@ -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,9 +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-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;
|
||||
@@ -46,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;
|
||||
|
||||
@@ -99,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 {
|
||||
@@ -131,8 +145,8 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
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%)')};
|
||||
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;
|
||||
@@ -143,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 */
|
||||
@@ -167,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;
|
||||
@@ -269,12 +305,10 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
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%)')};
|
||||
}
|
||||
|
||||
.add-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
@@ -339,13 +373,6 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.list-items::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -368,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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -376,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
|
||||
@@ -392,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
|
||||
@@ -438,16 +507,23 @@ 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}
|
||||
/>
|
||||
<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}
|
||||
@@ -457,26 +533,95 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
|
||||
</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();
|
||||
}
|
||||
}
|
||||
@@ -491,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;
|
||||
@@ -510,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) {
|
||||
@@ -570,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();
|
||||
}
|
||||
|
||||
@@ -52,80 +52,83 @@ export const demoFunc = () => html`
|
||||
<div class="section">
|
||||
<div class="section-title">Multi-Option Toggle</div>
|
||||
<div class="section-description">Select from multiple options with a smooth sliding indicator animation.</div>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Display Mode'}
|
||||
.description=${'Choose how content is displayed'}
|
||||
.options=${['List View', 'Grid View', 'Compact']}
|
||||
.selectedOption=${'Grid View'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<br><br>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'T-Shirt Size'}
|
||||
.description=${'Select your preferred size'}
|
||||
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
|
||||
.selectedOption=${'M'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-form>
|
||||
<dees-input-multitoggle
|
||||
.label=${'Display Mode'}
|
||||
.infoText=${'Choose how content is displayed'}
|
||||
.description=${'This setting affects how items appear on your dashboard'}
|
||||
.options=${['List View', 'Grid View', 'Compact']}
|
||||
.selectedOption=${'Grid View'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'T-Shirt Size'}
|
||||
.infoText=${'Select your preferred size'}
|
||||
.options=${['XS', 'S', 'M', 'L', 'XL', 'XXL']}
|
||||
.selectedOption=${'M'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Boolean Toggle</div>
|
||||
<div class="section-description">Simple on/off switches with customizable labels for clearer context.</div>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Notifications'}
|
||||
.description=${'Enable or disable push notifications'}
|
||||
.type=${'boolean'}
|
||||
.selectedOption=${'true'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<br><br>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Theme Mode'}
|
||||
.description=${'Switch between light and dark theme'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Dark'}
|
||||
.booleanFalseName=${'Light'}
|
||||
.selectedOption=${'Dark'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-form>
|
||||
<dees-input-multitoggle
|
||||
.label=${'Notifications'}
|
||||
.infoText=${'Enable or disable push notifications'}
|
||||
.type=${'boolean'}
|
||||
.selectedOption=${'true'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Theme Mode'}
|
||||
.infoText=${'Switch between light and dark theme'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Dark'}
|
||||
.booleanFalseName=${'Light'}
|
||||
.selectedOption=${'Dark'}
|
||||
></dees-input-multitoggle>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Settings Grid</div>
|
||||
<div class="section-description">Configuration options arranged in a responsive grid layout.</div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<dees-input-multitoggle
|
||||
.label=${'Auto-Save'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Enabled'}
|
||||
.booleanFalseName=${'Disabled'}
|
||||
.selectedOption=${'Enabled'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Language'}
|
||||
.options=${['English', 'German', 'French', 'Spanish']}
|
||||
.selectedOption=${'English'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Quality'}
|
||||
.options=${['Low', 'Medium', 'High', 'Ultra']}
|
||||
.selectedOption=${'High'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Privacy'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Private'}
|
||||
.booleanFalseName=${'Public'}
|
||||
.selectedOption=${'Private'}
|
||||
></dees-input-multitoggle>
|
||||
</div>
|
||||
|
||||
<dees-form>
|
||||
<div class="settings-grid">
|
||||
<dees-input-multitoggle
|
||||
.label=${'Auto-Save'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Enabled'}
|
||||
.booleanFalseName=${'Disabled'}
|
||||
.selectedOption=${'Enabled'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Language'}
|
||||
.options=${['English', 'German', 'French', 'Spanish']}
|
||||
.selectedOption=${'English'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Quality'}
|
||||
.options=${['Low', 'Medium', 'High', 'Ultra']}
|
||||
.selectedOption=${'High'}
|
||||
></dees-input-multitoggle>
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Privacy'}
|
||||
.type=${'boolean'}
|
||||
.booleanTrueName=${'Private'}
|
||||
.booleanFalseName=${'Public'}
|
||||
.selectedOption=${'Private'}
|
||||
></dees-input-multitoggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@@ -134,7 +137,7 @@ export const demoFunc = () => html`
|
||||
|
||||
<dees-input-multitoggle
|
||||
.label=${'Account Type'}
|
||||
.description=${'This setting is locked'}
|
||||
.infoText=${'This setting is locked'}
|
||||
.options=${['Free', 'Pro', 'Enterprise']}
|
||||
.selectedOption=${'Enterprise'}
|
||||
.disabled=${true}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<dees-label .label=${this.label} .infoText=${this.infoText}></dees-label>
|
||||
<div class="mainbox">
|
||||
<div class="selections">
|
||||
<div class="indicator"></div>
|
||||
@@ -158,6 +158,7 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -30,43 +24,46 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-phone
|
||||
.label=${'Phone Number'}
|
||||
.description=${'Enter your phone number with country code'}
|
||||
.infoText=${'Enter your phone number with country code'}
|
||||
.description=${'Include country code for international numbers'}
|
||||
.value=${'5551234567'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Contact Phone'}
|
||||
.description=${'Required for account verification'}
|
||||
.infoText=${'Required for account verification'}
|
||||
.required=${true}
|
||||
.placeholder=${'+1 (555) 000-0000'}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-phone
|
||||
.label=${'Mobile'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'4155551234'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Office'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.placeholder=${'+1 (800) 555-0000'}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-phone
|
||||
.label=${'Mobile'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${'4155551234'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Office'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.placeholder=${'+1 (800) 555-0000'}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-phone
|
||||
.label=${'International Contact'}
|
||||
.description=${'Automatically formats international numbers'}
|
||||
.infoText=${'Automatically formats international numbers'}
|
||||
.value=${'441234567890'}
|
||||
></dees-input-phone>
|
||||
|
||||
@@ -75,7 +72,7 @@ export const demoFunc = () => html`
|
||||
.value=${'911'}
|
||||
.disabled=${true}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Form Integration'} .subtitle=${'Phone input as part of a contact form'}>
|
||||
|
||||
@@ -47,7 +47,7 @@ export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<dees-label .label=${this.label} .infoText=${this.infoText}></dees-label>
|
||||
<dees-input-text
|
||||
.value=${this.formattedPhone}
|
||||
.disabled=${this.disabled}
|
||||
@@ -55,6 +55,7 @@ export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
|
||||
.placeholder=${this.placeholder}
|
||||
@input=${(event: InputEvent) => this.handlePhoneInput(event)}
|
||||
></dees-input-text>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shopping-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
@@ -66,19 +60,20 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.description=${'Select the desired quantity'}
|
||||
.infoText=${'Select the desired quantity'}
|
||||
.description=${'Minimum order quantity is 1 item'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Items in Cart'}
|
||||
.description=${'Adjust the quantity of items'}
|
||||
.infoText=${'Adjust the quantity of items'}
|
||||
.value=${3}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Modern e-commerce product cards with interactive quantity selectors'} .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
@@ -177,21 +172,21 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Number of Licenses'}
|
||||
.description=${'Select how many licenses you need'}
|
||||
.infoText=${'Select how many licenses you need'}
|
||||
.required=${true}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Fixed Quantity'}
|
||||
.description=${'This quantity cannot be changed'}
|
||||
.infoText=${'This quantity cannot be changed'}
|
||||
.disabled=${true}
|
||||
.value=${5}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Order Form'} .subtitle=${'Complete order form with quantity selection'}>
|
||||
@@ -204,7 +199,7 @@ export const demoFunc = () => html`
|
||||
></dees-input-dropdown>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.description=${'Number of licenses'}
|
||||
.infoText=${'Number of licenses'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
<dees-input-text
|
||||
|
||||
@@ -129,7 +129,7 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
${this.label ? html`<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>` : ''}
|
||||
${this.label ? html`<dees-label .label=${this.label} .infoText=${this.infoText} .required=${this.required}></dees-label>` : ''}
|
||||
<div
|
||||
class="quantity-container ${this.disabled ? 'disabled' : ''}"
|
||||
data-min="${this.value <= 0}"
|
||||
@@ -162,6 +162,7 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
|
||||
aria-label="Increase quantity"
|
||||
>+</div>
|
||||
</div>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -23,12 +23,6 @@ export const demoFunc = () => html`
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
@@ -48,25 +42,27 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'1. Basic Radio Groups'} .subtitle=${'Simple string options for common use cases'}>
|
||||
<div class="demo-grid">
|
||||
<dees-input-radiogroup
|
||||
.label=${'Subscription Plan'}
|
||||
.options=${['Basic - $9/month', 'Pro - $29/month', 'Enterprise - $99/month']}
|
||||
.selectedOption=${'Pro - $29/month'}
|
||||
.description=${'Choose your subscription tier'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Priority Level'}
|
||||
.options=${['High', 'Medium', 'Low']}
|
||||
.selectedOption=${'Medium'}
|
||||
.required=${true}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="demo-grid">
|
||||
<dees-input-radiogroup
|
||||
.label=${'Subscription Plan'}
|
||||
.options=${['Basic - $9/month', 'Pro - $29/month', 'Enterprise - $99/month']}
|
||||
.selectedOption=${'Pro - $29/month'}
|
||||
.description=${'Choose your subscription tier'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Priority Level'}
|
||||
.options=${['High', 'Medium', 'Low']}
|
||||
.selectedOption=${'Medium'}
|
||||
.required=${true}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-radiogroup
|
||||
.label=${'Do you agree with the terms?'}
|
||||
.options=${['Yes', 'No', 'Maybe']}
|
||||
@@ -81,7 +77,7 @@ export const demoFunc = () => html`
|
||||
.selectedOption=${'Intermediate'}
|
||||
.description=${'Select your experience level with web development'}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}>
|
||||
@@ -106,41 +102,45 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'4. Survey Example'} .subtitle=${'Multiple radio groups for surveys and forms'}>
|
||||
<div class="demo-grid">
|
||||
<dees-input-radiogroup
|
||||
.label=${'How satisfied are you?'}
|
||||
.options=${['Very Satisfied', 'Satisfied', 'Neutral', 'Dissatisfied', 'Very Dissatisfied']}
|
||||
.selectedOption=${'Satisfied'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Would you recommend us?'}
|
||||
.options=${['Definitely', 'Probably', 'Not Sure', 'Probably Not', 'Definitely Not']}
|
||||
.selectedOption=${'Probably'}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="demo-grid">
|
||||
<dees-input-radiogroup
|
||||
.label=${'How satisfied are you?'}
|
||||
.options=${['Very Satisfied', 'Satisfied', 'Neutral', 'Dissatisfied', 'Very Dissatisfied']}
|
||||
.selectedOption=${'Satisfied'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Would you recommend us?'}
|
||||
.options=${['Definitely', 'Probably', 'Not Sure', 'Probably Not', 'Definitely Not']}
|
||||
.selectedOption=${'Probably'}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'5. States & Validation'} .subtitle=${'Different states and validation examples'}>
|
||||
<div class="demo-grid">
|
||||
<dees-input-radiogroup
|
||||
.label=${'Required Selection'}
|
||||
.options=${['Option A', 'Option B', 'Option C']}
|
||||
.required=${true}
|
||||
.description=${'This field is required'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Disabled State'}
|
||||
.options=${['Disabled Option 1', 'Disabled Option 2', 'Disabled Option 3']}
|
||||
.selectedOption=${'Disabled Option 2'}
|
||||
.disabled=${true}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="demo-grid">
|
||||
<dees-input-radiogroup
|
||||
.label=${'Required Selection'}
|
||||
.options=${['Option A', 'Option B', 'Option C']}
|
||||
.required=${true}
|
||||
.description=${'This field is required'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Disabled State'}
|
||||
.options=${['Disabled Option 1', 'Disabled Option 2', 'Disabled Option 3']}
|
||||
.selectedOption=${'Disabled Option 2'}
|
||||
.disabled=${true}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-radiogroup
|
||||
.label=${'Theme Preference'}
|
||||
.options=${[
|
||||
@@ -165,7 +165,7 @@ export const demoFunc = () => html`
|
||||
.selectedOption=${'English'}
|
||||
.direction=${'horizontal'}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>
|
||||
|
||||
@@ -189,14 +189,6 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
margin-top: 10px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.003em;
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
:host([validationState="invalid"]) .radio-circle {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')};
|
||||
@@ -256,7 +248,7 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { DeesServiceLibLoader, type ITiptapBundle } from '../../../services/index.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -238,6 +239,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
this.editorElement = this.shadowRoot!.querySelector('.editor-content')!;
|
||||
this.linkInputElement = this.shadowRoot!.querySelector('.link-input input')!;
|
||||
this.initializeEditor();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private initializeEditor(): void {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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 richtextStyles = [
|
||||
themeDefaultStyles,
|
||||
...DeesInputBase.baseStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
@@ -20,25 +22,18 @@ export const richtextStyles = [
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: ${cssManager.bdTheme('200px', '200px')};
|
||||
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%)')};
|
||||
overflow: hidden;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
dees-tile {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.editor-container:hover {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
dees-tile:hover::part(outer) {
|
||||
border-color: var(--dees-color-border-strong);
|
||||
}
|
||||
|
||||
.editor-container.focused {
|
||||
dees-tile.focused::part(outer) {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
|
||||
}
|
||||
@@ -47,9 +42,7 @@ export const richtextStyles = [
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
padding: 4px 12px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
@@ -77,8 +70,8 @@ export const richtextStyles = [
|
||||
}
|
||||
|
||||
.toolbar-button: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%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.toolbar-button.active {
|
||||
@@ -94,22 +87,24 @@ export const richtextStyles = [
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-border-default);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
min-height: var(--min-height, 200px);
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror {
|
||||
outline: none;
|
||||
line-height: 1.6;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
min-height: 100%;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror p {
|
||||
@@ -156,7 +151,7 @@ export const richtextStyles = [
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror blockquote {
|
||||
border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-left: 4px solid var(--dees-color-border-default);
|
||||
margin: 1em 0;
|
||||
padding-left: 1em;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
@@ -164,12 +159,12 @@ export const richtextStyles = [
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror code {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
background: var(--dees-color-bg-tertiary);
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.4em;
|
||||
font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.editor-content .ProseMirror pre {
|
||||
@@ -199,14 +194,15 @@ export const richtextStyles = [
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
padding: 8px 12px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
color: var(--dees-color-text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.word-count {
|
||||
@@ -219,8 +215,8 @@ export const richtextStyles = [
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
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%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
border: 1px solid var(--dees-color-border-default);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
padding: 12px;
|
||||
@@ -234,12 +230,12 @@ export const richtextStyles = [
|
||||
.link-input input {
|
||||
width: 100%;
|
||||
padding: 8px 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: 6px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
color: var(--dees-color-text-primary);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@@ -256,19 +252,19 @@ export const richtextStyles = [
|
||||
|
||||
.link-input-buttons button {
|
||||
padding: 6px 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%)')};
|
||||
background: var(--dees-color-bg-primary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
color: var(--dees-color-text-muted);
|
||||
transition: all 0.15s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-input-buttons button: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%)')};
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.link-input-buttons button.primary {
|
||||
@@ -282,14 +278,7 @@ export const richtextStyles = [
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:host([disabled]) .editor-container {
|
||||
:host([disabled]) dees-tile {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ export const renderRichtext = (component: DeesInputRichtext): TemplateResult =>
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
${component.label ? html`<label class="label">${component.label}</label>` : ''}
|
||||
<div class="editor-container ${component.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${component.minHeight}px">
|
||||
<div class="editor-toolbar">
|
||||
<dees-tile class="${component.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${component.minHeight}px">
|
||||
<div slot="header" class="editor-toolbar">
|
||||
${component.renderToolbar()}
|
||||
<div class="link-input ${component.showLinkInput ? 'show' : ''}">
|
||||
<input type="url" placeholder="Enter URL..." @keydown=${component.handleLinkInputKeydown} />
|
||||
@@ -20,14 +20,14 @@ export const renderRichtext = (component: DeesInputRichtext): TemplateResult =>
|
||||
<div class="editor-content"></div>
|
||||
${component.showWordCount
|
||||
? html`
|
||||
<div class="editor-footer">
|
||||
<div slot="footer" class="editor-footer">
|
||||
<span class="word-count">${component.wordCount} word${component.wordCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
${component.description ? html`<div class="description">${component.description}</div>` : ''}
|
||||
</dees-tile>
|
||||
${component.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -115,24 +115,26 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'3. Limited Tags'} .subtitle=${'Restrict the number of tags users can add'}>
|
||||
<div class="grid-layout">
|
||||
<dees-input-tags
|
||||
.label=${'Top 3 Skills'}
|
||||
.placeholder=${'Add up to 3 skills...'}
|
||||
.maxTags=${3}
|
||||
.value=${['Design', 'Development']}
|
||||
.description=${'Maximum 3 tags allowed'}
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
.label=${'Categories (Max 5)'}
|
||||
.placeholder=${'Select categories...'}
|
||||
.maxTags=${5}
|
||||
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
|
||||
.value=${['Tutorial', 'Guide']}
|
||||
.description=${'Choose up to 5 categories'}
|
||||
></dees-input-tags>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="grid-layout">
|
||||
<dees-input-tags
|
||||
.label=${'Top 3 Skills'}
|
||||
.placeholder=${'Add up to 3 skills...'}
|
||||
.maxTags=${3}
|
||||
.value=${['Design', 'Development']}
|
||||
.description=${'Maximum 3 tags allowed'}
|
||||
></dees-input-tags>
|
||||
|
||||
<dees-input-tags
|
||||
.label=${'Categories (Max 5)'}
|
||||
.placeholder=${'Select categories...'}
|
||||
.maxTags=${5}
|
||||
.suggestions=${['Blog', 'Tutorial', 'News', 'Review', 'Guide', 'Case Study', 'Interview']}
|
||||
.value=${['Tutorial', 'Guide']}
|
||||
.description=${'Choose up to 5 categories'}
|
||||
></dees-input-tags>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'4. Required & Validation'} .subtitle=${'Tags input with validation requirements'}>
|
||||
|
||||
@@ -210,14 +210,6 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Description styles */
|
||||
.description {
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.suggestions-dropdown::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -302,9 +294,7 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
|
||||
<div class="validation-message">${this.validationText}</div>
|
||||
` : ''}
|
||||
|
||||
${this.description ? html`
|
||||
<div class="description">${this.description}</div>
|
||||
` : ''}
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -30,12 +30,6 @@ export const demoFunc = () => html`
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -89,17 +83,18 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Basic Text Inputs'} .subtitle=${'Standard text inputs with labels and descriptions'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'Username'}
|
||||
.value=${'johndoe'}
|
||||
.key=${'username'}
|
||||
.description=${'Your username will be visible to other users'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Email Address'}
|
||||
.value=${'john@example.com'}
|
||||
.description=${'We will never share your email with anyone'}
|
||||
.infoText=${'We will never share your email with anyone'}
|
||||
.key=${'email'}
|
||||
></dees-input-text>
|
||||
|
||||
@@ -109,7 +104,7 @@ export const demoFunc = () => html`
|
||||
.value=${'secret123'}
|
||||
.key=${'password'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -139,28 +134,30 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Multiple inputs arranged horizontally for compact forms'}>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-text
|
||||
.label=${'First Name'}
|
||||
.value=${'John'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'firstName'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Last Name'}
|
||||
.value=${'Doe'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'lastName'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Age'}
|
||||
.value=${'28'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'age'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
<dees-form>
|
||||
<div class="horizontal-group">
|
||||
<dees-input-text
|
||||
.label=${'First Name'}
|
||||
.value=${'John'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'firstName'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Last Name'}
|
||||
.value=${'Doe'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'lastName'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Age'}
|
||||
.value=${'28'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.key=${'age'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -180,7 +177,7 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Label Positions'} .subtitle=${'Different label positioning options for various layouts'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'Label on Top (Default)'}
|
||||
.value=${'Standard layout'}
|
||||
@@ -194,57 +191,25 @@ export const demoFunc = () => html`
|
||||
></dees-input-text>
|
||||
|
||||
<div class="grid-layout">
|
||||
<dees-input-text
|
||||
.label=${'City'}
|
||||
.value=${'New York'}
|
||||
<dees-input-text
|
||||
.label=${'City'}
|
||||
.value=${'New York'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
|
||||
|
||||
<dees-input-text
|
||||
.label=${'ZIP Code'}
|
||||
.value=${'10001'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
// Demonstrate validation states
|
||||
const requiredInput = elementArg.querySelector('dees-input-text[required]') as DeesInputText;
|
||||
const disabledInput = elementArg.querySelector('dees-input-text[disabled]') as DeesInputText;
|
||||
const errorInput = elementArg.querySelector('dees-input-text[validationState="invalid"]') as DeesInputText;
|
||||
|
||||
if (requiredInput) {
|
||||
// Show validation on blur for empty required field
|
||||
requiredInput.addEventListener('blur', () => {
|
||||
if (!requiredInput.getValue()) {
|
||||
console.log('Required field is empty!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (disabledInput) {
|
||||
console.log('Disabled input cannot be edited');
|
||||
}
|
||||
|
||||
if (errorInput) {
|
||||
console.log('Error input shows validation message:', errorInput.validationText);
|
||||
|
||||
// Simulate fixing the error
|
||||
errorInput.addEventListener('changeSubject', () => {
|
||||
const value = errorInput.getValue();
|
||||
if (value.includes('@') && value.includes('.')) {
|
||||
errorInput.validationState = 'valid';
|
||||
errorInput.validationText = '';
|
||||
console.log('Email validation passed!');
|
||||
}
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<dees-demowrapper>
|
||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
@@ -258,12 +223,17 @@ export const demoFunc = () => html`
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Field with Error'}
|
||||
.label=${'Email with Validation'}
|
||||
.value=${'invalid@'}
|
||||
.validationText=${'Please enter a valid email address'}
|
||||
.validationState=${'invalid'}
|
||||
.validationFunction=${(value: string) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailRegex.test(value)) {
|
||||
return { valid: true, message: 'Email address is valid' };
|
||||
}
|
||||
return { valid: false, message: 'Please enter a valid email address' };
|
||||
}}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -292,59 +262,40 @@ export const demoFunc = () => html`
|
||||
});
|
||||
}}>
|
||||
<dees-panel .title=${'Advanced Features'} .subtitle=${'Password visibility toggle and other advanced features'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
.label=${'Password with Toggle'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'mySecurePassword123'}
|
||||
.description=${'Click the eye icon to show/hide password'}
|
||||
.infoText=${'Click the eye icon to show/hide password'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'API Key'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'sk-1234567890abcdef'}
|
||||
.description=${'Keep this key secure and never share it'}
|
||||
.infoText=${'Keep this key secure and never share it'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
// Set up interactive example
|
||||
const dynamicInput = elementArg.querySelector('dees-input-text');
|
||||
const dynamicInput = elementArg.querySelector('dees-input-text') as DeesInputText;
|
||||
const output = elementArg.querySelector('#text-input-output');
|
||||
|
||||
|
||||
if (dynamicInput && output) {
|
||||
// Update output on every change
|
||||
dynamicInput.addEventListener('changeSubject', ((event: CustomEvent) => {
|
||||
const value = (event.detail as DeesInputText).getValue();
|
||||
output.textContent = `Current value: "${value}"`;
|
||||
}) as EventListener);
|
||||
|
||||
// Also track focus/blur events
|
||||
dynamicInput.addEventListener('focus', () => {
|
||||
console.log('Input focused');
|
||||
});
|
||||
|
||||
dynamicInput.addEventListener('blur', () => {
|
||||
console.log('Input blurred');
|
||||
});
|
||||
|
||||
// Track keypress events
|
||||
let keypressCount = 0;
|
||||
dynamicInput.addEventListener('keydown', () => {
|
||||
keypressCount++;
|
||||
console.log(`Keypress count: ${keypressCount}`);
|
||||
dynamicInput.changeSubject.subscribe(() => {
|
||||
output.textContent = `Current value: "${dynamicInput.getValue()}"`;
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Interactive Example'} .subtitle=${'Try typing in the inputs to see real-time value changes'}>
|
||||
<dees-input-text
|
||||
.label=${'Dynamic Input'}
|
||||
<dees-panel .title=${'Interactive Example'} .subtitle=${'Try typing in the input to see real-time value changes'}>
|
||||
<dees-input-text
|
||||
.label=${'Dynamic Input'}
|
||||
.placeholder=${'Type something here...'}
|
||||
></dees-input-text>
|
||||
|
||||
|
||||
<div class="interactive-section">
|
||||
<div id="text-input-output" class="output-text">Current value: ""</div>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
state,
|
||||
html,
|
||||
cssManager,
|
||||
css,
|
||||
} from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -44,7 +46,7 @@ export class DeesInputText extends DeesInputBase {
|
||||
accessor showPasswordBool = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
accessor validationState!: 'valid' | 'warn' | 'invalid';
|
||||
@@ -54,8 +56,19 @@ export class DeesInputText extends DeesInputBase {
|
||||
})
|
||||
accessor validationText: string = '';
|
||||
|
||||
@property({})
|
||||
accessor validationFunction!: (value: string) => boolean;
|
||||
@property({ attribute: false })
|
||||
accessor validationFunction!: (value: string) => { valid: boolean; message?: string };
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
accessor vintegrated: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor validConfirmed: boolean = false;
|
||||
|
||||
private validTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
@@ -121,7 +134,7 @@ export class DeesInputText extends DeesInputBase {
|
||||
.showPassword {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 50%;
|
||||
top: 20px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -139,6 +152,29 @@ export class DeesInputText extends DeesInputBase {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
||||
}
|
||||
|
||||
/* Valid checkmark icon */
|
||||
.validIcon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
}
|
||||
|
||||
.validIcon.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.validIcon dees-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
.validationContainer {
|
||||
margin-top: 4px;
|
||||
@@ -150,7 +186,7 @@ export class DeesInputText extends DeesInputBase {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.validationContainer.error {
|
||||
.validationContainer.invalid {
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
}
|
||||
@@ -166,34 +202,64 @@ export class DeesInputText extends DeesInputBase {
|
||||
}
|
||||
|
||||
/* Error state for input */
|
||||
:host([validation-state="invalid"]) input {
|
||||
:host([validationstate="invalid"]) input {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
}
|
||||
|
||||
:host([validation-state="invalid"]) input:focus {
|
||||
:host([validationstate="invalid"]) input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.05)', 'hsl(0 72.2% 50.6% / 0.05)')};
|
||||
}
|
||||
|
||||
/* Warning state for input */
|
||||
:host([validation-state="warn"]) input {
|
||||
:host([validationstate="warn"]) input {
|
||||
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||
}
|
||||
|
||||
:host([validation-state="warn"]) input:focus {
|
||||
:host([validationstate="warn"]) input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(25 95% 53% / 0.05)', 'hsl(25 95% 63% / 0.05)')};
|
||||
}
|
||||
|
||||
/* Valid state for input */
|
||||
:host([validation-state="valid"]) input {
|
||||
:host([validationstate="valid"]) input {
|
||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
}
|
||||
|
||||
:host([validation-state="valid"]) input:focus {
|
||||
:host([validationstate="valid"]) input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
|
||||
}
|
||||
|
||||
/* Visually integrated mode: shed chrome to blend into a host component
|
||||
(e.g. a dees-table cell in edit mode). */
|
||||
:host([vintegrated]) dees-label,
|
||||
:host([vintegrated]) .validationContainer {
|
||||
display: none;
|
||||
}
|
||||
:host([vintegrated]) .maincontainer {
|
||||
height: 40px;
|
||||
}
|
||||
:host([vintegrated]) input {
|
||||
height: 40px;
|
||||
line-height: 24px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
:host([vintegrated]) input:hover:not(:disabled):not(:focus),
|
||||
:host([vintegrated]) input:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
:host([vintegrated]) .showPassword {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -203,9 +269,9 @@ export class DeesInputText extends DeesInputBase {
|
||||
input {
|
||||
font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'};
|
||||
letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'};
|
||||
padding-right: ${this.isPasswordBool ? '48px' : '12px'};
|
||||
padding-right: ${this.isPasswordBool ? '48px' : this.validConfirmed ? '40px' : '12px'};
|
||||
}
|
||||
${this.validationText
|
||||
${this.validationText && !this.validConfirmed
|
||||
? css`
|
||||
.validationContainer {
|
||||
height: auto;
|
||||
@@ -223,7 +289,7 @@ export class DeesInputText extends DeesInputBase {
|
||||
`}
|
||||
</style>
|
||||
<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">
|
||||
<input
|
||||
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
|
||||
@@ -239,28 +305,114 @@ export class DeesInputText extends DeesInputBase {
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="validIcon ${this.validConfirmed ? 'show' : ''}">
|
||||
<dees-icon .icon=${'lucide:Check'}></dees-icon>
|
||||
</div>
|
||||
${this.validationText
|
||||
? html`
|
||||
<div class="validationContainer ${this.validationState || 'error'}">
|
||||
<div class="validationContainer ${this.validationState || 'invalid'}">
|
||||
${this.validationText}
|
||||
</div>
|
||||
`
|
||||
: html`<div class="validationContainer"></div>`}
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
// Input event handling is already done in updateValue method
|
||||
if (this.validationFunction && this.value) {
|
||||
const result = this.validationFunction(this.value);
|
||||
this.validationState = result.valid ? 'valid' : 'invalid';
|
||||
this.validationText = result.message || '';
|
||||
if (result.valid) {
|
||||
this.validConfirmed = false;
|
||||
this.validTimeout = setTimeout(() => {
|
||||
this.validConfirmed = true;
|
||||
this.validationState = undefined as any;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async updateValue(eventArg: Event) {
|
||||
const target: any = eventArg.target;
|
||||
this.value = target.value;
|
||||
if (this.validationFunction) {
|
||||
const result = this.validationFunction(this.value);
|
||||
this.validationState = result.valid ? 'valid' : 'invalid';
|
||||
this.validationText = result.message || '';
|
||||
if (result.valid) {
|
||||
this.validConfirmed = false;
|
||||
clearTimeout(this.validTimeout);
|
||||
this.validTimeout = setTimeout(() => {
|
||||
this.validConfirmed = true;
|
||||
this.validationState = undefined as any;
|
||||
}, 500);
|
||||
} else {
|
||||
clearTimeout(this.validTimeout);
|
||||
this.validConfirmed = false;
|
||||
}
|
||||
}
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
|
||||
public getContextMenuItems() {
|
||||
const input = this.shadowRoot!.querySelector('input')!;
|
||||
const hasSelection = input.selectionStart !== input.selectionEnd;
|
||||
return [
|
||||
{
|
||||
name: 'Cut',
|
||||
iconName: 'lucide:Scissors',
|
||||
shortcut: 'Cmd+X',
|
||||
disabled: !hasSelection,
|
||||
action: async () => {
|
||||
const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
|
||||
await navigator.clipboard.writeText(selected);
|
||||
const start = input.selectionStart!;
|
||||
this.value = this.value.substring(0, start) + this.value.substring(input.selectionEnd!);
|
||||
input.value = this.value;
|
||||
input.setSelectionRange(start, start);
|
||||
this.changeSubject.next(this);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Copy',
|
||||
iconName: 'lucide:Copy',
|
||||
shortcut: 'Cmd+C',
|
||||
disabled: !hasSelection,
|
||||
action: async () => {
|
||||
const selected = this.value.substring(input.selectionStart!, input.selectionEnd!);
|
||||
await navigator.clipboard.writeText(selected);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Paste',
|
||||
iconName: 'lucide:ClipboardPaste',
|
||||
shortcut: 'Cmd+V',
|
||||
action: async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const start = input.selectionStart!;
|
||||
const end = input.selectionEnd!;
|
||||
this.value = this.value.substring(0, start) + text + this.value.substring(end);
|
||||
input.value = this.value;
|
||||
input.setSelectionRange(start + text.length, start + text.length);
|
||||
this.changeSubject.next(this);
|
||||
},
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Select All',
|
||||
iconName: 'lucide:TextCursorInput',
|
||||
shortcut: 'Cmd+A',
|
||||
action: async () => {
|
||||
input.select();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Toggle'} .subtitle=${'Simple on/off toggle switch with drag support'}>
|
||||
<div class="toggle-group">
|
||||
<dees-form>
|
||||
<dees-input-toggle
|
||||
.label=${'Enable feature'}
|
||||
.value=${false}
|
||||
@@ -135,12 +135,12 @@ export const demoFunc = () => html`
|
||||
.description=${'This toggle has additional helper text explaining its purpose'}
|
||||
.key=${'withDesc'}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
<p class="drag-hint">Tip: You can drag the toggle knob to switch states</p>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Toggle States'} .subtitle=${'Different toggle states and configurations'}>
|
||||
<div class="toggle-group">
|
||||
<dees-form>
|
||||
<dees-input-toggle
|
||||
.label=${'Default (off)'}
|
||||
.value=${false}
|
||||
@@ -169,42 +169,44 @@ export const demoFunc = () => html`
|
||||
.required=${true}
|
||||
.description=${'This toggle cannot be turned off'}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Toggles arranged horizontally for compact interfaces'}>
|
||||
<div class="horizontal-toggles">
|
||||
<dees-input-toggle
|
||||
.label=${'WiFi'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
<dees-form>
|
||||
<div class="horizontal-toggles">
|
||||
<dees-input-toggle
|
||||
.label=${'WiFi'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
|
||||
<dees-input-toggle
|
||||
.label=${'Bluetooth'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
<dees-input-toggle
|
||||
.label=${'Bluetooth'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
|
||||
<dees-input-toggle
|
||||
.label=${'GPS'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
<dees-input-toggle
|
||||
.label=${'GPS'}
|
||||
.value=${true}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
|
||||
<dees-input-toggle
|
||||
.label=${'NFC'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
<dees-input-toggle
|
||||
.label=${'NFC'}
|
||||
.value=${false}
|
||||
.layoutMode=${'horizontal'}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Settings Example'} .subtitle=${'Toggles in a typical settings context'}>
|
||||
<div class="settings-section">
|
||||
<h4 class="section-title">Notification Settings</h4>
|
||||
|
||||
<div class="toggle-group">
|
||||
<dees-form>
|
||||
<dees-input-toggle
|
||||
.label=${'Push notifications'}
|
||||
.value=${true}
|
||||
@@ -232,7 +234,7 @@ export const demoFunc = () => html`
|
||||
.description=${'Vibrate for notifications'}
|
||||
.key=${'vibration'}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
@@ -243,7 +245,7 @@ export const demoFunc = () => html`
|
||||
</div>
|
||||
|
||||
<div class="feature-toggles">
|
||||
<div class="toggle-group">
|
||||
<dees-form>
|
||||
<dees-input-toggle
|
||||
.label=${'Dark Mode'}
|
||||
.value=${true}
|
||||
@@ -273,12 +275,12 @@ export const demoFunc = () => html`
|
||||
.value=${false}
|
||||
.key=${'beta'}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Interactive Example'} .subtitle=${'Toggle to see value changes in real-time'}>
|
||||
<div class="toggle-group">
|
||||
<dees-form>
|
||||
<dees-input-toggle
|
||||
.label=${'Airplane mode'}
|
||||
.value=${false}
|
||||
@@ -300,7 +302,7 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}
|
||||
></dees-input-toggle>
|
||||
</div>
|
||||
</dees-form>
|
||||
|
||||
<div class="interactive-section">
|
||||
<div id="airplane-output" class="output-text">Airplane mode: OFF</div>
|
||||
|
||||
@@ -170,12 +170,6 @@ export class DeesInputToggle extends DeesInputBase<DeesInputToggle> {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.description-text {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
line-height: 1.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -199,7 +193,7 @@ export class DeesInputToggle extends DeesInputBase<DeesInputToggle> {
|
||||
</div>
|
||||
<div class="label-container">
|
||||
${this.label ? html`<div class="toggle-label">${this.label}</div>` : ''}
|
||||
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,12 +13,6 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
@@ -45,61 +39,62 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-typelist
|
||||
.label=${'Tags'}
|
||||
.description=${'Add tags by typing and pressing Enter'}
|
||||
.infoText=${'Add tags by typing and pressing Enter'}
|
||||
.description=${'Tags help categorize and filter your content'}
|
||||
.value=${['javascript', 'typescript', 'web-components']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Team Members'}
|
||||
.description=${'Add email addresses of team members'}
|
||||
.infoText=${'Add email addresses of team members'}
|
||||
.value=${['alice@example.com', 'bob@example.com']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-typelist
|
||||
.label=${'Your Skills'}
|
||||
.description=${'List your professional skills'}
|
||||
.infoText=${'List your professional skills'}
|
||||
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-typelist
|
||||
.label=${'Categories'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${['Technology', 'Design', 'Business']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Keywords'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${['innovation', 'startup', 'growth']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</div>
|
||||
<dees-input-typelist
|
||||
.label=${'Categories'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${['Technology', 'Design', 'Business']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Keywords'}
|
||||
.layoutMode=${'horizontal'}
|
||||
.value=${['innovation', 'startup', 'growth']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}>
|
||||
<div class="input-group">
|
||||
<dees-form>
|
||||
<dees-input-typelist
|
||||
.label=${'Project Dependencies'}
|
||||
.description=${'List all required npm packages'}
|
||||
.infoText=${'List all required npm packages'}
|
||||
.required=${true}
|
||||
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'System Tags'}
|
||||
.description=${'These tags are managed by the system'}
|
||||
.infoText=${'These tags are managed by the system'}
|
||||
.disabled=${true}
|
||||
.value=${['system', 'protected', 'readonly']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</dees-form>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Article Publishing Form'} .subtitle=${'Complete form with tag management'}>
|
||||
@@ -108,16 +103,16 @@ export const demoFunc = () => html`
|
||||
<dees-input-text
|
||||
.label=${'Summary'}
|
||||
.inputType=${'textarea'}
|
||||
.description=${'Brief description of the article'}
|
||||
.infoText=${'Brief description of the article'}
|
||||
></dees-input-text>
|
||||
<dees-input-typelist
|
||||
.label=${'Tags'}
|
||||
.description=${'Add relevant tags for better discoverability'}
|
||||
.infoText=${'Add relevant tags for better discoverability'}
|
||||
.value=${['tutorial', 'web-development']}
|
||||
></dees-input-typelist>
|
||||
<dees-input-typelist
|
||||
.label=${'Co-Authors'}
|
||||
.description=${'Add email addresses of co-authors'}
|
||||
.infoText=${'Add email addresses of co-authors'}
|
||||
></dees-input-typelist>
|
||||
</dees-form>
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="input-wrapper">
|
||||
<dees-label .label=${this.label} .description=${this.description}></dees-label>
|
||||
<dees-label .label=${this.label} .infoText=${this.infoText}></dees-label>
|
||||
<div class="mainbox">
|
||||
<div class="tags" @click=${() => {
|
||||
this.shadowRoot!.querySelector('input')!.focus();
|
||||
@@ -188,6 +188,7 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
|
||||
.disabled=${this.disabled}
|
||||
/>
|
||||
</div>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -292,17 +292,18 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
return html`
|
||||
<dees-label
|
||||
.label="${this.label}"
|
||||
.description="${this.description}"
|
||||
.infoText="${this.infoText}"
|
||||
.required="${this.required}"
|
||||
></dees-label>
|
||||
<div class="wysiwyg-container">
|
||||
<div
|
||||
<div
|
||||
class="editor-content ${this.draggedBlockId ? 'dragging' : ''}"
|
||||
id="editor-content"
|
||||
>
|
||||
<!-- Blocks will be rendered programmatically -->
|
||||
</div>
|
||||
</div>
|
||||
${this.renderDescription()}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
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="profile-container"
|
||||
@@ -329,6 +329,7 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
|
||||
accept="${this.acceptedFormats.join(',')}"
|
||||
@change=${this.handleFileSelect}
|
||||
/>
|
||||
${this.renderDescription()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ export function demoFunc() {
|
||||
<dees-heading level="4">This is a H4 heading</dees-heading>
|
||||
<dees-heading level="5">This is a H5 heading</dees-heading>
|
||||
<dees-heading level="6">This is a H6 heading</dees-heading>
|
||||
<dees-heading level="hr">This is an hr heading</dees-heading>
|
||||
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
|
||||
<dees-heading level="hr">This is an hr heading (level="hr")</dees-heading>
|
||||
<dees-heading level="7">This is an hr heading (level="7")</dees-heading>
|
||||
<dees-heading level="hr-small">This is an hr-small heading (level="hr-small")</dees-heading>
|
||||
<dees-heading level="8">This is an hr-small heading (level="8")</dees-heading>
|
||||
`;
|
||||
}
|
||||
@@ -27,68 +27,104 @@ export class DeesHeading extends DeesElement {
|
||||
|
||||
// properties
|
||||
/**
|
||||
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
|
||||
* Heading level:
|
||||
* '1'-'6' → <h1>..<h6>
|
||||
* '7'|'hr' → horizontal-rule style heading
|
||||
* '8'|'hr-small' → small horizontal-rule style heading
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
|
||||
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | 'hr' | 'hr-small' = '1';
|
||||
|
||||
// STATIC STYLES
|
||||
public static styles: CSSResult[] = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
/* Heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;}
|
||||
h2 { font-size: 28px; }
|
||||
h3 { font-size: 24px; }
|
||||
h4 { font-size: 20px; }
|
||||
h5 { font-size: 16px; }
|
||||
h6 { font-size: 14px; }
|
||||
|
||||
/* Heading styles.
|
||||
* Color hierarchy: h1-h2 stay prominent with text-primary; h3-h6 step
|
||||
* down to text-secondary so they read as subheadings instead of
|
||||
* mini-h1s. Keeps the visual loudness out of smaller headings. */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
h1, h2 {
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
h3, h4, h5, h6 {
|
||||
color: var(--dees-color-text-secondary);
|
||||
}
|
||||
|
||||
/* Per-level typography + spacing.
|
||||
* Margin scales with importance: h1 gets the most breathing room,
|
||||
* h6 the least. Top margin > bottom margin so headings group with
|
||||
* the content that follows them. */
|
||||
h1 {
|
||||
/* h1 uses weight 500, not 600: the Cal Sans display font is
|
||||
* already stylized enough that bold + max contrast + 32px stacks
|
||||
* too much emphasis. 500 keeps the typographic impact without
|
||||
* shouting. */
|
||||
font-weight: 500;
|
||||
font-size: 32px;
|
||||
font-family: ${cssCalSansFontFamily};
|
||||
letter-spacing: 0.025em;
|
||||
margin: var(--dees-spacing-2xl) 0 var(--dees-spacing-lg);
|
||||
}
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
|
||||
}
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
|
||||
}
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
margin: var(--dees-spacing-lg) 0 var(--dees-spacing-sm);
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
margin: var(--dees-spacing-md) 0 var(--dees-spacing-sm);
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
margin: var(--dees-spacing-md) 0 var(--dees-spacing-xs);
|
||||
}
|
||||
|
||||
/* Horizontal rule style heading */
|
||||
.heading-hr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
margin: var(--dees-spacing-lg) 0;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
/* Fade lines toward and away from text for hr style */
|
||||
.heading-hr::before {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade in toward center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, transparent, #ccc)',
|
||||
'linear-gradient(to right, transparent, #333)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
background: linear-gradient(to right, transparent, var(--dees-color-border-strong));
|
||||
margin: 0 var(--dees-spacing-sm);
|
||||
}
|
||||
.heading-hr::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade out away from center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, #ccc, transparent)',
|
||||
'linear-gradient(to right, #333, transparent)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
background: linear-gradient(to right, var(--dees-color-border-strong), transparent);
|
||||
margin: 0 var(--dees-spacing-sm);
|
||||
}
|
||||
/* Small hr variant with reduced margins */
|
||||
.heading-hr.heading-hr-small {
|
||||
margin: 8px 0;
|
||||
margin: var(--dees-spacing-sm) 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.heading-hr.heading-hr-small::before,
|
||||
.heading-hr.heading-hr-small::after {
|
||||
margin: 0 8px;
|
||||
margin: 0 var(--dees-spacing-sm);
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -109,8 +145,10 @@ export class DeesHeading extends DeesElement {
|
||||
return html`<h5><slot></slot></h5>`;
|
||||
case '6':
|
||||
return html`<h6><slot></slot></h6>`;
|
||||
case '7':
|
||||
case 'hr':
|
||||
return html`<div class="heading-hr"><slot></slot></div>`;
|
||||
case '8':
|
||||
case 'hr-small':
|
||||
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,128 @@
|
||||
import { html, cssManager } from '@design.estate/dees-element';
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => {
|
||||
return html`
|
||||
<dees-label .label=${'a label'}></dees-label>
|
||||
`;
|
||||
}
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.label-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.label-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.label-row .annotation {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
color: ${cssManager.bdTheme('#888', '#666')};
|
||||
flex-shrink: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Basic Label</h3>
|
||||
<p>A simple text label with no additional indicators.</p>
|
||||
<div class="label-grid">
|
||||
<div class="label-row">
|
||||
<span class="annotation">label="Username"</span>
|
||||
<dees-label .label=${'Username'}></dees-label>
|
||||
</div>
|
||||
<div class="label-row">
|
||||
<span class="annotation">label="Email Address"</span>
|
||||
<dees-label .label=${'Email Address'}></dees-label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Required Indicator</h3>
|
||||
<p>When <code>required</code> is set, a red asterisk appears after the label text.</p>
|
||||
<div class="label-grid">
|
||||
<div class="label-row">
|
||||
<span class="annotation">required=${'{true}'}</span>
|
||||
<dees-label .label=${'Full Name'} .required=${true}></dees-label>
|
||||
</div>
|
||||
<div class="label-row">
|
||||
<span class="annotation">required=${'{false}'} (default)</span>
|
||||
<dees-label .label=${'Nickname'} .required=${false}></dees-label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Info Text (Info Icon)</h3>
|
||||
<p>When <code>infoText</code> is set, an info icon appears next to the label. Hover over it to see the tooltip.</p>
|
||||
<div class="label-grid">
|
||||
<div class="label-row">
|
||||
<span class="annotation">infoText="..."</span>
|
||||
<dees-label .label=${'API Key'} .infoText=${'Your API key can be found in the developer settings dashboard.'}></dees-label>
|
||||
</div>
|
||||
<div class="label-row">
|
||||
<span class="annotation">short infoText</span>
|
||||
<dees-label .label=${'Region'} .infoText=${'Select your nearest datacenter.'}></dees-label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Required + Info Text</h3>
|
||||
<p>Both indicators can be combined. The asterisk appears first, then the info icon.</p>
|
||||
<div class="label-grid">
|
||||
<div class="label-row">
|
||||
<span class="annotation">required + infoText</span>
|
||||
<dees-label .label=${'Password'} .required=${true} .infoText=${'Must be at least 8 characters with one uppercase letter and one number.'}></dees-label>
|
||||
</div>
|
||||
<div class="label-row">
|
||||
<span class="annotation">required + infoText</span>
|
||||
<dees-label .label=${'Email Address'} .required=${true} .infoText=${'We will send a verification link to this address.'}></dees-label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Empty Label</h3>
|
||||
<p>When <code>label</code> is empty or not set, nothing is rendered. The element below has no label text:</p>
|
||||
<div class="label-grid">
|
||||
<div class="label-row">
|
||||
<span class="annotation">label="" (empty)</span>
|
||||
<dees-label .label=${''}></dees-label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -32,7 +32,7 @@ export class DeesLabel extends DeesElement {
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
accessor description!: string;
|
||||
accessor infoText!: string;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
@@ -50,7 +50,8 @@ export class DeesLabel extends DeesElement {
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
@@ -66,13 +67,26 @@ export class DeesLabel extends DeesElement {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
dees-icon {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
transform: translateY(1px);
|
||||
margin-left: 4px;
|
||||
.description-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: -6px 0 -6px 2px;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.description-icon:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 100% / 0.08)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 80%)')};
|
||||
}
|
||||
|
||||
.description-icon dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -84,10 +98,12 @@ export class DeesLabel extends DeesElement {
|
||||
<div class="label">
|
||||
${this.label}
|
||||
${this.required ? html`<span class="required">*</span>` : ''}
|
||||
${this.description
|
||||
${this.infoText
|
||||
? html`
|
||||
<dees-icon .icon=${'lucide:info'}></dees-icon>
|
||||
<dees-speechbubble .text=${this.description}></dees-speechbubble>
|
||||
<div class="description-icon">
|
||||
<dees-icon .icon=${'lucide:info'}></dees-icon>
|
||||
</div>
|
||||
<dees-speechbubble .text=${this.infoText}></dees-speechbubble>
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import './dees-settings.js';
|
||||
import type { ISettingsField, ISettingsAction } from './dees-settings.js';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const acmeFields: ISettingsField[] = [
|
||||
{ key: 'email', label: 'Account email', value: 'admin@example.com' },
|
||||
{ key: 'status', label: 'Status', value: 'enabled' },
|
||||
{ key: 'mode', label: 'Mode', value: 'production' },
|
||||
{ key: 'autoRenew', label: 'Auto-renew', value: 'on' },
|
||||
{ key: 'threshold', label: 'Renewal threshold', value: '30 days' },
|
||||
];
|
||||
|
||||
const acmeActions: ISettingsAction[] = [
|
||||
{ name: 'Edit', action: () => console.log('Edit clicked') },
|
||||
];
|
||||
|
||||
const emptyActions: ISettingsAction[] = [
|
||||
{ name: 'Configure', action: () => console.log('Configure clicked') },
|
||||
];
|
||||
|
||||
const multiActions: ISettingsAction[] = [
|
||||
{ name: 'Reset', action: () => console.log('Reset clicked') },
|
||||
{ name: 'Edit', action: () => console.log('Edit clicked') },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demoBox {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
max-width: 600px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoBox">
|
||||
<dees-settings
|
||||
.heading=${'ACME Settings'}
|
||||
.settingsFields=${acmeFields}
|
||||
.actions=${acmeActions}
|
||||
></dees-settings>
|
||||
|
||||
<dees-settings
|
||||
.heading=${'ACME Settings'}
|
||||
.description=${'No ACME configuration yet. Click Configure to set up automated TLS certificate issuance.'}
|
||||
.actions=${emptyActions}
|
||||
></dees-settings>
|
||||
|
||||
<dees-settings
|
||||
.heading=${'Server Config'}
|
||||
.settingsFields=${[
|
||||
{ key: 'host', label: 'Hostname', value: 'proxy.example.com' },
|
||||
{ key: 'port', label: 'Port', value: '443' },
|
||||
]}
|
||||
.actions=${multiActions}
|
||||
></dees-settings>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
196
ts_web/elements/00group-layout/dees-settings/dees-settings.ts
Normal file
196
ts_web/elements/00group-layout/dees-settings/dees-settings.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
property,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { demoFunc } from './dees-settings.demo.js';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import '../../00group-layout/dees-tile/dees-tile.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-settings': DeesSettings;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISettingsField {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string | TemplateResult;
|
||||
}
|
||||
|
||||
export interface ISettingsAction {
|
||||
name: string;
|
||||
action: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* dees-settings — a read-only settings display tile with modal-style footer actions.
|
||||
*
|
||||
* Renders a dees-tile with a heading, a grid of label/value fields,
|
||||
* and a footer action bar. When an action is clicked the component
|
||||
* dispatches a `settings-action` CustomEvent with the action name.
|
||||
*/
|
||||
@customElement('dees-settings')
|
||||
export class DeesSettings extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Layout'];
|
||||
|
||||
@property({ type: String })
|
||||
accessor heading: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor description: string = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor settingsFields: ISettingsField[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor actions: ISettingsAction[] = [];
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${cssGeistFontFamily};
|
||||
}
|
||||
|
||||
.settingsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px 24px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.settingsField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
font-size: 13px;
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.settingsDescription {
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
|
||||
.bottomButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton {
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 1px solid var(--dees-color-border-subtle);
|
||||
color: var(--dees-color-text-muted);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton:hover {
|
||||
background: var(--dees-color-hover);
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton:active {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(0 0% 13%)')};
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton.primary {
|
||||
color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')};
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton.primary:hover {
|
||||
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.08)', 'hsl(213.1 93.9% 67.8% / 0.08)')};
|
||||
color: ${cssManager.bdTheme('hsl(217.2 91.2% 50%)', 'hsl(213.1 93.9% 75%)')};
|
||||
}
|
||||
|
||||
.bottomButtons .bottomButton.primary:active {
|
||||
background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.15)', 'hsl(213.1 93.9% 67.8% / 0.15)')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const hasFields = this.settingsFields.length > 0;
|
||||
const hasActions = this.actions.length > 0;
|
||||
|
||||
return html`
|
||||
<dees-tile .heading=${this.heading}>
|
||||
${hasFields
|
||||
? html`
|
||||
<div class="settingsGrid">
|
||||
${this.settingsFields.map(
|
||||
(field) => html`
|
||||
<div class="settingsField">
|
||||
<span class="fieldLabel">${field.label}</span>
|
||||
<span class="fieldValue">${field.value}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="settingsDescription">${this.description}</div>
|
||||
`}
|
||||
${hasActions
|
||||
? html`
|
||||
<div slot="footer" class="bottomButtons">
|
||||
${this.actions.map(
|
||||
(actionArg, index) => html`
|
||||
<div
|
||||
class="bottomButton ${index === this.actions.length - 1 ? 'primary' : ''}"
|
||||
@click=${() => actionArg.action()}
|
||||
>
|
||||
${actionArg.name}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</dees-tile>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-layout/dees-settings/index.ts
Normal file
1
ts_web/elements/00group-layout/dees-settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-settings.js';
|
||||
@@ -1,134 +1,137 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { DeesStepper, type IStep } from './dees-stepper.js';
|
||||
|
||||
const demoSteps: IStep[] = [
|
||||
{
|
||||
title: 'Account Setup',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text key="email" label="Work Email" required></dees-input-text>
|
||||
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Profile Details',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text key="firstName" label="First Name" required></dees-input-text>
|
||||
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Contact Information',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone>
|
||||
<dees-input-text key="company" label="Company"></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Team Size',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
key="teamSize"
|
||||
label="How big is your team?"
|
||||
.options=${[
|
||||
{ label: '1-5', value: '1-5' },
|
||||
{ label: '6-20', value: '6-20' },
|
||||
{ label: '21-50', value: '21-50' },
|
||||
{ label: '51+', value: '51+' },
|
||||
]}
|
||||
required
|
||||
></dees-input-dropdown>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Goals',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-multitoggle
|
||||
key="goal"
|
||||
label="Main objective"
|
||||
.options=${[
|
||||
{ label: 'Onboarding', value: 'onboarding' },
|
||||
{ label: 'Analytics', value: 'analytics' },
|
||||
{ label: 'Automation', value: 'automation' },
|
||||
]}
|
||||
required
|
||||
></dees-input-multitoggle>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Brand Preferences',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
||||
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Integrations',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-list
|
||||
key="integrations"
|
||||
label="Integrations in use"
|
||||
placeholder="Add integration"
|
||||
></dees-input-list>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Continue', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Review & Launch',
|
||||
content: html`
|
||||
<dees-panel>
|
||||
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
||||
</dees-panel>
|
||||
`,
|
||||
menuOptions: [
|
||||
{ name: 'Launch', action: async (stepper) => stepper!.goNext() },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const cloneSteps = (): IStep[] => demoSteps.map((step) => ({ ...step }));
|
||||
|
||||
export const stepperDemo = () => html`
|
||||
<dees-stepper
|
||||
.steps=${[
|
||||
{
|
||||
title: 'Account Setup',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text key="email" label="Work Email" required></dees-input-text>
|
||||
<dees-input-text key="password" label="Create Password" type="password" required></dees-input-text>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Profile Details',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text key="firstName" label="First Name" required></dees-input-text>
|
||||
<dees-input-text key="lastName" label="Last Name" required></dees-input-text>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Contact Information',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone>
|
||||
<dees-input-text key="company" label="Company"></dees-input-text>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Team Size',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-dropdown
|
||||
key="teamSize"
|
||||
label="How big is your team?"
|
||||
.options=${[
|
||||
{ label: '1-5', value: '1-5' },
|
||||
{ label: '6-20', value: '6-20' },
|
||||
{ label: '21-50', value: '21-50' },
|
||||
{ label: '51+', value: '51+' },
|
||||
]}
|
||||
required
|
||||
></dees-input-dropdown>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Goals',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-multitoggle
|
||||
key="goal"
|
||||
label="Main objective"
|
||||
.options=${[
|
||||
{ label: 'Onboarding', value: 'onboarding' },
|
||||
{ label: 'Analytics', value: 'analytics' },
|
||||
{ label: 'Automation', value: 'automation' },
|
||||
]}
|
||||
required
|
||||
></dees-input-multitoggle>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Brand Preferences',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text key="brandColor" label="Primary brand color"></dees-input-text>
|
||||
<dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Integrations',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-list
|
||||
key="integrations"
|
||||
label="Integrations in use"
|
||||
placeholder="Add integration"
|
||||
></dees-input-list>
|
||||
<dees-form-submit>Continue</dees-form-submit>
|
||||
</dees-form>
|
||||
`,
|
||||
validationFunc: async (stepperArg, elementArg) => {
|
||||
const deesForm = elementArg.querySelector('dees-form');
|
||||
deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Review & Launch',
|
||||
content: html`
|
||||
<dees-panel>
|
||||
<p>Almost there! Review your selections and launch whenever you're ready.</p>
|
||||
</dees-panel>
|
||||
`,
|
||||
},
|
||||
] as const}
|
||||
></dees-stepper>
|
||||
<div style="position: absolute; inset: 0;">
|
||||
<div
|
||||
style="position: absolute; top: 16px; left: 50%; transform: translateX(-50%); z-index: 10;"
|
||||
>
|
||||
<dees-button
|
||||
@click=${async () => {
|
||||
await DeesStepper.createAndShow({ steps: cloneSteps() });
|
||||
}}
|
||||
>Open stepper as overlay</dees-button>
|
||||
</div>
|
||||
<dees-stepper .steps=${cloneSteps()}></dees-stepper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user