Compare commits
	
		
			122 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 70c29c778c | |||
| 0fc302699e | |||
| dcb7ca2df3 | |||
| ccbb0415e4 | |||
| 496f54cedd | |||
| 83b5ecebeb | |||
| 53b5cbed07 | |||
| 352fe79791 | |||
| a95d5a96a0 | |||
| ece7bb9a94 | |||
| d42859b7b2 | |||
| f5655ad20b | |||
| d3463f009b | |||
| bb883ce341 | |||
| d9703d3ce3 | |||
| 7b5ba74d8b | |||
| a61f57db13 | |||
| c33ad2e405 | |||
| 4190324cb4 | |||
| 1b108fcc8c | |||
| 0b2675c7e5 | |||
| 12b0aa0aad | |||
| 987ae70e7a | |||
| 3ba673282a | |||
| 20a52d1b3e | |||
| dafcf3834c | |||
| 639672358a | |||
| 671fb7dc66 | |||
| b92966ef28 | |||
| c1102634f3 | |||
| ee470775b2 | |||
| ba0f1602a1 | |||
| 682955212e | |||
| 0410f6c196 | |||
| 24aa7588c5 | |||
| b46fe8fe93 | |||
| b47c2053b5 | |||
| 16bf8001ae | |||
| 792e77f824 | |||
| 9b39196195 | |||
| ad59e3d334 | |||
| 0de4283fae | |||
| 6f9c92a866 | |||
| 0ec2f2aebb | |||
| cd22106597 | |||
| a212536cfa | |||
| 18297d54c4 | |||
| f790ca38d0 | |||
| ce2b42ecd5 | |||
| 09e299bc2e | |||
| bbc7dfe29a | |||
| 49b9e833e8 | |||
| f739bb608e | |||
| 286a6f9088 | |||
| e32b9589a5 | |||
| 6427510c98 | |||
| cf92a423cf | |||
| 3f3677ebaa | |||
| edc15a727c | |||
| 960085145d | |||
| 7fdb4f19a8 | |||
| e21fb79731 | |||
| 05f669a7bd | |||
| 8137d79e18 | |||
| 3b474b7dcc | |||
| e449b413d1 | |||
| 8918dc94bd | |||
| 2c595bf803 | |||
| 75f31a6cec | |||
| b211c0d068 | |||
| 911159ee55 | |||
| c0dbc3c0d0 | |||
| 7eea21c9d4 | |||
| 2f17dea480 | |||
| ce33aff843 | |||
| 09eea844d7 | |||
| 956edf0d63 | |||
| 1db74177b3 | |||
| 1c25554c38 | |||
| 7d1e06701b | |||
| aae4427281 | |||
| 911c51d078 | |||
| 2c12c22666 | |||
| 60a811fd18 | |||
| 9a9aea56da | |||
| 49ad998b2c | |||
| 5066681b3a | |||
| ee22879c00 | |||
| 9b0ff2d856 | |||
| 7e14645ed7 | |||
| 811737adcd | |||
| 7b6c135cd3 | |||
| 46065b2424 | |||
| e76a6c3632 | |||
| 896bc2bbb1 | |||
| 296d254ba2 | |||
| ecad05098f | |||
| 956964f5b9 | |||
| ed73e16bbb | |||
| 7817b4a440 | |||
| 03f25b7f10 | |||
| 24957f02d4 | |||
| fe3cd0591f | |||
| 56f5f5887b | |||
| 2e0bf26301 | |||
| 3d7f5253e8 | |||
| 669f12e822 | |||
| 8b870a8e46 | |||
| 9148f0595a | |||
| ea7da1c9b9 | |||
| 3e81f54e99 | |||
| 65aa9f3c06 | |||
| 82ebd9c556 | |||
| 50aa071e2e | |||
| 807e1ff733 | |||
| 4146a348ab | |||
| bd10b4e64d | |||
| 243ecddd42 | |||
| d7b690621e | |||
| 60951330d1 | |||
| 7095197d07 | |||
| 3ee48e80ad | 
							
								
								
									
										1
									
								
								.serena/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.serena/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | /cache | ||||||
							
								
								
									
										6
									
								
								.serena/memories/done_checklist.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.serena/memories/done_checklist.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | Before finishing a task: | ||||||
|  | - Run `pnpm run build` to ensure TypeScript compile + bundling succeed. | ||||||
|  | - Verify `dist_ts_web/` and `dist_bundle/bundle.js` updated. | ||||||
|  | - Optionally run `pnpm run test` and inspect failures. | ||||||
|  | - Avoid changing public APIs unless required; keep changes scoped. | ||||||
|  | - Update readme or inline docs only if user-facing behavior changes. | ||||||
							
								
								
									
										11
									
								
								.serena/memories/project_overview.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.serena/memories/project_overview.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | Project: @design.estate/dees-catalog | ||||||
|  | Purpose: A component library of dynamic Web Components (TypeScript) for building modern web apps. | ||||||
|  | Tech stack: TypeScript (ES2022, NodeNext), decorators, custom elements via @design.estate/dees-element (Lit-style), bundling with esbuild via @git.zone/tsbundle, TypeScript building via @git.zone/tsbuild (tsfolders), tests with @git.zone/tstest, various UI libs (tiptap, apexcharts, monaco-editor runtime via CDN), DOM helpers via @design.estate/dees-domtools. | ||||||
|  | Structure:  | ||||||
|  | - ts_web/: source of web components and pages | ||||||
|  | - dist_ts_web/: transpiled TS output | ||||||
|  | - dist_bundle/: production bundle (bundle.js + map) | ||||||
|  | - test/: tests | ||||||
|  | - html/: static demo assets | ||||||
|  | Key configs: tsconfig.json sets ES2022, NodeNext module/resolution, decorators enabled, skipLibCheck enabled to avoid third-party d.ts issues. | ||||||
|  | Entrypoints: ts_web/index.ts for bundling; custom elements annotated with @customElement. | ||||||
							
								
								
									
										5
									
								
								.serena/memories/style_and_conventions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.serena/memories/style_and_conventions.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | Language: TypeScript, ES2022 target, NodeNext module + resolution. | ||||||
|  | Patterns: Web Components with @customElement decorators; class-based components extending DeesElement; styles via css/cssManager; template render via html tagged literal. | ||||||
|  | Typing: Prefer explicit types where practical; tolerate `any` for external browser-injected libs (e.g., monaco) to keep build healthy. | ||||||
|  | Config: skipLibCheck enabled to avoid third-party d.ts breakages; exclude built declaration outputs. | ||||||
|  | Formatting/Linting: Not explicitly configured; follow existing style (2-space indents, single quotes often, semicolons present). | ||||||
							
								
								
									
										6
									
								
								.serena/memories/suggested_commands.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.serena/memories/suggested_commands.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | Build: pnpm run build | ||||||
|  | Watch: pnpm run watch | ||||||
|  | Test: pnpm run test | ||||||
|  | Docs: pnpm run buildDocs | ||||||
|  | Inspect bundle size: ls -lh dist_bundle/bundle.js | ||||||
|  | Open demo (if applicable): serve static `html/` with any web server | ||||||
							
								
								
									
										67
									
								
								.serena/project.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								.serena/project.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | # language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) | ||||||
|  | #  * For C, use cpp | ||||||
|  | #  * For JavaScript, use typescript | ||||||
|  | # Special requirements: | ||||||
|  | #  * csharp: Requires the presence of a .sln file in the project folder. | ||||||
|  | language: typescript | ||||||
|  |  | ||||||
|  | # whether to use the project's gitignore file to ignore files | ||||||
|  | # Added on 2025-04-07 | ||||||
|  | ignore_all_files_in_gitignore: true | ||||||
|  | # list of additional paths to ignore | ||||||
|  | # same syntax as gitignore, so you can use * and ** | ||||||
|  | # Was previously called `ignored_dirs`, please update your config if you are using that. | ||||||
|  | # Added (renamed) on 2025-04-07 | ||||||
|  | ignored_paths: [] | ||||||
|  |  | ||||||
|  | # whether the project is in read-only mode | ||||||
|  | # If set to true, all editing tools will be disabled and attempts to use them will result in an error | ||||||
|  | # Added on 2025-04-18 | ||||||
|  | read_only: false | ||||||
|  |  | ||||||
|  | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. | ||||||
|  | # Below is the complete list of tools for convenience. | ||||||
|  | # To make sure you have the latest list of tools, and to view their descriptions,  | ||||||
|  | # execute `uv run scripts/print_tool_overview.py`. | ||||||
|  | # | ||||||
|  | #  * `activate_project`: Activates a project by name. | ||||||
|  | #  * `check_onboarding_performed`: Checks whether project onboarding was already performed. | ||||||
|  | #  * `create_text_file`: Creates/overwrites a file in the project directory. | ||||||
|  | #  * `delete_lines`: Deletes a range of lines within a file. | ||||||
|  | #  * `delete_memory`: Deletes a memory from Serena's project-specific memory store. | ||||||
|  | #  * `execute_shell_command`: Executes a shell command. | ||||||
|  | #  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. | ||||||
|  | #  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). | ||||||
|  | #  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). | ||||||
|  | #  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. | ||||||
|  | #  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. | ||||||
|  | #  * `initial_instructions`: Gets the initial instructions for the current project. | ||||||
|  | #     Should only be used in settings where the system prompt cannot be set, | ||||||
|  | #     e.g. in clients you have no control over, like Claude Desktop. | ||||||
|  | #  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. | ||||||
|  | #  * `insert_at_line`: Inserts content at a given line in a file. | ||||||
|  | #  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. | ||||||
|  | #  * `list_dir`: Lists files and directories in the given directory (optionally with recursion). | ||||||
|  | #  * `list_memories`: Lists memories in Serena's project-specific memory store. | ||||||
|  | #  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). | ||||||
|  | #  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). | ||||||
|  | #  * `read_file`: Reads a file within the project directory. | ||||||
|  | #  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. | ||||||
|  | #  * `remove_project`: Removes a project from the Serena configuration. | ||||||
|  | #  * `replace_lines`: Replaces a range of lines within a file with new content. | ||||||
|  | #  * `replace_symbol_body`: Replaces the full definition of a symbol. | ||||||
|  | #  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. | ||||||
|  | #  * `search_for_pattern`: Performs a search for a pattern in the project. | ||||||
|  | #  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. | ||||||
|  | #  * `switch_modes`: Activates modes by providing a list of their names | ||||||
|  | #  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. | ||||||
|  | #  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. | ||||||
|  | #  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. | ||||||
|  | #  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. | ||||||
|  | excluded_tools: [] | ||||||
|  |  | ||||||
|  | # initial prompt for the project. It will always be given to the LLM upon activating the project | ||||||
|  | # (contrary to the memories, which are loaded on demand). | ||||||
|  | initial_prompt: "" | ||||||
|  |  | ||||||
|  | project_name: "dees-catalog" | ||||||
							
								
								
									
										205
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										205
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,208 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-10-23 - 1.12.6 - fix(dependencies) | ||||||
|  | Bump FontAwesome to ^7.1.0 and add local claude settings | ||||||
|  |  | ||||||
|  | - Updated @fortawesome packages (@fortawesome/fontawesome-svg-core, @fortawesome/free-brands-svg-icons, @fortawesome/free-regular-svg-icons, @fortawesome/free-solid-svg-icons) to ^7.1.0 in package.json | ||||||
|  | - Added .claude/settings.local.json to configure local Claude/tooling permissions for repository operations | ||||||
|  |  | ||||||
|  | ## 2025-09-23 - 1.12.5 - fix(ci) | ||||||
|  | Add local permissions settings for development | ||||||
|  |  | ||||||
|  | - Adds a new local settings file: .claude/settings.local.json | ||||||
|  | - Provides explicit permission entries for development tasks (allow running pnpm scripts, reading files, searching/replacing patterns, activating project, and helper tooling) | ||||||
|  | - Intended for local dev environment to enable tool automation without changing repository code | ||||||
|  |  | ||||||
|  | ## 2025-09-20 - 1.12.4 - fix(ci) | ||||||
|  | Add local assistant settings to enable permitted dev tooling commands | ||||||
|  |  | ||||||
|  | - Add a local assistant settings file to configure allowed development tooling commands. | ||||||
|  | - Allows running pnpm scripts, file read/search/replace operations and other local project helper actions. | ||||||
|  | - Local configuration only — does not change library code or public API. | ||||||
|  |  | ||||||
|  | ## 2025-09-19 - 1.12.3 - fix(dees-input-fileupload) | ||||||
|  | Show selected files inside dropzone and improve file upload UX | ||||||
|  |  | ||||||
|  | - Render the selected file list inside the dropzone container so files are displayed inline with the drop area | ||||||
|  | - Add dropzone--has-files class and styles to visually indicate when files are present | ||||||
|  | - Avoid opening the file selector when clicking on the browse button or inside the file list (prevents accidental re-opening) | ||||||
|  | - Refine file list and file-row styles (sizes, paddings, border radius, hover/background behavior and thumbnail/icon sizes) for a more compact and consistent appearance | ||||||
|  | - Simplify empty-state handling by returning an empty template when no files are present (file list is only rendered when files exist) | ||||||
|  |  | ||||||
|  | ## 2025-09-18 - 1.12.2 - fix(dees-input-wysiwyg) | ||||||
|  | Integrate output format preview into WYSIWYG demo; update plan and add local dev settings | ||||||
|  |  | ||||||
|  | - Wire output format preview into the WYSIWYG demo (ts_web/elements/dees-input-wysiwyg.demo.ts) by calling setupOutputFormatDemo(editors.meeting, editors.recipe) so HTML/Markdown preview controls are initialized. | ||||||
|  | - Update readme.plan.md: mark the Output Formats review tasks as completed and document that preview controls were added. | ||||||
|  | - Add a local settings file to allow running local tooling tasks (grants permission for pnpm run scripts and related local commands). | ||||||
|  | - No library API or runtime component behavior changed — this is a demo/documentation and local-settings update. | ||||||
|  |  | ||||||
|  | ## 2025-09-18 - 1.12.1 - fix(ci) | ||||||
|  | Add local settings to allow running pnpm scripts and enable dev chat permission | ||||||
|  |  | ||||||
|  | - Add a repository-local settings file granting permission to run pnpm scripts (Bash(pnpm run:*)) for development tooling. | ||||||
|  | - Enable the mcp__zen__chat permission for local dev workflows. | ||||||
|  |  | ||||||
|  | ## 2025-09-18 - 1.12.0 - feat(dees-stepper) | ||||||
|  | Revamp dees-stepper: modern styling, new steps and improved navigation/validation | ||||||
|  |  | ||||||
|  | - Visual refresh for dees-stepper: updated card shapes, shadows, refined borders and stronger selected-state visuals for a modern shadcn-inspired look | ||||||
|  | - Improved transitions and animations (transform, box-shadow, filter) for smoother step selection and show/hide behavior | ||||||
|  | - Expanded default/demo steps: replaced small sample with a richer multi-step flow (Account Setup, Profile Details, Contact Information, Team Size, Goals, Brand Preferences, Integrations, Review & Launch) | ||||||
|  | - Enhanced step interactions: safer goNext/goBack handling with boundary checks and reset of validation flags to avoid stale validation state | ||||||
|  | - Better toolbar/controls placement for stepper demo (spacing, counters, accessible back control) and improved keyboard/UX affordances | ||||||
|  | - Minor documentation and meta updates: readme.plan.md extended with dees-stepper plan items and added .claude/settings.local.json | ||||||
|  |  | ||||||
|  | ## 2025-09-18 - 1.11.8 - fix(ci) | ||||||
|  | Add local tool permissions config to allow running pnpm scripts and enable mcp__zen__chat | ||||||
|  |  | ||||||
|  | - Add local settings file to grant permission to run pnpm scripts (Bash(pnpm run:*)) | ||||||
|  | - Enable mcp__zen__chat permission in local tool settings | ||||||
|  |  | ||||||
|  | ## 2025-09-16 - 1.11.7 - fix(readme) | ||||||
|  | Expand README with comprehensive component documentation, examples and developer guide; add local Claude settings | ||||||
|  |  | ||||||
|  | - Expanded README substantially: installation, component overview, detailed component docs, usage examples, demos and developer guidance | ||||||
|  | - Updated many example snippets and API usage examples (icons, inputs, editor, forms, overlays, charts, etc.) to be more explicit and consistent | ||||||
|  | - Added .claude/settings.local.json to configure local Claude permissions for repository tooling | ||||||
|  | - No runtime or library code changes — documentation and demo content only | ||||||
|  |  | ||||||
|  | ## 2025-09-16 - 1.11.6 - fix(dees-table) | ||||||
|  | Improve Lucene range comparisons, pin monaco-editor to 0.52.2, and add local dev metadata | ||||||
|  |  | ||||||
|  | - Fix lucene inRange behavior to correctly compare homogeneous types (strings, numbers, dates) and fall back to string comparison when needed (ts_web/elements/dees-table/lucene.ts). | ||||||
|  | - Pin monaco-editor to 0.52.2 in package.json to avoid a breaking upgrade regression observed with ^0.53.0. | ||||||
|  | - Add local development/tooling metadata and conveniences: .claude/settings.local.json (tool permissions) and .serena/ memory files (done_checklist, project_overview, style_and_conventions, suggested_commands). | ||||||
|  | - Minor housekeeping: update project dev docs / memories to capture build/test/checklist guidance. | ||||||
|  |  | ||||||
|  | ## 2025-09-16 - 1.11.5 - fix(ci) | ||||||
|  | Add local Claude agent settings for CI tooling | ||||||
|  |  | ||||||
|  | - Add .claude/settings.local.json to configure local Claude agent permissions | ||||||
|  | - Allow Bash commands matching pnpm run:* and the mcp__zen__chat permission for development tooling | ||||||
|  |  | ||||||
|  | ## 2025-09-10 - 1.11.4 - fix(readme) | ||||||
|  | Rewrite and expand README with Quick Start, feature highlights, demos and usage examples; add local Claude settings file | ||||||
|  |  | ||||||
|  | - Completely rewritten and reorganized README: added Quick Start, component highlights, usage examples, demos, development workflow, troubleshooting and links. | ||||||
|  | - Added .claude/settings.local.json with local Claude permission configuration. | ||||||
|  |  | ||||||
|  | ## 2025-09-08 - 1.11.3 - fix(dees-input-list) | ||||||
|  | Prevent list animations from affecting scroll bounds and fix content-visibility issues in dees-input-list; add local developer settings | ||||||
|  |  | ||||||
|  | - dees-input-list: add overflow:hidden to list items to prevent animations from altering scroll bounds and causing visual/scroll glitches | ||||||
|  | - dees-input-list: force content-visibility/contain to visible/none to avoid unexpected scrolling/layout issues when items animate | ||||||
|  | - Add .claude/settings.local.json with local developer permissions (allows running pnpm scripts via Claude-local tooling) | ||||||
|  |  | ||||||
|  | ## 2025-09-07 - 1.11.2 - fix(DeesFormSubmit) | ||||||
|  | Make form submit robust by locating nearest dees-form via closest(); add local CLAUDE settings | ||||||
|  |  | ||||||
|  | - Fix: DeesFormSubmit.submit now walks up the DOM with closest('dees-form') to find and call gatherAndDispatch on the parent form. This fixes cases where the submit button is slotted or not a direct child of the form. | ||||||
|  | - Chore: Add .claude/settings.local.json to permit running pnpm scripts in the local CLAUDE environment (allows Bash(pnpm run:*)). | ||||||
|  |  | ||||||
|  | ## 2025-09-06 - 1.11.1 - fix(dees-input-text) | ||||||
|  | Normalize Lucide icon names for password toggle | ||||||
|  |  | ||||||
|  | - Updated password visibility toggle icons in dees-input-text from 'lucide:eye'/'lucide:eye-off' to 'lucide:Eye'/'lucide:EyeOff' to match Lucide exports and avoid missing icon rendering. | ||||||
|  |  | ||||||
|  | ## 2025-09-05 - 1.11.0 - feat(dees-icon) | ||||||
|  | Add full icon list and improve dees-icon demo with copy-all functionality and UI tweaks | ||||||
|  |  | ||||||
|  | - Added readme.icons.md containing 1900+ icon identifiers (FontAwesome + Lucide) for easy reference and tooling | ||||||
|  | - Enhanced ts_web/elements/dees-icon.demo.ts: added a 'Copy All Icon Names' button that copies prefixed icon names (fa:..., lucide:...) to the clipboard and shows temporary feedback | ||||||
|  | - Updated demo presentation: prefixed displayed icon names (fa: / lucide:), improved search-container spacing and added button styling for better UX | ||||||
|  | - Changes are documentation/demo only — no production runtime component logic changed | ||||||
|  |  | ||||||
|  | ## 2025-09-05 - 1.10.12 - fix(dees-simple-appdash) | ||||||
|  | Fix icon rendering in dees-simple-appdash to respect provided icon strings | ||||||
|  |  | ||||||
|  | - dees-simple-appdash: stop forcing a 'lucide:' prefix when rendering view icons — use the icon string as provided. | ||||||
|  | - Prevents incorrect/missing icons when the iconName already includes a library prefix (e.g. 'fa:' or 'lucide:'). | ||||||
|  |  | ||||||
|  | ## 2025-09-05 - 1.10.11 - fix(dees-simple-appdash) | ||||||
|  | Bump deps and fix dees-simple-appdash icon binding and terminal sizing | ||||||
|  |  | ||||||
|  | - Updated runtime dependencies: @design.estate/dees-element -> ^2.1.2, @design.estate/dees-wcctools -> ^1.1.1, @fortawesome/* -> ^7.0.1, apexcharts -> ^5.3.4, lucide -> ^0.542.0 (compatibility/security/stability updates) | ||||||
|  | - Updated dev tooling: @git.zone/tsbuild -> ^2.6.8, @git.zone/tstest -> ^2.3.6, @git.zone/tswatch -> ^2.2.1 | ||||||
|  | - Fix: dees-simple-appdash — use proper string interpolation for lucide icon properties (prevents incorrect icon rendering) | ||||||
|  | - Fix: dees-simple-appdash — enforce terminal maxWidth/maxHeight to avoid overflow and improve layout stability | ||||||
|  | - Cosmetic: small style/behavior tweaks to dees-simple-appdash (logout/terminal/wifi icon bindings corrected) | ||||||
|  |  | ||||||
|  | ## 2025-06-29 - 1.10.10 - improve(dees-dashboardgrid, dees-input-wysiwyg) | ||||||
|  | Enhanced dashboard grid component with advanced spacing and layout features inspired by gridstack.js | ||||||
|  |  | ||||||
|  | Dashboard Grid improvements: | ||||||
|  | - Improved margin system supporting uniform or individual margins (top, right, bottom, left) | ||||||
|  | - Added collision detection to prevent widget overlap during drag operations | ||||||
|  | - Implemented auto-positioning for new widgets to find first available space | ||||||
|  | - Added compact() method to eliminate gaps and compress layout vertically or horizontally | ||||||
|  | - Enhanced resize constraints with minW, maxW, minH, maxH support | ||||||
|  | - Added optional grid lines visualization for better layout understanding | ||||||
|  | - Improved resize handles with better visibility and hover states | ||||||
|  | - Added RTL (right-to-left) layout support | ||||||
|  | - Implemented cellHeightUnit option supporting 'px', 'em', 'rem', or 'auto' (square cells) | ||||||
|  | - Added configurable animation with enableAnimation property | ||||||
|  | - Enhanced demo with interactive controls for testing all features | ||||||
|  | - Better calculation of widget positions accounting for margins between cells | ||||||
|  | - Added findAvailablePosition() for intelligent widget placement | ||||||
|  | - Improved drag and resize calculations for pixel-perfect positioning | ||||||
|  |  | ||||||
|  | WYSIWYG editor drag and drop fixes: | ||||||
|  | - Fixed drop indicator positioning to properly account for block margins | ||||||
|  | - Added defensive checks in drag event handlers to prevent potential crashes | ||||||
|  | - Improved updateBlockPositions with null checks and error handling | ||||||
|  | - Updated drop indicator calculation to use simplified margin approach | ||||||
|  | - Fixed drop indicator height to match the exact space occupied by dragged blocks | ||||||
|  | - Improved drop indicator positioning algorithm to accurately show where blocks will land | ||||||
|  | - Simplified visual block position calculations accounting for CSS transforms | ||||||
|  | - Enhanced margin calculation to use correct values based on block type (16px for paragraphs, 24px for headings, 20px for code/quotes) | ||||||
|  | - Fixed index calculation issue when dragging blocks downward by adjusting target index for excluded dragged block | ||||||
|  |  | ||||||
|  | ## 2025-06-28 - 1.10.9 - feat(dees-dashboardgrid) | ||||||
|  | Add new dashboard grid component with drag-and-drop and resize capabilities | ||||||
|  |  | ||||||
|  | - Created dees-dashboardgrid component for building flexible dashboard layouts | ||||||
|  | - Features drag-and-drop functionality for rearranging widgets | ||||||
|  | - Includes resize handles for adjusting widget dimensions | ||||||
|  | - Supports configurable grid properties (columns, cell height, gap) | ||||||
|  | - Provides widget locking and editable mode controls | ||||||
|  | - Styled with shadcn design principles | ||||||
|  | - No external dependencies - built with native browser APIs | ||||||
|  | - Emits events for widget movements and resizes | ||||||
|  | - Includes comprehensive demo with sample dashboard widgets | ||||||
|  |  | ||||||
|  | ## 2025-06-27 - 1.10.8 - feat(ui-components) | ||||||
|  | Update multiple components with shadcn-aligned styling and improved animations | ||||||
|  |  | ||||||
|  | - Updated dees-modal with shadcn colors, borders, and subtle shadows | ||||||
|  | - Updated dees-chips with shadcn styling and fixed selection logic bug | ||||||
|  | - Updated dees-dataview-codebox with shadcn syntax highlighting colors and responsive label layout | ||||||
|  | - Updated dees-input-multitoggle with transparent blue indicator and smooth animations | ||||||
|  | - Updated dees-appui-tabs with animated sliding indicator for both horizontal and vertical layouts | ||||||
|  | - Fixed indicator positioning to be perfectly centered on tab content | ||||||
|  | - Indicator width is content width + 8px for minimal visual padding | ||||||
|  | - Fixed tab content centering by using consistent padding (12px → 16px on all sides) | ||||||
|  | - Fixed icon rendering by correcting property name from .iconName to .icon | ||||||
|  | - Added visual separators between tabs for better distinction | ||||||
|  | - Added subtle hover backgrounds for improved interactivity | ||||||
|  | - Refactored tabs component code for better maintainability and elegance | ||||||
|  | - Updated dees-appui-activitylog with shadcn-aligned styling: | ||||||
|  |   - Updated background and text colors to match shadcn palette | ||||||
|  |   - Enhanced topbar with better spacing and typography | ||||||
|  |   - Improved activity entries with subtle hover states and better spacing | ||||||
|  |   - Added activity type icons with color-coded backgrounds (login, logout, view, create, update) | ||||||
|  |   - Added date separators ("Today", "Yesterday") for better temporal organization | ||||||
|  |   - Enhanced streaming indicators with animated pulse effect | ||||||
|  |   - Redesigned searchbox with modern input styling, search icon, and focus states | ||||||
|  |   - Added custom scrollbar styling for consistency | ||||||
|  |   - Updated timestamps to be more subtle with tabular number formatting | ||||||
|  |   - Refined shadow effects for better visual hierarchy | ||||||
|  |   - Added subtle box shadow to component for depth | ||||||
|  |   - Added fade-in animation for new activity entries | ||||||
|  |   - Improved user name highlighting with better typography | ||||||
|  |   - Updated context menu with more relevant actions | ||||||
|  | - Improved overall spacing and visual consistency across components | ||||||
|  |  | ||||||
| ## 2025-06-27 - 1.10.1 - fix(modal) | ## 2025-06-27 - 1.10.1 - fix(modal) | ||||||
| Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container | Improve modal overscroll behavior by adding 'overscroll-behavior: contain' to content container | ||||||
|  |  | ||||||
| @@ -71,7 +274,7 @@ Add dees-searchbar component with live search and filter demo | |||||||
| ## 2025-04-22 - 1.6.0 - feat(documentation/dees-heading) | ## 2025-04-22 - 1.6.0 - feat(documentation/dees-heading) | ||||||
| Add codex documentation overview and dees-heading component demo | Add codex documentation overview and dees-heading component demo | ||||||
|  |  | ||||||
| - Introduce 'codex.md' to provide a high-level overview of project layout, component patterns, and build workflow | - Introduce contributor overview doc (`codex.md`, now consolidated into `readme.info.md`) to provide a high-level overview of project layout, component patterns, and build workflow | ||||||
| - Add and update dees-heading component with demo to support multiple heading levels and horizontal rule styles | - Add and update dees-heading component with demo to support multiple heading levels and horizontal rule styles | ||||||
| - Update component export index to include dees-heading | - Update component export index to include dees-heading | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								codex.md
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								codex.md
									
									
									
									
									
								
							| @@ -1,43 +0,0 @@ | |||||||
| # Codex: Project Overview and Codebase Structure |  | ||||||
|  |  | ||||||
| ## Project Overview |  | ||||||
| - Package: `@design.estate/dees-catalog` |  | ||||||
| - Focus: Web Components library providing UI elements and layouts for modern web apps. |  | ||||||
|  |  | ||||||
| ## Directory Layout |  | ||||||
| - ts_web/: TypeScript source files |  | ||||||
|   - elements/: Individual Web Component definitions |  | ||||||
|   - pages/: Page-level templates for composite layouts |  | ||||||
| - html/: Demo/app entry point loading the bundled scripts |  | ||||||
| - dist_bundle/: Bundled browser JS and source maps |  | ||||||
| - dist_ts_web/: ES module outputs for TypeScript/web consumers |  | ||||||
| - dist_watch/: Watch-mode development bundle with live reload |  | ||||||
| - test/: Browser-based tests using `@push.rocks/tapbundle` |  | ||||||
|  |  | ||||||
| ## Component Patterns |  | ||||||
| - Each component in ts_web/elements/: |  | ||||||
|   - Decorated with `@customElement('tag-name')` |  | ||||||
|   - Extends `DeesElement` from `@design.estate/dees-element` |  | ||||||
|   - Uses `@property` for reactive, reflected attributes |  | ||||||
|   - Defines `static styles = [cssManager.defaultStyles, css`...`]` |  | ||||||
|   - Implements `render()` returning a Lit `html` template with slots or markup |  | ||||||
|   - Exposes a demo via `public static demo` linking to `.demo.ts` files |  | ||||||
|  |  | ||||||
| ## Build & Development Workflow |  | ||||||
| - Install dependencies: `npm install` or `pnpm install` |  | ||||||
| - Build production bundle: `npm run build` |  | ||||||
| - Start dev watch mode: `npm run watch` |  | ||||||
| - Run tests: `npm test` (launches browser fixtures) |  | ||||||
|  |  | ||||||
| ## Theming & Utilities |  | ||||||
| - Default global styles via `cssManager.defaultStyles` |  | ||||||
| - Theme-aware values with `cssManager.bdTheme(light, dark)` |  | ||||||
| - DOM utilities set up in `html/index.ts` using `@design.estate/dees-domtools` |  | ||||||
|  |  | ||||||
| ## Documentation |  | ||||||
| - `readme.md` provides an overview of all components and basic usage |  | ||||||
| - Live examples in `.demo.ts` files |  | ||||||
|   accessible via component `demo` static property |  | ||||||
|  |  | ||||||
| ## Updates to this file |  | ||||||
| If you have pattern insisights or general changes to the codebase, please update this file. |  | ||||||
							
								
								
									
										34
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,30 +1,31 @@ | |||||||
| { | { | ||||||
|   "name": "@design.estate/dees-catalog", |   "name": "@design.estate/dees-catalog", | ||||||
|   "version": "1.10.1", |   "version": "1.12.6", | ||||||
|   "private": false, |   "private": false, | ||||||
|   "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", |   "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", | ||||||
|   "main": "dist_ts_web/index.js", |   "main": "dist_ts_web/index.js", | ||||||
|   "typings": "dist_ts_web/index.d.ts", |   "typings": "dist_ts_web/index.d.ts", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "test": "tstest test/ --web --verbose --timeout 30", |     "test": "tstest test/ --web --verbose --timeout 30 --logfile", | ||||||
|     "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild", |     "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild", | ||||||
|     "watch": "tswatch element", |     "watch": "tswatch element", | ||||||
|     "buildDocs": "tsdoc" |     "buildDocs": "tsdoc", | ||||||
|  |     "postinstall": "node scripts/update-monaco-version.cjs" | ||||||
|   }, |   }, | ||||||
|   "author": "Lossless GmbH", |   "author": "Lossless GmbH", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@design.estate/dees-domtools": "^2.3.3", |     "@design.estate/dees-domtools": "^2.3.3", | ||||||
|     "@design.estate/dees-element": "^2.0.45", |     "@design.estate/dees-element": "^2.1.2", | ||||||
|     "@design.estate/dees-wcctools": "^1.0.101", |     "@design.estate/dees-wcctools": "^1.2.0", | ||||||
|     "@fortawesome/fontawesome-svg-core": "^6.7.2", |     "@fortawesome/fontawesome-svg-core": "^7.1.0", | ||||||
|     "@fortawesome/free-brands-svg-icons": "^6.7.2", |     "@fortawesome/free-brands-svg-icons": "^7.1.0", | ||||||
|     "@fortawesome/free-regular-svg-icons": "^6.7.2", |     "@fortawesome/free-regular-svg-icons": "^7.1.0", | ||||||
|     "@fortawesome/free-solid-svg-icons": "^6.7.2", |     "@fortawesome/free-solid-svg-icons": "^7.1.0", | ||||||
|     "@push.rocks/smarti18n": "^1.0.4", |     "@push.rocks/smarti18n": "^1.0.4", | ||||||
|     "@push.rocks/smartpromise": "^4.2.0", |     "@push.rocks/smartpromise": "^4.2.0", | ||||||
|     "@push.rocks/smartstring": "^4.0.15", |     "@push.rocks/smartstring": "^4.1.0", | ||||||
|     "@tiptap/core": "^2.23.0", |     "@tiptap/core": "^2.23.0", | ||||||
|     "@tiptap/extension-link": "^2.23.0", |     "@tiptap/extension-link": "^2.23.0", | ||||||
|     "@tiptap/extension-text-align": "^2.23.0", |     "@tiptap/extension-text-align": "^2.23.0", | ||||||
| @@ -33,20 +34,21 @@ | |||||||
|     "@tiptap/starter-kit": "^2.23.0", |     "@tiptap/starter-kit": "^2.23.0", | ||||||
|     "@tsclass/tsclass": "^9.2.0", |     "@tsclass/tsclass": "^9.2.0", | ||||||
|     "@webcontainer/api": "1.2.0", |     "@webcontainer/api": "1.2.0", | ||||||
|     "apexcharts": "^4.7.0", |     "apexcharts": "^5.3.5", | ||||||
|     "highlight.js": "11.11.1", |     "highlight.js": "11.11.1", | ||||||
|     "ibantools": "^4.5.1", |     "ibantools": "^4.5.1", | ||||||
|     "lucide": "^0.523.0", |     "lit": "^3.3.1", | ||||||
|     "monaco-editor": "^0.52.2", |     "lucide": "^0.544.0", | ||||||
|  |     "monaco-editor": "0.52.2", | ||||||
|     "pdfjs-dist": "^4.10.38", |     "pdfjs-dist": "^4.10.38", | ||||||
|     "xterm": "^5.3.0", |     "xterm": "^5.3.0", | ||||||
|     "xterm-addon-fit": "^0.8.0" |     "xterm-addon-fit": "^0.8.0" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@git.zone/tsbuild": "^2.6.4", |     "@git.zone/tsbuild": "^2.6.8", | ||||||
|     "@git.zone/tsbundle": "^2.5.1", |     "@git.zone/tsbundle": "^2.5.1", | ||||||
|     "@git.zone/tstest": "^2.3.1", |     "@git.zone/tstest": "^2.3.8", | ||||||
|     "@git.zone/tswatch": "^2.1.2", |     "@git.zone/tswatch": "^2.2.1", | ||||||
|     "@push.rocks/projectinfo": "^5.0.2", |     "@push.rocks/projectinfo": "^5.0.2", | ||||||
|     "@push.rocks/tapbundle": "^6.0.3", |     "@push.rocks/tapbundle": "^6.0.3", | ||||||
|     "@types/node": "^22.0.0" |     "@types/node": "^22.0.0" | ||||||
|   | |||||||
							
								
								
									
										3345
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3345
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										4
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | onlyBuiltDependencies: | ||||||
|  |   - esbuild | ||||||
|  |   - mongodb-memory-server | ||||||
|  |   - puppeteer | ||||||
| @@ -1,513 +0,0 @@ | |||||||
| # Building Applications with dees-appui Architecture |  | ||||||
|  |  | ||||||
| ## Overview |  | ||||||
|  |  | ||||||
| The dees-appui system provides a comprehensive framework for building desktop-style web applications with a consistent layout, navigation, and view management system. This document outlines the architecture and best practices for building applications using these components. |  | ||||||
|  |  | ||||||
| ## Core Architecture |  | ||||||
|  |  | ||||||
| ### Component Hierarchy |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| dees-appui-base |  | ||||||
| ├── dees-appui-appbar (top menu bar) |  | ||||||
| ├── dees-appui-mainmenu (left sidebar - primary navigation) |  | ||||||
| ├── dees-appui-mainselector (second sidebar - contextual navigation) |  | ||||||
| ├── dees-appui-maincontent (main content area) |  | ||||||
| │   └── dees-appui-view (view container) |  | ||||||
| │       └── dees-appui-tabs (tab navigation within views) |  | ||||||
| └── dees-appui-activitylog (right sidebar - optional) |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### View-Based Architecture |  | ||||||
|  |  | ||||||
| The system is built around the concept of **Views** - self-contained modules that represent different sections of your application. Each view can have: |  | ||||||
|  |  | ||||||
| - Its own tabs for sub-navigation |  | ||||||
| - Menu items for the selector (contextual navigation) |  | ||||||
| - Content areas with dynamic loading |  | ||||||
| - State management |  | ||||||
| - Event handling |  | ||||||
|  |  | ||||||
| ## Implementation Plan |  | ||||||
|  |  | ||||||
| ### Phase 1: Application Shell Setup |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // app-shell.ts |  | ||||||
| import { LitElement, html, css } from 'lit'; |  | ||||||
| import { customElement, property } from 'lit/decorators.js'; |  | ||||||
| import type { IAppView } from '@design.estate/dees-catalog'; |  | ||||||
|  |  | ||||||
| @customElement('my-app-shell') |  | ||||||
| export class MyAppShell extends LitElement { |  | ||||||
|   @property({ type: Array }) |  | ||||||
|   views: IAppView[] = []; |  | ||||||
|  |  | ||||||
|   @property({ type: String }) |  | ||||||
|   activeViewId: string = ''; |  | ||||||
|  |  | ||||||
|   render() { |  | ||||||
|     const activeView = this.views.find(v => v.id === this.activeViewId); |  | ||||||
|      |  | ||||||
|     return html` |  | ||||||
|       <dees-appui-base |  | ||||||
|         .appbarMenuItems=${this.getAppBarMenuItems()} |  | ||||||
|         .appbarBreadcrumbs=${this.getBreadcrumbs()} |  | ||||||
|         .appbarTheme=${'dark'} |  | ||||||
|         .appbarUser=${{ name: 'User', status: 'online' }} |  | ||||||
|         .mainmenuTabs=${this.getMainMenuTabs()} |  | ||||||
|         .mainselectorOptions=${activeView?.menuItems || []} |  | ||||||
|         @mainmenu-tab-select=${this.handleMainMenuSelect} |  | ||||||
|         @mainselector-option-select=${this.handleSelectorSelect} |  | ||||||
|       > |  | ||||||
|         <dees-appui-view |  | ||||||
|           slot="maincontent" |  | ||||||
|           .viewConfig=${activeView} |  | ||||||
|           @view-tab-select=${this.handleViewTabSelect} |  | ||||||
|         ></dees-appui-view> |  | ||||||
|       </dees-appui-base> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Phase 2: View Definition |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // views/dashboard-view.ts |  | ||||||
| export const dashboardView: IAppView = { |  | ||||||
|   id: 'dashboard', |  | ||||||
|   name: 'Dashboard', |  | ||||||
|   description: 'System overview and metrics', |  | ||||||
|   iconName: 'home', |  | ||||||
|   tabs: [ |  | ||||||
|     { |  | ||||||
|       key: 'overview', |  | ||||||
|       iconName: 'chart-line', |  | ||||||
|       action: () => console.log('Overview selected'), |  | ||||||
|       content: () => html` |  | ||||||
|         <dashboard-overview></dashboard-overview> |  | ||||||
|       ` |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       key: 'metrics', |  | ||||||
|       iconName: 'tachometer-alt', |  | ||||||
|       action: () => console.log('Metrics selected'), |  | ||||||
|       content: () => html` |  | ||||||
|         <dashboard-metrics></dashboard-metrics> |  | ||||||
|       ` |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       key: 'alerts', |  | ||||||
|       iconName: 'bell', |  | ||||||
|       action: () => console.log('Alerts selected'), |  | ||||||
|       content: () => html` |  | ||||||
|         <dashboard-alerts></dashboard-alerts> |  | ||||||
|       ` |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   menuItems: [ |  | ||||||
|     { key: 'Time Range', action: () => showTimeRangeSelector() }, |  | ||||||
|     { key: 'Refresh Rate', action: () => showRefreshSettings() }, |  | ||||||
|     { key: 'Export Data', action: () => exportDashboardData() } |  | ||||||
|   ] |  | ||||||
| }; |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Phase 3: View Management System |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // services/view-manager.ts |  | ||||||
| export class ViewManager { |  | ||||||
|   private views: Map<string, IAppView> = new Map(); |  | ||||||
|   private activeView: IAppView | null = null; |  | ||||||
|   private viewCache: Map<string, any> = new Map(); |  | ||||||
|  |  | ||||||
|   registerView(view: IAppView) { |  | ||||||
|     this.views.set(view.id, view); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async activateView(viewId: string) { |  | ||||||
|     const view = this.views.get(viewId); |  | ||||||
|     if (!view) throw new Error(`View ${viewId} not found`); |  | ||||||
|  |  | ||||||
|     // Deactivate current view |  | ||||||
|     if (this.activeView) { |  | ||||||
|       await this.deactivateView(this.activeView.id); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Activate new view |  | ||||||
|     this.activeView = view; |  | ||||||
|      |  | ||||||
|     // Update navigation |  | ||||||
|     this.updateMainSelector(view.menuItems); |  | ||||||
|     this.updateBreadcrumbs(view); |  | ||||||
|      |  | ||||||
|     // Load view data if needed |  | ||||||
|     if (!this.viewCache.has(viewId)) { |  | ||||||
|       await this.loadViewData(view); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return view; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async loadViewData(view: IAppView) { |  | ||||||
|     // Implement lazy loading of view data |  | ||||||
|     const viewData = await import(`./views/${view.id}/data.js`); |  | ||||||
|     this.viewCache.set(view.id, viewData); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Phase 4: Navigation Integration |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // navigation/app-navigation.ts |  | ||||||
| export class AppNavigation { |  | ||||||
|   constructor( |  | ||||||
|     private viewManager: ViewManager, |  | ||||||
|     private appShell: MyAppShell |  | ||||||
|   ) {} |  | ||||||
|  |  | ||||||
|   setupMainMenu(): ITab[] { |  | ||||||
|     return [ |  | ||||||
|       { |  | ||||||
|         key: 'dashboard', |  | ||||||
|         iconName: 'home', |  | ||||||
|         action: () => this.navigateToView('dashboard') |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         key: 'projects', |  | ||||||
|         iconName: 'folder', |  | ||||||
|         action: () => this.navigateToView('projects') |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         key: 'analytics', |  | ||||||
|         iconName: 'chart-bar', |  | ||||||
|         action: () => this.navigateToView('analytics') |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         key: 'settings', |  | ||||||
|         iconName: 'cog', |  | ||||||
|         action: () => this.navigateToView('settings') |  | ||||||
|       } |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async navigateToView(viewId: string) { |  | ||||||
|     const view = await this.viewManager.activateView(viewId); |  | ||||||
|     this.appShell.activeViewId = viewId; |  | ||||||
|      |  | ||||||
|     // Update URL |  | ||||||
|     window.history.pushState( |  | ||||||
|       { viewId },  |  | ||||||
|       view.name,  |  | ||||||
|       `/${viewId}` |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   handleBrowserNavigation() { |  | ||||||
|     window.addEventListener('popstate', (event) => { |  | ||||||
|       if (event.state?.viewId) { |  | ||||||
|         this.navigateToView(event.state.viewId); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Phase 5: Dynamic View Loading |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // views/view-loader.ts |  | ||||||
| export class ViewLoader { |  | ||||||
|   private loadedViews: Set<string> = new Set(); |  | ||||||
|  |  | ||||||
|   async loadView(viewId: string): Promise<IAppView> { |  | ||||||
|     if (this.loadedViews.has(viewId)) { |  | ||||||
|       return this.getViewConfig(viewId); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Dynamic import |  | ||||||
|     const viewModule = await import(`./views/${viewId}/index.js`); |  | ||||||
|     const viewConfig = viewModule.default as IAppView; |  | ||||||
|      |  | ||||||
|     // Register custom elements if needed |  | ||||||
|     if (viewModule.registerElements) { |  | ||||||
|       await viewModule.registerElements(); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     this.loadedViews.add(viewId); |  | ||||||
|     return viewConfig; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   async preloadViews(viewIds: string[]) { |  | ||||||
|     const promises = viewIds.map(id => this.loadView(id)); |  | ||||||
|     await Promise.all(promises); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Best Practices |  | ||||||
|  |  | ||||||
| ### 1. View Organization |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| src/ |  | ||||||
| ├── views/ |  | ||||||
| │   ├── dashboard/ |  | ||||||
| │   │   ├── index.ts          # View configuration |  | ||||||
| │   │   ├── data.ts           # Data fetching/management |  | ||||||
| │   │   ├── components/       # View-specific components |  | ||||||
| │   │   │   ├── dashboard-overview.ts |  | ||||||
| │   │   │   ├── dashboard-metrics.ts |  | ||||||
| │   │   │   └── dashboard-alerts.ts |  | ||||||
| │   │   └── styles.ts         # View-specific styles |  | ||||||
| │   ├── projects/ |  | ||||||
| │   │   └── ... |  | ||||||
| │   └── settings/ |  | ||||||
| │       └── ... |  | ||||||
| ├── services/ |  | ||||||
| │   ├── view-manager.ts |  | ||||||
| │   ├── navigation.ts |  | ||||||
| │   └── state-manager.ts |  | ||||||
| └── app-shell.ts |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 2. State Management |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // services/state-manager.ts |  | ||||||
| export class StateManager { |  | ||||||
|   private viewStates: Map<string, any> = new Map(); |  | ||||||
|  |  | ||||||
|   saveViewState(viewId: string, state: any) { |  | ||||||
|     this.viewStates.set(viewId, { |  | ||||||
|       ...this.getViewState(viewId), |  | ||||||
|       ...state, |  | ||||||
|       lastUpdated: Date.now() |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getViewState(viewId: string): any { |  | ||||||
|     return this.viewStates.get(viewId) || {}; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Persist to localStorage |  | ||||||
|   persistState() { |  | ||||||
|     const serialized = JSON.stringify( |  | ||||||
|       Array.from(this.viewStates.entries()) |  | ||||||
|     ); |  | ||||||
|     localStorage.setItem('app-state', serialized); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   restoreState() { |  | ||||||
|     const saved = localStorage.getItem('app-state'); |  | ||||||
|     if (saved) { |  | ||||||
|       const entries = JSON.parse(saved); |  | ||||||
|       this.viewStates = new Map(entries); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 3. View Communication |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // events/view-events.ts |  | ||||||
| export class ViewEventBus { |  | ||||||
|   private eventTarget = new EventTarget(); |  | ||||||
|  |  | ||||||
|   emit(eventName: string, detail: any) { |  | ||||||
|     this.eventTarget.dispatchEvent( |  | ||||||
|       new CustomEvent(eventName, { detail }) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   on(eventName: string, handler: (detail: any) => void) { |  | ||||||
|     this.eventTarget.addEventListener(eventName, (e: CustomEvent) => { |  | ||||||
|       handler(e.detail); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Cross-view communication |  | ||||||
|   sendMessage(fromView: string, toView: string, message: any) { |  | ||||||
|     this.emit('view-message', { |  | ||||||
|       from: fromView, |  | ||||||
|       to: toView, |  | ||||||
|       message |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 4. Responsive Design |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // views/responsive-view.ts |  | ||||||
| export const createResponsiveView = (config: IAppView): IAppView => { |  | ||||||
|   return { |  | ||||||
|     ...config, |  | ||||||
|     tabs: config.tabs.map(tab => ({ |  | ||||||
|       ...tab, |  | ||||||
|       content: () => html` |  | ||||||
|         <div class="view-content ${getDeviceClass()}"> |  | ||||||
|           ${tab.content()} |  | ||||||
|         </div> |  | ||||||
|       ` |  | ||||||
|     })) |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| function getDeviceClass(): string { |  | ||||||
|   const width = window.innerWidth; |  | ||||||
|   if (width < 768) return 'mobile'; |  | ||||||
|   if (width < 1024) return 'tablet'; |  | ||||||
|   return 'desktop'; |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 5. Performance Optimization |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // optimization/lazy-components.ts |  | ||||||
| export const lazyComponent = ( |  | ||||||
|   importFn: () => Promise<any>, |  | ||||||
|   componentName: string |  | ||||||
| ) => { |  | ||||||
|   let loaded = false; |  | ||||||
|    |  | ||||||
|   return () => { |  | ||||||
|     if (!loaded) { |  | ||||||
|       importFn().then(() => { |  | ||||||
|         loaded = true; |  | ||||||
|       }); |  | ||||||
|       return html`<dees-spinner></dees-spinner>`; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return html`<${componentName}></${componentName}>`; |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Usage in view |  | ||||||
| tabs: [ |  | ||||||
|   { |  | ||||||
|     key: 'heavy-component', |  | ||||||
|     content: lazyComponent( |  | ||||||
|       () => import('./components/heavy-component.js'), |  | ||||||
|       'heavy-component' |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
| ] |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Advanced Features |  | ||||||
|  |  | ||||||
| ### 1. View Permissions |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| interface IAppViewWithPermissions extends IAppView { |  | ||||||
|   requiredPermissions?: string[]; |  | ||||||
|   visibleTo?: (user: User) => boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class PermissionManager { |  | ||||||
|   canAccessView(view: IAppViewWithPermissions, user: User): boolean { |  | ||||||
|     if (view.visibleTo) { |  | ||||||
|       return view.visibleTo(user); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (view.requiredPermissions) { |  | ||||||
|       return view.requiredPermissions.every( |  | ||||||
|         perm => user.permissions.includes(perm) |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 2. View Lifecycle Hooks |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| interface IAppViewLifecycle extends IAppView { |  | ||||||
|   onActivate?: () => Promise<void>; |  | ||||||
|   onDeactivate?: () => Promise<void>; |  | ||||||
|   onTabChange?: (oldTab: string, newTab: string) => void; |  | ||||||
|   onDestroy?: () => void; |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 3. Dynamic Menu Generation |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| class DynamicMenuBuilder { |  | ||||||
|   buildMainMenu(views: IAppView[], user: User): ITab[] { |  | ||||||
|     return views |  | ||||||
|       .filter(view => this.canShowInMenu(view, user)) |  | ||||||
|       .map(view => ({ |  | ||||||
|         key: view.id, |  | ||||||
|         iconName: view.iconName || 'file', |  | ||||||
|         action: () => this.navigation.navigateToView(view.id) |  | ||||||
|       })); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   buildSelectorMenu(view: IAppView, context: any): ISelectionOption[] { |  | ||||||
|     const baseItems = view.menuItems || []; |  | ||||||
|     const contextItems = this.getContextualItems(view, context); |  | ||||||
|      |  | ||||||
|     return [...baseItems, ...contextItems]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Migration Strategy |  | ||||||
|  |  | ||||||
| For existing applications: |  | ||||||
|  |  | ||||||
| 1. **Identify Views**: Map existing routes/pages to views |  | ||||||
| 2. **Extract Components**: Move page-specific components into view folders |  | ||||||
| 3. **Define View Configs**: Create IAppView configurations |  | ||||||
| 4. **Update Navigation**: Replace existing routing with view navigation |  | ||||||
| 5. **Migrate State**: Move page state to ViewManager |  | ||||||
| 6. **Test & Optimize**: Ensure smooth transitions and performance |  | ||||||
|  |  | ||||||
| ## Example Application Structure |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| // main.ts |  | ||||||
| import { ViewManager } from './services/view-manager.js'; |  | ||||||
| import { AppNavigation } from './services/navigation.js'; |  | ||||||
| import { dashboardView } from './views/dashboard/index.js'; |  | ||||||
| import { projectsView } from './views/projects/index.js'; |  | ||||||
| import { settingsView } from './views/settings/index.js'; |  | ||||||
|  |  | ||||||
| const app = new MyAppShell(); |  | ||||||
| const viewManager = new ViewManager(); |  | ||||||
| const navigation = new AppNavigation(viewManager, app); |  | ||||||
|  |  | ||||||
| // Register views |  | ||||||
| viewManager.registerView(dashboardView); |  | ||||||
| viewManager.registerView(projectsView); |  | ||||||
| viewManager.registerView(settingsView); |  | ||||||
|  |  | ||||||
| // Setup navigation |  | ||||||
| app.views = [dashboardView, projectsView, settingsView]; |  | ||||||
| navigation.setupMainMenu(); |  | ||||||
| navigation.handleBrowserNavigation(); |  | ||||||
|  |  | ||||||
| // Initial navigation |  | ||||||
| navigation.navigateToView('dashboard'); |  | ||||||
|  |  | ||||||
| document.body.appendChild(app); |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| This architecture provides: |  | ||||||
| - **Modularity**: Each view is self-contained |  | ||||||
| - **Scalability**: Easy to add new views |  | ||||||
| - **Performance**: Lazy loading and caching |  | ||||||
| - **Consistency**: Unified navigation and layout |  | ||||||
| - **Flexibility**: Customizable per view |  | ||||||
| - **Maintainability**: Clear separation of concerns |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| !!! Please pay attention to the following points when writing the readme: !!! | !!! Please pay attention to the following points when writing the readme: !!! | ||||||
| * Give a short rundown of components and a few points abputspecific features on each. | * Give a short rundown of components and a few points abput specific features on each. | ||||||
| * Try to list all components in a summary. | * Try to list all components in a summary. | ||||||
| * Then list all components with a short description. | * Then list all components with a short description. | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1906
									
								
								readme.icons.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1906
									
								
								readme.icons.md
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										80
									
								
								readme.info.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								readme.info.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | # Contributor Information | ||||||
|  |  | ||||||
|  | This reference consolidates the helper notes previously split across `codex.md` and `CLAUDE.md`. Use it to get oriented quickly when working on `@design.estate/dees-catalog`, a TypeScript/Lit web-components library that ships themed UI building blocks for modern web applications. | ||||||
|  |  | ||||||
|  | ## Project Snapshot | ||||||
|  | - Package: `@design.estate/dees-catalog` | ||||||
|  | - Description: Comprehensive catalog of reusable web components with cohesive design, advanced form inputs, data displays, and layout scaffolding. | ||||||
|  | - Entry points: builds ship to `dist_ts_web/` (ES modules) and `dist_bundle/` (browser bundle); demos live in `html/`. | ||||||
|  | - Type system: strict TypeScript targeting modern browsers (see `tsconfig.json`). | ||||||
|  |  | ||||||
|  | ## Repository Layout | ||||||
|  | - `ts_web/` – TypeScript source | ||||||
|  |   - `elements/` – component implementations (`00*.ts` shared utilities, `dees-*.ts` components, `*.demo.ts` demos) | ||||||
|  |   - `pages/` – showcase pages aggregating demos | ||||||
|  |   - `index.ts` – main export surface | ||||||
|  | - `html/` – demo entry point bootstrapping bundles | ||||||
|  | - `dist_bundle/`, `dist_ts_web/`, `dist_watch/` – build outputs (production, module, and watch bundles) | ||||||
|  | - `test/` – browser/node tests powered by `@push.rocks/tapbundle` | ||||||
|  | - `scripts/` – maintenance utilities (e.g., Monaco version sync postinstall) | ||||||
|  |  | ||||||
|  | ## Build & Development Commands | ||||||
|  | All workflows use pnpm (see `package.json`). | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | pnpm install           # install dependencies | ||||||
|  | pnpm run build         # tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild | ||||||
|  | pnpm run watch         # tswatch element (development watch/dev server) | ||||||
|  | pnpm test              # tstest test/ --web --verbose --timeout 30 --logfile | ||||||
|  | pnpm run buildDocs     # tsdoc (generates docs) | ||||||
|  | tsx test/test.file.ts  # run a specific test file (file must be named test.*) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | `postinstall` runs `node scripts/update-monaco-version.cjs` to sync the Monaco editor version, so keep the script intact when updating dependencies. | ||||||
|  |  | ||||||
|  | ## Testing Guidelines | ||||||
|  | - Framework: `@push.rocks/tapbundle` with smartexpect assertions. Always review https://code.foss.global/push.rocks/smartexpect/raw/branch/master/readme.md when adding tests. | ||||||
|  | - Import pattern: | ||||||
|  |   ```typescript | ||||||
|  |   import { tap, expect } from '@push.rocks/tapbundle'; | ||||||
|  |   ``` | ||||||
|  | - Test naming: `test.*.both.ts` for dual runtime, `.node.ts` for Node-only, `.browser.ts` for browser-only suites. | ||||||
|  | - Prefer `pnpm test` for full runs; use `tsx` for focused debugging. Type-check failing tests with `tsc --noEmit`. | ||||||
|  | - Logs live under `.nogit/testlogs/`; put ad-hoc debug artefacts in `.nogit/debug/`. | ||||||
|  |  | ||||||
|  | ## Component Architecture | ||||||
|  | - **Base pattern**: Components extend `DeesElement` from `@design.estate/dees-element`, use Lit decorators (`@customElement`, `@property`), and combine `cssManager.defaultStyles` with component styles. Rendering happens via Lit `html` templates; demos sit on a static `demo` property referencing a `.demo.ts` module. | ||||||
|  | - **Theming**: `cssManager.bdTheme(light, dark)` selects theme-aware values. Shared palettes live in `ts_web/elements/00colors.ts`. | ||||||
|  | - **Z-index management**: Overlays consult the registry in `ts_web/elements/00zindex.ts` (`ZIndexRegistry`) to coordinate stacking. | ||||||
|  | - **Component families**: | ||||||
|  |   - Core UI (`dees-button`, `dees-badge`, `dees-icon`, …) focus on consistent theming and interactions. | ||||||
|  |   - Form inputs (`dees-form`, `dees-input-*`) build on `DeesInputBase` and communicate through subjects/events for validation. | ||||||
|  |   - Layout shells (`dees-appui-*`) orchestrate responsive app frames with centralized event rebroadcasts. | ||||||
|  |   - Data views (`dees-table`, `dees-dataview-*`, `dees-statsgrid`) handle large datasets with virtualisation and chart integrations. | ||||||
|  |   - Overlays (`dees-modal`, `dees-contextmenu`, `dees-toast`) respect the z-index registry and use shared window-layer utilities. | ||||||
|  | - **WYSIWYG editor**: `dees-input-wysiwyg` coordinates specialized handler classes (`WysiwygInputHandler`, `WysiwygKeyboardHandler`, drag/drop & modal managers) and global menus (`DeesSlashMenu`, `DeesFormattingMenu`). Rendering is imperative to preserve caret focus. | ||||||
|  |  | ||||||
|  | ## Implementation Guidelines | ||||||
|  | - Import external modules through `ts_web/elements/00plugins.ts`: `import * as plugins from './plugins.ts';` then reference `plugins.moduleName`. | ||||||
|  | - When creating new components: | ||||||
|  |   1. Extend `DeesElement` and decorate with `@customElement('dees-component')`. | ||||||
|  |   2. Support theming, slots, and accessibility; provide meaningful default styles. | ||||||
|  |   3. Expose a `.demo.ts` for the component and re-export via `elements/index.ts`. | ||||||
|  | - Form components must implement `getValue()` / `setValue()` and emit through `changeSubject` while honoring `disabled` and `required` states. | ||||||
|  | - Overlay components retrieve z-indices from the registry, register/unregister on show/hide, and use `DeesWindowLayer` for backdrops when appropriate. | ||||||
|  | - Avoid simplifying away functionality; prefer small, targeted changes and keep compatibility with existing APIs. | ||||||
|  |  | ||||||
|  | ## Common Patterns & Pitfalls | ||||||
|  | - Focus management: schedule DOM updates with `requestAnimationFrame` inside interactive editors to avoid focus loss. | ||||||
|  | - Event handling: stop propagation where nested interactive elements coexist; mix pointer and keyboard handling for accessibility. | ||||||
|  | - Performance: heavy blocks/components may load lazily; charts use debounced observers, tables rely on virtual scrolling. Watch bundle size when adding dependencies. | ||||||
|  |  | ||||||
|  | ## Documentation & Demos | ||||||
|  | - `readme.md` surfaces component overviews; demos in `.demo.ts` illustrate real usage. | ||||||
|  | - Update this `readme.info.md` when architectural patterns or workflows change so contributors stay in sync. | ||||||
|  |  | ||||||
|  | ## Recent Highlights | ||||||
|  | - Z-index registry overhaul enables dynamic stacking control across overlays. | ||||||
|  | - WYSIWYG refactor separated block handlers for maintainability. | ||||||
|  | - Dashboard grid enhancements added live drag-and-drop previews and overlap fixes. | ||||||
|  | - Monaco editor integration now reads the installed version at build time. | ||||||
							
								
								
									
										
											BIN
										
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								readme.plan.md
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										784
									
								
								readme.playbook.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										784
									
								
								readme.playbook.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,784 @@ | |||||||
|  | # UI Components Playbook | ||||||
|  |  | ||||||
|  | This playbook provides comprehensive guidance for creating and maintaining UI components in the @design.estate/dees-catalog library. Follow these patterns and best practices to ensure consistency, maintainability, and quality. | ||||||
|  |  | ||||||
|  | ## Table of Contents | ||||||
|  |  | ||||||
|  | 1. [Component Creation Checklist](#component-creation-checklist) | ||||||
|  | 2. [Architectural Patterns](#architectural-patterns) | ||||||
|  | 3. [Component Types and Base Classes](#component-types-and-base-classes) | ||||||
|  | 4. [Theming System](#theming-system) | ||||||
|  | 5. [Event Handling](#event-handling) | ||||||
|  | 6. [State Management](#state-management) | ||||||
|  | 7. [Form Components](#form-components) | ||||||
|  | 8. [Overlay Components](#overlay-components) | ||||||
|  | 9. [Complex Components](#complex-components) | ||||||
|  | 10. [Performance Optimization](#performance-optimization) | ||||||
|  | 11. [Focus Management](#focus-management) | ||||||
|  | 12. [Demo System](#demo-system) | ||||||
|  | 13. [Common Pitfalls and Anti-patterns](#common-pitfalls-and-anti-patterns) | ||||||
|  | 14. [Code Examples](#code-examples) | ||||||
|  |  | ||||||
|  | ## Component Creation Checklist | ||||||
|  |  | ||||||
|  | When creating a new component, follow this checklist: | ||||||
|  |  | ||||||
|  | - [ ] Choose the appropriate base class (`DeesElement` or `DeesInputBase`) | ||||||
|  | - [ ] Use `@customElement('dees-componentname')` decorator | ||||||
|  | - [ ] Implement consistent theming with `cssManager.bdTheme()` | ||||||
|  | - [ ] Create demo function in separate `.demo.ts` file | ||||||
|  | - [ ] Export component from `ts_web/elements/index.ts` | ||||||
|  | - [ ] Use proper TypeScript types and interfaces (prefix with `I` for interfaces, `T` for types) | ||||||
|  | - [ ] Implement proper event handling with bubbling and composition | ||||||
|  | - [ ] Consider mobile responsiveness | ||||||
|  | - [ ] Add focus states for accessibility | ||||||
|  | - [ ] Clean up resources in `destroy()` method | ||||||
|  | - [ ] Follow lowercase naming convention for files | ||||||
|  | - [ ] Add z-index registry support if it's an overlay component | ||||||
|  |  | ||||||
|  | ## Architectural Patterns | ||||||
|  |  | ||||||
|  | ### Base Component Structure | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | import { customElement, property, state, css, TemplateResult, html } from '@design.estate/dees-element'; | ||||||
|  | import { DeesElement } from '@design.estate/dees-element'; | ||||||
|  | import * as cssManager from './00colors.js'; | ||||||
|  | import * as demoFunc from './dees-componentname.demo.js'; | ||||||
|  |  | ||||||
|  | @customElement('dees-componentname') | ||||||
|  | export class DeesComponentName extends DeesElement { | ||||||
|  |   // Static demo reference | ||||||
|  |   public static demo = demoFunc.demoFunc; | ||||||
|  |  | ||||||
|  |   // Public properties (reactive, can be set via attributes) | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public label: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true }) | ||||||
|  |   public disabled: boolean = false; | ||||||
|  |  | ||||||
|  |   // Internal state (reactive, but not exposed as attributes) | ||||||
|  |   @state() | ||||||
|  |   private internalState: string = ''; | ||||||
|  |  | ||||||
|  |   // Static styles with theme support | ||||||
|  |   public static styles = [ | ||||||
|  |     cssManager.defaultStyles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         display: block; | ||||||
|  |         background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|  |       } | ||||||
|  |     ` | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   // Render method | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     return html` | ||||||
|  |       <div class="main-container"> | ||||||
|  |         <!-- Component content --> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Lifecycle methods | ||||||
|  |   public connectedCallback() { | ||||||
|  |     super.connectedCallback(); | ||||||
|  |     // Setup that needs DOM access | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async firstUpdated() { | ||||||
|  |     // One-time initialization after first render | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Cleanup | ||||||
|  |   public destroy() { | ||||||
|  |     // Clean up listeners, observers, registrations | ||||||
|  |     super.destroy(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Advanced Patterns | ||||||
|  |  | ||||||
|  | #### 1. Separation of Concerns (Complex Components) | ||||||
|  |  | ||||||
|  | For complex components like WYSIWYG editors, separate concerns into handler classes: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | export class DeesComplexComponent extends DeesElement { | ||||||
|  |   // Orchestrator pattern - main component coordinates handlers | ||||||
|  |   private inputHandler: InputHandler; | ||||||
|  |   private stateHandler: StateHandler; | ||||||
|  |   private renderHandler: RenderHandler; | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     super(); | ||||||
|  |     this.inputHandler = new InputHandler(this); | ||||||
|  |     this.stateHandler = new StateHandler(this); | ||||||
|  |     this.renderHandler = new RenderHandler(this); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### 2. Singleton Pattern (Global Components) | ||||||
|  |  | ||||||
|  | For global UI elements like menus: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | export class DeesGlobalMenu extends DeesElement { | ||||||
|  |   private static instance: DeesGlobalMenu; | ||||||
|  |  | ||||||
|  |   public static getInstance(): DeesGlobalMenu { | ||||||
|  |     if (!DeesGlobalMenu.instance) { | ||||||
|  |       DeesGlobalMenu.instance = new DeesGlobalMenu(); | ||||||
|  |       document.body.appendChild(DeesGlobalMenu.instance); | ||||||
|  |     } | ||||||
|  |     return DeesGlobalMenu.instance; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### 3. Registry Pattern (Z-Index Management) | ||||||
|  |  | ||||||
|  | Use centralized registries for global state: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | class ComponentRegistry { | ||||||
|  |   private static instance: ComponentRegistry; | ||||||
|  |   private registry = new WeakMap<HTMLElement, number>(); | ||||||
|  |    | ||||||
|  |   public register(element: HTMLElement, value: number) { | ||||||
|  |     this.registry.set(element, value); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   public unregister(element: HTMLElement) { | ||||||
|  |     this.registry.delete(element); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Component Types and Base Classes | ||||||
|  |  | ||||||
|  | ### Standard Component (extends DeesElement) | ||||||
|  |  | ||||||
|  | Use for most UI components: | ||||||
|  | - Buttons, badges, icons | ||||||
|  | - Layout components | ||||||
|  | - Data display components | ||||||
|  | - Overlay components | ||||||
|  |  | ||||||
|  | ### Form Input Component (extends DeesInputBase) | ||||||
|  |  | ||||||
|  | Use for all form inputs: | ||||||
|  | - Text inputs, dropdowns, checkboxes | ||||||
|  | - Date pickers, file uploads | ||||||
|  | - Rich text editors | ||||||
|  |  | ||||||
|  | **Required implementations:** | ||||||
|  | ```typescript | ||||||
|  | export class DeesInputCustom extends DeesInputBase<ValueType> { | ||||||
|  |   // Required: Get current value | ||||||
|  |   public getValue(): ValueType { | ||||||
|  |     return this.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Required: Set value programmatically | ||||||
|  |   public setValue(value: ValueType): void { | ||||||
|  |     this.value = value; | ||||||
|  |     this.changeSubject.next(this); // Notify form | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Optional: Custom validation | ||||||
|  |   public async validate(): Promise<boolean> { | ||||||
|  |     // Custom validation logic | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Theming System | ||||||
|  |  | ||||||
|  | ### DO: Use Theme Functions | ||||||
|  |  | ||||||
|  | Always use `cssManager.bdTheme()` for colors that change between themes: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ✅ CORRECT | ||||||
|  | background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|  | color: ${cssManager.bdTheme('#000000', '#ffffff')}; | ||||||
|  | border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333333')}; | ||||||
|  |  | ||||||
|  | // ❌ INCORRECT | ||||||
|  | background: #ffffff; // Hard-coded color | ||||||
|  | color: var(--custom-color); // Custom CSS variable | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Use Consistent Color Values | ||||||
|  |  | ||||||
|  | Reference shared color constants when possible: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // From 00colors.ts | ||||||
|  | background: ${cssManager.bdTheme(colors.bright.background, colors.dark.background)}; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Event Handling | ||||||
|  |  | ||||||
|  | ### DO: Dispatch Custom Events Properly | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ✅ CORRECT - Events bubble and cross shadow DOM | ||||||
|  | this.dispatchEvent(new CustomEvent('dees-componentname-change', { | ||||||
|  |   detail: { value: this.value }, | ||||||
|  |   bubbles: true, | ||||||
|  |   composed: true | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | // ❌ INCORRECT - Event won't propagate properly | ||||||
|  | this.dispatchEvent(new CustomEvent('change', { | ||||||
|  |   detail: { value: this.value } | ||||||
|  |   // Missing bubbles and composed | ||||||
|  | })); | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Use Event Delegation | ||||||
|  |  | ||||||
|  | For dynamic content, use event delegation: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ✅ CORRECT - Single listener for all items | ||||||
|  | this.addEventListener('click', (e: MouseEvent) => { | ||||||
|  |   const item = (e.target as HTMLElement).closest('.item'); | ||||||
|  |   if (item) { | ||||||
|  |     this.handleItemClick(item); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | // ❌ INCORRECT - Multiple listeners | ||||||
|  | this.items.forEach(item => { | ||||||
|  |   item.addEventListener('click', () => this.handleItemClick(item)); | ||||||
|  | }); | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## State Management | ||||||
|  |  | ||||||
|  | ### DO: Use Appropriate Property Decorators | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // Public API - use @property | ||||||
|  | @property({ type: String }) | ||||||
|  | public label: string; | ||||||
|  |  | ||||||
|  | // Internal state - use @state | ||||||
|  | @state() | ||||||
|  | private isLoading: boolean = false; | ||||||
|  |  | ||||||
|  | // Reflect to attribute when needed | ||||||
|  | @property({ type: Boolean, reflect: true }) | ||||||
|  | public disabled: boolean = false; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DON'T: Manipulate State in Render | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ INCORRECT - Side effects in render | ||||||
|  | public render() { | ||||||
|  |   this.counter++; // Don't modify state | ||||||
|  |   return html`<div>${this.counter}</div>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ✅ CORRECT - Pure render function | ||||||
|  | public render() { | ||||||
|  |   return html`<div>${this.counter}</div>`; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Form Components | ||||||
|  |  | ||||||
|  | ### DO: Extend DeesInputBase | ||||||
|  |  | ||||||
|  | All form inputs must extend the base class: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | export class DeesInputNew extends DeesInputBase<string> { | ||||||
|  |   // Inherits: key, label, value, required, disabled, validationState | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Emit Changes Consistently | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | private handleInput(e: Event) { | ||||||
|  |   this.value = (e.target as HTMLInputElement).value; | ||||||
|  |   this.changeSubject.next(this); // Notify form system | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Support Standard Form Properties | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // All form inputs should support: | ||||||
|  | @property() public key: string; | ||||||
|  | @property() public label: string; | ||||||
|  | @property() public required: boolean = false; | ||||||
|  | @property() public disabled: boolean = false; | ||||||
|  | @property() public validationState: 'valid' | 'warn' | 'invalid'; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Overlay Components | ||||||
|  |  | ||||||
|  | ### DO: Use Z-Index Registry | ||||||
|  |  | ||||||
|  | Never hardcode z-index values: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ✅ CORRECT | ||||||
|  | import { zIndexRegistry } from './00zindex.js'; | ||||||
|  |  | ||||||
|  | public async show() { | ||||||
|  |   this.modalZIndex = zIndexRegistry.getNextZIndex(); | ||||||
|  |   zIndexRegistry.register(this, this.modalZIndex); | ||||||
|  |   this.style.zIndex = `${this.modalZIndex}`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public async hide() { | ||||||
|  |   zIndexRegistry.unregister(this); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ❌ INCORRECT | ||||||
|  | public async show() { | ||||||
|  |   this.style.zIndex = '9999'; // Hardcoded z-index | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Use Window Layers | ||||||
|  |  | ||||||
|  | For modal backdrops: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | import { DeesWindowLayer } from './dees-windowlayer.js'; | ||||||
|  |  | ||||||
|  | private windowLayer: DeesWindowLayer; | ||||||
|  |  | ||||||
|  | public async show() { | ||||||
|  |   this.windowLayer = new DeesWindowLayer(); | ||||||
|  |   this.windowLayer.zIndex = zIndexRegistry.getNextZIndex(); | ||||||
|  |   document.body.append(this.windowLayer); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Complex Components | ||||||
|  |  | ||||||
|  | ### DO: Use Handler Classes | ||||||
|  |  | ||||||
|  | For complex logic, separate into specialized handlers: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // wysiwyg/handlers/input.handler.ts | ||||||
|  | export class InputHandler { | ||||||
|  |   constructor(private component: DeesInputWysiwyg) {} | ||||||
|  |    | ||||||
|  |   public handleInput(event: InputEvent) { | ||||||
|  |     // Specialized input handling | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Main component orchestrates | ||||||
|  | export class DeesInputWysiwyg extends DeesInputBase { | ||||||
|  |   private inputHandler = new InputHandler(this); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Use Programmatic Rendering | ||||||
|  |  | ||||||
|  | For performance-critical updates that shouldn't trigger re-renders: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ✅ CORRECT - Direct DOM manipulation when needed | ||||||
|  | private updateBlockContent(blockId: string, content: string) { | ||||||
|  |   const blockElement = this.shadowRoot.querySelector(`#${blockId}`); | ||||||
|  |   if (blockElement) { | ||||||
|  |     blockElement.textContent = content; // Direct update | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ❌ INCORRECT - Triggering full re-render | ||||||
|  | private updateBlockContent(blockId: string, content: string) { | ||||||
|  |   this.blocks.find(b => b.id === blockId).content = content; | ||||||
|  |   this.requestUpdate(); // Unnecessary re-render | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Performance Optimization | ||||||
|  |  | ||||||
|  | ### DO: Debounce Expensive Operations | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | private resizeTimeout: number; | ||||||
|  |  | ||||||
|  | private handleResize = () => { | ||||||
|  |   clearTimeout(this.resizeTimeout); | ||||||
|  |   this.resizeTimeout = window.setTimeout(() => { | ||||||
|  |     this.updateLayout(); | ||||||
|  |   }, 250); | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Use Observers Efficiently | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // Clean up observers | ||||||
|  | public disconnectedCallback() { | ||||||
|  |   super.disconnectedCallback(); | ||||||
|  |   this.resizeObserver?.disconnect(); | ||||||
|  |   this.mutationObserver?.disconnect(); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Implement Virtual Scrolling | ||||||
|  |  | ||||||
|  | For large lists: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // Only render visible items | ||||||
|  | private getVisibleItems() { | ||||||
|  |   const scrollTop = this.scrollContainer.scrollTop; | ||||||
|  |   const containerHeight = this.scrollContainer.clientHeight; | ||||||
|  |   const itemHeight = 50; | ||||||
|  |    | ||||||
|  |   const startIndex = Math.floor(scrollTop / itemHeight); | ||||||
|  |   const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight); | ||||||
|  |    | ||||||
|  |   return this.items.slice(startIndex, endIndex); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Focus Management | ||||||
|  |  | ||||||
|  | ### DO: Handle Focus Timing | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ✅ CORRECT - Wait for render | ||||||
|  | async focusInput() { | ||||||
|  |   await this.updateComplete; | ||||||
|  |   await new Promise(resolve => requestAnimationFrame(resolve)); | ||||||
|  |   this.inputElement?.focus(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ❌ INCORRECT - Focus too early | ||||||
|  | focusInput() { | ||||||
|  |   this.inputElement?.focus(); // Element might not exist | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Prevent Focus Loss | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // For global menus | ||||||
|  | constructor() { | ||||||
|  |   super(); | ||||||
|  |   // Prevent focus loss when clicking menu | ||||||
|  |   this.addEventListener('mousedown', (e) => { | ||||||
|  |     e.preventDefault(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Implement Blur Debouncing | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | private blurTimeout: number; | ||||||
|  |  | ||||||
|  | private handleBlur = () => { | ||||||
|  |   clearTimeout(this.blurTimeout); | ||||||
|  |   this.blurTimeout = window.setTimeout(() => { | ||||||
|  |     // Check if truly blurred | ||||||
|  |     if (!this.contains(document.activeElement)) { | ||||||
|  |       this.handleTrueBlur(); | ||||||
|  |     } | ||||||
|  |   }, 100); | ||||||
|  | }; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Demo System | ||||||
|  |  | ||||||
|  | ### DO: Create Comprehensive Demos | ||||||
|  |  | ||||||
|  | Every component needs a demo: | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // dees-button.demo.ts | ||||||
|  | import { html } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const demoFunc = () => html` | ||||||
|  |   <dees-button>Default Button</dees-button> | ||||||
|  |   <dees-button type="primary">Primary Button</dees-button> | ||||||
|  |   <dees-button type="danger" disabled>Disabled Danger</dees-button> | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | // In component file | ||||||
|  | import * as demoFunc from './dees-button.demo.js'; | ||||||
|  |  | ||||||
|  | export class DeesButton extends DeesElement { | ||||||
|  |   public static demo = demoFunc.demoFunc; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### DO: Include All Variants | ||||||
|  |  | ||||||
|  | Show all component states and variations in demos: | ||||||
|  | - Default state | ||||||
|  | - Different types/variants | ||||||
|  | - Disabled state | ||||||
|  | - Loading state | ||||||
|  | - Error states | ||||||
|  | - Edge cases (long text, empty content) | ||||||
|  |  | ||||||
|  | ## Common Pitfalls and Anti-patterns | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Hardcode Z-Index Values | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | this.style.zIndex = '9999'; | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | this.style.zIndex = `${zIndexRegistry.getNextZIndex()}`; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Skip Base Classes | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG - Form input without base class | ||||||
|  | export class DeesInputCustom extends DeesElement { | ||||||
|  |   // Missing standard form functionality | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | export class DeesInputCustom extends DeesInputBase<string> { | ||||||
|  |   // Inherits all form functionality | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Forget Theme Support | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | background-color: #ffffff; | ||||||
|  | color: #000000; | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | background-color: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|  | color: ${cssManager.bdTheme('#000000', '#ffffff')}; | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Create Components Without Demos | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | export class DeesComponent extends DeesElement { | ||||||
|  |   // No demo property | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | export class DeesComponent extends DeesElement { | ||||||
|  |   public static demo = demoFunc.demoFunc; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Emit Non-Bubbling Events | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | this.dispatchEvent(new CustomEvent('change', { | ||||||
|  |   detail: this.value | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | this.dispatchEvent(new CustomEvent('change', { | ||||||
|  |   detail: this.value, | ||||||
|  |   bubbles: true, | ||||||
|  |   composed: true | ||||||
|  | })); | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Skip Cleanup | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | public connectedCallback() { | ||||||
|  |   window.addEventListener('resize', this.handleResize); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | public connectedCallback() { | ||||||
|  |   super.connectedCallback(); | ||||||
|  |   window.addEventListener('resize', this.handleResize); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | public disconnectedCallback() { | ||||||
|  |   super.disconnectedCallback(); | ||||||
|  |   window.removeEventListener('resize', this.handleResize); | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Use Inline Styles for Theming | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | <div style="background-color: ${this.darkMode ? '#000' : '#fff'}"> | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | <div class="themed-container"> | ||||||
|  | // In styles: | ||||||
|  | .themed-container { | ||||||
|  |   background-color: ${cssManager.bdTheme('#ffffff', '#000000')}; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### ❌ DON'T: Forget Mobile Responsiveness | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // ❌ WRONG | ||||||
|  | :host { | ||||||
|  |   width: 800px; // Fixed width | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ✅ CORRECT | ||||||
|  | :host { | ||||||
|  |   width: 100%; | ||||||
|  |   max-width: 800px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   :host { | ||||||
|  |     /* Mobile adjustments */ | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Code Examples | ||||||
|  |  | ||||||
|  | ### Example: Creating a New Button Variant | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // dees-special-button.ts | ||||||
|  | import { customElement, property, css, html } from '@design.estate/dees-element'; | ||||||
|  | import { DeesElement } from '@design.estate/dees-element'; | ||||||
|  | import * as cssManager from './00colors.js'; | ||||||
|  | import * as demoFunc from './dees-special-button.demo.js'; | ||||||
|  |  | ||||||
|  | @customElement('dees-special-button') | ||||||
|  | export class DeesSpecialButton extends DeesElement { | ||||||
|  |   public static demo = demoFunc.demoFunc; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public text: string = 'Click me'; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true }) | ||||||
|  |   public loading: boolean = false; | ||||||
|  |  | ||||||
|  |   public static styles = [ | ||||||
|  |     cssManager.defaultStyles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         display: inline-block; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button { | ||||||
|  |         padding: 8px 16px; | ||||||
|  |         background: ${cssManager.bdTheme('#0066ff', '#0044cc')}; | ||||||
|  |         color: white; | ||||||
|  |         border: none; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         cursor: pointer; | ||||||
|  |         transition: all 0.2s; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button:hover { | ||||||
|  |         transform: translateY(-2px); | ||||||
|  |         box-shadow: 0 4px 8px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([loading]) .button { | ||||||
|  |         opacity: 0.7; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |       } | ||||||
|  |     ` | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   public render() { | ||||||
|  |     return html` | ||||||
|  |       <button class="button" ?disabled=${this.loading} @click=${this.handleClick}> | ||||||
|  |         ${this.loading ? html`<dees-spinner size="small"></dees-spinner>` : this.text} | ||||||
|  |       </button> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleClick() { | ||||||
|  |     this.dispatchEvent(new CustomEvent('special-click', { | ||||||
|  |       bubbles: true, | ||||||
|  |       composed: true | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Example: Creating a Form Input | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | // dees-input-special.ts | ||||||
|  | export class DeesInputSpecial extends DeesInputBase<string> { | ||||||
|  |   public static demo = demoFunc.demoFunc; | ||||||
|  |  | ||||||
|  |   public render() { | ||||||
|  |     return html` | ||||||
|  |       <dees-label .label=${this.label} .required=${this.required}> | ||||||
|  |         <input | ||||||
|  |           type="text" | ||||||
|  |           .value=${this.value || ''} | ||||||
|  |           ?disabled=${this.disabled} | ||||||
|  |           @input=${this.handleInput} | ||||||
|  |           @blur=${this.handleBlur} | ||||||
|  |         /> | ||||||
|  |       </dees-label> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleInput(e: Event) { | ||||||
|  |     this.value = (e.target as HTMLInputElement).value; | ||||||
|  |     this.changeSubject.next(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleBlur() { | ||||||
|  |     this.dispatchEvent(new CustomEvent('blur', { | ||||||
|  |       bubbles: true, | ||||||
|  |       composed: true | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getValue(): string { | ||||||
|  |     return this.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public setValue(value: string): void { | ||||||
|  |     this.value = value; | ||||||
|  |     this.changeSubject.next(this); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Summary | ||||||
|  |  | ||||||
|  | This playbook represents the collective wisdom and patterns found in the @design.estate/dees-catalog component library. Following these guidelines will help you create components that are: | ||||||
|  |  | ||||||
|  | - **Consistent**: Following established patterns | ||||||
|  | - **Maintainable**: Easy to understand and modify | ||||||
|  | - **Performant**: Optimized for real-world use | ||||||
|  | - **Accessible**: Usable by everyone | ||||||
|  | - **Theme-aware**: Supporting light and dark modes | ||||||
|  | - **Well-integrated**: Working seamlessly with the component ecosystem | ||||||
|  |  | ||||||
|  | Remember: When in doubt, look at existing components for examples. The codebase itself is the best documentation of these patterns in action. | ||||||
| @@ -1,138 +0,0 @@ | |||||||
| # WYSIWYG Editor Refactoring Progress Summary |  | ||||||
|  |  | ||||||
| ## Latest Updates |  | ||||||
|  |  | ||||||
| ### Selection Highlighting Fix ✅ |  | ||||||
| - **Issue**: "Paragraphs are not highlighted consistently, headings are always highlighted" |  | ||||||
| - **Root Cause**: The `shouldUpdate` method in `dees-wysiwyg-block.ts` was using a generic `.block` selector that would match the first element with that class, not necessarily the correct block element |  | ||||||
| - **Solution**: Changed the selector to be more specific: `.block.${blockType}` which ensures the correct element is found for each block type |  | ||||||
| - **Result**: All block types now highlight consistently when selected |  | ||||||
|  |  | ||||||
| ### Enter Key Block Creation Fix ✅ |  | ||||||
| - **Issue**: "When pressing enter and jumping to new block then typing something: The cursor is not at the beginning of the new block and there is content" |  | ||||||
| - **Root Cause**: Block handlers were rendering content with template syntax `${block.content || ''}` in their render methods, which violates the static HTML principle |  | ||||||
| - **Solution**:  |  | ||||||
|   - Removed all `${block.content}` from render methods in paragraph, heading, quote, and code block handlers |  | ||||||
|   - Content is now set programmatically in the setup() method only when needed |  | ||||||
|   - Fixed `setCursorToStart` and `setCursorToEnd` to always find elements fresh instead of relying on cached `blockElement` |  | ||||||
| - **Result**: New empty blocks remain truly empty, cursor positioning works correctly |  | ||||||
|  |  | ||||||
| ### Backspace Key Deletion Fix ✅ |  | ||||||
| - **Issue**: "After typing in a new block, pressing backspace deletes the whole block instead of just the last character" |  | ||||||
| - **Root Cause**:  |  | ||||||
|   1. `getCursorPositionInElement` was using `element.contains()` which doesn't work across Shadow DOM boundaries |  | ||||||
|   2. The backspace handler was checking `block.content === ''` which only contains the stored content, not the actual DOM content |  | ||||||
| - **Solution**: |  | ||||||
|   1. Fixed `getCursorPositionInElement` to use `containsAcrossShadowDOM` for proper Shadow DOM support |  | ||||||
|   2. Updated backspace handler to get actual content from DOM using `blockComponent.getContent()` instead of relying on stored `block.content` |  | ||||||
|   3. Added debug logging to track cursor position and content state |  | ||||||
| - **Result**: Backspace now correctly deletes individual characters instead of the whole block |  | ||||||
|  |  | ||||||
| ### Arrow Left Navigation Fix ✅ |  | ||||||
| - **Issue**: "When jumping to the previous block from the beginning of a block with arrow left, the cursor should be at the end of the previous block, not at the start" |  | ||||||
| - **Root Cause**: Browser's default focus behavior places cursor at the beginning of contenteditable elements, overriding our cursor positioning |  | ||||||
| - **Solution**: For 'end' position, set up the selection range BEFORE focusing the element: |  | ||||||
|   1. Create a range pointing to the end of content |  | ||||||
|   2. Apply the selection |  | ||||||
|   3. Then focus the element (which preserves the existing selection) |  | ||||||
|   4. Only use setCursorToEnd for empty blocks |  | ||||||
| - **Result**: Arrow left navigation now correctly places cursor at the end of the previous block |  | ||||||
|  |  | ||||||
| ## Completed Phases |  | ||||||
|  |  | ||||||
| ### Phase 1: Infrastructure ✅ |  | ||||||
| - Created modular block handler architecture |  | ||||||
| - Implemented `IBlockHandler` interface and `BaseBlockHandler` class |  | ||||||
| - Created `BlockRegistry` for dynamic block type registration |  | ||||||
| - Set up proper file structure under `blocks/` directory |  | ||||||
|  |  | ||||||
| ### Phase 2: Proof of Concept ✅ |  | ||||||
| - Successfully migrated divider block as the simplest example |  | ||||||
| - Validated the architecture works correctly |  | ||||||
| - Established patterns for block migration |  | ||||||
|  |  | ||||||
| ### Phase 3: Text Blocks ✅ |  | ||||||
| - **Paragraph Block**: Full editing support with text splitting, selection handling, and cursor tracking |  | ||||||
| - **Heading Blocks**: All three heading levels (h1, h2, h3) with unified handler |  | ||||||
| - **Quote Block**: Italic styling with border, full editing capabilities |  | ||||||
| - **Code Block**: Monospace font, tab handling, plain text paste support |  | ||||||
| - **List Block**: Bullet/numbered lists with proper list item management |  | ||||||
|  |  | ||||||
| ## Key Achievements |  | ||||||
|  |  | ||||||
| ### 1. Preserved Critical Knowledge |  | ||||||
| - **Static Rendering**: Blocks use `innerHTML` in `firstUpdated` to prevent focus loss during typing |  | ||||||
| - **Shadow DOM Selection**: Implemented `containsAcrossShadowDOM` utility for proper selection detection |  | ||||||
| - **Cursor Position Tracking**: All editable blocks track cursor position across multiple events |  | ||||||
| - **Content Splitting**: HTML-aware splitting using Range API preserves formatting |  | ||||||
| - **Focus Management**: Microtask-based focus restoration ensures reliable cursor placement |  | ||||||
|  |  | ||||||
| ### 2. Enhanced Architecture |  | ||||||
| - Each block type is now self-contained in its own file |  | ||||||
| - Block handlers are dynamically registered and loaded |  | ||||||
| - Common functionality is shared through base classes |  | ||||||
| - Styles are co-located with their block handlers |  | ||||||
|  |  | ||||||
| ### 3. Maintained Functionality |  | ||||||
| - All keyboard navigation works (arrows, backspace, delete, enter) |  | ||||||
| - Text selection across Shadow DOM boundaries functions correctly |  | ||||||
| - Block merging and splitting behave as before |  | ||||||
| - IME (Input Method Editor) support is preserved |  | ||||||
| - Formatting shortcuts (Cmd/Ctrl+B/I/U/K) continue to work |  | ||||||
|  |  | ||||||
| ## Code Organization |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| ts_web/elements/wysiwyg/ |  | ||||||
| ├── dees-wysiwyg-block.ts (simplified main component) |  | ||||||
| ├── wysiwyg.selection.ts (Shadow DOM selection utilities) |  | ||||||
| ├── wysiwyg.blockregistration.ts (handler registration) |  | ||||||
| └── blocks/ |  | ||||||
|     ├── index.ts (exports and registry) |  | ||||||
|     ├── block.base.ts (base handler interface) |  | ||||||
|     ├── decorative/ |  | ||||||
|     │   └── divider.block.ts |  | ||||||
|     └── text/ |  | ||||||
|         ├── paragraph.block.ts |  | ||||||
|         ├── heading.block.ts |  | ||||||
|         ├── quote.block.ts |  | ||||||
|         ├── code.block.ts |  | ||||||
|         └── list.block.ts |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Next Steps |  | ||||||
|  |  | ||||||
| ### Phase 4: Media Blocks (In Progress) |  | ||||||
| - Image block with upload/drag-drop support |  | ||||||
| - YouTube block with video embedding |  | ||||||
| - Attachment block for file uploads |  | ||||||
|  |  | ||||||
| ### Phase 5: Content Blocks |  | ||||||
| - Markdown block with preview toggle |  | ||||||
| - HTML block with raw HTML editing |  | ||||||
|  |  | ||||||
| ### Phase 6: Cleanup |  | ||||||
| - Remove old code from main component |  | ||||||
| - Optimize bundle size |  | ||||||
| - Update documentation |  | ||||||
|  |  | ||||||
| ## Technical Improvements |  | ||||||
|  |  | ||||||
| 1. **Modularity**: Each block type is now completely self-contained |  | ||||||
| 2. **Extensibility**: New blocks can be added by creating a handler and registering it |  | ||||||
| 3. **Maintainability**: Files are smaller and focused on single responsibilities |  | ||||||
| 4. **Type Safety**: Strong TypeScript interfaces ensure consistent implementation |  | ||||||
| 5. **Performance**: No degradation in performance; potential for lazy loading in future |  | ||||||
|  |  | ||||||
| ## Migration Pattern |  | ||||||
|  |  | ||||||
| For future block migrations, follow this pattern: |  | ||||||
|  |  | ||||||
| 1. Create block handler extending `BaseBlockHandler` |  | ||||||
| 2. Implement required methods: `render()`, `setup()`, `getStyles()` |  | ||||||
| 3. Add helper methods for cursor/content management |  | ||||||
| 4. Handle Shadow DOM selection properly using utilities |  | ||||||
| 5. Register handler in `wysiwyg.blockregistration.ts` |  | ||||||
| 6. Test all interactions (typing, selection, navigation) |  | ||||||
|  |  | ||||||
| The refactoring has been successful in making the codebase more maintainable while preserving all the hard-won functionality and edge case handling from the original implementation. |  | ||||||
| @@ -1,82 +0,0 @@ | |||||||
| # WYSIWYG Editor Refactoring |  | ||||||
|  |  | ||||||
| ## Summary of Changes |  | ||||||
|  |  | ||||||
| This refactoring cleaned up the wysiwyg editor implementation to fix focus, cursor position, and selection issues. |  | ||||||
|  |  | ||||||
| ### Phase 1: Code Organization |  | ||||||
|  |  | ||||||
| #### 1. Removed Duplicate Code |  | ||||||
| - Removed duplicate `handleBlockInput` method from main component (was already in inputHandler) |  | ||||||
| - Removed duplicate `handleBlockKeyDown` method from main component (was already in keyboardHandler) |  | ||||||
| - Consolidated all input handling in the respective handler classes |  | ||||||
|  |  | ||||||
| #### 2. Simplified Focus Management |  | ||||||
| - Removed complex `updated` lifecycle method that was trying to maintain focus |  | ||||||
| - Simplified `handleBlockBlur` to not immediately close menus |  | ||||||
| - Added `requestAnimationFrame` to focus operations for better timing |  | ||||||
| - Removed `slashMenuOpenTime` tracking which was no longer needed |  | ||||||
|  |  | ||||||
| #### 3. Fixed Slash Menu Behavior |  | ||||||
| - Changed from `@mousedown` to `@click` events for better UX |  | ||||||
| - Added proper event prevention to avoid focus loss |  | ||||||
| - Menu now closes when clicking outside |  | ||||||
| - Simplified the insertBlock method to close menu first |  | ||||||
|  |  | ||||||
| ### Phase 2: Cursor & Selection Fixes |  | ||||||
|  |  | ||||||
| #### 4. Enhanced Cursor Position Management |  | ||||||
| - Added `focusWithCursor()` method to block component for precise cursor positioning |  | ||||||
| - Improved `handleSlashCommand` to preserve cursor position when menu opens |  | ||||||
| - Added `getCaretCoordinates()` for accurate menu positioning based on cursor location |  | ||||||
| - Updated `focusBlock()` to support numeric cursor positions |  | ||||||
|  |  | ||||||
| #### 5. Fixed Selection Across Shadow DOM |  | ||||||
| - Added custom `block-text-selected` event to communicate selections across shadow boundaries |  | ||||||
| - Implemented `handleMouseUp()` in block component to detect selections |  | ||||||
| - Updated main component to listen for selection events from blocks |  | ||||||
| - Selection now works properly even with nested shadow DOMs |  | ||||||
|  |  | ||||||
| #### 6. Improved Slash Menu Close Behavior |  | ||||||
| - Added optional `clearSlash` parameter to `closeSlashMenu()` |  | ||||||
| - Escape key now properly clears the slash command |  | ||||||
| - Clicking outside clears the slash if menu is open |  | ||||||
| - Selecting an item preserves content and just transforms the block |  | ||||||
|  |  | ||||||
| ### Technical Improvements |  | ||||||
|  |  | ||||||
| #### Block Component (`dees-wysiwyg-block`) |  | ||||||
| - Better focus management with immediate focus (removed unnecessary requestAnimationFrame) |  | ||||||
| - Added cursor position control methods |  | ||||||
| - Custom event dispatching for cross-shadow-DOM communication |  | ||||||
| - Improved content handling for different block types |  | ||||||
|  |  | ||||||
| #### Input Handler |  | ||||||
| - Preserves cursor position when showing slash menu |  | ||||||
| - Better caret coordinate calculation for menu positioning |  | ||||||
| - Ensures focus stays in the block when menu appears |  | ||||||
|  |  | ||||||
| #### Block Operations |  | ||||||
| - Enhanced `focusBlock()` to support start/end/numeric positions |  | ||||||
| - Better timing with requestAnimationFrame for focus operations |  | ||||||
|  |  | ||||||
| ### Key Benefits |  | ||||||
| - Slash menu no longer causes focus or cursor position loss |  | ||||||
| - Text selection works properly across shadow DOM boundaries |  | ||||||
| - Cursor position is preserved when interacting with menus |  | ||||||
| - Cleaner, more maintainable code structure |  | ||||||
| - Better separation of concerns |  | ||||||
|  |  | ||||||
| ## Testing |  | ||||||
|  |  | ||||||
| Use the test files in `.nogit/debug/`: |  | ||||||
| - `test-slash-menu.html` - Tests slash menu focus behavior |  | ||||||
| - `test-wysiwyg-formatting.html` - Tests text formatting |  | ||||||
|  |  | ||||||
| ## Known Issues Fixed |  | ||||||
| - Slash menu disappearing immediately on first "/" |  | ||||||
| - Focus lost when slash menu opens |  | ||||||
| - Cursor position lost when typing "/" |  | ||||||
| - Text selection not working properly |  | ||||||
| - Selection events not propagating across shadow DOM |  | ||||||
| - Duplicate event handling causing conflicts |  | ||||||
							
								
								
									
										44
									
								
								scripts/update-monaco-version.cjs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										44
									
								
								scripts/update-monaco-version.cjs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | #!/usr/bin/env node | ||||||
|  | const fs = require('fs'); | ||||||
|  | const path = require('path'); | ||||||
|  |  | ||||||
|  | const projectRoot = path.resolve(__dirname, '..'); | ||||||
|  |  | ||||||
|  | function resolveMonacoPackageJson() { | ||||||
|  |   try { | ||||||
|  |     const resolvedPath = require.resolve('monaco-editor/package.json', { | ||||||
|  |       paths: [projectRoot], | ||||||
|  |     }); | ||||||
|  |     return resolvedPath; | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('[dees-editor] Unable to resolve monaco-editor/package.json'); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getMonacoVersion() { | ||||||
|  |   const monacoPackagePath = resolveMonacoPackageJson(); | ||||||
|  |   const monacoPackage = require(monacoPackagePath); | ||||||
|  |   if (!monacoPackage.version) { | ||||||
|  |     throw new Error('[dees-editor] monaco-editor/package.json does not expose a version field'); | ||||||
|  |   } | ||||||
|  |   return monacoPackage.version; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function writeVersionModule(version) { | ||||||
|  |   const targetDir = path.join(projectRoot, 'ts_web', 'elements', 'dees-editor'); | ||||||
|  |   fs.mkdirSync(targetDir, { recursive: true }); | ||||||
|  |   const targetFile = path.join(targetDir, 'version.ts'); | ||||||
|  |   const fileContent = `// Auto-generated by scripts/update-monaco-version.cjs\nexport const MONACO_VERSION = '${version}';\n`; | ||||||
|  |   fs.writeFileSync(targetFile, fileContent, 'utf8'); | ||||||
|  |   console.log(`[dees-editor] Wrote ${path.relative(projectRoot, targetFile)} with monaco-editor@${version}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | try { | ||||||
|  |   const version = getMonacoVersion(); | ||||||
|  |   writeVersionModule(version); | ||||||
|  | } catch (error) { | ||||||
|  |   console.error('[dees-editor] Failed to update Monaco version module.'); | ||||||
|  |   console.error(error instanceof Error ? error.message : error); | ||||||
|  |   process.exitCode = 1; | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								test/test.contextmenu-demo.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								test/test.contextmenu-demo.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js'; | ||||||
|  | import { demoFunc } from '../ts_web/elements/dees-contextmenu.demo.js'; | ||||||
|  |  | ||||||
|  | tap.test('should render context menu demo', async () => { | ||||||
|  |   // Create demo container | ||||||
|  |   const demoContainer = document.createElement('div'); | ||||||
|  |   document.body.appendChild(demoContainer); | ||||||
|  |    | ||||||
|  |   // Render the demo | ||||||
|  |   const demoContent = demoFunc(); | ||||||
|  |    | ||||||
|  |   // Create a temporary element to hold the rendered template | ||||||
|  |   const tempDiv = document.createElement('div'); | ||||||
|  |   tempDiv.innerHTML = demoContent.strings.join(''); | ||||||
|  |    | ||||||
|  |   // Check that panels are rendered | ||||||
|  |   const panels = tempDiv.querySelectorAll('dees-panel'); | ||||||
|  |   expect(panels.length).toEqual(4); | ||||||
|  |    | ||||||
|  |   // Check panel headings | ||||||
|  |   expect(panels[0].getAttribute('heading')).toEqual('Basic Context Menu with Nested Submenus'); | ||||||
|  |   expect(panels[1].getAttribute('heading')).toEqual('Component-Specific Context Menu'); | ||||||
|  |   expect(panels[2].getAttribute('heading')).toEqual('Advanced Context Menu Example'); | ||||||
|  |   expect(panels[3].getAttribute('heading')).toEqual('Static Context Menu (Always Visible)'); | ||||||
|  |    | ||||||
|  |   // Check that static context menu exists | ||||||
|  |   const staticMenu = tempDiv.querySelector('dees-contextmenu'); | ||||||
|  |   expect(staticMenu).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   demoContainer.remove(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										93
									
								
								test/test.contextmenu-nested-close.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								test/test.contextmenu-nested-close.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js'; | ||||||
|  |  | ||||||
|  | tap.test('should close all parent menus when clicking action in nested submenu', async () => { | ||||||
|  |   let actionCalled = false; | ||||||
|  |    | ||||||
|  |   // Create a test element | ||||||
|  |   const testDiv = document.createElement('div'); | ||||||
|  |   testDiv.style.width = '300px'; | ||||||
|  |   testDiv.style.height = '300px'; | ||||||
|  |   testDiv.style.background = '#f0f0f0'; | ||||||
|  |   testDiv.innerHTML = 'Right-click for nested menu test'; | ||||||
|  |   document.body.appendChild(testDiv); | ||||||
|  |    | ||||||
|  |   // Simulate right-click to open context menu | ||||||
|  |   const contextMenuEvent = new MouseEvent('contextmenu', { | ||||||
|  |     clientX: 150, | ||||||
|  |     clientY: 150, | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Open context menu with nested structure | ||||||
|  |   DeesContextmenu.openContextMenuWithOptions(contextMenuEvent, [ | ||||||
|  |     { | ||||||
|  |       name: 'Parent Item', | ||||||
|  |       iconName: 'folder', | ||||||
|  |       action: async () => {}, // Parent items with submenus need an action | ||||||
|  |       submenu: [ | ||||||
|  |         { | ||||||
|  |           name: 'Child Item', | ||||||
|  |           iconName: 'file', | ||||||
|  |           action: async () => { | ||||||
|  |             actionCalled = true; | ||||||
|  |             console.log('Child action called'); | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: 'Another Child', | ||||||
|  |           iconName: 'fileText', | ||||||
|  |           action: async () => console.log('Another child') | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       name: 'Regular Item', | ||||||
|  |       iconName: 'box', | ||||||
|  |       action: async () => console.log('Regular item') | ||||||
|  |     } | ||||||
|  |   ]); | ||||||
|  |    | ||||||
|  |   // Wait for main menu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 150)); | ||||||
|  |    | ||||||
|  |   // Check main menu exists | ||||||
|  |   const mainMenu = document.querySelector('dees-contextmenu'); | ||||||
|  |   expect(mainMenu).toBeInstanceOf(DeesContextmenu); | ||||||
|  |    | ||||||
|  |   // Hover over "Parent Item" to trigger submenu | ||||||
|  |   const parentItem = mainMenu!.shadowRoot!.querySelector('.menuitem'); | ||||||
|  |   expect(parentItem).toBeTruthy(); | ||||||
|  |   parentItem!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); | ||||||
|  |    | ||||||
|  |   // Wait for submenu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 300)); | ||||||
|  |    | ||||||
|  |   // Check submenu exists | ||||||
|  |   const allMenus = document.querySelectorAll('dees-contextmenu'); | ||||||
|  |   expect(allMenus.length).toEqual(2); // Main menu and submenu | ||||||
|  |    | ||||||
|  |   const submenu = allMenus[1]; | ||||||
|  |   expect(submenu).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Click on "Child Item" in submenu | ||||||
|  |   const childItem = submenu.shadowRoot!.querySelector('.menuitem'); | ||||||
|  |   expect(childItem).toBeTruthy(); | ||||||
|  |   childItem!.click(); | ||||||
|  |    | ||||||
|  |   // Wait for menus to close | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 200)); | ||||||
|  |    | ||||||
|  |   // Verify action was called | ||||||
|  |   expect(actionCalled).toEqual(true); | ||||||
|  |    | ||||||
|  |   // Verify all menus are closed | ||||||
|  |   const remainingMenus = document.querySelectorAll('dees-contextmenu'); | ||||||
|  |   expect(remainingMenus.length).toEqual(0); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   testDiv.remove(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										71
									
								
								test/test.contextmenu-shadowdom.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								test/test.contextmenu-shadowdom.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js'; | ||||||
|  | import { DeesElement, customElement, html } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | // Create a test element with shadow DOM | ||||||
|  | @customElement('test-shadow-element') | ||||||
|  | class TestShadowElement extends DeesElement { | ||||||
|  |   public getContextMenuItems() { | ||||||
|  |     return [ | ||||||
|  |       { name: 'Shadow Item 1', iconName: 'box', action: async () => console.log('Shadow 1') }, | ||||||
|  |       { name: 'Shadow Item 2', iconName: 'package', action: async () => console.log('Shadow 2') } | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   render() { | ||||||
|  |     return html` | ||||||
|  |       <div style="padding: 40px; background: #eee; border-radius: 8px;"> | ||||||
|  |         <h3>Shadow DOM Content</h3> | ||||||
|  |         <p>Right-click anywhere inside this shadow DOM</p> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tap.test('should show context menu when right-clicking inside shadow DOM', async () => { | ||||||
|  |   // Create the shadow DOM element | ||||||
|  |   const shadowElement = document.createElement('test-shadow-element'); | ||||||
|  |   document.body.appendChild(shadowElement); | ||||||
|  |    | ||||||
|  |   // Wait for element to be ready | ||||||
|  |   await shadowElement.updateComplete; | ||||||
|  |    | ||||||
|  |   // Get the content inside shadow DOM | ||||||
|  |   const shadowContent = shadowElement.shadowRoot!.querySelector('div'); | ||||||
|  |   expect(shadowContent).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Simulate right-click on content inside shadow DOM | ||||||
|  |   const contextMenuEvent = new MouseEvent('contextmenu', { | ||||||
|  |     clientX: 100, | ||||||
|  |     clientY: 100, | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true, | ||||||
|  |     composed: true // Important for shadow DOM | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   shadowContent!.dispatchEvent(contextMenuEvent); | ||||||
|  |    | ||||||
|  |   // Wait for context menu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Check if context menu is created | ||||||
|  |   const contextMenu = document.querySelector('dees-contextmenu'); | ||||||
|  |   expect(contextMenu).toBeInstanceOf(DeesContextmenu); | ||||||
|  |    | ||||||
|  |   // Check if menu items from shadow element are rendered | ||||||
|  |   const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem'); | ||||||
|  |   expect(menuItems.length).toBeGreaterThanOrEqual(2); | ||||||
|  |    | ||||||
|  |   // Check menu item text | ||||||
|  |   const menuTexts = Array.from(menuItems).map(item =>  | ||||||
|  |     item.querySelector('.menuitem-text')?.textContent | ||||||
|  |   ); | ||||||
|  |   expect(menuTexts).toContain('Shadow Item 1'); | ||||||
|  |   expect(menuTexts).toContain('Shadow Item 2'); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   contextMenu!.remove(); | ||||||
|  |   shadowElement.remove(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										77
									
								
								test/test.contextmenu.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								test/test.contextmenu.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js'; | ||||||
|  |  | ||||||
|  | tap.test('should show context menu with nested submenu', async () => { | ||||||
|  |   // Create a test element with context menu items | ||||||
|  |   const testDiv = document.createElement('div'); | ||||||
|  |   testDiv.style.width = '200px'; | ||||||
|  |   testDiv.style.height = '200px'; | ||||||
|  |   testDiv.style.background = '#eee'; | ||||||
|  |   testDiv.innerHTML = 'Right-click me'; | ||||||
|  |    | ||||||
|  |   // Add getContextMenuItems method | ||||||
|  |   (testDiv as any).getContextMenuItems = () => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         name: 'Change Type', | ||||||
|  |         iconName: 'type', | ||||||
|  |         submenu: [ | ||||||
|  |           { name: 'Paragraph', iconName: 'text', action: () => console.log('Paragraph') }, | ||||||
|  |           { name: 'Heading 1', iconName: 'heading1', action: () => console.log('Heading 1') }, | ||||||
|  |           { name: 'Heading 2', iconName: 'heading2', action: () => console.log('Heading 2') }, | ||||||
|  |           { divider: true }, | ||||||
|  |           { name: 'Code Block', iconName: 'fileCode', action: () => console.log('Code') }, | ||||||
|  |           { name: 'Quote', iconName: 'quote', action: () => console.log('Quote') } | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { divider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'Delete', | ||||||
|  |         iconName: 'trash2', | ||||||
|  |         action: () => console.log('Delete') | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }; | ||||||
|  |    | ||||||
|  |   document.body.appendChild(testDiv); | ||||||
|  |    | ||||||
|  |   // Simulate right-click | ||||||
|  |   const contextMenuEvent = new MouseEvent('contextmenu', { | ||||||
|  |     clientX: 100, | ||||||
|  |     clientY: 100, | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   testDiv.dispatchEvent(contextMenuEvent); | ||||||
|  |    | ||||||
|  |   // Wait for context menu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Check if context menu is created | ||||||
|  |   const contextMenu = document.querySelector('dees-contextmenu'); | ||||||
|  |   expect(contextMenu).toBeInstanceOf(DeesContextmenu); | ||||||
|  |    | ||||||
|  |   // Check if menu items are rendered | ||||||
|  |   const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem'); | ||||||
|  |   expect(menuItems.length).toEqual(2); // "Change Type" and "Delete" | ||||||
|  |    | ||||||
|  |   // Hover over "Change Type" to trigger submenu | ||||||
|  |   const changeTypeItem = menuItems[0] as HTMLElement; | ||||||
|  |   changeTypeItem.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); | ||||||
|  |    | ||||||
|  |   // Wait for submenu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 300)); | ||||||
|  |    | ||||||
|  |   // Check if submenu is created | ||||||
|  |   const submenus = document.querySelectorAll('dees-contextmenu'); | ||||||
|  |   expect(submenus.length).toEqual(2); // Main menu and submenu | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   contextMenu!.remove(); | ||||||
|  |   const submenu = submenus[1]; | ||||||
|  |   if (submenu) submenu.remove(); | ||||||
|  |   testDiv.remove(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										28
									
								
								test/test.dashboardgrid-layout.node.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								test/test.dashboardgrid-layout.node.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import { tap, expect } from '@push.rocks/tapbundle'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   resolveWidgetPlacement, | ||||||
|  |   collectCollisions, | ||||||
|  | } from '../ts_web/elements/dees-dashboardgrid/layout.ts'; | ||||||
|  | import type { DashboardWidget } from '../ts_web/elements/dees-dashboardgrid/types.ts'; | ||||||
|  |  | ||||||
|  | tap.test('dashboardgrid does not overlap widgets after swap attempt', async () => { | ||||||
|  |   const widgets: DashboardWidget[] = [ | ||||||
|  |     { id: 'w0', x: 6, y: 5, w: 1, h: 3 }, | ||||||
|  |     { id: 'w1', x: 6, y: 1, w: 1, h: 3 }, | ||||||
|  |     { id: 'w2', x: 3, y: 0, w: 2, h: 2 }, | ||||||
|  |     { id: 'w3', x: 9, y: 0, w: 1, h: 2 }, | ||||||
|  |     { id: 'w4', x: 4, y: 3, w: 1, h: 2 }, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   const placement = resolveWidgetPlacement(widgets, 'w0', { x: 6, y: 3 }, 12); | ||||||
|  |   expect(placement).toBeTruthy(); | ||||||
|  |  | ||||||
|  |   const layout = placement!.widgets; | ||||||
|  |   for (const widget of layout) { | ||||||
|  |     const collisions = collectCollisions(layout, widget, widget.x, widget.y, widget.w, widget.h); | ||||||
|  |     expect(collisions).toBeEmptyArray(); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										146
									
								
								test/test.tabs-indicator.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								test/test.tabs-indicator.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import * as deesCatalog from '../ts_web/index.js'; | ||||||
|  |  | ||||||
|  | tap.test('tabs indicator positioning - detailed measurements', async () => { | ||||||
|  |   // Create tabs element with different length labels | ||||||
|  |   const tabsElement = new deesCatalog.DeesAppuiTabs(); | ||||||
|  |   tabsElement.tabs = [ | ||||||
|  |     { key: 'Home', iconName: 'lucide:home', action: () => {} }, | ||||||
|  |     { key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => {} }, | ||||||
|  |     { key: 'User Settings', iconName: 'lucide:settings', action: () => {} }, | ||||||
|  |   ]; | ||||||
|  |    | ||||||
|  |   document.body.appendChild(tabsElement); | ||||||
|  |   await tabsElement.updateComplete; | ||||||
|  |    | ||||||
|  |   // Wait for fonts and indicator initialization | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 200)); | ||||||
|  |    | ||||||
|  |   // Get all elements | ||||||
|  |   const shadowRoot = tabsElement.shadowRoot; | ||||||
|  |   const wrapper = shadowRoot.querySelector('.tabs-wrapper') as HTMLElement; | ||||||
|  |   const container = shadowRoot.querySelector('.tabsContainer') as HTMLElement; | ||||||
|  |   const tabs = shadowRoot.querySelectorAll('.tab'); | ||||||
|  |   const firstTab = tabs[0] as HTMLElement; | ||||||
|  |   const firstContent = firstTab.querySelector('.tab-content') as HTMLElement; | ||||||
|  |   const indicator = shadowRoot.querySelector('.tabIndicator') as HTMLElement; | ||||||
|  |    | ||||||
|  |   // Verify all elements exist | ||||||
|  |   expect(wrapper).toBeInstanceOf(HTMLElement); | ||||||
|  |   expect(container).toBeInstanceOf(HTMLElement); | ||||||
|  |   expect(firstTab).toBeInstanceOf(HTMLElement); | ||||||
|  |   expect(firstContent).toBeInstanceOf(HTMLElement); | ||||||
|  |   expect(indicator).toBeInstanceOf(HTMLElement); | ||||||
|  |    | ||||||
|  |   // Get all measurements | ||||||
|  |   const wrapperRect = wrapper.getBoundingClientRect(); | ||||||
|  |   const containerRect = container.getBoundingClientRect(); | ||||||
|  |   const tabRect = firstTab.getBoundingClientRect(); | ||||||
|  |   const contentRect = firstContent.getBoundingClientRect(); | ||||||
|  |   const indicatorRect = indicator.getBoundingClientRect(); | ||||||
|  |    | ||||||
|  |   console.log('\n=== DETAILED MEASUREMENTS ==='); | ||||||
|  |   console.log('Document body left:', document.body.getBoundingClientRect().left); | ||||||
|  |   console.log('Wrapper left:', wrapperRect.left); | ||||||
|  |   console.log('Container left:', containerRect.left); | ||||||
|  |   console.log('Tab left:', tabRect.left); | ||||||
|  |   console.log('Content left:', contentRect.left); | ||||||
|  |   console.log('Indicator left (actual):', indicatorRect.left); | ||||||
|  |    | ||||||
|  |   console.log('\n=== RELATIVE POSITIONS ==='); | ||||||
|  |   console.log('Container padding (container - wrapper):', containerRect.left - wrapperRect.left); | ||||||
|  |   console.log('Tab position in container:', tabRect.left - containerRect.left); | ||||||
|  |   console.log('Content position in tab:', contentRect.left - tabRect.left); | ||||||
|  |   console.log('Content relative to wrapper:', contentRect.left - wrapperRect.left); | ||||||
|  |   console.log('Indicator relative to wrapper (actual):', indicatorRect.left - wrapperRect.left); | ||||||
|  |    | ||||||
|  |   console.log('\n=== WIDTHS ==='); | ||||||
|  |   console.log('Tab width:', tabRect.width); | ||||||
|  |   console.log('Content width:', contentRect.width); | ||||||
|  |   console.log('Indicator width:', indicatorRect.width); | ||||||
|  |    | ||||||
|  |   console.log('\n=== STYLES (what we set) ==='); | ||||||
|  |   console.log('Indicator style.left:', indicator.style.left); | ||||||
|  |   console.log('Indicator style.width:', indicator.style.width); | ||||||
|  |    | ||||||
|  |   console.log('\n=== CALCULATIONS ==='); | ||||||
|  |   const expectedIndicatorLeft = contentRect.left - wrapperRect.left - 4; // We subtract 4 to center | ||||||
|  |   const expectedIndicatorWidth = contentRect.width + 8; // We add 8 in the code | ||||||
|  |   console.log('Expected indicator left:', expectedIndicatorLeft); | ||||||
|  |   console.log('Expected indicator width:', expectedIndicatorWidth); | ||||||
|  |   console.log('Actual indicator left (from style):', parseFloat(indicator.style.left)); | ||||||
|  |   console.log('Actual indicator width (from style):', parseFloat(indicator.style.width)); | ||||||
|  |    | ||||||
|  |   console.log('\n=== VISUAL ALIGNMENT CHECK ==='); | ||||||
|  |   const tabCenter = tabRect.left + (tabRect.width / 2); | ||||||
|  |   const contentCenter = contentRect.left + (contentRect.width / 2); | ||||||
|  |   const indicatorCenter = indicatorRect.left + (indicatorRect.width / 2); | ||||||
|  |    | ||||||
|  |   console.log('Tab center:', tabCenter); | ||||||
|  |   console.log('Content center:', contentCenter); | ||||||
|  |   console.log('Indicator center:', indicatorCenter); | ||||||
|  |   console.log('Content offset from tab center:', contentCenter - tabCenter); | ||||||
|  |   console.log('Indicator offset from content center:', indicatorCenter - contentCenter); | ||||||
|  |   console.log('Indicator offset from tab center:', indicatorCenter - tabCenter); | ||||||
|  |   console.log('---'); | ||||||
|  |   console.log('Indicator extends left of content by:', contentRect.left - indicatorRect.left); | ||||||
|  |   console.log('Indicator extends right of content by:', (indicatorRect.left + indicatorRect.width) - (contentRect.left + contentRect.width)); | ||||||
|  |    | ||||||
|  |   // Check if icons are rendering | ||||||
|  |   const icon = firstContent.querySelector('dees-icon'); | ||||||
|  |   console.log('\n=== ICON CHECK ==='); | ||||||
|  |   console.log('Icon element found:', icon ? 'YES' : 'NO'); | ||||||
|  |   if (icon) { | ||||||
|  |     const iconRect = icon.getBoundingClientRect(); | ||||||
|  |     console.log('Icon width:', iconRect.width); | ||||||
|  |     console.log('Icon height:', iconRect.height); | ||||||
|  |     console.log('Icon visible:', iconRect.width > 0 && iconRect.height > 0 ? 'YES' : 'NO'); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Verify indicator is visible | ||||||
|  |   expect(indicator.style.opacity).toEqual('1'); | ||||||
|  |    | ||||||
|  |   // Verify positioning calculations | ||||||
|  |   expect(parseFloat(indicator.style.left)).toBeCloseTo(expectedIndicatorLeft, 1); | ||||||
|  |   expect(parseFloat(indicator.style.width)).toBeCloseTo(expectedIndicatorWidth, 1); | ||||||
|  |    | ||||||
|  |   // Verify visual centering on content (should be perfectly centered) | ||||||
|  |   expect(Math.abs(indicatorCenter - contentCenter)).toBeLessThan(1); | ||||||
|  |    | ||||||
|  |   document.body.removeChild(tabsElement); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('tabs indicator should move when tab is clicked', async () => { | ||||||
|  |   // Create tabs element | ||||||
|  |   const tabsElement = new deesCatalog.DeesAppuiTabs(); | ||||||
|  |   tabsElement.tabs = [ | ||||||
|  |     { key: 'Home', iconName: 'lucide:home', action: () => {} }, | ||||||
|  |     { key: 'Analytics', iconName: 'lucide:barChart', action: () => {} }, | ||||||
|  |     { key: 'Settings', iconName: 'lucide:settings', action: () => {} }, | ||||||
|  |   ]; | ||||||
|  |    | ||||||
|  |   document.body.appendChild(tabsElement); | ||||||
|  |   await tabsElement.updateComplete; | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   const shadowRoot = tabsElement.shadowRoot; | ||||||
|  |   const tabs = shadowRoot.querySelectorAll('.tab'); | ||||||
|  |   const indicator = shadowRoot.querySelector('.tabIndicator') as HTMLElement; | ||||||
|  |    | ||||||
|  |   // Get initial position | ||||||
|  |   const initialLeft = parseFloat(indicator.style.left); | ||||||
|  |    | ||||||
|  |   // Click second tab | ||||||
|  |   (tabs[1] as HTMLElement).click(); | ||||||
|  |   await tabsElement.updateComplete; | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Position should have changed | ||||||
|  |   const newLeft = parseFloat(indicator.style.left); | ||||||
|  |   expect(newLeft).not.toEqual(initialLeft); | ||||||
|  |   expect(newLeft).toBeGreaterThan(initialLeft); | ||||||
|  |    | ||||||
|  |   document.body.removeChild(tabsElement); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										85
									
								
								test/test.wysiwyg-blockmovement.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								test/test.wysiwyg-blockmovement.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg block movement during drag', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Block 1' }, | ||||||
|  |     { id: 'block2', type: 'paragraph', content: 'Block 2' }, | ||||||
|  |     { id: 'block3', type: 'paragraph', content: 'Block 3' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |   const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |    | ||||||
|  |   // Start dragging block 1 | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: () => {}, | ||||||
|  |       setDragImage: () => {} | ||||||
|  |     }, | ||||||
|  |     clientY: 50, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |    | ||||||
|  |   // Wait for dragging class | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 20)); | ||||||
|  |    | ||||||
|  |   // Verify drag state | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); | ||||||
|  |    | ||||||
|  |   // Check that drag height was calculated | ||||||
|  |   console.log('Checking drag height...'); | ||||||
|  |   const dragHandler = element.dragDropHandler as any; | ||||||
|  |   console.log('draggedBlockHeight:', dragHandler.draggedBlockHeight); | ||||||
|  |   console.log('draggedBlockContentHeight:', dragHandler.draggedBlockContentHeight); | ||||||
|  |    | ||||||
|  |   // Manually call updateBlockPositions to simulate drag movement | ||||||
|  |   console.log('Simulating drag movement...'); | ||||||
|  |   const updateBlockPositions = dragHandler.updateBlockPositions.bind(dragHandler); | ||||||
|  |    | ||||||
|  |   // Simulate dragging down past block 2 | ||||||
|  |   const block2 = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement; | ||||||
|  |   const block2Rect = block2.getBoundingClientRect(); | ||||||
|  |   const dragToY = block2Rect.bottom + 10; | ||||||
|  |    | ||||||
|  |   console.log('Dragging to Y position:', dragToY); | ||||||
|  |   updateBlockPositions(dragToY); | ||||||
|  |    | ||||||
|  |   // Check if blocks have moved | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 50)); | ||||||
|  |    | ||||||
|  |   const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper')); | ||||||
|  |   console.log('Block states after drag:'); | ||||||
|  |   blocks.forEach((block, i) => { | ||||||
|  |     const classes = block.className; | ||||||
|  |     const offset = (block as HTMLElement).style.getPropertyValue('--drag-offset'); | ||||||
|  |     console.log(`Block ${i}: classes="${classes}", offset="${offset}"`); | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Check that at least one block has move class | ||||||
|  |   const movedUpBlocks = editorContent.querySelectorAll('.block-wrapper.move-up'); | ||||||
|  |   const movedDownBlocks = editorContent.querySelectorAll('.block-wrapper.move-down'); | ||||||
|  |   console.log('Moved up blocks:', movedUpBlocks.length); | ||||||
|  |   console.log('Moved down blocks:', movedDownBlocks.length); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   element.dragDropHandler.handleDragEnd(); | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										98
									
								
								test/test.wysiwyg-blocktype-change.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								test/test.wysiwyg-blocktype-change.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  | import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js'; | ||||||
|  |  | ||||||
|  | tap.test('should change block type via context menu', async () => { | ||||||
|  |   // Create WYSIWYG editor with a paragraph | ||||||
|  |   const wysiwygEditor = new DeesInputWysiwyg(); | ||||||
|  |   wysiwygEditor.value = '<p>This is a test paragraph</p>'; | ||||||
|  |   document.body.appendChild(wysiwygEditor); | ||||||
|  |    | ||||||
|  |   // Wait for editor to be ready | ||||||
|  |   await wysiwygEditor.updateComplete; | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Get the first block | ||||||
|  |   const firstBlock = wysiwygEditor.blocks[0]; | ||||||
|  |   expect(firstBlock.type).toEqual('paragraph'); | ||||||
|  |    | ||||||
|  |   // Get the block element | ||||||
|  |   const firstBlockWrapper = wysiwygEditor.shadowRoot!.querySelector('.block-wrapper'); | ||||||
|  |   expect(firstBlockWrapper).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   const blockComponent = firstBlockWrapper!.querySelector('dees-wysiwyg-block') as any; | ||||||
|  |   expect(blockComponent).toBeTruthy(); | ||||||
|  |   await blockComponent.updateComplete; | ||||||
|  |    | ||||||
|  |   // Get the editable content inside the block's shadow DOM | ||||||
|  |   const editableBlock = blockComponent.shadowRoot!.querySelector('.block'); | ||||||
|  |   expect(editableBlock).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Simulate right-click on the editable block | ||||||
|  |   const contextMenuEvent = new MouseEvent('contextmenu', { | ||||||
|  |     clientX: 200, | ||||||
|  |     clientY: 200, | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true, | ||||||
|  |     composed: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   editableBlock!.dispatchEvent(contextMenuEvent); | ||||||
|  |    | ||||||
|  |   // Wait for context menu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Check if context menu is created | ||||||
|  |   const contextMenu = document.querySelector('dees-contextmenu'); | ||||||
|  |   expect(contextMenu).toBeInstanceOf(DeesContextmenu); | ||||||
|  |    | ||||||
|  |   // Find "Change Type" menu item | ||||||
|  |   const menuItems = Array.from(contextMenu!.shadowRoot!.querySelectorAll('.menuitem')); | ||||||
|  |   const changeTypeItem = menuItems.find(item =>  | ||||||
|  |     item.querySelector('.menuitem-text')?.textContent?.trim() === 'Change Type' | ||||||
|  |   ); | ||||||
|  |   expect(changeTypeItem).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Hover over "Change Type" to trigger submenu | ||||||
|  |   changeTypeItem!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true })); | ||||||
|  |    | ||||||
|  |   // Wait for submenu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 300)); | ||||||
|  |    | ||||||
|  |   // Check if submenu is created | ||||||
|  |   const allMenus = document.querySelectorAll('dees-contextmenu'); | ||||||
|  |   expect(allMenus.length).toEqual(2); | ||||||
|  |    | ||||||
|  |   const submenu = allMenus[1]; | ||||||
|  |   const submenuItems = Array.from(submenu.shadowRoot!.querySelectorAll('.menuitem')); | ||||||
|  |    | ||||||
|  |   // Find "Heading 1" option | ||||||
|  |   const heading1Item = submenuItems.find(item =>  | ||||||
|  |     item.querySelector('.menuitem-text')?.textContent?.trim() === 'Heading 1' | ||||||
|  |   ); | ||||||
|  |   expect(heading1Item).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Click on "Heading 1" | ||||||
|  |   (heading1Item as HTMLElement).click(); | ||||||
|  |    | ||||||
|  |   // Wait for menu to close and block to update | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 300)); | ||||||
|  |    | ||||||
|  |   // Verify block type has changed | ||||||
|  |   expect(wysiwygEditor.blocks[0].type).toEqual('heading-1'); | ||||||
|  |    | ||||||
|  |   // Verify DOM has been updated | ||||||
|  |   const updatedBlockComponent = wysiwygEditor.shadowRoot! | ||||||
|  |     .querySelector('.block-wrapper')! | ||||||
|  |     .querySelector('dees-wysiwyg-block') as any; | ||||||
|  |    | ||||||
|  |   await updatedBlockComponent.updateComplete; | ||||||
|  |    | ||||||
|  |   const updatedBlock = updatedBlockComponent.shadowRoot!.querySelector('.block'); | ||||||
|  |   expect(updatedBlock?.classList.contains('heading-1')).toEqual(true); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   wysiwygEditor.remove(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										68
									
								
								test/test.wysiwyg-contextmenu.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								test/test.wysiwyg-contextmenu.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  | import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js'; | ||||||
|  |  | ||||||
|  | tap.test('should show context menu on WYSIWYG blocks', async () => { | ||||||
|  |   // Create WYSIWYG editor | ||||||
|  |   const wysiwygEditor = new DeesInputWysiwyg(); | ||||||
|  |   wysiwygEditor.value = '<p>Test paragraph</p><h1>Test heading</h1>'; | ||||||
|  |   document.body.appendChild(wysiwygEditor); | ||||||
|  |    | ||||||
|  |   // Wait for editor to be ready | ||||||
|  |   await wysiwygEditor.updateComplete; | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Get the first block element | ||||||
|  |   const firstBlockWrapper = wysiwygEditor.shadowRoot!.querySelector('.block-wrapper'); | ||||||
|  |   expect(firstBlockWrapper).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   const blockComponent = firstBlockWrapper!.querySelector('dees-wysiwyg-block') as any; | ||||||
|  |   expect(blockComponent).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Wait for block to be ready | ||||||
|  |   await blockComponent.updateComplete; | ||||||
|  |    | ||||||
|  |   // Get the editable content inside the block's shadow DOM | ||||||
|  |   const editableBlock = blockComponent.shadowRoot!.querySelector('.block'); | ||||||
|  |   expect(editableBlock).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Simulate right-click on the editable block | ||||||
|  |   const contextMenuEvent = new MouseEvent('contextmenu', { | ||||||
|  |     clientX: 200, | ||||||
|  |     clientY: 200, | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true, | ||||||
|  |     composed: true // Important for shadow DOM | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   editableBlock!.dispatchEvent(contextMenuEvent); | ||||||
|  |    | ||||||
|  |   // Wait for context menu to appear | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |    | ||||||
|  |   // Check if context menu is created | ||||||
|  |   const contextMenu = document.querySelector('dees-contextmenu'); | ||||||
|  |   expect(contextMenu).toBeInstanceOf(DeesContextmenu); | ||||||
|  |    | ||||||
|  |   // Check if menu items from WYSIWYG block are rendered | ||||||
|  |   const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem'); | ||||||
|  |   const menuTexts = Array.from(menuItems).map(item =>  | ||||||
|  |     item.querySelector('.menuitem-text')?.textContent?.trim() | ||||||
|  |   ); | ||||||
|  |    | ||||||
|  |   // Should have "Change Type" and "Delete Block" items | ||||||
|  |   expect(menuTexts).toContain('Change Type'); | ||||||
|  |   expect(menuTexts).toContain('Delete Block'); | ||||||
|  |    | ||||||
|  |   // Check if "Change Type" has submenu indicator | ||||||
|  |   const changeTypeItem = Array.from(menuItems).find(item =>  | ||||||
|  |     item.querySelector('.menuitem-text')?.textContent?.trim() === 'Change Type' | ||||||
|  |   ); | ||||||
|  |   expect(changeTypeItem?.classList.contains('has-submenu')).toEqual(true); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   contextMenu!.remove(); | ||||||
|  |   wysiwygEditor.remove(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
							
								
								
									
										95
									
								
								test/test.wysiwyg-dragdrop-simple.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								test/test.wysiwyg-dragdrop-simple.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag handler initialization', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   // Wait for element to be ready | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Check that drag handler is initialized | ||||||
|  |   expect(element.dragDropHandler).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Set initial content with multiple blocks | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'First paragraph' }, | ||||||
|  |     { id: 'block2', type: 'paragraph', content: 'Second paragraph' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Check that editor content ref exists | ||||||
|  |   console.log('editorContentRef:', element.editorContentRef); | ||||||
|  |   expect(element.editorContentRef).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Check that blocks are rendered | ||||||
|  |   const blockWrappers = element.shadowRoot!.querySelectorAll('.block-wrapper'); | ||||||
|  |   console.log('Number of block wrappers:', blockWrappers.length); | ||||||
|  |   expect(blockWrappers.length).toEqual(2); | ||||||
|  |    | ||||||
|  |   // Check drag handles | ||||||
|  |   const dragHandles = element.shadowRoot!.querySelectorAll('.drag-handle'); | ||||||
|  |   console.log('Number of drag handles:', dragHandles.length); | ||||||
|  |   expect(dragHandles.length).toEqual(2); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag start behavior', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Test block' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const dragHandle = element.shadowRoot!.querySelector('.drag-handle') as HTMLElement; | ||||||
|  |   expect(dragHandle).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Check that drag handle has draggable attribute | ||||||
|  |   console.log('Drag handle draggable:', dragHandle.draggable); | ||||||
|  |   expect(dragHandle.draggable).toBeTrue(); | ||||||
|  |    | ||||||
|  |   // Test drag handler state before drag | ||||||
|  |   console.log('Initial drag state:', element.dragDropHandler.dragState); | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull(); | ||||||
|  |    | ||||||
|  |   // Try to manually call handleDragStart | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: (type: string, data: string) => { | ||||||
|  |         console.log('setData called with:', type, data); | ||||||
|  |       }, | ||||||
|  |       setDragImage: (img: any, x: number, y: number) => { | ||||||
|  |         console.log('setDragImage called'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     clientY: 100, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |    | ||||||
|  |   // Check drag state after drag start | ||||||
|  |   console.log('Drag state after start:', element.dragDropHandler.dragState); | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   element.dragDropHandler.handleDragEnd(); | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										133
									
								
								test/test.wysiwyg-dragdrop-visual.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								test/test.wysiwyg-dragdrop-visual.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag visual feedback - block movement', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Block 1' }, | ||||||
|  |     { id: 'block2', type: 'paragraph', content: 'Block 2' }, | ||||||
|  |     { id: 'block3', type: 'paragraph', content: 'Block 3' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |   const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |    | ||||||
|  |   // Manually start drag | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: (type: string, data: string) => {}, | ||||||
|  |       setDragImage: (img: any, x: number, y: number) => {} | ||||||
|  |     }, | ||||||
|  |     clientY: 50, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |    | ||||||
|  |   // Wait for dragging class | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 20)); | ||||||
|  |    | ||||||
|  |   // Check dragging state | ||||||
|  |   console.log('Block 1 classes:', block1.className); | ||||||
|  |   console.log('Editor content classes:', editorContent.className); | ||||||
|  |   expect(block1.classList.contains('dragging')).toBeTrue(); | ||||||
|  |   expect(editorContent.classList.contains('dragging')).toBeTrue(); | ||||||
|  |    | ||||||
|  |   // Check drop indicator exists | ||||||
|  |   const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement; | ||||||
|  |   console.log('Drop indicator:', dropIndicator); | ||||||
|  |   expect(dropIndicator).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Test block movement calculation | ||||||
|  |   console.log('Testing updateBlockPositions...'); | ||||||
|  |    | ||||||
|  |   // Access private method for testing | ||||||
|  |   const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler); | ||||||
|  |    | ||||||
|  |   // Simulate dragging to different position | ||||||
|  |   updateBlockPositions(150); // Move down | ||||||
|  |    | ||||||
|  |   // Check if blocks have move classes | ||||||
|  |   const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper')); | ||||||
|  |   console.log('Block classes after move:'); | ||||||
|  |   blocks.forEach((block, i) => { | ||||||
|  |     console.log(`Block ${i}:`, block.className, 'transform:', (block as HTMLElement).style.getPropertyValue('--drag-offset')); | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   element.dragDropHandler.handleDragEnd(); | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drop indicator positioning', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Paragraph 1' }, | ||||||
|  |     { id: 'block2', type: 'heading-2', content: 'Heading 2' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |    | ||||||
|  |   // Start dragging first block | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: (type: string, data: string) => {}, | ||||||
|  |       setDragImage: (img: any, x: number, y: number) => {} | ||||||
|  |     }, | ||||||
|  |     clientY: 50, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |    | ||||||
|  |   // Wait for initialization | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 20)); | ||||||
|  |    | ||||||
|  |   // Get drop indicator | ||||||
|  |   const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement; | ||||||
|  |   expect(dropIndicator).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Check initial display state | ||||||
|  |   console.log('Drop indicator initial display:', dropIndicator.style.display); | ||||||
|  |    | ||||||
|  |   // Trigger updateBlockPositions to see drop indicator | ||||||
|  |   const updateBlockPositions = element.dragDropHandler['updateBlockPositions'].bind(element.dragDropHandler); | ||||||
|  |   updateBlockPositions(100); | ||||||
|  |    | ||||||
|  |   // Check drop indicator position | ||||||
|  |   console.log('Drop indicator after update:'); | ||||||
|  |   console.log('- display:', dropIndicator.style.display); | ||||||
|  |   console.log('- top:', dropIndicator.style.top); | ||||||
|  |   console.log('- height:', dropIndicator.style.height); | ||||||
|  |    | ||||||
|  |   expect(dropIndicator.style.display).toEqual('block'); | ||||||
|  |   expect(dropIndicator.style.top).toBeTruthy(); | ||||||
|  |   expect(dropIndicator.style.height).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   element.dragDropHandler.handleDragEnd(); | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										172
									
								
								test/test.wysiwyg-dragdrop.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								test/test.wysiwyg-dragdrop.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag and drop should work correctly', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   // Wait for element to be ready | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content with multiple blocks | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'First paragraph' }, | ||||||
|  |     { id: 'block2', type: 'heading-2', content: 'Test Heading' }, | ||||||
|  |     { id: 'block3', type: 'paragraph', content: 'Second paragraph' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Check that blocks are rendered | ||||||
|  |   const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |   expect(editorContent).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   const blockWrappers = editorContent.querySelectorAll('.block-wrapper'); | ||||||
|  |   expect(blockWrappers.length).toEqual(3); | ||||||
|  |    | ||||||
|  |   // Test drag handles exist for non-divider blocks | ||||||
|  |   const dragHandles = editorContent.querySelectorAll('.drag-handle'); | ||||||
|  |   expect(dragHandles.length).toEqual(3); | ||||||
|  |    | ||||||
|  |   // Get references to specific blocks | ||||||
|  |   const firstBlock = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |   const secondBlock = editorContent.querySelector('[data-block-id="block2"]') as HTMLElement; | ||||||
|  |   const firstDragHandle = firstBlock.querySelector('.drag-handle') as HTMLElement; | ||||||
|  |    | ||||||
|  |   expect(firstBlock).toBeTruthy(); | ||||||
|  |   expect(secondBlock).toBeTruthy(); | ||||||
|  |   expect(firstDragHandle).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Test drag initialization | ||||||
|  |   console.log('Testing drag initialization...'); | ||||||
|  |    | ||||||
|  |   // Create drag event | ||||||
|  |   const dragStartEvent = new DragEvent('dragstart', { | ||||||
|  |     dataTransfer: new DataTransfer(), | ||||||
|  |     clientY: 100, | ||||||
|  |     bubbles: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Simulate drag start | ||||||
|  |   firstDragHandle.dispatchEvent(dragStartEvent); | ||||||
|  |    | ||||||
|  |   // Check that drag state is initialized | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); | ||||||
|  |    | ||||||
|  |   // Check that dragging class is applied | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 20)); // Wait for setTimeout in drag start | ||||||
|  |   expect(firstBlock.classList.contains('dragging')).toBeTrue(); | ||||||
|  |   expect(editorContent.classList.contains('dragging')).toBeTrue(); | ||||||
|  |    | ||||||
|  |   // Test drop indicator creation | ||||||
|  |   const dropIndicator = editorContent.querySelector('.drop-indicator'); | ||||||
|  |   expect(dropIndicator).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Simulate drag over | ||||||
|  |   const dragOverEvent = new DragEvent('dragover', { | ||||||
|  |     dataTransfer: new DataTransfer(), | ||||||
|  |     clientY: 200, | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   document.dispatchEvent(dragOverEvent); | ||||||
|  |    | ||||||
|  |   // Check that blocks move out of the way | ||||||
|  |   console.log('Checking block movements...'); | ||||||
|  |   const blocks = Array.from(editorContent.querySelectorAll('.block-wrapper')); | ||||||
|  |   const hasMovedBlocks = blocks.some(block =>  | ||||||
|  |     block.classList.contains('move-up') || block.classList.contains('move-down') | ||||||
|  |   ); | ||||||
|  |    | ||||||
|  |   console.log('Blocks with move classes:', blocks.filter(block =>  | ||||||
|  |     block.classList.contains('move-up') || block.classList.contains('move-down') | ||||||
|  |   ).length); | ||||||
|  |    | ||||||
|  |   // Test drag end | ||||||
|  |   const dragEndEvent = new DragEvent('dragend', { | ||||||
|  |     bubbles: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   document.dispatchEvent(dragEndEvent); | ||||||
|  |    | ||||||
|  |   // Wait for cleanup | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 150)); | ||||||
|  |    | ||||||
|  |   // Check that drag state is cleaned up | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toBeNull(); | ||||||
|  |   expect(firstBlock.classList.contains('dragging')).toBeFalse(); | ||||||
|  |   expect(editorContent.classList.contains('dragging')).toBeFalse(); | ||||||
|  |    | ||||||
|  |   // Check that drop indicator is removed | ||||||
|  |   const dropIndicatorAfter = editorContent.querySelector('.drop-indicator'); | ||||||
|  |   expect(dropIndicatorAfter).toBeFalsy(); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag and drop visual feedback', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Block 1' }, | ||||||
|  |     { id: 'block2', type: 'paragraph', content: 'Block 2' }, | ||||||
|  |     { id: 'block3', type: 'paragraph', content: 'Block 3' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |   const block1 = editorContent.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |   const dragHandle1 = block1.querySelector('.drag-handle') as HTMLElement; | ||||||
|  |    | ||||||
|  |   // Start dragging block 1 | ||||||
|  |   const dragStartEvent = new DragEvent('dragstart', { | ||||||
|  |     dataTransfer: new DataTransfer(), | ||||||
|  |     clientY: 50, | ||||||
|  |     bubbles: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   dragHandle1.dispatchEvent(dragStartEvent); | ||||||
|  |    | ||||||
|  |   // Wait for dragging class | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 20)); | ||||||
|  |    | ||||||
|  |   // Simulate dragging down | ||||||
|  |   const dragOverEvent = new DragEvent('dragover', { | ||||||
|  |     dataTransfer: new DataTransfer(), | ||||||
|  |     clientY: 150, // Move down past block 2 | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Trigger the global drag over handler | ||||||
|  |   element.dragDropHandler['handleGlobalDragOver'](dragOverEvent); | ||||||
|  |    | ||||||
|  |   // Check that transform is applied to dragged block | ||||||
|  |   const transform = block1.style.transform; | ||||||
|  |   console.log('Dragged block transform:', transform); | ||||||
|  |   expect(transform).toContain('translateY'); | ||||||
|  |    | ||||||
|  |   // Check drop indicator position | ||||||
|  |   const dropIndicator = editorContent.querySelector('.drop-indicator') as HTMLElement; | ||||||
|  |   if (dropIndicator) { | ||||||
|  |     const indicatorStyle = dropIndicator.style; | ||||||
|  |     console.log('Drop indicator position:', indicatorStyle.top, 'display:', indicatorStyle.display); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										124
									
								
								test/test.wysiwyg-dragissue.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								test/test.wysiwyg-dragissue.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag full flow without await', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Test block' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Mock drag event | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: (type: string, data: string) => { | ||||||
|  |         console.log('setData:', type, data); | ||||||
|  |       }, | ||||||
|  |       setDragImage: (img: any, x: number, y: number) => { | ||||||
|  |         console.log('setDragImage'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     clientY: 100, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   console.log('Starting drag...'); | ||||||
|  |   element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |   console.log('Drag started'); | ||||||
|  |    | ||||||
|  |   // Check immediate state | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); | ||||||
|  |    | ||||||
|  |   // Instead of await with setTimeout, use a done callback | ||||||
|  |   return new Promise<void>((resolve) => { | ||||||
|  |     console.log('Setting up delayed check...'); | ||||||
|  |      | ||||||
|  |     // Use regular setTimeout | ||||||
|  |     setTimeout(() => { | ||||||
|  |       console.log('In setTimeout callback'); | ||||||
|  |        | ||||||
|  |       try { | ||||||
|  |         const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |         const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |          | ||||||
|  |         console.log('Block has dragging class:', block1?.classList.contains('dragging')); | ||||||
|  |         console.log('Editor has dragging class:', editorContent?.classList.contains('dragging')); | ||||||
|  |          | ||||||
|  |         // Clean up | ||||||
|  |         element.dragDropHandler.handleDragEnd(); | ||||||
|  |         document.body.removeChild(element); | ||||||
|  |          | ||||||
|  |         resolve(); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('Error in setTimeout:', error); | ||||||
|  |         throw error; | ||||||
|  |       } | ||||||
|  |     }, 50); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('identify the crash point', async () => { | ||||||
|  |   console.log('Test started'); | ||||||
|  |    | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   console.log('Element created'); | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   console.log('Setting blocks'); | ||||||
|  |   element.blocks = [{ id: 'block1', type: 'paragraph', content: 'Test' }]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   console.log('Waiting for update'); | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   console.log('Creating mock event'); | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: () => {}, | ||||||
|  |       setDragImage: () => {} | ||||||
|  |     }, | ||||||
|  |     clientY: 100, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   console.log('Calling handleDragStart'); | ||||||
|  |   element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |    | ||||||
|  |   console.log('handleDragStart completed'); | ||||||
|  |    | ||||||
|  |   // Try different wait methods | ||||||
|  |   console.log('About to wait...'); | ||||||
|  |    | ||||||
|  |   // Method 1: Direct promise | ||||||
|  |   await Promise.resolve(); | ||||||
|  |   console.log('Promise.resolve completed'); | ||||||
|  |    | ||||||
|  |   // Method 2: setTimeout 0 | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 0)); | ||||||
|  |   console.log('setTimeout 0 completed'); | ||||||
|  |    | ||||||
|  |   // Method 3: requestAnimationFrame | ||||||
|  |   await new Promise(resolve => requestAnimationFrame(() => resolve(undefined))); | ||||||
|  |   console.log('requestAnimationFrame completed'); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   element.dragDropHandler.handleDragEnd(); | ||||||
|  |   document.body.removeChild(element); | ||||||
|  |   console.log('Cleanup completed'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										108
									
								
								test/test.wysiwyg-dropindicator.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								test/test.wysiwyg-dropindicator.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drop indicator creation', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Test block' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Check editorContentRef | ||||||
|  |   console.log('editorContentRef exists:', !!element.editorContentRef); | ||||||
|  |   console.log('editorContentRef tagName:', element.editorContentRef?.tagName); | ||||||
|  |   expect(element.editorContentRef).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Check initial state - no drop indicator | ||||||
|  |   let dropIndicator = element.shadowRoot!.querySelector('.drop-indicator'); | ||||||
|  |   console.log('Drop indicator before drag:', dropIndicator); | ||||||
|  |   expect(dropIndicator).toBeFalsy(); | ||||||
|  |    | ||||||
|  |   // Manually call createDropIndicator | ||||||
|  |   try { | ||||||
|  |     console.log('Calling createDropIndicator...'); | ||||||
|  |     element.dragDropHandler['createDropIndicator'](); | ||||||
|  |     console.log('createDropIndicator succeeded'); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error creating drop indicator:', error); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Check drop indicator was created | ||||||
|  |   dropIndicator = element.shadowRoot!.querySelector('.drop-indicator'); | ||||||
|  |   console.log('Drop indicator after creation:', dropIndicator); | ||||||
|  |   console.log('Drop indicator parent:', dropIndicator?.parentElement?.className); | ||||||
|  |   expect(dropIndicator).toBeTruthy(); | ||||||
|  |   expect(dropIndicator!.style.display).toEqual('none'); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg drag initialization with drop indicator', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Test block' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Mock drag event | ||||||
|  |   const mockDragEvent = { | ||||||
|  |     dataTransfer: { | ||||||
|  |       effectAllowed: '', | ||||||
|  |       setData: (type: string, data: string) => { | ||||||
|  |         console.log('setData:', type, data); | ||||||
|  |       }, | ||||||
|  |       setDragImage: (img: any, x: number, y: number) => { | ||||||
|  |         console.log('setDragImage'); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     clientY: 100, | ||||||
|  |     preventDefault: () => {}, | ||||||
|  |   } as any; | ||||||
|  |    | ||||||
|  |   console.log('Starting drag...'); | ||||||
|  |    | ||||||
|  |   try { | ||||||
|  |     element.dragDropHandler.handleDragStart(mockDragEvent, element.blocks[0]); | ||||||
|  |     console.log('Drag start succeeded'); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error during drag start:', error); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Wait for async operations | ||||||
|  |   await new Promise(resolve => setTimeout(resolve, 20)); | ||||||
|  |    | ||||||
|  |   // Check drop indicator exists | ||||||
|  |   const dropIndicator = element.shadowRoot!.querySelector('.drop-indicator'); | ||||||
|  |   console.log('Drop indicator after drag start:', dropIndicator); | ||||||
|  |   expect(dropIndicator).toBeTruthy(); | ||||||
|  |    | ||||||
|  |   // Check drag state | ||||||
|  |   console.log('Drag state:', element.dragDropHandler.dragState); | ||||||
|  |   expect(element.dragDropHandler.dragState.draggedBlockId).toEqual('block1'); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   element.dragDropHandler.handleDragEnd(); | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
							
								
								
									
										114
									
								
								test/test.wysiwyg-eventlisteners.browser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								test/test.wysiwyg-eventlisteners.browser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||||
|  | import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  |  | ||||||
|  | // Initialize the element | ||||||
|  | DeesInputWysiwyg; | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg global event listeners', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Test block' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |   console.log('Block 1 found:', !!block1); | ||||||
|  |    | ||||||
|  |   // Set up drag state manually without using handleDragStart | ||||||
|  |   element.dragDropHandler['draggedBlockId'] = 'block1'; | ||||||
|  |   element.dragDropHandler['draggedBlockElement'] = block1; | ||||||
|  |   element.dragDropHandler['initialMouseY'] = 100; | ||||||
|  |    | ||||||
|  |   // Create drop indicator manually | ||||||
|  |   element.dragDropHandler['createDropIndicator'](); | ||||||
|  |    | ||||||
|  |   // Test adding global event listeners | ||||||
|  |   console.log('Adding event listeners...'); | ||||||
|  |   const handleGlobalDragOver = element.dragDropHandler['handleGlobalDragOver']; | ||||||
|  |   const handleGlobalDragEnd = element.dragDropHandler['handleGlobalDragEnd']; | ||||||
|  |    | ||||||
|  |   try { | ||||||
|  |     document.addEventListener('dragover', handleGlobalDragOver); | ||||||
|  |     console.log('dragover listener added'); | ||||||
|  |      | ||||||
|  |     document.addEventListener('dragend', handleGlobalDragEnd); | ||||||
|  |     console.log('dragend listener added'); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error adding event listeners:', error); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Test firing a dragover event | ||||||
|  |   console.log('Creating dragover event...'); | ||||||
|  |   const dragOverEvent = new Event('dragover', { | ||||||
|  |     bubbles: true, | ||||||
|  |     cancelable: true | ||||||
|  |   }); | ||||||
|  |   Object.defineProperty(dragOverEvent, 'clientY', { value: 150 }); | ||||||
|  |    | ||||||
|  |   console.log('Dispatching dragover event...'); | ||||||
|  |   document.dispatchEvent(dragOverEvent); | ||||||
|  |   console.log('dragover event dispatched'); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   document.removeEventListener('dragover', handleGlobalDragOver); | ||||||
|  |   document.removeEventListener('dragend', handleGlobalDragEnd); | ||||||
|  |    | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('wysiwyg setTimeout in drag start', async () => { | ||||||
|  |   const element = document.createElement('dees-input-wysiwyg'); | ||||||
|  |   document.body.appendChild(element); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   // Set initial content | ||||||
|  |   element.blocks = [ | ||||||
|  |     { id: 'block1', type: 'paragraph', content: 'Test block' }, | ||||||
|  |   ]; | ||||||
|  |   element.renderBlocksProgrammatically(); | ||||||
|  |    | ||||||
|  |   await element.updateComplete; | ||||||
|  |    | ||||||
|  |   const block1 = element.shadowRoot!.querySelector('[data-block-id="block1"]') as HTMLElement; | ||||||
|  |   const editorContent = element.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|  |    | ||||||
|  |   // Set drag state | ||||||
|  |   element.dragDropHandler['draggedBlockId'] = 'block1'; | ||||||
|  |   element.dragDropHandler['draggedBlockElement'] = block1; | ||||||
|  |    | ||||||
|  |   console.log('Testing setTimeout callback...'); | ||||||
|  |    | ||||||
|  |   // Test the setTimeout callback directly | ||||||
|  |   try { | ||||||
|  |     if (block1) { | ||||||
|  |       console.log('Adding dragging class to block...'); | ||||||
|  |       block1.classList.add('dragging'); | ||||||
|  |       console.log('Block classes:', block1.className); | ||||||
|  |     } | ||||||
|  |     if (editorContent) { | ||||||
|  |       console.log('Adding dragging class to editor...'); | ||||||
|  |       editorContent.classList.add('dragging'); | ||||||
|  |       console.log('Editor classes:', editorContent.className); | ||||||
|  |     } | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error in setTimeout callback:', error); | ||||||
|  |     throw error; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   expect(block1.classList.contains('dragging')).toBeTrue(); | ||||||
|  |   expect(editorContent.classList.contains('dragging')).toBeTrue(); | ||||||
|  |    | ||||||
|  |   // Clean up | ||||||
|  |   document.body.removeChild(element); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.start(); | ||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@design.estate/dees-catalog', |   name: '@design.estate/dees-catalog', | ||||||
|   version: '1.10.1', |   version: '1.12.6', | ||||||
|   description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' |   description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								ts_web/elements/00fonts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ts_web/elements/00fonts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import { unsafeCSS } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Geist Sans font family - Main font for the design system | ||||||
|  |  * Already available in the environment, no need to load | ||||||
|  |  */ | ||||||
|  | export const geistSansFont = 'Geist Sans'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Intel One Mono font family - Monospace font for code and technical content | ||||||
|  |  * Already available in the environment, no need to load | ||||||
|  |  */ | ||||||
|  | export const intelOneMonoFont = 'Intel One Mono'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Complete font family stacks with fallbacks | ||||||
|  |  */ | ||||||
|  | export const geistFontFamily = `'${geistSansFont}', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif`; | ||||||
|  |  | ||||||
|  | export const monoFontFamily = `'${intelOneMonoFont}', 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace`; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * CSS-ready font family values using unsafeCSS | ||||||
|  |  * Use these in component styles | ||||||
|  |  */ | ||||||
|  | export const cssGeistFontFamily = unsafeCSS(geistFontFamily); | ||||||
|  | export const cssMonoFontFamily = unsafeCSS(monoFontFamily); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Cal Sans font for headings - Display font | ||||||
|  |  * May need to be loaded separately | ||||||
|  |  */ | ||||||
|  | export const calSansFont = 'Cal Sans'; | ||||||
|  | export const calSansFontFamily = `'${calSansFont}', ${geistFontFamily}`; | ||||||
|  | export const cssCalSansFontFamily = unsafeCSS(calSansFontFamily); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Roboto Slab font for special content - Serif font | ||||||
|  |  * May need to be loaded separately | ||||||
|  |  */ | ||||||
|  | export const robotoSlabFont = 'Roboto Slab'; | ||||||
|  | export const robotoSlabFontFamily = `'${robotoSlabFont}', Georgia, serif`; | ||||||
|  | export const cssRobotoSlabFontFamily = unsafeCSS(robotoSlabFontFamily); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Base font styles that can be applied to components | ||||||
|  |  */ | ||||||
|  | export const baseFontStyles = unsafeCSS(` | ||||||
|  |   font-family: ${geistFontFamily}; | ||||||
|  |   -webkit-font-smoothing: antialiased; | ||||||
|  |   -moz-osx-font-smoothing: grayscale; | ||||||
|  |   font-feature-settings: 'cv11', 'tnum', 'cv05' 1; | ||||||
|  | `); | ||||||
| @@ -11,27 +11,46 @@ import { | |||||||
|  |  | ||||||
| import * as domtools from '@design.estate/dees-domtools'; | import * as domtools from '@design.estate/dees-domtools'; | ||||||
| import { DeesContextmenu } from './dees-contextmenu.js'; | import { DeesContextmenu } from './dees-contextmenu.js'; | ||||||
|  | import './dees-icon.js'; | ||||||
|  |  | ||||||
| @customElement('dees-appui-activitylog') | @customElement('dees-appui-activitylog') | ||||||
| export class DeesAppuiActivitylog extends DeesElement { | export class DeesAppuiActivitylog extends DeesElement { | ||||||
|   // STATIC |   // STATIC | ||||||
|   public static demo = () => html`<dees-appui-activitylog></dees-appui-activitylog>`; |   public static demo = () => html` | ||||||
|  |     <style> | ||||||
|  |       .demo-container { | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         height: 600px; | ||||||
|  |         background: ${cssManager.bdTheme('#f4f4f5', '#09090b')}; | ||||||
|  |         padding: 32px; | ||||||
|  |       } | ||||||
|  |     </style> | ||||||
|  |     <div class="demo-container"> | ||||||
|  |       <dees-appui-activitylog></dees-appui-activitylog> | ||||||
|  |     </div> | ||||||
|  |   `; | ||||||
|  |  | ||||||
|   // INSTANCE |   // INSTANCE | ||||||
|   public static styles = [ |   public static styles = [ | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     css` |     css` | ||||||
|       :host { |       :host { | ||||||
|         color: ${cssManager.bdTheme('#333', '#fff')}; |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|         position: relative; |         position: relative; | ||||||
|         display: block; |         display: block; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         max-width: 300px; |         max-width: 320px; | ||||||
|         height: 100%; |         height: 100%; | ||||||
|         background: ${cssManager.bdTheme('#f8f8f8', '#111c28')}; |         background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; | ||||||
|         font-family: 'Intel One Mono', sans-serif; |         font-family: 'Geist Mono', monospace; | ||||||
|         border-left: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; |         border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|         cursor: default; |         cursor: default; | ||||||
|  |         box-shadow: ${cssManager.bdTheme( | ||||||
|  |           '-4px 0 12px rgba(0, 0, 0, 0.02)', | ||||||
|  |           '-4px 0 12px rgba(0, 0, 0, 0.2)' | ||||||
|  |         )}; | ||||||
|       } |       } | ||||||
|       .maincontainer { |       .maincontainer { | ||||||
|         position: absolute; |         position: absolute; | ||||||
| @@ -44,108 +63,265 @@ export class DeesAppuiActivitylog extends DeesElement { | |||||||
|       .topbar { |       .topbar { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 0px; |         top: 0px; | ||||||
|         height: 32px; |         height: 40px; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         padding: 0px 12px 0px 12px; |         padding: 0px 16px; | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#0e151f')}; |         background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|         border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; |         border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .topbar .heading { |       .topbar .heading { | ||||||
|         text-align: left; |         font-weight: 600; | ||||||
|         line-height: 24px; |  | ||||||
|         padding-top: 8px; |  | ||||||
|         font-weight: 500; |  | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|         font-family: 'Geist Sans', sans-serif; |         font-family: 'Geist Sans', sans-serif; | ||||||
|         color: ${cssManager.bdTheme('#666', '#ccc')}; |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .activityContainer { |       .activityContainer { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 32px; |         top: 40px; | ||||||
|         bottom: 40px; |         bottom: 48px; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         padding: 8px 0px; |         padding: 12px 0px; | ||||||
|         overflow-y: scroll; |         overflow-y: auto; | ||||||
|  |         scrollbar-width: thin; | ||||||
|  |         scrollbar-color: ${cssManager.bdTheme('#e5e7eb', '#27272a')} transparent; | ||||||
|  |       } | ||||||
|        |        | ||||||
|  |       .activityContainer::-webkit-scrollbar { | ||||||
|  |         width: 6px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activityContainer::-webkit-scrollbar-track { | ||||||
|  |         background: transparent; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activityContainer::-webkit-scrollbar-thumb { | ||||||
|  |         background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         border-radius: 3px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activityContainer::-webkit-scrollbar-thumb:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .streamingIndicator { |       .streamingIndicator { | ||||||
|         font-size: 12px; |         font-size: 11px; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|         padding-top: 16px; |         padding: 16px; | ||||||
|         color: ${cssManager.bdTheme('#666', '#888')} |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         font-family: 'Geist Sans', sans-serif; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         letter-spacing: 0.05em; | ||||||
|  |         font-weight: 500; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         gap: 8px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .streamingIndicator::before { | ||||||
|  |         content: ''; | ||||||
|  |         width: 6px; | ||||||
|  |         height: 6px; | ||||||
|  |         background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         border-radius: 50%; | ||||||
|  |         animation: pulse 2s ease-in-out infinite; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       @keyframes pulse { | ||||||
|  |         0%, 100% { opacity: 0.4; transform: scale(0.8); } | ||||||
|  |         50% { opacity: 1; transform: scale(1.2); } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .streamingIndicator.bottom { |       .streamingIndicator.bottom { | ||||||
|         padding-top: 0px; |         padding-top: 8px; | ||||||
|         padding-bottom: 16px; |         padding-bottom: 16px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .activityentry { |       .activityentry { | ||||||
|         min-height: 30px; |         min-height: 36px; | ||||||
|         font-size: 12px; |         font-size: 13px; | ||||||
|         padding: 8px 16px; |         padding: 10px 16px; | ||||||
|         border-bottom: 1px dotted ${cssManager.bdTheme('#00000020', '#ffffff20')}; |         border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')}; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |         line-height: 1.4; | ||||||
|  |         animation: fadeIn 0.3s ease-out; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       @keyframes fadeIn { | ||||||
|  |         from { | ||||||
|  |           opacity: 0; | ||||||
|  |           transform: translateY(-4px); | ||||||
|  |         } | ||||||
|  |         to { | ||||||
|  |           opacity: 1; | ||||||
|  |           transform: translateY(0); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .activityentry:last-of-type { |       .activityentry:last-of-type { | ||||||
|         border-bottom: 1px solid transparent; |         border-bottom: none; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .activityentry:hover { |       .activityentry:hover { | ||||||
|         background: ${cssManager.bdTheme('#00000005', '#00000080')}; |         background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .timestamp { |       .timestamp { | ||||||
|         color: ${cssManager.bdTheme('#e57373', '#ff8787')}; |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         font-weight: 500; | ||||||
|  |         font-size: 12px; | ||||||
|  |         font-variant-numeric: tabular-nums; | ||||||
|  |         flex-shrink: 0; | ||||||
|  |         min-width: 45px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-icon { | ||||||
|  |         width: 28px; | ||||||
|  |         height: 28px; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         flex-shrink: 0; | ||||||
|  |         font-size: 14px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-icon.login { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.1)')}; | ||||||
|  |         color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-icon.logout { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')}; | ||||||
|  |         color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-icon.view { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')}; | ||||||
|  |         color: ${cssManager.bdTheme('#2563eb', '#3b82f6')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-icon.create { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.1)')}; | ||||||
|  |         color: ${cssManager.bdTheme('#9333ea', '#a855f7')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-icon.update { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.1)', 'rgba(251, 146, 60, 0.1)')}; | ||||||
|  |         color: ${cssManager.bdTheme('#ea580c', '#fb923c')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-text { | ||||||
|  |         flex: 1; | ||||||
|  |         color: ${cssManager.bdTheme('#18181b', '#e4e4e7')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .activity-user { | ||||||
|  |         font-weight: 600; | ||||||
|  |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .date-separator { | ||||||
|  |         padding: 12px 16px 8px; | ||||||
|  |         font-size: 11px; | ||||||
|  |         font-weight: 600; | ||||||
|  |         text-transform: uppercase; | ||||||
|  |         letter-spacing: 0.05em; | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         background: ${cssManager.bdTheme('#f9fafb', '#09090b')}; | ||||||
|  |         border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')}; | ||||||
|  |         position: sticky; | ||||||
|  |         top: 0; | ||||||
|  |         z-index: 1; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .searchbox { |       .searchbox { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         bottom: 0px; |         bottom: 0px; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 40px; |         height: 48px; | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#0e151f')}; |         background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|         border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; |         border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         padding: 8px; | ||||||
|       } |       } | ||||||
|       .searchbox input { |        | ||||||
|         color: ${cssManager.bdTheme('#333', '#fff')}; |       .search-wrapper { | ||||||
|         background: none; |         position: relative; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 40px; |         height: 32px; | ||||||
|         line-height: 32px; |       } | ||||||
|         border: none; |        | ||||||
|         padding: 4px 12px; |       .search-icon { | ||||||
|         font-family: 'Intel One Mono', sans-serif; |         position: absolute; | ||||||
|  |         left: 10px; | ||||||
|  |         top: 50%; | ||||||
|  |         transform: translateY(-50%); | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         font-size: 14px; | ||||||
|  |         pointer-events: none; | ||||||
|  |         transition: color 0.15s ease; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .searchbox input { | ||||||
|  |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |         background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         padding: 0 12px 0 36px; | ||||||
|  |         font-family: 'Geist Sans', sans-serif; | ||||||
|  |         font-size: 13px; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .searchbox input::placeholder { | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .searchbox input:focus { |       .searchbox input:focus { | ||||||
|         outline: none; |         outline: none; | ||||||
|  |         border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .searchbox input:focus ~ .search-icon, | ||||||
|  |       .search-wrapper:has(input:focus) .search-icon { | ||||||
|  |         color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .bottomShadow { |       .bottomShadow { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 32px; |         height: 24px; | ||||||
|         bottom: 40px; |         bottom: 48px; | ||||||
|         background: ${cssManager.bdTheme( |         background: ${cssManager.bdTheme( | ||||||
|           'linear-gradient(180deg, #f8f8f800 0%, #ffffff 100%)', |           'linear-gradient(180deg, transparent 0%, #fafafa 100%)', | ||||||
|           'linear-gradient(180deg, #111c2800 0%, #0e151f 100%)' |           'linear-gradient(180deg, transparent 0%, #0a0a0a 100%)' | ||||||
|         )}; |         )}; | ||||||
|         pointer-events: none; |         pointer-events: none; | ||||||
|  |         opacity: 0.8; | ||||||
|       } |       } | ||||||
|  |        | ||||||
|       .topShadow { |       .topShadow { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 32px; |         height: 24px; | ||||||
|         top: 32px; |         top: 40px; | ||||||
|         background: ${cssManager.bdTheme( |         background: ${cssManager.bdTheme( | ||||||
|           'linear-gradient(0deg, #f8f8f800 0%, #ffffff 100%)', |           'linear-gradient(0deg, transparent 0%, #fafafa 100%)', | ||||||
|           'linear-gradient(0deg, #111c2800 0%, #0e151f 100%)' |           'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)' | ||||||
|         )}; |         )}; | ||||||
|         pointer-events: none; |         pointer-events: none; | ||||||
|  |         opacity: 0.8; | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| @@ -159,86 +335,174 @@ export class DeesAppuiActivitylog extends DeesElement { | |||||||
|           <div class="heading">Activity Log</div> |           <div class="heading">Activity Log</div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="activityContainer"> |         <div class="activityContainer"> | ||||||
|           <div class="streamingIndicator">streaming...</div> |           <div class="streamingIndicator">Live Updates</div> | ||||||
|  |            | ||||||
|  |           <div class="date-separator">Today</div> | ||||||
|  |            | ||||||
|           <div class="activityentry" @contextmenu=${async eventArg => { |           <div class="activityentry" @contextmenu=${async eventArg => { | ||||||
|             DeesContextmenu.openContextMenuWithOptions(eventArg, [ |             DeesContextmenu.openContextMenuWithOptions(eventArg, [ | ||||||
|               { |               { | ||||||
|                 name: 'app settings', |                 name: 'Copy activity', | ||||||
|                 action: async () => {}, |                 action: async () => {}, | ||||||
|               }, |               }, | ||||||
|               { |               { | ||||||
|                 name: 'account settings', |                 name: 'View details', | ||||||
|                 action: async () => {}, |                 action: async () => {}, | ||||||
|               }, |               }, | ||||||
|               { |               { | ||||||
|                 name: 'logout', |                 name: 'Filter by user', | ||||||
|                 action: async () => {}, |                 action: async () => {}, | ||||||
|               }, |               }, | ||||||
|             ]); |             ]); | ||||||
|           }}> |           }}> | ||||||
|             <span class="timestamp">22:01:</span> Max Mustermann logged in |             <span class="timestamp">22:20</span> | ||||||
|  |             <div class="activity-icon logout"> | ||||||
|  |               <dees-icon .icon=${'lucide:logOut'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> logged out | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:02:</span> Max Mustermann viewed an invoice |             <span class="timestamp">22:19</span> | ||||||
|  |             <div class="activity-icon update"> | ||||||
|  |               <dees-icon .icon=${'lucide:checkCircle'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> approved a payment | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:03:</span> Max Mustermann added a new contact |             <span class="timestamp">22:18</span> | ||||||
|  |             <div class="activity-icon view"> | ||||||
|  |               <dees-icon .icon=${'lucide:archive'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> archived an invoice | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:04:</span> Max Mustermann updated account settings |             <span class="timestamp">22:17</span> | ||||||
|  |             <div class="activity-icon login"> | ||||||
|  |               <dees-icon .icon=${'lucide:logIn'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> logged in | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:05:</span> Max Mustermann logged out |             <span class="timestamp">22:16</span> | ||||||
|  |             <div class="activity-icon logout"> | ||||||
|  |               <dees-icon .icon=${'lucide:logOut'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> logged out | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:06:</span> Max Mustermann logged in |             <span class="timestamp">22:15</span> | ||||||
|  |             <div class="activity-icon update"> | ||||||
|  |               <dees-icon .icon=${'lucide:key'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> changed password | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:07:</span> Max Mustermann created a new invoice |             <span class="timestamp">22:14</span> | ||||||
|  |             <div class="activity-icon create"> | ||||||
|  |               <dees-icon .icon=${'lucide:userPlus'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> added a new user | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:08:</span> Max Mustermann sent an invoice |             <span class="timestamp">22:13</span> | ||||||
|  |             <div class="activity-icon view"> | ||||||
|  |               <dees-icon .icon=${'lucide:messageCircle'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> contacted support | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|  |           <div class="date-separator">Yesterday</div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:09:</span> Max Mustermann viewed reports |             <span class="timestamp">18:45</span> | ||||||
|  |             <div class="activity-icon update"> | ||||||
|  |               <dees-icon .icon=${'lucide:trash2'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> deleted an invoice | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:10:</span> Max Mustermann logged out |             <span class="timestamp">17:30</span> | ||||||
|  |             <div class="activity-icon login"> | ||||||
|  |               <dees-icon .icon=${'lucide:logIn'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> logged in | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:11:</span> Max Mustermann logged in |             <span class="timestamp">16:15</span> | ||||||
|  |             <div class="activity-icon logout"> | ||||||
|  |               <dees-icon .icon=${'lucide:logOut'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> logged out | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:12:</span> Max Mustermann deleted an invoice |             <span class="timestamp">14:20</span> | ||||||
|  |             <div class="activity-icon view"> | ||||||
|  |               <dees-icon .icon=${'lucide:barChart'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> viewed reports | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:13:</span> Max Mustermann contacted support |             <span class="timestamp">13:45</span> | ||||||
|  |             <div class="activity-icon create"> | ||||||
|  |               <dees-icon .icon=${'lucide:send'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> sent an invoice | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |            | ||||||
|           <div class="activityentry"> |           <div class="activityentry"> | ||||||
|             <span class="timestamp">22:14:</span> Max Mustermann added a new user |             <span class="timestamp">13:30</span> | ||||||
|  |             <div class="activity-icon create"> | ||||||
|  |               <dees-icon .icon=${'lucide:filePlus'}></dees-icon> | ||||||
|  |             </div> | ||||||
|  |             <div class="activity-text"> | ||||||
|  |               <span class="activity-user">Max Mustermann</span> created a new invoice | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|           <div class="activityentry"> |            | ||||||
|             <span class="timestamp">22:15:</span> Max Mustermann changed password |           <div class="streamingIndicator bottom">Loading History</div> | ||||||
|           </div> |  | ||||||
|           <div class="activityentry"> |  | ||||||
|             <span class="timestamp">22:16:</span> Max Mustermann logged out |  | ||||||
|           </div> |  | ||||||
|           <div class="activityentry"> |  | ||||||
|             <span class="timestamp">22:17:</span> Max Mustermann logged in |  | ||||||
|           </div> |  | ||||||
|           <div class="activityentry"> |  | ||||||
|             <span class="timestamp">22:18:</span> Max Mustermann archived an invoice |  | ||||||
|           </div> |  | ||||||
|           <div class="activityentry"> |  | ||||||
|             <span class="timestamp">22:19:</span> Max Mustermann approved a payment |  | ||||||
|           </div> |  | ||||||
|           <div class="activityentry"> |  | ||||||
|             <span class="timestamp">22:20:</span> Max Mustermann logged out |  | ||||||
|           </div> |  | ||||||
|           <div class="streamingIndicator bottom">loading more...</div> |  | ||||||
|         </div> |         </div> | ||||||
|         <div class="searchbox"> |         <div class="searchbox"> | ||||||
|           <input type="text" placeholder="Search" /> |           <div class="search-wrapper"> | ||||||
|  |             <dees-icon class="search-icon" .icon=${'lucide:search'}></dees-icon> | ||||||
|  |             <input type="text" placeholder="Search activities, users..." /> | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="topShadow"></div> |         <div class="topShadow"></div> | ||||||
|         <div class="bottomShadow"></div> |         <div class="bottomShadow"></div> | ||||||
|   | |||||||
| @@ -5,19 +5,19 @@ import { | |||||||
|   property, |   property, | ||||||
|   state, |   state, | ||||||
|   html, |   html, | ||||||
|   css, |  | ||||||
|   cssManager, |  | ||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| 
 | 
 | ||||||
| import * as domtools from '@design.estate/dees-domtools'; | import * as domtools from '@design.estate/dees-domtools'; | ||||||
| import * as interfaces from './interfaces/index.js'; | import * as interfaces from '../interfaces/index.js'; | ||||||
| import * as plugins from './00plugins.js'; | import * as plugins from '../00plugins.js'; | ||||||
| import { demoFunc } from './dees-appui-appbar.demo.js'; | import { demoFunc } from './demo.js'; | ||||||
|  | import { appuiAppbarStyles } from './styles.js'; | ||||||
|  | import { renderAppuiAppbar } from './template.js'; | ||||||
| 
 | 
 | ||||||
| // Import required components
 | // Import required components
 | ||||||
| import './dees-icon.js'; | import '../dees-icon.js'; | ||||||
| import './dees-windowcontrols.js'; | import '../dees-windowcontrols.js'; | ||||||
| import './dees-appui-profiledropdown.js'; | import '../dees-appui-profiledropdown.js'; | ||||||
| 
 | 
 | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -73,259 +73,16 @@ export class DeesAppuiBar extends DeesElement { | |||||||
|   @state() |   @state() | ||||||
|   private isProfileDropdownOpen: boolean = false; |   private isProfileDropdownOpen: boolean = false; | ||||||
| 
 | 
 | ||||||
|   public static styles = [ |   public static styles = appuiAppbarStyles; | ||||||
|     cssManager.defaultStyles, |  | ||||||
|     css` |  | ||||||
|       :host { |  | ||||||
|         /* CSS Variables for theming */ |  | ||||||
|         --appbar-height: 40px; |  | ||||||
|         --appbar-font-size: 12px; |  | ||||||
|          |  | ||||||
|         display: block; |  | ||||||
|         position: relative; |  | ||||||
|         width: 100%; |  | ||||||
|         height: var(--appbar-height); |  | ||||||
|         border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; |  | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#000000')}; |  | ||||||
|         color: ${cssManager.bdTheme('#00000080', '#ffffff80')}; |  | ||||||
|         font-size: var(--appbar-font-size); |  | ||||||
|         display: grid; |  | ||||||
|         grid-template-columns: ${cssManager.cssGridColumns(3, 20)}; |  | ||||||
|         -webkit-app-region: drag; |  | ||||||
|         user-select: none; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .menus { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         gap: 4px; |  | ||||||
|         padding: 0 8px; |  | ||||||
|         cursor: default; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .menuItem { |  | ||||||
|         position: relative; |  | ||||||
|         line-height: 24px; |  | ||||||
|         padding: 0px 12px; |  | ||||||
|         margin: 8px 0px; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         -webkit-app-region: no-drag; |  | ||||||
|         transition: all 0.2s ease; |  | ||||||
|         cursor: default; |  | ||||||
|         outline: none; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         gap: 4px; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       /* Optional: Style for menu items with icons (not typically used for top-level items) */ |  | ||||||
|       .menuItem dees-icon { |  | ||||||
|         font-size: 14px; |  | ||||||
|         opacity: 0.8; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .menuItem:hover { |  | ||||||
|         background: ${cssManager.bdTheme('#00000010', '#ffffff20')}; |  | ||||||
|         color: ${cssManager.bdTheme('#000000', '#ffffff')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .menuItem.active { |  | ||||||
|         background: ${cssManager.bdTheme('#00000020', '#ffffff30')}; |  | ||||||
|         color: ${cssManager.bdTheme('#000000', '#ffffff')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .menuItem[disabled] { |  | ||||||
|         opacity: 0.5; |  | ||||||
|         cursor: not-allowed; |  | ||||||
|         pointer-events: none; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .menuItem:focus-visible { |  | ||||||
|         box-shadow: 0 0 0 2px ${cssManager.bdTheme('#00000080', '#ffffff80')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|       /* Dropdown styles */ |  | ||||||
|       .dropdown { |  | ||||||
|         position: absolute; |  | ||||||
|         top: 100%; |  | ||||||
|         left: 0; |  | ||||||
|         min-width: 200px; |  | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#000000')}; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         box-shadow: ${cssManager.bdTheme('0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.3)')}; |  | ||||||
|         margin-top: 4px; |  | ||||||
|         z-index: 1000; |  | ||||||
|         opacity: 0; |  | ||||||
|         transform: translateY(-10px); |  | ||||||
|         transition: opacity 0.2s, transform 0.2s; |  | ||||||
|         pointer-events: none; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .dropdown.open { |  | ||||||
|         opacity: 1; |  | ||||||
|         transform: translateY(0); |  | ||||||
|         pointer-events: auto; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .dropdown-item { |  | ||||||
|         padding: 8px 16px; |  | ||||||
|         cursor: default; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         gap: 8px; |  | ||||||
|         transition: background 0.1s; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .dropdown-item:hover, |  | ||||||
|       .dropdown-item.focused { |  | ||||||
|         background: ${cssManager.bdTheme('#00000010', '#ffffff20')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .dropdown-divider { |  | ||||||
|         height: 1px; |  | ||||||
|         background: ${cssManager.bdTheme('#e0e0e0', '#202020')}; |  | ||||||
|         margin: 4px 0; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .dropdown-item[disabled] { |  | ||||||
|         opacity: 0.5; |  | ||||||
|         cursor: not-allowed; |  | ||||||
|         pointer-events: none; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .dropdown-item .shortcut { |  | ||||||
|         margin-left: auto; |  | ||||||
|         opacity: 0.6; |  | ||||||
|         font-size: 11px; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       /* Breadcrumbs */ |  | ||||||
|       .breadcrumbs { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         height: 100%; |  | ||||||
|         padding: 0 16px; |  | ||||||
|         overflow: hidden; |  | ||||||
|         text-overflow: ellipsis; |  | ||||||
|         white-space: nowrap; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .breadcrumb-item { |  | ||||||
|         color: ${cssManager.bdTheme('#00000080', '#ffffff80')}; |  | ||||||
|         cursor: default; |  | ||||||
|         transition: color 0.2s; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .breadcrumb-item:hover { |  | ||||||
|         color: ${cssManager.bdTheme('#000000', '#ffffff')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .breadcrumb-separator { |  | ||||||
|         margin: 0 8px; |  | ||||||
|         opacity: 0.5; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       /* Account section */ |  | ||||||
|       .account { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: flex-end; |  | ||||||
|         padding: 0 16px; |  | ||||||
|         gap: 12px; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .search-icon { |  | ||||||
|         cursor: default; |  | ||||||
|         opacity: 0.7; |  | ||||||
|         transition: opacity 0.2s; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .search-icon:hover { |  | ||||||
|         opacity: 1; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-info { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         gap: 8px; |  | ||||||
|         cursor: default; |  | ||||||
|         padding: 4px 8px; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         transition: background 0.2s; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-info:hover { |  | ||||||
|         background: ${cssManager.bdTheme('#00000010', '#ffffff20')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-avatar { |  | ||||||
|         position: relative; |  | ||||||
|         width: 24px; |  | ||||||
|         height: 24px; |  | ||||||
|         border-radius: 50%; |  | ||||||
|         background: ${cssManager.bdTheme('#00000020', '#ffffff30')}; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         font-size: 10px; |  | ||||||
|         font-weight: bold; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-avatar img { |  | ||||||
|         width: 100%; |  | ||||||
|         height: 100%; |  | ||||||
|         border-radius: 50%; |  | ||||||
|         object-fit: cover; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-status { |  | ||||||
|         position: absolute; |  | ||||||
|         bottom: -2px; |  | ||||||
|         right: -2px; |  | ||||||
|         width: 8px; |  | ||||||
|         height: 8px; |  | ||||||
|         border-radius: 50%; |  | ||||||
|         border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')}; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-status.online { |  | ||||||
|         background: #4caf50; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-status.offline { |  | ||||||
|         background: #757575; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-status.busy { |  | ||||||
|         background: #f44336; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .user-status.away { |  | ||||||
|         background: #ff9800; |  | ||||||
|       } |  | ||||||
|     `,
 |  | ||||||
|   ]; |  | ||||||
| 
 | 
 | ||||||
|   // INSTANCE
 |   // INSTANCE
 | ||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|     return html` |     return renderAppuiAppbar(this); | ||||||
|       <div class="menus"> |  | ||||||
|         ${this.showWindowControls ? html`<dees-windowcontrols></dees-windowcontrols>` : ''} |  | ||||||
|         ${this.renderMenuItems()} |  | ||||||
|       </div> |  | ||||||
|       <div class="breadcrumbs"> |  | ||||||
|         ${this.renderBreadcrumbs()} |  | ||||||
|       </div> |  | ||||||
|       <div class="account"> |  | ||||||
|         ${this.renderAccountSection()} |  | ||||||
|       </div> |  | ||||||
|     `;
 |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private renderMenuItems(): TemplateResult { | 
 | ||||||
|  | 
 | ||||||
|  |   public renderMenuItems(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${this.menuItems.map((item, index) => this.renderMenuItem(item, `menu-${index}`))} |       ${this.menuItems.map((item, index) => this.renderMenuItem(item, `menu-${index}`))} | ||||||
|     `;
 |     `;
 | ||||||
| @@ -398,7 +155,7 @@ export class DeesAppuiBar extends DeesElement { | |||||||
|     `;
 |     `;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private renderBreadcrumbs(): TemplateResult { |   public renderBreadcrumbs(): TemplateResult { | ||||||
|     if (!this.breadcrumbs) { |     if (!this.breadcrumbs) { | ||||||
|       return html``; |       return html``; | ||||||
|     } |     } | ||||||
| @@ -417,7 +174,7 @@ export class DeesAppuiBar extends DeesElement { | |||||||
|     `;
 |     `;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private renderAccountSection(): TemplateResult { |   public renderAccountSection(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${this.showSearch ? html` |       ${this.showSearch ? html` | ||||||
|         <dees-icon  |         <dees-icon  | ||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { html, css } from '@design.estate/dees-element'; | import { html, css } from '@design.estate/dees-element'; | ||||||
| import type { DeesAppuiBar } from './dees-appui-appbar.js'; | import type { DeesAppuiBar } from './component.js'; | ||||||
| import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js'; | import type { IAppBarMenuItem } from '../interfaces/appbarmenuitem.js'; | ||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  | import './component.js'; | ||||||
| 
 | 
 | ||||||
| export const demoFunc = () => { | export const demoFunc = () => { | ||||||
|   // Sample menu items with various configurations
 |   // Sample menu items with various configurations
 | ||||||
							
								
								
									
										3
									
								
								ts_web/elements/dees-appui-appbar/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts_web/elements/dees-appui-appbar/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | export * from './component.js'; | ||||||
|  | export { appuiAppbarStyles } from './styles.js'; | ||||||
|  | export { renderAppuiAppbar } from './template.js'; | ||||||
							
								
								
									
										238
									
								
								ts_web/elements/dees-appui-appbar/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								ts_web/elements/dees-appui-appbar/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,238 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const appuiAppbarStyles = [ | ||||||
|  |     cssManager.defaultStyles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         /* CSS Variables for theming */ | ||||||
|  |         --appbar-height: 40px; | ||||||
|  |         --appbar-font-size: 12px; | ||||||
|  |          | ||||||
|  |         display: block; | ||||||
|  |         position: relative; | ||||||
|  |         width: 100%; | ||||||
|  |         height: var(--appbar-height); | ||||||
|  |         border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; | ||||||
|  |         background: ${cssManager.bdTheme('#ffffff', '#000000')}; | ||||||
|  |         color: ${cssManager.bdTheme('#00000080', '#ffffff80')}; | ||||||
|  |         font-size: var(--appbar-font-size); | ||||||
|  |         display: grid; | ||||||
|  |         grid-template-columns: ${cssManager.cssGridColumns(3, 20)}; | ||||||
|  |         -webkit-app-region: drag; | ||||||
|  |         user-select: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menus { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 4px; | ||||||
|  |         padding: 0 8px; | ||||||
|  |         cursor: default; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menuItem { | ||||||
|  |         position: relative; | ||||||
|  |         line-height: 24px; | ||||||
|  |         padding: 0px 12px; | ||||||
|  |         margin: 8px 0px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         -webkit-app-region: no-drag; | ||||||
|  |         transition: all 0.2s ease; | ||||||
|  |         cursor: default; | ||||||
|  |         outline: none; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 4px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Optional: Style for menu items with icons (not typically used for top-level items) */ | ||||||
|  |       .menuItem dees-icon { | ||||||
|  |         font-size: 14px; | ||||||
|  |         opacity: 0.8; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menuItem:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#00000010', '#ffffff20')}; | ||||||
|  |         color: ${cssManager.bdTheme('#000000', '#ffffff')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menuItem.active { | ||||||
|  |         background: ${cssManager.bdTheme('#00000020', '#ffffff30')}; | ||||||
|  |         color: ${cssManager.bdTheme('#000000', '#ffffff')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menuItem[disabled] { | ||||||
|  |         opacity: 0.5; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |         pointer-events: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menuItem:focus-visible { | ||||||
|  |         box-shadow: 0 0 0 2px ${cssManager.bdTheme('#00000080', '#ffffff80')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       /* Dropdown styles */ | ||||||
|  |       .dropdown { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 100%; | ||||||
|  |         left: 0; | ||||||
|  |         min-width: 200px; | ||||||
|  |         background: ${cssManager.bdTheme('#ffffff', '#000000')}; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')}; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         box-shadow: ${cssManager.bdTheme('0 4px 12px rgba(0, 0, 0, 0.15)', '0 4px 12px rgba(0, 0, 0, 0.3)')}; | ||||||
|  |         margin-top: 4px; | ||||||
|  |         z-index: 1000; | ||||||
|  |         opacity: 0; | ||||||
|  |         transform: translateY(-10px); | ||||||
|  |         transition: opacity 0.2s, transform 0.2s; | ||||||
|  |         pointer-events: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .dropdown.open { | ||||||
|  |         opacity: 1; | ||||||
|  |         transform: translateY(0); | ||||||
|  |         pointer-events: auto; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .dropdown-item { | ||||||
|  |         padding: 8px 16px; | ||||||
|  |         cursor: default; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |         transition: background 0.1s; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .dropdown-item:hover, | ||||||
|  |       .dropdown-item.focused { | ||||||
|  |         background: ${cssManager.bdTheme('#00000010', '#ffffff20')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .dropdown-divider { | ||||||
|  |         height: 1px; | ||||||
|  |         background: ${cssManager.bdTheme('#e0e0e0', '#202020')}; | ||||||
|  |         margin: 4px 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .dropdown-item[disabled] { | ||||||
|  |         opacity: 0.5; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |         pointer-events: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .dropdown-item .shortcut { | ||||||
|  |         margin-left: auto; | ||||||
|  |         opacity: 0.6; | ||||||
|  |         font-size: 11px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Breadcrumbs */ | ||||||
|  |       .breadcrumbs { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         height: 100%; | ||||||
|  |         padding: 0 16px; | ||||||
|  |         overflow: hidden; | ||||||
|  |         text-overflow: ellipsis; | ||||||
|  |         white-space: nowrap; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .breadcrumb-item { | ||||||
|  |         color: ${cssManager.bdTheme('#00000080', '#ffffff80')}; | ||||||
|  |         cursor: default; | ||||||
|  |         transition: color 0.2s; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .breadcrumb-item:hover { | ||||||
|  |         color: ${cssManager.bdTheme('#000000', '#ffffff')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .breadcrumb-separator { | ||||||
|  |         margin: 0 8px; | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Account section */ | ||||||
|  |       .account { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: flex-end; | ||||||
|  |         padding: 0 16px; | ||||||
|  |         gap: 12px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .search-icon { | ||||||
|  |         cursor: default; | ||||||
|  |         opacity: 0.7; | ||||||
|  |         transition: opacity 0.2s; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .search-icon:hover { | ||||||
|  |         opacity: 1; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-info { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |         cursor: default; | ||||||
|  |         padding: 4px 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         transition: background 0.2s; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-info:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#00000010', '#ffffff20')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-avatar { | ||||||
|  |         position: relative; | ||||||
|  |         width: 24px; | ||||||
|  |         height: 24px; | ||||||
|  |         border-radius: 50%; | ||||||
|  |         background: ${cssManager.bdTheme('#00000020', '#ffffff30')}; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         font-size: 10px; | ||||||
|  |         font-weight: bold; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-avatar img { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         border-radius: 50%; | ||||||
|  |         object-fit: cover; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-status { | ||||||
|  |         position: absolute; | ||||||
|  |         bottom: -2px; | ||||||
|  |         right: -2px; | ||||||
|  |         width: 8px; | ||||||
|  |         height: 8px; | ||||||
|  |         border-radius: 50%; | ||||||
|  |         border: 2px solid ${cssManager.bdTheme('#ffffff', '#000000')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-status.online { | ||||||
|  |         background: #4caf50; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-status.offline { | ||||||
|  |         background: #757575; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-status.busy { | ||||||
|  |         background: #f44336; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .user-status.away { | ||||||
|  |         background: #ff9800; | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								ts_web/elements/dees-appui-appbar/template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								ts_web/elements/dees-appui-appbar/template.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | import { html, type TemplateResult } from '@design.estate/dees-element'; | ||||||
|  | import type { DeesAppuiBar } from './component.js'; | ||||||
|  |  | ||||||
|  | export const renderAppuiAppbar = (component: DeesAppuiBar): TemplateResult => { | ||||||
|  |       return html` | ||||||
|  |         <div class="menus"> | ||||||
|  |           ${component.showWindowControls ? html`<dees-windowcontrols></dees-windowcontrols>` : ''} | ||||||
|  |           ${component.renderMenuItems()} | ||||||
|  |         </div> | ||||||
|  |         <div class="breadcrumbs"> | ||||||
|  |           ${component.renderBreadcrumbs()} | ||||||
|  |         </div> | ||||||
|  |         <div class="account"> | ||||||
|  |           ${component.renderAccountSection()} | ||||||
|  |         </div> | ||||||
|  |       `; | ||||||
|  |    | ||||||
|  | }; | ||||||
| @@ -65,10 +65,10 @@ export const demoFunc = () => { | |||||||
|  |  | ||||||
|   // Main menu tabs (left sidebar) |   // Main menu tabs (left sidebar) | ||||||
|   const mainMenuTabs: ITab[] = [ |   const mainMenuTabs: ITab[] = [ | ||||||
|     { key: 'dashboard', iconName: 'home', action: () => console.log('Dashboard selected') }, |     { key: 'dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard selected') }, | ||||||
|     { key: 'projects', iconName: 'folder', action: () => console.log('Projects selected') }, |     { key: 'projects', iconName: 'lucide:folder', action: () => console.log('Projects selected') }, | ||||||
|     { key: 'analytics', iconName: 'lineChart', action: () => console.log('Analytics selected') }, |     { key: 'analytics', iconName: 'lucide:lineChart', action: () => console.log('Analytics selected') }, | ||||||
|     { key: 'settings', iconName: 'settings', action: () => console.log('Settings selected') }, |     { key: 'settings', iconName: 'lucide:settings', action: () => console.log('Settings selected') }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   // Selector options (second sidebar) |   // Selector options (second sidebar) | ||||||
| @@ -83,9 +83,9 @@ export const demoFunc = () => { | |||||||
|  |  | ||||||
|   // Main content tabs |   // Main content tabs | ||||||
|   const mainContentTabs: ITab[] = [ |   const mainContentTabs: ITab[] = [ | ||||||
|     { key: 'Details', iconName: 'file', action: () => console.log('Details tab') }, |     { key: 'Details', iconName: 'lucide:file', action: () => console.log('Details tab') }, | ||||||
|     { key: 'Logs', iconName: 'list', action: () => console.log('Logs tab') }, |     { key: 'Logs', iconName: 'lucide:list', action: () => console.log('Logs tab') }, | ||||||
|     { key: 'Metrics', iconName: 'lineChart', action: () => console.log('Metrics tab') }, |     { key: 'Metrics', iconName: 'lucide:lineChart', action: () => console.log('Metrics tab') }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   // Profile menu items |   // Profile menu items | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import { | |||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| import * as interfaces from './interfaces/index.js'; | import * as interfaces from './interfaces/index.js'; | ||||||
| import * as plugins from './00plugins.js'; | import * as plugins from './00plugins.js'; | ||||||
| import type { DeesAppuiBar } from './dees-appui-appbar.js'; | import type { DeesAppuiBar } from './dees-appui-appbar/index.js'; | ||||||
| import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js'; | import type { DeesAppuiMainmenu } from './dees-appui-mainmenu.js'; | ||||||
| import type { DeesAppuiMainselector } from './dees-appui-mainselector.js'; | import type { DeesAppuiMainselector } from './dees-appui-mainselector.js'; | ||||||
| import type { DeesAppuiMaincontent } from './dees-appui-maincontent.js'; | import type { DeesAppuiMaincontent } from './dees-appui-maincontent.js'; | ||||||
| @@ -18,7 +18,7 @@ import type { DeesAppuiActivitylog } from './dees-appui-activitylog.js'; | |||||||
| import { demoFunc } from './dees-appui-base.demo.js'; | import { demoFunc } from './dees-appui-base.demo.js'; | ||||||
|  |  | ||||||
| // Import child components | // Import child components | ||||||
| import './dees-appui-appbar.js'; | import './dees-appui-appbar/index.js'; | ||||||
| import './dees-appui-mainmenu.js'; | import './dees-appui-mainmenu.js'; | ||||||
| import './dees-appui-mainselector.js'; | import './dees-appui-mainselector.js'; | ||||||
| import './dees-appui-maincontent.js'; | import './dees-appui-maincontent.js'; | ||||||
|   | |||||||
| @@ -19,9 +19,9 @@ export class DeesAppuiMaincontent extends DeesElement { | |||||||
|   public static demo = () => html` |   public static demo = () => html` | ||||||
|     <dees-appui-maincontent |     <dees-appui-maincontent | ||||||
|       .tabs=${[ |       .tabs=${[ | ||||||
|         { key: 'Overview', iconName: 'home', action: () => console.log('Overview') }, |         { key: 'Overview', iconName: 'lucide:home', action: () => console.log('Overview') }, | ||||||
|         { key: 'Details', iconName: 'file', action: () => console.log('Details') }, |         { key: 'Details', iconName: 'lucide:file', action: () => console.log('Details') }, | ||||||
|         { key: 'Settings', iconName: 'cog', action: () => console.log('Settings') }, |         { key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') }, | ||||||
|       ]} |       ]} | ||||||
|     > |     > | ||||||
|       <div slot="content" style="padding: 40px; color: #ccc;"> |       <div slot="content" style="padding: 40px; color: #ccc;"> | ||||||
|   | |||||||
| @@ -22,10 +22,10 @@ export class DeesAppuiMainmenu extends DeesElement { | |||||||
|   public static demo = () => html` |   public static demo = () => html` | ||||||
|     <dees-appui-mainmenu |     <dees-appui-mainmenu | ||||||
|       .tabs=${[ |       .tabs=${[ | ||||||
|         { key: 'Dashboard', iconName: 'home', action: () => console.log('Dashboard') }, |         { key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard') }, | ||||||
|         { key: 'Projects', iconName: 'folder', action: () => console.log('Projects') }, |         { key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects') }, | ||||||
|         { key: 'Analytics', iconName: 'lineChart', action: () => console.log('Analytics') }, |         { key: 'Analytics', iconName: 'lucide:lineChart', action: () => console.log('Analytics') }, | ||||||
|         { key: 'Settings', iconName: 'settings', action: () => console.log('Settings') }, |         { key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') }, | ||||||
|       ]} |       ]} | ||||||
|     ></dees-appui-mainmenu> |     ></dees-appui-mainmenu> | ||||||
|   `; |   `; | ||||||
| @@ -35,7 +35,7 @@ export class DeesAppuiMainmenu extends DeesElement { | |||||||
|   // INSTANCE |   // INSTANCE | ||||||
|   @property({ type: Array }) |   @property({ type: Array }) | ||||||
|   public tabs: interfaces.ITab[] = [ |   public tabs: interfaces.ITab[] = [ | ||||||
|     { key: '⚠️ Please set tabs', iconName: 'alertTriangle', action: () => console.warn('No tabs configured for mainmenu') }, |     { key: '⚠️ Please set tabs', iconName: 'lucide:alertTriangle', action: () => console.warn('No tabs configured for mainmenu') }, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   @property() |   @property() | ||||||
| @@ -112,7 +112,7 @@ export class DeesAppuiMainmenu extends DeesElement { | |||||||
|                   this.updateTab(tabArg); |                   this.updateTab(tabArg); | ||||||
|                 }}" |                 }}" | ||||||
|               > |               > | ||||||
|                 <dees-icon .icon="${tabArg.iconName ? `lucide:${tabArg.iconName}` : ''}"></dees-icon> |                 <dees-icon .icon="${tabArg.iconName || ''}"></dees-icon> | ||||||
|               </div> |               </div> | ||||||
|             `; |             `; | ||||||
|           })} |           })} | ||||||
|   | |||||||
| @@ -14,15 +14,94 @@ import * as domtools from '@design.estate/dees-domtools'; | |||||||
|  |  | ||||||
| @customElement('dees-appui-tabs') | @customElement('dees-appui-tabs') | ||||||
| export class DeesAppuiTabs extends DeesElement { | export class DeesAppuiTabs extends DeesElement { | ||||||
|   public static demo = () => html` |   public static demo = () => { | ||||||
|     <dees-appui-tabs |     const horizontalTabs: interfaces.ITab[] = [ | ||||||
|       .tabs=${[ |       { key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') }, | ||||||
|         { key: 'Tab 1', action: () => console.log('Tab 1 clicked') }, |       { key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') }, | ||||||
|         { key: 'Tab 2', action: () => console.log('Tab 2 clicked') }, |       { key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') }, | ||||||
|         { key: 'Tab 3', action: () => console.log('Tab 3 clicked') }, |       { key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') }, | ||||||
|       ]} |       { key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') }, | ||||||
|     ></dees-appui-tabs> |     ]; | ||||||
|   `; |  | ||||||
|  |     const verticalTabs: interfaces.ITab[] = [ | ||||||
|  |       { key: 'Profile', iconName: 'lucide:user', action: () => console.log('Profile clicked') }, | ||||||
|  |       { key: 'Security', iconName: 'lucide:shield', action: () => console.log('Security clicked') }, | ||||||
|  |       { key: 'Notifications', iconName: 'lucide:bell', action: () => console.log('Notifications clicked') }, | ||||||
|  |       { key: 'Integrations', iconName: 'lucide:link', action: () => console.log('Integrations clicked') }, | ||||||
|  |       { key: 'Advanced', iconName: 'lucide:code', action: () => console.log('Advanced clicked') }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     const noIndicatorTabs: interfaces.ITab[] = [ | ||||||
|  |       { key: 'All', action: () => console.log('All clicked') }, | ||||||
|  |       { key: 'Active', action: () => console.log('Active clicked') }, | ||||||
|  |       { key: 'Completed', action: () => console.log('Completed clicked') }, | ||||||
|  |       { key: 'Archived', action: () => console.log('Archived clicked') }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     const demoContent = (text: string) => html` | ||||||
|  |       <div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};"> | ||||||
|  |         ${text} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <style> | ||||||
|  |         .demo-container { | ||||||
|  |           display: flex; | ||||||
|  |           flex-direction: column; | ||||||
|  |           gap: 32px; | ||||||
|  |           padding: 48px; | ||||||
|  |           background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; | ||||||
|  |           min-height: 100vh; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .section { | ||||||
|  |           background: ${cssManager.bdTheme('#ffffff', '#18181b')}; | ||||||
|  |           border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |           border-radius: 8px; | ||||||
|  |           padding: 24px; | ||||||
|  |           box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .section-title { | ||||||
|  |           font-size: 18px; | ||||||
|  |           font-weight: 600; | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |           color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .two-column { | ||||||
|  |           display: grid; | ||||||
|  |           grid-template-columns: 200px 1fr; | ||||||
|  |           gap: 24px; | ||||||
|  |           align-items: start; | ||||||
|  |         } | ||||||
|  |       </style> | ||||||
|  |       <div class="demo-container"> | ||||||
|  |         <div class="section"> | ||||||
|  |           <div class="section-title">Horizontal Tabs with Animated Indicator</div> | ||||||
|  |           <dees-appui-tabs .tabs=${horizontalTabs}> | ||||||
|  |             ${demoContent('Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.')} | ||||||
|  |           </dees-appui-tabs> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="section"> | ||||||
|  |           <div class="section-title">Vertical Tabs Layout</div> | ||||||
|  |           <div class="two-column"> | ||||||
|  |             <dees-appui-tabs .tabStyle=${'vertical'} .tabs=${verticalTabs}></dees-appui-tabs> | ||||||
|  |             ${demoContent('Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.')} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="section"> | ||||||
|  |           <div class="section-title">Without Indicator</div> | ||||||
|  |           <dees-appui-tabs .showTabIndicator=${false} .tabs=${noIndicatorTabs}> | ||||||
|  |             ${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')} | ||||||
|  |           </dees-appui-tabs> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   // INSTANCE |   // INSTANCE | ||||||
|   @property({ |   @property({ | ||||||
| @@ -50,148 +129,217 @@ export class DeesAppuiTabs extends DeesElement { | |||||||
|  |  | ||||||
|       .tabs-wrapper { |       .tabs-wrapper { | ||||||
|         position: relative; |         position: relative; | ||||||
|         background: ${cssManager.bdTheme('#f5f5f5', '#000000')}; |       } | ||||||
|         height: 52px; |  | ||||||
|  |       .tabs-wrapper.horizontal-wrapper { | ||||||
|  |         border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tabsContainer { |       .tabsContainer { | ||||||
|         position: relative; |         position: relative; | ||||||
|         z-index: 1; |  | ||||||
|         user-select: none; |         user-select: none; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tabsContainer.horizontal { |       .tabsContainer.horizontal { | ||||||
|         display: grid; |         display: flex; | ||||||
|         padding-top: 20px; |         align-items: center; | ||||||
|         padding-bottom: 0px; |  | ||||||
|         margin-left: 24px; |  | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|  |         overflow-x: auto; | ||||||
|  |         scrollbar-width: none; | ||||||
|  |         height: 48px; | ||||||
|  |         padding: 0 16px; | ||||||
|  |         gap: 4px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .tabsContainer.horizontal::-webkit-scrollbar { | ||||||
|  |         display: none; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tabsContainer.vertical { |       .tabsContainer.vertical { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         padding: 20px; |         padding: 8px; | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|  |         gap: 2px; | ||||||
|  |         position: relative; | ||||||
|  |         background: ${cssManager.bdTheme('#f9fafb', '#18181b')}; | ||||||
|  |         border-radius: 8px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tab { |       .tab { | ||||||
|         color: ${cssManager.bdTheme('#666', '#a0a0a0')}; |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|         white-space: nowrap; |         white-space: nowrap; | ||||||
|         cursor: default; |         cursor: pointer; | ||||||
|         transition: color 0.1s; |         transition: color 0.15s ease; | ||||||
|  |         font-weight: 500; | ||||||
|  |         position: relative; | ||||||
|  |         z-index: 2; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .horizontal .tab { |       .horizontal .tab { | ||||||
|         margin-right: 30px; |         padding: 0 16px; | ||||||
|         padding-top: 4px; |         height: 100%; | ||||||
|         padding-bottom: 12px; |         display: inline-flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |         position: relative; | ||||||
|  |         border-radius: 6px 6px 0 0; | ||||||
|  |         transition: background-color 0.15s ease; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .vertical .tab { |       .horizontal .tab:not(:last-child)::after { | ||||||
|         padding: 12px 16px; |         content: ''; | ||||||
|         margin-bottom: 4px; |         position: absolute; | ||||||
|         border-radius: 4px; |         right: -2px; | ||||||
|         width: 100%; |         top: 50%; | ||||||
|         display: flex; |         transform: translateY(-50%); | ||||||
|  |         height: 20px; | ||||||
|  |         width: 1px; | ||||||
|  |         background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .horizontal .tab .tab-content { | ||||||
|  |         display: inline-flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         gap: 8px; |         gap: 8px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       .vertical .tab { | ||||||
|  |         padding: 10px 16px; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         width: 100%; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       .tab:hover { |       .tab:hover { | ||||||
|         color: ${cssManager.bdTheme('#000', '#ffffff')}; |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .horizontal .tab:hover { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.03)')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .horizontal .tab:hover::after, | ||||||
|  |       .horizontal .tab:hover + .tab::after { | ||||||
|  |         opacity: 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .vertical .tab:hover { |       .vertical .tab:hover { | ||||||
|         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')}; |         background: ${cssManager.bdTheme('rgba(244, 244, 245, 0.5)', 'rgba(39, 39, 42, 0.5)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tab.selectedTab { |       .horizontal .tab.selectedTab { | ||||||
|         color: ${cssManager.bdTheme('#333', '#e0e0e0')}; |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .horizontal .tab.selectedTab::after, | ||||||
|  |       .horizontal .tab.selectedTab + .tab::after { | ||||||
|  |         opacity: 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .vertical .tab.selectedTab { |       .vertical .tab.selectedTab { | ||||||
|         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|         color: ${cssManager.bdTheme('#000', '#ffffff')}; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tab dees-icon { |       .tab dees-icon { | ||||||
|         font-size: 16px; |         font-size: 16px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .tabs-wrapper .tabIndicator { |       .tabIndicator { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         z-index: 0; |         transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|         left: 40px; |         opacity: 0; | ||||||
|         bottom: 0px; |  | ||||||
|         height: 40px; |  | ||||||
|         width: 40px; |  | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#161616')}; |  | ||||||
|         transition: all 0.1s; |  | ||||||
|         border-top-left-radius: 8px; |  | ||||||
|         border-top-right-radius: 8px; |  | ||||||
|         border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')}; |  | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .vertical .tabIndicator { |       .tabIndicator.no-transition { | ||||||
|         display: none; |         transition: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .tabs-wrapper .tabIndicator { | ||||||
|  |         height: 3px; | ||||||
|  |         bottom: 0; | ||||||
|  |         background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         border-radius: 3px 3px 0 0; | ||||||
|  |         z-index: 3; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .vertical-wrapper { | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .vertical-wrapper .tabIndicator { | ||||||
|  |         left: 8px; | ||||||
|  |         right: 8px; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         background: ${cssManager.bdTheme('#ffffff', '#27272a')}; | ||||||
|  |         z-index: 1; | ||||||
|  |         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .content { |       .content { | ||||||
|         margin-top: 20px; |         padding: 32px 24px; | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       ${this.tabStyle === 'horizontal' ? html` |       ${this.renderTabsWrapper()} | ||||||
|         <style> |  | ||||||
|           .tabsContainer.horizontal { |  | ||||||
|             grid-template-columns: repeat(${this.tabs.length}, min-content); |  | ||||||
|           } |  | ||||||
|         </style> |  | ||||||
|         <div class="tabs-wrapper"> |  | ||||||
|           <div class="tabsContainer horizontal"> |  | ||||||
|             ${this.tabs.map((tabArg) => { |  | ||||||
|               return html` |  | ||||||
|                 <div |  | ||||||
|                   class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}" |  | ||||||
|                   @click="${() => this.selectTab(tabArg)}" |  | ||||||
|                 > |  | ||||||
|                   ${tabArg.key} |  | ||||||
|                 </div> |  | ||||||
|               `; |  | ||||||
|             })} |  | ||||||
|           </div> |  | ||||||
|           ${this.showTabIndicator ? html` |  | ||||||
|             <div class="tabIndicator"></div> |  | ||||||
|           ` : ''} |  | ||||||
|         </div> |  | ||||||
|       ` : html` |  | ||||||
|         <div class="tabsContainer vertical"> |  | ||||||
|           ${this.tabs.map((tabArg) => { |  | ||||||
|             return html` |  | ||||||
|               <div |  | ||||||
|                 class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}" |  | ||||||
|                 @click="${() => this.selectTab(tabArg)}" |  | ||||||
|               > |  | ||||||
|                 ${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''} |  | ||||||
|                 ${tabArg.key} |  | ||||||
|               </div> |  | ||||||
|             `; |  | ||||||
|           })} |  | ||||||
|         </div> |  | ||||||
|       `} |  | ||||||
|       <div class="content"> |       <div class="content"> | ||||||
|         <slot></slot> |         <slot></slot> | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private renderTabsWrapper(): TemplateResult { | ||||||
|  |     const isHorizontal = this.tabStyle === 'horizontal'; | ||||||
|  |     const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper'; | ||||||
|  |     const containerClass = `tabsContainer ${this.tabStyle}`; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="${wrapperClass}"> | ||||||
|  |         <div class="${containerClass}"> | ||||||
|  |           ${this.tabs.map(tab => this.renderTab(tab, isHorizontal))} | ||||||
|  |         </div> | ||||||
|  |         ${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderTab(tab: interfaces.ITab, isHorizontal: boolean): TemplateResult { | ||||||
|  |     const isSelected = tab === this.selectedTab; | ||||||
|  |     const classes = `tab ${isSelected ? 'selectedTab' : ''}`; | ||||||
|  |      | ||||||
|  |     const content = isHorizontal ? html` | ||||||
|  |       <span class="tab-content"> | ||||||
|  |         ${this.renderTabIcon(tab)} | ||||||
|  |         ${tab.key} | ||||||
|  |       </span> | ||||||
|  |     ` : html` | ||||||
|  |       ${this.renderTabIcon(tab)} | ||||||
|  |       ${tab.key} | ||||||
|  |     `; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div | ||||||
|  |         class="${classes}" | ||||||
|  |         @click="${() => this.selectTab(tab)}" | ||||||
|  |       > | ||||||
|  |         ${content} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderTabIcon(tab: interfaces.ITab): TemplateResult | '' { | ||||||
|  |     return tab.iconName ? html`<dees-icon .icon=${tab.iconName}></dees-icon>` : ''; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private selectTab(tabArg: interfaces.ITab) { |   private selectTab(tabArg: interfaces.ITab) { | ||||||
|     this.selectedTab = tabArg; |     this.selectedTab = tabArg; | ||||||
|     this.updateTabIndicator(); |  | ||||||
|     tabArg.action(); |     tabArg.action(); | ||||||
|      |      | ||||||
|     // Emit tab-select event |     // Emit tab-select event | ||||||
| @@ -202,31 +350,6 @@ export class DeesAppuiTabs extends DeesElement { | |||||||
|     })); |     })); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * updates the indicator position |  | ||||||
|    */ |  | ||||||
|   private updateTabIndicator() { |  | ||||||
|     if (!this.showTabIndicator || this.tabStyle !== 'horizontal' || !this.selectedTab) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     const tabIndex = this.tabs.indexOf(this.selectedTab); |  | ||||||
|     const selectedTabElement: HTMLElement = this.shadowRoot.querySelector( |  | ||||||
|       `.tabs-wrapper .tabsContainer .tab:nth-child(${tabIndex + 1})` |  | ||||||
|     ); |  | ||||||
|      |  | ||||||
|     if (!selectedTabElement) return; |  | ||||||
|      |  | ||||||
|     const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabsContainer'); |  | ||||||
|     const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left")); |  | ||||||
|     const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabIndicator'); |  | ||||||
|      |  | ||||||
|     if (tabIndicator) { |  | ||||||
|       tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px'; |  | ||||||
|       tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px'; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   firstUpdated() { |   firstUpdated() { | ||||||
|     if (this.tabs && this.tabs.length > 0) { |     if (this.tabs && this.tabs.length > 0) { | ||||||
|       this.selectTab(this.tabs[0]); |       this.selectTab(this.tabs[0]); | ||||||
| @@ -241,7 +364,88 @@ export class DeesAppuiTabs extends DeesElement { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) { |     if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) { | ||||||
|       this.updateTabIndicator(); |       await this.updateComplete; | ||||||
|  |       // Wait for fonts to load on first update | ||||||
|  |       if (!this.indicatorInitialized && document.fonts) { | ||||||
|  |         await document.fonts.ready; | ||||||
|  |       } | ||||||
|  |       requestAnimationFrame(() => { | ||||||
|  |         this.updateTabIndicator(); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private indicatorInitialized = false; | ||||||
|  |  | ||||||
|  |   private updateTabIndicator() { | ||||||
|  |     if (!this.shouldShowIndicator()) return; | ||||||
|  |  | ||||||
|  |     const selectedTabElement = this.getSelectedTabElement(); | ||||||
|  |     if (!selectedTabElement) return; | ||||||
|  |  | ||||||
|  |     const indicator = this.getIndicatorElement(); | ||||||
|  |     if (!indicator) return; | ||||||
|  |  | ||||||
|  |     this.handleInitialTransition(indicator); | ||||||
|  |      | ||||||
|  |     if (this.tabStyle === 'horizontal') { | ||||||
|  |       this.updateHorizontalIndicator(indicator, selectedTabElement); | ||||||
|  |     } else { | ||||||
|  |       this.updateVerticalIndicator(indicator, selectedTabElement); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     indicator.style.opacity = '1'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private shouldShowIndicator(): boolean { | ||||||
|  |     return this.selectedTab && this.showTabIndicator && this.tabs.includes(this.selectedTab); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getSelectedTabElement(): HTMLElement | null { | ||||||
|  |     const selectedIndex = this.tabs.indexOf(this.selectedTab); | ||||||
|  |     const isHorizontal = this.tabStyle === 'horizontal'; | ||||||
|  |     const selector = isHorizontal  | ||||||
|  |       ? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})` | ||||||
|  |       : `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`; | ||||||
|  |        | ||||||
|  |     return this.shadowRoot.querySelector(selector); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getIndicatorElement(): HTMLElement | null { | ||||||
|  |     return this.shadowRoot.querySelector('.tabIndicator'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleInitialTransition(indicator: HTMLElement): void { | ||||||
|  |     if (!this.indicatorInitialized) { | ||||||
|  |       indicator.classList.add('no-transition'); | ||||||
|  |       this.indicatorInitialized = true; | ||||||
|  |        | ||||||
|  |       setTimeout(() => { | ||||||
|  |         indicator.classList.remove('no-transition'); | ||||||
|  |       }, 50); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private updateHorizontalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void { | ||||||
|  |     const tabContent = tabElement.querySelector('.tab-content') as HTMLElement; | ||||||
|  |     if (!tabContent) return; | ||||||
|  |  | ||||||
|  |     const wrapperRect = indicator.parentElement.getBoundingClientRect(); | ||||||
|  |     const contentRect = tabContent.getBoundingClientRect(); | ||||||
|  |      | ||||||
|  |     const contentLeft = contentRect.left - wrapperRect.left; | ||||||
|  |     const indicatorWidth = contentRect.width + 8; | ||||||
|  |     const indicatorLeft = contentLeft - 4; | ||||||
|  |      | ||||||
|  |     indicator.style.width = `${indicatorWidth}px`; | ||||||
|  |     indicator.style.left = `${indicatorLeft}px`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private updateVerticalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void { | ||||||
|  |     const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement; | ||||||
|  |     if (!tabsContainer) return; | ||||||
|  |      | ||||||
|  |     indicator.style.top = `${tabElement.offsetTop + tabsContainer.offsetTop}px`; | ||||||
|  |     indicator.style.height = `${tabElement.clientHeight}px`; | ||||||
|  |   } | ||||||
| } | } | ||||||
| @@ -35,17 +35,17 @@ export class DeesAppuiView extends DeesElement { | |||||||
|         id: 'demo-view', |         id: 'demo-view', | ||||||
|         name: 'Demo View', |         name: 'Demo View', | ||||||
|         description: 'A demonstration view', |         description: 'A demonstration view', | ||||||
|         iconName: 'home', |         iconName: 'lucide:home', | ||||||
|         tabs: [ |         tabs: [ | ||||||
|           { |           { | ||||||
|             key: 'overview', |             key: 'overview', | ||||||
|             iconName: 'chart-line', |             iconName: 'lucide:lineChart', | ||||||
|             action: () => console.log('Overview tab'), |             action: () => console.log('Overview tab'), | ||||||
|             content: html`<div style="padding: 20px;">Overview Content</div>` |             content: html`<div style="padding: 20px;">Overview Content</div>` | ||||||
|           }, |           }, | ||||||
|           { |           { | ||||||
|             key: 'details', |             key: 'details', | ||||||
|             iconName: 'file-alt', |             iconName: 'lucide:fileText', | ||||||
|             action: () => console.log('Details tab'), |             action: () => console.log('Details tab'), | ||||||
|             content: html`<div style="padding: 20px;">Details Content</div>` |             content: html`<div style="padding: 20px;">Details Content</div>` | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -1,77 +1,421 @@ | |||||||
| import { html, css } from '@design.estate/dees-element'; | import { html, css, cssManager, domtools } from '@design.estate/dees-element'; | ||||||
|  | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  | import './dees-panel.js'; | ||||||
|  | import './dees-form.js'; | ||||||
|  | import './dees-form-submit.js'; | ||||||
|  | import './dees-input-text.js'; | ||||||
|  | import './dees-icon.js'; | ||||||
|  | import type { DeesButton } from './dees-button.js'; | ||||||
|  |  | ||||||
| export const demoFunc = () => html` | export const demoFunc = () => html` | ||||||
|   <style> |   <style> | ||||||
|     ${css` |     ${css` | ||||||
|       h3 { |       .demo-container { | ||||||
|         margin-top: 32px; |         display: flex; | ||||||
|         margin-bottom: 16px; |         flex-direction: column; | ||||||
|         color: #333; |         gap: 24px; | ||||||
|  |         padding: 24px; | ||||||
|  |         max-width: 1200px; | ||||||
|  |         margin: 0 auto; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       @media (prefers-color-scheme: dark) { |       dees-panel { | ||||||
|         h3 { |         margin-bottom: 24px; | ||||||
|           color: #ccc; |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .form-demo { |       dees-panel:last-child { | ||||||
|         background: #f5f5f5; |         margin-bottom: 0; | ||||||
|         padding: 20px; |  | ||||||
|         border-radius: 8px; |  | ||||||
|         margin: 20px 0; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       @media (prefers-color-scheme: dark) { |  | ||||||
|         .form-demo { |  | ||||||
|           background: #1a1a1a; |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .button-group { |       .button-group { | ||||||
|         display: flex; |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 12px; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .vertical-group { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         gap: 8px; | ||||||
|  |         max-width: 300px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .horizontal-group { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|         gap: 16px; |         gap: 16px; | ||||||
|         margin: 20px 0; |         flex-wrap: wrap; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .demo-output { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 12px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-family: monospace; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .icon-row { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 12px; | ||||||
|  |         margin: 8px 0; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .code-snippet { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')}; | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-family: monospace; | ||||||
|  |         font-size: 13px; | ||||||
|  |         display: inline-block; | ||||||
|  |         margin: 4px 0; | ||||||
|       } |       } | ||||||
|     `} |     `} | ||||||
|   </style> |   </style> | ||||||
|    |    | ||||||
|   <h3>Button Types</h3> |   <div class="demo-container"> | ||||||
|   <dees-button>This is a slotted Text</dees-button> |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|   <p> |       // Log button clicks for demo purposes | ||||||
|     <dees-button text="Highlighted: This text shows" type="highlighted">Highlighted</dees-button> |       const buttons = elementArg.querySelectorAll('dees-button'); | ||||||
|   </p> |       buttons.forEach((button) => { | ||||||
|   <p><dees-button type="discreet">This is discreete button</dees-button></p> |         button.addEventListener('clicked', () => { | ||||||
|   <p><dees-button disabled>This is a disabled button</dees-button></p> |           const type = button.getAttribute('type') || 'default'; | ||||||
|   <p><dees-button type="big">This is a slotted Text</dees-button></p> |           console.log(`Button variant clicked: ${type}`); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'1. Button Variants'} .subtitle=${'Different visual styles for various use cases'}> | ||||||
|  |         <div class="button-group"> | ||||||
|  |           <dees-button type="default">Default</dees-button> | ||||||
|  |           <dees-button type="secondary">Secondary</dees-button> | ||||||
|  |           <dees-button type="destructive">Destructive</dees-button> | ||||||
|  |           <dees-button type="outline">Outline</dees-button> | ||||||
|  |           <dees-button type="ghost">Ghost</dees-button> | ||||||
|  |           <dees-button type="link">Link Button</dees-button> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|      |      | ||||||
|   <h3>Button States</h3> |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|   <p><dees-button status="normal">Normal Status</dees-button></p> |       // Demonstrate size differences programmatically | ||||||
|   <p><dees-button disabled status="pending">Pending Status</dees-button></p> |       const buttons = elementArg.querySelectorAll('dees-button'); | ||||||
|   <p><dees-button disabled status="success">Success Status</dees-button></p> |       buttons.forEach((button) => { | ||||||
|   <p><dees-button disabled status="error">Error Status</dees-button></p> |         button.addEventListener('clicked', () => { | ||||||
|  |           const size = button.getAttribute('size') || 'default'; | ||||||
|  |           console.log(`Button size: ${size}`); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'2. Button Sizes'} .subtitle=${'Multiple sizes for different contexts and use cases'}> | ||||||
|  |         <div class="button-group"> | ||||||
|  |           <dees-button size="sm">Small Button</dees-button> | ||||||
|  |           <dees-button size="default">Default Size</dees-button> | ||||||
|  |           <dees-button size="lg">Large Button</dees-button> | ||||||
|  |           <dees-button size="icon" type="outline" .text=${'🚀'}></dees-button> | ||||||
|  |         </div> | ||||||
|          |          | ||||||
|   <h3>Buttons in Forms (Auto-spacing)</h3> |         <div class="button-group" style="margin-top: 16px;"> | ||||||
|   <div class="form-demo"> |           <dees-button size="sm" type="secondary">Small Secondary</dees-button> | ||||||
|     <dees-form> |           <dees-button size="default" type="destructive">Default Destructive</dees-button> | ||||||
|       <dees-input-text label="Name" key="name"></dees-input-text> |           <dees-button size="lg" type="outline">Large Outline</dees-button> | ||||||
|       <dees-input-text label="Email" key="email"></dees-input-text> |         </div> | ||||||
|       <dees-button>Save Draft</dees-button> |       </dees-panel> | ||||||
|       <dees-button type="highlighted">Save and Continue</dees-button> |     </dees-demowrapper> | ||||||
|       <dees-form-submit>Submit Form</dees-form-submit> |  | ||||||
|     </dees-form> |  | ||||||
|   </div> |  | ||||||
|      |      | ||||||
|   <h3>Buttons Outside Forms (No auto-spacing)</h3> |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|   <div class="button-group"> |       // Track icon button clicks | ||||||
|     <dees-button>Button 1</dees-button> |       const iconButtons = elementArg.querySelectorAll('dees-button'); | ||||||
|     <dees-button>Button 2</dees-button> |       iconButtons.forEach((button) => { | ||||||
|     <dees-button>Button 3</dees-button> |         button.addEventListener('clicked', () => { | ||||||
|   </div> |           const hasIcon = button.querySelector('dees-icon'); | ||||||
|  |           if (hasIcon) { | ||||||
|  |             const iconName = hasIcon.getAttribute('iconFA') || 'unknown'; | ||||||
|  |             console.log(`Icon button clicked: ${iconName}`); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}> | ||||||
|  |         <div class="icon-row"> | ||||||
|  |           <dees-button> | ||||||
|  |             <dees-icon iconFA="faPlus"></dees-icon> | ||||||
|  |             Add Item | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button type="destructive"> | ||||||
|  |             <dees-icon iconFA="faTrash"></dees-icon> | ||||||
|  |             Delete | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button type="outline"> | ||||||
|  |             <dees-icon iconFA="faDownload"></dees-icon> | ||||||
|  |             Download | ||||||
|  |           </dees-button> | ||||||
|  |         </div> | ||||||
|          |          | ||||||
|   <h3>Manual Form Spacing</h3> |         <div class="icon-row"> | ||||||
|   <div> |           <dees-button type="secondary" size="sm"> | ||||||
|     <dees-button inside-form="true">Manually spaced button 1</dees-button> |             <dees-icon iconFA="faCog"></dees-icon> | ||||||
|     <dees-button inside-form="true">Manually spaced button 2</dees-button> |             Settings | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button type="ghost"> | ||||||
|  |             <dees-icon iconFA="faChevronLeft"></dees-icon> | ||||||
|  |             Back | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button type="ghost"> | ||||||
|  |             Next | ||||||
|  |             <dees-icon iconFA="faChevronRight"></dees-icon> | ||||||
|  |           </dees-button> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="icon-row"> | ||||||
|  |           <dees-button size="icon" type="default"> | ||||||
|  |             <dees-icon iconFA="faPlus"></dees-icon> | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button size="icon" type="secondary"> | ||||||
|  |             <dees-icon iconFA="faCog"></dees-icon> | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button size="icon" type="outline"> | ||||||
|  |             <dees-icon iconFA="faSearch"></dees-icon> | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button size="icon" type="ghost"> | ||||||
|  |             <dees-icon iconFA="faEllipsisV"></dees-icon> | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button size="icon" type="destructive"> | ||||||
|  |             <dees-icon iconFA="faTrash"></dees-icon> | ||||||
|  |           </dees-button> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate status changes | ||||||
|  |       const pendingButton = elementArg.querySelector('dees-button[status="pending"]'); | ||||||
|  |       const successButton = elementArg.querySelector('dees-button[status="success"]'); | ||||||
|  |       const errorButton = elementArg.querySelector('dees-button[status="error"]'); | ||||||
|  |        | ||||||
|  |       // Simulate status changes | ||||||
|  |       if (pendingButton) { | ||||||
|  |         setTimeout(() => { | ||||||
|  |           console.log('Pending button is showing loading state'); | ||||||
|  |         }, 1000); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (successButton) { | ||||||
|  |         successButton.addEventListener('clicked', () => { | ||||||
|  |           console.log('Success state button clicked'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (errorButton) { | ||||||
|  |         errorButton.addEventListener('clicked', () => { | ||||||
|  |           console.log('Error state button clicked'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'4. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}> | ||||||
|  |         <div class="button-group"> | ||||||
|  |           <dees-button status="normal">Normal</dees-button> | ||||||
|  |           <dees-button status="pending">Processing...</dees-button> | ||||||
|  |           <dees-button status="success">Success!</dees-button> | ||||||
|  |           <dees-button status="error">Error!</dees-button> | ||||||
|  |           <dees-button disabled>Disabled</dees-button> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="button-group" style="margin-top: 16px;"> | ||||||
|  |           <dees-button type="secondary" status="pending" size="sm">Small Loading</dees-button> | ||||||
|  |           <dees-button type="outline" status="pending">Default Loading</dees-button> | ||||||
|  |           <dees-button type="destructive" status="pending" size="lg">Large Loading</dees-button> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Set up click handlers with the output element | ||||||
|  |       const output = elementArg.querySelector('#click-output'); | ||||||
|  |        | ||||||
|  |       const clickMeBtn = elementArg.querySelector('dees-button:first-of-type'); | ||||||
|  |       const dataBtn = elementArg.querySelector('dees-button[type="secondary"]'); | ||||||
|  |       const asyncBtn = elementArg.querySelector('dees-button[type="destructive"]'); | ||||||
|  |        | ||||||
|  |       if (clickMeBtn && output) { | ||||||
|  |         clickMeBtn.addEventListener('clicked', () => { | ||||||
|  |           output.textContent = `Clicked: Default button at ${new Date().toLocaleTimeString()}`; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (dataBtn && output) { | ||||||
|  |         dataBtn.addEventListener('clicked', (e: CustomEvent) => { | ||||||
|  |           output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (asyncBtn && output) { | ||||||
|  |         asyncBtn.addEventListener('clicked', async () => { | ||||||
|  |           output.textContent = 'Processing...'; | ||||||
|  |           await domtools.plugins.smartdelay.delayFor(2000); | ||||||
|  |           output.textContent = 'Action completed!'; | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}> | ||||||
|  |         <div class="button-group"> | ||||||
|  |           <dees-button>Click Me</dees-button> | ||||||
|  |           <dees-button type="secondary" .eventDetailData=${'custom-data-123'}> | ||||||
|  |             Click with Data | ||||||
|  |           </dees-button> | ||||||
|  |           <dees-button type="destructive">Async Action</dees-button> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div id="click-output" class="demo-output"> | ||||||
|  |           <em>Click a button to see the result...</em> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Set up form submission handling | ||||||
|  |       const form = elementArg.querySelector('dees-form'); | ||||||
|  |       const output = elementArg.querySelector('#form-output'); | ||||||
|  |        | ||||||
|  |       if (form && output) { | ||||||
|  |         form.addEventListener('formData', (e: CustomEvent) => { | ||||||
|  |           output.innerHTML = '<strong>Form submitted with data:</strong><br>' +  | ||||||
|  |             JSON.stringify(e.detail.data, null, 2); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Track non-submit button clicks | ||||||
|  |       const draftBtn = elementArg.querySelector('dees-button[type="secondary"]'); | ||||||
|  |       const cancelBtn = elementArg.querySelector('dees-button[type="ghost"]'); | ||||||
|  |        | ||||||
|  |       if (draftBtn) { | ||||||
|  |         draftBtn.addEventListener('clicked', () => { | ||||||
|  |           console.log('Save Draft clicked'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (cancelBtn) { | ||||||
|  |         cancelBtn.addEventListener('clicked', () => { | ||||||
|  |           console.log('Cancel clicked'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}> | ||||||
|  |         <dees-form> | ||||||
|  |           <dees-input-text label="Name" key="name" required></dees-input-text> | ||||||
|  |           <dees-input-text label="Email" key="email" type="email" required></dees-input-text> | ||||||
|  |           <dees-input-text label="Message" key="message" isMultiline></dees-input-text> | ||||||
|  |            | ||||||
|  |           <dees-button type="secondary">Save Draft</dees-button> | ||||||
|  |           <dees-button type="ghost">Cancel</dees-button> | ||||||
|  |           <dees-form-submit>Submit Form</dees-form-submit> | ||||||
|  |         </dees-form> | ||||||
|  |          | ||||||
|  |         <div id="form-output" class="demo-output" style="white-space: pre-wrap;"> | ||||||
|  |           <em>Submit the form to see the data...</em> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Log legacy type mappings | ||||||
|  |       const buttons = elementArg.querySelectorAll('dees-button'); | ||||||
|  |       buttons.forEach((button) => { | ||||||
|  |         const type = button.getAttribute('type'); | ||||||
|  |         if (type) { | ||||||
|  |           console.log(`Legacy type "${type}" is supported for backward compatibility`); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'7. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}> | ||||||
|  |         <div class="button-group"> | ||||||
|  |           <dees-button type="normal">Normal → Default</dees-button> | ||||||
|  |           <dees-button type="highlighted">Highlighted → Destructive</dees-button> | ||||||
|  |           <dees-button type="discreet">Discreet → Outline</dees-button> | ||||||
|  |           <dees-button type="big">Big → Large Size</dees-button> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <p style="margin-top: 16px; font-size: 14px; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};"> | ||||||
|  |           These legacy type values are maintained for backward compatibility but we recommend using the new variant system. | ||||||
|  |         </p> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Track action group clicks | ||||||
|  |       const actionGroup = elementArg.querySelectorAll('.vertical-group')[0]; | ||||||
|  |       const dangerGroup = elementArg.querySelectorAll('.vertical-group')[1]; | ||||||
|  |        | ||||||
|  |       if (actionGroup) { | ||||||
|  |         const buttons = actionGroup.querySelectorAll('dees-button'); | ||||||
|  |         buttons.forEach((button, index) => { | ||||||
|  |           button.addEventListener('clicked', () => { | ||||||
|  |             const action = ['Save Changes', 'Discard', 'Help'][index]; | ||||||
|  |             console.log(`Action group: ${action} clicked`); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (dangerGroup) { | ||||||
|  |         const buttons = dangerGroup.querySelectorAll('dees-button'); | ||||||
|  |         buttons.forEach((button, index) => { | ||||||
|  |           button.addEventListener('clicked', () => { | ||||||
|  |             const action = ['Delete Account', 'Archive Data', 'Not Available'][index]; | ||||||
|  |             if (index !== 2) { // Skip disabled button | ||||||
|  |               console.log(`Danger zone: ${action} clicked`); | ||||||
|  |             } | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'8. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}> | ||||||
|  |         <div class="horizontal-group"> | ||||||
|  |           <div class="vertical-group"> | ||||||
|  |             <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Action Group</h4> | ||||||
|  |             <dees-button type="default" size="sm"> | ||||||
|  |               <dees-icon iconFA="faSave"></dees-icon> | ||||||
|  |               Save Changes | ||||||
|  |             </dees-button> | ||||||
|  |             <dees-button type="secondary" size="sm"> | ||||||
|  |               <dees-icon iconFA="faUndo"></dees-icon> | ||||||
|  |               Discard | ||||||
|  |             </dees-button> | ||||||
|  |             <dees-button type="ghost" size="sm"> | ||||||
|  |               <dees-icon iconFA="faQuestionCircle"></dees-icon> | ||||||
|  |               Help | ||||||
|  |             </dees-button> | ||||||
|  |           </div> | ||||||
|  |            | ||||||
|  |           <div class="vertical-group"> | ||||||
|  |             <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Danger Zone</h4> | ||||||
|  |             <dees-button type="destructive" size="sm"> | ||||||
|  |               <dees-icon iconFA="faTrash"></dees-icon> | ||||||
|  |               Delete Account | ||||||
|  |             </dees-button> | ||||||
|  |             <dees-button type="outline" size="sm"> | ||||||
|  |               <dees-icon iconFA="faArchive"></dees-icon> | ||||||
|  |               Archive Data | ||||||
|  |             </dees-button> | ||||||
|  |             <dees-button type="ghost" size="sm" disabled> | ||||||
|  |               <dees-icon iconFA="faBan"></dees-icon> | ||||||
|  |               Not Available | ||||||
|  |             </dees-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div style="margin-top: 24px;"> | ||||||
|  |           <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Code Example:</h4> | ||||||
|  |           <div class="code-snippet"> | ||||||
|  |             <dees-button type="default" size="sm" @clicked="\${handleClick}"><br> | ||||||
|  |               <dees-icon iconFA="faSave"></dees-icon><br> | ||||||
|  |               Save Changes<br> | ||||||
|  |             </dees-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|   </div> |   </div> | ||||||
| `; | `; | ||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { demoFunc } from './dees-button.demo.js'; |  | ||||||
| import { | import { | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
| @@ -12,6 +11,7 @@ import { | |||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
| import * as domtools from '@design.estate/dees-domtools'; | import * as domtools from '@design.estate/dees-domtools'; | ||||||
|  | import { demoFunc } from './dees-button.demo.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -48,7 +48,12 @@ export class DeesButton extends DeesElement { | |||||||
|   @property({ |   @property({ | ||||||
|     type: String |     type: String | ||||||
|   }) |   }) | ||||||
|   public type: 'normal' | 'highlighted' | 'discreet' | 'big' = 'normal'; |   public type: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default'; | ||||||
|  |    | ||||||
|  |   @property({ | ||||||
|  |     type: String | ||||||
|  |   }) | ||||||
|  |   public size: 'default' | 'sm' | 'lg' | 'icon' = 'default'; | ||||||
|  |  | ||||||
|   @property({ |   @property({ | ||||||
|     type: String |     type: String | ||||||
| @@ -77,25 +82,23 @@ export class DeesButton extends DeesElement { | |||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     css` |     css` | ||||||
|       :host { |       :host { | ||||||
|         display: block; |         display: inline-block; | ||||||
|         box-sizing: border-box; |         box-sizing: border-box; | ||||||
|         font-family: 'Geist Sans', 'monospace'; |         font-family: inherit; | ||||||
|       } |       } | ||||||
|       :host([hidden]) { |       :host([hidden]) { | ||||||
|         display: none; |         display: none; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       /* Form spacing styles */ |       /* Form spacing styles */ | ||||||
|       /* Default vertical form layout */ |  | ||||||
|       :host([inside-form]) { |       :host([inside-form]) { | ||||||
|         margin-bottom: 16px; /* Using standard 16px like inputs */ |         margin-bottom: 16px; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       :host([inside-form]:last-child) { |       :host([inside-form]:last-child) { | ||||||
|         margin-bottom: 0; |         margin-bottom: 0; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       /* Horizontal form layout - auto-detected via parent */ |  | ||||||
|       dees-form[horizontal-layout] :host([inside-form]) { |       dees-form[horizontal-layout] :host([inside-form]) { | ||||||
|         display: inline-block; |         display: inline-block; | ||||||
|         margin-right: 16px; |         margin-right: 16px; | ||||||
| @@ -107,114 +110,260 @@ export class DeesButton extends DeesElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button { |       .button { | ||||||
|         transition: all 0.1s , color 0s; |  | ||||||
|         position: relative; |         position: relative; | ||||||
|         font-size: 14px; |         display: inline-flex; | ||||||
|         font-weight: 400; |  | ||||||
|         display: flex; |  | ||||||
|         justify-content: center; |  | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         background: ${cssManager.bdTheme('#fff', '#333')}; |         justify-content: center; | ||||||
|         box-shadow: ${cssManager.bdTheme('0px 1px 3px rgba(0,0,0,0.3)', 'none')}; |         white-space: nowrap; | ||||||
|         border: 1px solid ${cssManager.bdTheme('#eee', '#333')}; |         border-radius: 6px; | ||||||
|         border-top: ${cssManager.bdTheme('1px solid #eee', '1px solid #444')}; |         font-weight: 500; | ||||||
|         border-radius: 4px; |         transition: all 0.15s ease; | ||||||
|         height: 40px; |         cursor: pointer; | ||||||
|         padding: 0px 8px; |  | ||||||
|         min-width: 100px; |  | ||||||
|         user-select: none; |         user-select: none; | ||||||
|         color: ${cssManager.bdTheme('#333', ' #ccc')}; |         outline: none; | ||||||
|         max-width: 500px; |         letter-spacing: -0.01em; | ||||||
|  |         gap: 8px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button:hover { |       /* Size variants */ | ||||||
|         background: #0050b9; |       .button.size-default { | ||||||
|         color: #ffffff; |         height: 36px; | ||||||
|         border: 1px solid #0050b9; |         padding: 0 16px; | ||||||
|         border-top: 1px solid #0050b9; |         font-size: 14px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button:active { |       .button.size-sm { | ||||||
|         background: #0069f2; |         height: 32px; | ||||||
|         border-top: 1px solid #0069f2; |         padding: 0 12px; | ||||||
|  |         font-size: 13px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button.highlighted { |       .button.size-lg { | ||||||
|         background: #e4002b; |         height: 44px; | ||||||
|  |         padding: 0 24px; | ||||||
|  |         font-size: 16px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.size-icon { | ||||||
|  |         height: 36px; | ||||||
|  |         width: 36px; | ||||||
|  |         padding: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Default variant */ | ||||||
|  |       .button.default { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 11.8%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |         box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.default:hover:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 10.2%)')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 20%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.default:active:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 9%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Destructive variant */ | ||||||
|  |       .button.destructive { | ||||||
|  |         background: hsl(0 84.2% 60.2%); | ||||||
|  |         color: hsl(0 0% 98%); | ||||||
|  |         border: 1px solid transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.destructive:hover:not(.disabled) { | ||||||
|  |         background: hsl(0 84.2% 56.2%); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.destructive:active:not(.disabled) { | ||||||
|  |         background: hsl(0 84.2% 52.2%); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Outline variant */ | ||||||
|  |       .button.outline { | ||||||
|  |         background: transparent; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.outline:hover:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 26.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.outline:active:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Secondary variant */ | ||||||
|  |       .button.secondary { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | ||||||
|  |         border: 1px solid transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.secondary:hover:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.secondary:active:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 11.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Ghost variant */ | ||||||
|  |       .button.ghost { | ||||||
|  |         background: transparent; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | ||||||
|  |         border: 1px solid transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.ghost:hover:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.ghost:active:not(.disabled) { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Link variant */ | ||||||
|  |       .button.link { | ||||||
|  |         background: transparent; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')}; | ||||||
|         border: none; |         border: none; | ||||||
|         color: #fff; |         text-decoration: underline; | ||||||
|  |         text-decoration-color: transparent; | ||||||
|  |         height: auto; | ||||||
|  |         padding: 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button.highlighted:hover { |       .button.link:hover:not(.disabled) { | ||||||
|         background: #b50021; |         text-decoration-color: currentColor; | ||||||
|         border: none; |  | ||||||
|         color: #fff; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button.discreet { |       /* Status states */ | ||||||
|         background: none; |       .button.pending, | ||||||
|         border: 1px solid #9b9b9e; |       .button.success, | ||||||
|         color: ${cssManager.bdTheme('#000', '#fff')}; |       .button.error { | ||||||
|  |         pointer-events: none; | ||||||
|  |         padding-left: 36px; /* Space for spinner */ | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .button.discreet:hover { |       .button.size-sm.pending, | ||||||
|         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; |       .button.size-sm.success, | ||||||
|  |       .button.size-sm.error { | ||||||
|  |         padding-left: 32px; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|  |       .button.size-lg.pending, | ||||||
|  |       .button.size-lg.success, | ||||||
|  |       .button.size-lg.error { | ||||||
|  |         padding-left: 44px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.pending { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8% / 0.2)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(213.1 93.9% 67.8%)')}; | ||||||
|  |         border: 1px solid transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.success { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3% / 0.2)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(142.1 70.6% 45.3%)')}; | ||||||
|  |         border: 1px solid transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .button.error { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 62.8% 70.6% / 0.2)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 62.8% 70.6%)')}; | ||||||
|  |         border: 1px solid transparent; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Disabled state */ | ||||||
|       .button.disabled { |       .button.disabled { | ||||||
|         background: ${cssManager.bdTheme('#ffffff00', '#11111100')}; |         opacity: 0.5; | ||||||
|         border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; |         cursor: not-allowed; | ||||||
|         color: #9b9b9e; |         pointer-events: none; | ||||||
|         cursor: default; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       /* Hidden state */ | ||||||
|       .button.hidden { |       .button.hidden { | ||||||
|         display: none; |         display: none; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .button.big { |       /* Focus state */ | ||||||
|         width: 300px; |       .button:focus-visible { | ||||||
|         line-height: 48px; |         outline: 2px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')}; | ||||||
|         font-size: 16px; |         outline-offset: 2px; | ||||||
|         padding: 0px 48px; |  | ||||||
|         margin-top: 32px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .button.pending { |  | ||||||
|         border: 1px dashed ${cssManager.bdTheme('#0069f2', '#0069f270')}; |  | ||||||
|         background: ${cssManager.bdTheme('#0069f2', '#0069f270')}; |  | ||||||
|         color: #fff; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .button.success { |  | ||||||
|         border: 1px dashed ${cssManager.bdTheme('#689F38', '#8BC34A70')}; |  | ||||||
|         background: ${cssManager.bdTheme('#689F38', '#8BC34A70')}; |  | ||||||
|         color: #fff; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .button.error { |  | ||||||
|         border: 1px dashed ${cssManager.bdTheme('#B71C1C', '#E64A1970')}; |  | ||||||
|         background: ${cssManager.bdTheme('#B71C1C', '#E64A1970')}; |  | ||||||
|         color: #fff; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       /* Loading spinner */ | ||||||
|       dees-spinner { |       dees-spinner { | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         left: 10px; |         left: 10px; | ||||||
|  |         width: 16px; | ||||||
|  |         height: 16px; | ||||||
|       } |       } | ||||||
|  |        | ||||||
|  |       .button.size-sm dees-spinner { | ||||||
|  |         left: 8px; | ||||||
|  |         width: 14px; | ||||||
|  |         height: 14px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .button.size-lg dees-spinner { | ||||||
|  |         left: 14px; | ||||||
|  |         width: 18px; | ||||||
|  |         height: 18px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       /* Icon sizing within buttons */ | ||||||
|  |       .button dees-icon { | ||||||
|  |         width: 16px; | ||||||
|  |         height: 16px; | ||||||
|  |         flex-shrink: 0; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .button.size-sm dees-icon { | ||||||
|  |         width: 14px; | ||||||
|  |         height: 14px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .button.size-lg dees-icon { | ||||||
|  |         width: 18px; | ||||||
|  |         height: 18px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|  |     // Map old types to new types for backward compatibility | ||||||
|  |     const typeMap: {[key: string]: string} = { | ||||||
|  |       'normal': 'default', | ||||||
|  |       'highlighted': 'destructive', | ||||||
|  |       'discreet': 'outline', | ||||||
|  |       'big': 'default' // Will use size instead | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     const actualType = typeMap[this.type] || this.type; | ||||||
|  |     const actualSize = this.type === 'big' ? 'lg' : this.size; | ||||||
|  |      | ||||||
|     return html` |     return html` | ||||||
|       <div |       <div | ||||||
|         class="button ${this.isHidden ? 'hidden' : 'block'}  ${this.type} ${this.status} ${this.disabled |         class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} ${this.status} ${this.disabled | ||||||
|           ? 'disabled' |           ? 'disabled' | ||||||
|           : null}" |           : ''}" | ||||||
|         @click="${this.dispatchClick}" |         @click="${this.dispatchClick}" | ||||||
|       > |       > | ||||||
|         ${this.status === 'normal' ? html``: html` |         ${this.status === 'normal' ? html``: html` | ||||||
|           <dees-spinner .bnw=${true} status="${this.status}"></dees-spinner> |           <dees-spinner  | ||||||
|  |             .bnw=${true}  | ||||||
|  |             status="${this.status}" | ||||||
|  |             size="${actualSize === 'sm' ? 14 : actualSize === 'lg' ? 18 : 16}" | ||||||
|  |           ></dees-spinner> | ||||||
|         `} |         `} | ||||||
|         <div class="textbox">${this.text || html`<slot>Button</slot>`}</div> |         <div class="textbox">${this.text || html`<slot>Button</slot>`}</div> | ||||||
|       </div> |       </div> | ||||||
|   | |||||||
| @@ -1,531 +0,0 @@ | |||||||
| import { |  | ||||||
|   DeesElement, |  | ||||||
|   css, |  | ||||||
|   cssManager, |  | ||||||
|   customElement, |  | ||||||
|   html, |  | ||||||
|   property, |  | ||||||
|   state, |  | ||||||
|   type TemplateResult, |  | ||||||
| } from '@design.estate/dees-element'; |  | ||||||
|  |  | ||||||
| import * as domtools from '@design.estate/dees-domtools'; |  | ||||||
| import { demoFunc } from './dees-chart-area.demo.js'; |  | ||||||
|  |  | ||||||
| import ApexCharts from 'apexcharts'; |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     'dees-chart-area': DeesChartArea; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement('dees-chart-area') |  | ||||||
| export class DeesChartArea extends DeesElement { |  | ||||||
|   public static demo = demoFunc; |  | ||||||
|  |  | ||||||
|   // instance |  | ||||||
|   @state() |  | ||||||
|   public chart: ApexCharts; |  | ||||||
|  |  | ||||||
|   @property() |  | ||||||
|   public label: string = 'Untitled Chart'; |  | ||||||
|  |  | ||||||
|   @property({ type: Array }) |  | ||||||
|   public series: ApexAxisChartSeries = []; |  | ||||||
|    |  | ||||||
|   // Override getter to return internal chart data |  | ||||||
|   get chartSeries(): ApexAxisChartSeries { |  | ||||||
|     return this.internalChartData.length > 0 ? this.internalChartData : this.series; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @property({ attribute: false }) |  | ||||||
|   public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`; |  | ||||||
|    |  | ||||||
|   @property({ type: Number }) |  | ||||||
|   public rollingWindow: number = 0; // 0 means no rolling window |  | ||||||
|    |  | ||||||
|   @property({ type: Boolean }) |  | ||||||
|   public realtimeMode: boolean = false; |  | ||||||
|    |  | ||||||
|   @property({ type: String }) |  | ||||||
|   public yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic'; |  | ||||||
|    |  | ||||||
|   @property({ type: Number }) |  | ||||||
|   public yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage' |  | ||||||
|    |  | ||||||
|   @property({ type: Number }) |  | ||||||
|   public autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable) |  | ||||||
|  |  | ||||||
|   private resizeObserver: ResizeObserver; |  | ||||||
|   private resizeTimeout: number; |  | ||||||
|   private internalChartData: ApexAxisChartSeries = []; |  | ||||||
|   private autoScrollTimer: number | null = null; |  | ||||||
|  |  | ||||||
|   constructor() { |  | ||||||
|     super(); |  | ||||||
|     domtools.elementBasic.setup(); |  | ||||||
|  |  | ||||||
|     this.resizeObserver = new ResizeObserver((entries) => { |  | ||||||
|       // Debounce resize calls to prevent excessive updates |  | ||||||
|       if (this.resizeTimeout) { |  | ||||||
|         clearTimeout(this.resizeTimeout); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       this.resizeTimeout = window.setTimeout(() => { |  | ||||||
|         for (let entry of entries) { |  | ||||||
|           if (entry.target.classList.contains('mainbox') && this.chart) { |  | ||||||
|             this.resizeChart(); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }, 100); // 100ms debounce |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     this.registerStartupFunction(async () => { |  | ||||||
|       this.updateComplete.then(() => { |  | ||||||
|         const mainbox = this.shadowRoot.querySelector('.mainbox'); |  | ||||||
|         if (mainbox) { |  | ||||||
|           this.resizeObserver.observe(mainbox); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     this.registerGarbageFunction(async () => { |  | ||||||
|       if (this.resizeTimeout) { |  | ||||||
|         clearTimeout(this.resizeTimeout); |  | ||||||
|       } |  | ||||||
|       this.resizeObserver.disconnect(); |  | ||||||
|       this.stopAutoScroll(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public static styles = [ |  | ||||||
|     cssManager.defaultStyles, |  | ||||||
|     css` |  | ||||||
|       :host { |  | ||||||
|         font-family: 'Geist Sans', sans-serif; |  | ||||||
|         color: #ccc; |  | ||||||
|         font-weight: 600; |  | ||||||
|         font-size: 12px; |  | ||||||
|       } |  | ||||||
|       .mainbox { |  | ||||||
|         position: relative; |  | ||||||
|         width: 100%; |  | ||||||
|         height: 400px; |  | ||||||
|         background: #111; |  | ||||||
|         border-radius: 8px; |  | ||||||
|         overflow: hidden; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .chartTitle { |  | ||||||
|         position: absolute; |  | ||||||
|         top: 0; |  | ||||||
|         left: 0; |  | ||||||
|         width: 100%; |  | ||||||
|         text-align: center; |  | ||||||
|         padding-top: 16px; |  | ||||||
|         z-index: 10; |  | ||||||
|       } |  | ||||||
|       .chartContainer { |  | ||||||
|         position: absolute; |  | ||||||
|         top: 0px; |  | ||||||
|         left: 0px; |  | ||||||
|         bottom: 0px; |  | ||||||
|         right: 0px; |  | ||||||
|         padding: 32px 16px 16px 0px; |  | ||||||
|         overflow: hidden; |  | ||||||
|       } |  | ||||||
|     `, |  | ||||||
|   ]; |  | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |  | ||||||
|     return html` |  | ||||||
|       <div class="mainbox"> |  | ||||||
|         <div class="chartTitle">${this.label}</div> |  | ||||||
|         <div class="chartContainer"></div> |  | ||||||
|       </div> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async firstUpdated() { |  | ||||||
|     await this.domtoolsPromise; |  | ||||||
|      |  | ||||||
|     // Wait for next animation frame to ensure layout is complete |  | ||||||
|     await new Promise(resolve => requestAnimationFrame(resolve)); |  | ||||||
|      |  | ||||||
|     // Get actual dimensions of the container |  | ||||||
|     const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox'); |  | ||||||
|     const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer'); |  | ||||||
|      |  | ||||||
|     if (!mainbox || !chartContainer) { |  | ||||||
|       console.error('Chart containers not found'); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Calculate initial dimensions |  | ||||||
|     const styleChartContainer = window.getComputedStyle(chartContainer); |  | ||||||
|     const paddingTop = parseInt(styleChartContainer.paddingTop, 10); |  | ||||||
|     const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10); |  | ||||||
|     const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10); |  | ||||||
|     const paddingRight = parseInt(styleChartContainer.paddingRight, 10); |  | ||||||
|      |  | ||||||
|     const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight; |  | ||||||
|     const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom; |  | ||||||
|      |  | ||||||
|     // Use provided series data or default demo data |  | ||||||
|     const chartSeries = this.series.length > 0 ? this.series : [ |  | ||||||
|       { |  | ||||||
|         name: 'cpu', |  | ||||||
|         data: [ |  | ||||||
|           { x: '2025-01-15T03:00:00', y: 25 }, |  | ||||||
|           { x: '2025-01-15T07:00:00', y: 30 }, |  | ||||||
|           { x: '2025-01-15T11:00:00', y: 20 }, |  | ||||||
|           { x: '2025-01-15T15:00:00', y: 35 }, |  | ||||||
|           { x: '2025-01-15T19:00:00', y: 25 }, |  | ||||||
|         ], |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'memory', |  | ||||||
|         data: [ |  | ||||||
|           { x: '2025-01-15T03:00:00', y: 10 }, |  | ||||||
|           { x: '2025-01-15T07:00:00', y: 12 }, |  | ||||||
|           { x: '2025-01-15T11:00:00', y: 10 }, |  | ||||||
|           { x: '2025-01-15T15:00:00', y: 30 }, |  | ||||||
|           { x: '2025-01-15T19:00:00', y: 40 }, |  | ||||||
|         ], |  | ||||||
|       }, |  | ||||||
|     ]; |  | ||||||
|      |  | ||||||
|     // Store internal data |  | ||||||
|     this.internalChartData = chartSeries; |  | ||||||
|      |  | ||||||
|     var options: ApexCharts.ApexOptions = { |  | ||||||
|       series: chartSeries, |  | ||||||
|       chart: { |  | ||||||
|         width: initialWidth || 100, // Use actual width or fallback |  | ||||||
|         height: initialHeight || 100, // Use actual height or fallback |  | ||||||
|         type: 'area', |  | ||||||
|         toolbar: { |  | ||||||
|           show: false, // This line disables the toolbar |  | ||||||
|         }, |  | ||||||
|         animations: { |  | ||||||
|           enabled: !this.realtimeMode, // Disable animations in realtime mode |  | ||||||
|           speed: 400, |  | ||||||
|           animateGradually: { |  | ||||||
|             enabled: false, // Disable gradual animation for cleaner updates |  | ||||||
|             delay: 0 |  | ||||||
|           }, |  | ||||||
|           dynamicAnimation: { |  | ||||||
|             enabled: !this.realtimeMode, |  | ||||||
|             speed: 350 |  | ||||||
|           } |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       dataLabels: { |  | ||||||
|         enabled: false, |  | ||||||
|       }, |  | ||||||
|       stroke: { |  | ||||||
|         width: 1, |  | ||||||
|         curve: 'smooth', |  | ||||||
|       }, |  | ||||||
|       xaxis: { |  | ||||||
|         type: 'datetime', // Time-series data |  | ||||||
|         labels: { |  | ||||||
|           format: 'HH:mm:ss', // Time formatting with seconds |  | ||||||
|           datetimeUTC: false, |  | ||||||
|           style: { |  | ||||||
|             colors: '#9e9e9e', // Label color |  | ||||||
|             fontSize: '11px', |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         axisBorder: { |  | ||||||
|           show: false, // Hide x-axis border |  | ||||||
|         }, |  | ||||||
|         axisTicks: { |  | ||||||
|           show: false, // Hide x-axis ticks |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       yaxis: { |  | ||||||
|         min: 0, |  | ||||||
|         max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax, |  | ||||||
|         labels: { |  | ||||||
|           formatter: this.yAxisFormatter, |  | ||||||
|           style: { |  | ||||||
|             colors: '#9e9e9e', // Label color |  | ||||||
|             fontSize: '12px', |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         axisBorder: { |  | ||||||
|           show: false, // Hide y-axis border |  | ||||||
|         }, |  | ||||||
|         axisTicks: { |  | ||||||
|           show: false, // Hide y-axis ticks |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       tooltip: { |  | ||||||
|         shared: true, // Enables the tooltip to display across series |  | ||||||
|         intersect: false, // Allows hovering anywhere on the chart |  | ||||||
|         followCursor: true, // Makes tooltip follow mouse even between points |  | ||||||
|         x: { |  | ||||||
|           format: 'dd/MM/yy HH:mm', |  | ||||||
|         }, |  | ||||||
|         custom: function ({ series, dataPointIndex, w }: any) { |  | ||||||
|           // Iterate through each series and get its value |  | ||||||
|           let tooltipContent = `<div style="padding: 10px; background: #1e1e2f; color: white; border-radius: 5px;">`; |  | ||||||
|  |  | ||||||
|           series.forEach((s: number[], index: number) => { |  | ||||||
|             const label = w.globals.seriesNames[index]; // Get series label |  | ||||||
|             const value = s[dataPointIndex]; // Get value at data point |  | ||||||
|             tooltipContent += `<strong>${label}:</strong> ${value} Mbps<br/>`; |  | ||||||
|           }); |  | ||||||
|  |  | ||||||
|           tooltipContent += `</div>`; |  | ||||||
|           return tooltipContent; |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       grid: { |  | ||||||
|         xaxis: { |  | ||||||
|           lines: { |  | ||||||
|             show: true, // This enables the grid lines along the x-axis |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         yaxis: { |  | ||||||
|           lines: { |  | ||||||
|             show: true, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         borderColor: '#333', // Set the color of the grid lines |  | ||||||
|         strokeDashArray: 0, // Solid line |  | ||||||
|         row: { |  | ||||||
|           colors: [], // This can be used to alternate the shading of the horizontal rows |  | ||||||
|           opacity: 0.1, |  | ||||||
|         }, |  | ||||||
|         column: { |  | ||||||
|           colors: [], // For vertical column bands, not needed here but available for customization |  | ||||||
|           opacity: 0.1, |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|       fill: { |  | ||||||
|         type: 'gradient', // Gradient fill for the area |  | ||||||
|         gradient: { |  | ||||||
|           shade: 'dark', |  | ||||||
|           type: 'vertical', |  | ||||||
|           gradientToColors: ['#9c27b0'], // Gradient color ending |  | ||||||
|           stops: [0, 100], |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|     this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options); |  | ||||||
|     await this.chart.render(); |  | ||||||
|      |  | ||||||
|     // Give the chart a moment to fully initialize before resizing |  | ||||||
|     await new Promise(resolve => setTimeout(resolve, 100)); |  | ||||||
|     await this.resizeChart(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async updated(changedProperties: Map<string, any>) { |  | ||||||
|     super.updated(changedProperties); |  | ||||||
|      |  | ||||||
|     // Update chart if series data changes |  | ||||||
|     if (changedProperties.has('series') && this.chart && this.series.length > 0) { |  | ||||||
|       await this.updateSeries(this.series); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Update y-axis formatter if it changes |  | ||||||
|     if (changedProperties.has('yAxisFormatter') && this.chart) { |  | ||||||
|       await this.chart.updateOptions({ |  | ||||||
|         yaxis: { |  | ||||||
|           labels: { |  | ||||||
|             formatter: this.yAxisFormatter, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Handle realtime mode changes |  | ||||||
|     if (changedProperties.has('realtimeMode') && this.chart) { |  | ||||||
|       await this.chart.updateOptions({ |  | ||||||
|         chart: { |  | ||||||
|           animations: { |  | ||||||
|             enabled: !this.realtimeMode, |  | ||||||
|             speed: 400, |  | ||||||
|             animateGradually: { |  | ||||||
|               enabled: false, |  | ||||||
|               delay: 0 |  | ||||||
|             }, |  | ||||||
|             dynamicAnimation: { |  | ||||||
|               enabled: !this.realtimeMode, |  | ||||||
|               speed: 350 |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // Start/stop auto-scroll based on realtime mode |  | ||||||
|       if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) { |  | ||||||
|         this.startAutoScroll(); |  | ||||||
|       } else { |  | ||||||
|         this.stopAutoScroll(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Handle auto-scroll interval changes |  | ||||||
|     if (changedProperties.has('autoScrollInterval') && this.chart) { |  | ||||||
|       this.stopAutoScroll(); |  | ||||||
|       if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) { |  | ||||||
|         this.startAutoScroll(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Handle y-axis scaling changes |  | ||||||
|     if ((changedProperties.has('yAxisScaling') || changedProperties.has('yAxisMax')) && this.chart) { |  | ||||||
|       await this.chart.updateOptions({ |  | ||||||
|         yaxis: { |  | ||||||
|           min: 0, |  | ||||||
|           max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async updateSeries(newSeries: ApexAxisChartSeries, animate: boolean = true) { |  | ||||||
|     if (!this.chart) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Store the new data first |  | ||||||
|     this.internalChartData = newSeries; |  | ||||||
|      |  | ||||||
|     // Handle rolling window if enabled |  | ||||||
|     if (this.rollingWindow > 0 && this.realtimeMode) { |  | ||||||
|       const now = Date.now(); |  | ||||||
|       const cutoffTime = now - this.rollingWindow; |  | ||||||
|        |  | ||||||
|       // Filter data to only include points within the rolling window |  | ||||||
|       const filteredSeries = newSeries.map(series => ({ |  | ||||||
|         name: series.name, |  | ||||||
|         data: (series.data as any[]).filter(point => { |  | ||||||
|           if (typeof point === 'object' && point !== null && 'x' in point) { |  | ||||||
|             return new Date(point.x).getTime() > cutoffTime; |  | ||||||
|           } |  | ||||||
|           return false; |  | ||||||
|         }) |  | ||||||
|       })); |  | ||||||
|        |  | ||||||
|       // Only update if we have data |  | ||||||
|       if (filteredSeries.some(s => s.data.length > 0)) { |  | ||||||
|         // Handle y-axis scaling first |  | ||||||
|         if (this.yAxisScaling === 'dynamic') { |  | ||||||
|           const allValues = filteredSeries.flatMap(s => (s.data as any[]).map(d => d.y)); |  | ||||||
|           if (allValues.length > 0) { |  | ||||||
|             const maxValue = Math.max(...allValues); |  | ||||||
|             const dynamicMax = Math.ceil(maxValue * 1.1); |  | ||||||
|             await this.chart.updateOptions({ |  | ||||||
|               yaxis: { |  | ||||||
|                 min: 0, |  | ||||||
|                 max: dynamicMax |  | ||||||
|               } |  | ||||||
|             }, false, false); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         this.chart.updateSeries(filteredSeries, false); |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       this.chart.updateSeries(newSeries, animate); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   // New method to update just the x-axis for smooth scrolling |  | ||||||
|   public async updateTimeWindow() { |  | ||||||
|     if (!this.chart || this.rollingWindow <= 0) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     const now = Date.now(); |  | ||||||
|     const cutoffTime = now - this.rollingWindow; |  | ||||||
|      |  | ||||||
|     await this.chart.updateOptions({ |  | ||||||
|       xaxis: { |  | ||||||
|         min: cutoffTime, |  | ||||||
|         max: now, |  | ||||||
|         labels: { |  | ||||||
|           format: 'HH:mm:ss', |  | ||||||
|           datetimeUTC: false, |  | ||||||
|           style: { |  | ||||||
|             colors: '#9e9e9e', |  | ||||||
|             fontSize: '11px', |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|         tickAmount: 6, |  | ||||||
|       } |  | ||||||
|     }, false, false); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async appendData(newData: { data: any[] }[]) { |  | ||||||
|     if (!this.chart) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Use ApexCharts' appendData method for smoother real-time updates |  | ||||||
|     this.chart.appendData(newData); |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   public async updateOptions(options: ApexCharts.ApexOptions, redrawPaths?: boolean, animate?: boolean) { |  | ||||||
|     if (!this.chart) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return this.chart.updateOptions(options, redrawPaths, animate); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async resizeChart() { |  | ||||||
|     if (!this.chart) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox'); |  | ||||||
|     const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer'); |  | ||||||
|      |  | ||||||
|     if (!mainbox || !chartContainer) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Get computed style of the element |  | ||||||
|     const styleChartContainer = window.getComputedStyle(chartContainer); |  | ||||||
|  |  | ||||||
|     // Extract padding values |  | ||||||
|     const paddingTop = parseInt(styleChartContainer.paddingTop, 10); |  | ||||||
|     const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10); |  | ||||||
|     const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10); |  | ||||||
|     const paddingRight = parseInt(styleChartContainer.paddingRight, 10); |  | ||||||
|  |  | ||||||
|     // Calculate the actual width and height to use, subtracting padding |  | ||||||
|     const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight; |  | ||||||
|     const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom; |  | ||||||
|  |  | ||||||
|     await this.chart.updateOptions({ |  | ||||||
|       chart: { |  | ||||||
|         width: actualWidth, |  | ||||||
|         height: actualHeight, |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   private startAutoScroll() { |  | ||||||
|     if (this.autoScrollTimer) { |  | ||||||
|       return; // Already running |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     this.autoScrollTimer = window.setInterval(() => { |  | ||||||
|       this.updateTimeWindow(); |  | ||||||
|     }, this.autoScrollInterval); |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   private stopAutoScroll() { |  | ||||||
|     if (this.autoScrollTimer) { |  | ||||||
|       window.clearInterval(this.autoScrollTimer); |  | ||||||
|       this.autoScrollTimer = null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										667
									
								
								ts_web/elements/dees-chart-area/component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										667
									
								
								ts_web/elements/dees-chart-area/component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,667 @@ | |||||||
|  | import { | ||||||
|  |   DeesElement, | ||||||
|  |   customElement, | ||||||
|  |   property, | ||||||
|  |   state, | ||||||
|  |   type TemplateResult, | ||||||
|  | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | import * as domtools from '@design.estate/dees-domtools'; | ||||||
|  | import { demoFunc } from './demo.js'; | ||||||
|  | import { chartAreaStyles } from './styles.js'; | ||||||
|  | import { renderChartArea } from './template.js'; | ||||||
|  |  | ||||||
|  | import ApexCharts from 'apexcharts'; | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     'dees-chart-area': DeesChartArea; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @customElement('dees-chart-area') | ||||||
|  | export class DeesChartArea extends DeesElement { | ||||||
|  |   public static demo = demoFunc; | ||||||
|  |  | ||||||
|  |   // instance | ||||||
|  |   @state() | ||||||
|  |   public chart: ApexCharts; | ||||||
|  |  | ||||||
|  |   @property() | ||||||
|  |   public label: string = 'Untitled Chart'; | ||||||
|  |  | ||||||
|  |   @property({ type: Array }) | ||||||
|  |   public series: ApexAxisChartSeries = []; | ||||||
|  |    | ||||||
|  |   // Override getter to return internal chart data | ||||||
|  |   get chartSeries(): ApexAxisChartSeries { | ||||||
|  |     return this.internalChartData.length > 0 ? this.internalChartData : this.series; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`; | ||||||
|  |    | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public rollingWindow: number = 0; // 0 means no rolling window | ||||||
|  |    | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public realtimeMode: boolean = false; | ||||||
|  |    | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic'; | ||||||
|  |    | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage' | ||||||
|  |    | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable) | ||||||
|  |  | ||||||
|  |   private resizeObserver: ResizeObserver; | ||||||
|  |   private resizeTimeout: number; | ||||||
|  |   private internalChartData: ApexAxisChartSeries = []; | ||||||
|  |   private autoScrollTimer: number | null = null; | ||||||
|  |   private readonly DEBUG_RESIZE = false; // Set to true to enable resize debugging | ||||||
|  |    | ||||||
|  |   // Chart color schemes | ||||||
|  |   private readonly CHART_COLORS = { | ||||||
|  |     dark: [ | ||||||
|  |       'hsl(217.2 91.2% 59.8%)', // Blue | ||||||
|  |       'hsl(173.4 80.4% 40%)',   // Teal | ||||||
|  |       'hsl(280.3 87.4% 66.7%)', // Purple | ||||||
|  |       'hsl(24.6 95% 53.1%)',    // Orange | ||||||
|  |     ], | ||||||
|  |     light: [ | ||||||
|  |       'hsl(222.2 47.4% 51.2%)', // Blue (shadcn primary) | ||||||
|  |       'hsl(142.1 76.2% 36.3%)', // Green (shadcn success) | ||||||
|  |       'hsl(280.3 47.7% 50.2%)', // Purple (muted) | ||||||
|  |       'hsl(20.5 90.2% 48.2%)',  // Orange (shadcn destructive variant) | ||||||
|  |     ] | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     super(); | ||||||
|  |     domtools.elementBasic.setup(); | ||||||
|  |  | ||||||
|  |     this.resizeObserver = new ResizeObserver((entries) => { | ||||||
|  |       // Debounce resize calls to prevent excessive updates | ||||||
|  |       if (this.resizeTimeout) { | ||||||
|  |         clearTimeout(this.resizeTimeout); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       this.resizeTimeout = window.setTimeout(() => { | ||||||
|  |         // Simply resize if we have a chart, since we're only observing the mainbox | ||||||
|  |         if (this.chart) { | ||||||
|  |           // Log resize event for debugging | ||||||
|  |           if (this.DEBUG_RESIZE && entries.length > 0) { | ||||||
|  |             const entry = entries[0]; | ||||||
|  |             console.log('DeesChartArea - Resize detected:', { | ||||||
|  |               width: entry.contentRect.width, | ||||||
|  |               height: entry.contentRect.height | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |           this.resizeChart(); | ||||||
|  |         } | ||||||
|  |       }, 100); // 100ms debounce | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // Note: ResizeObserver is now set up after chart initialization in firstUpdated() | ||||||
|  |     // to ensure proper timing and avoid race conditions | ||||||
|  |      | ||||||
|  |     this.registerGarbageFunction(async () => { | ||||||
|  |       if (this.resizeTimeout) { | ||||||
|  |         clearTimeout(this.resizeTimeout); | ||||||
|  |       } | ||||||
|  |       if (this.resizeObserver) { | ||||||
|  |         this.resizeObserver.disconnect(); | ||||||
|  |       } | ||||||
|  |       this.stopAutoScroll(); | ||||||
|  |        | ||||||
|  |       // Critical: Destroy chart instance to prevent memory leak | ||||||
|  |       if (this.chart) { | ||||||
|  |         try { | ||||||
|  |           this.chart.destroy(); | ||||||
|  |           this.chart = null; | ||||||
|  |         } catch (error) { | ||||||
|  |           console.error('Error destroying chart:', error); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   public async connectedCallback() { | ||||||
|  |     super.connectedCallback(); | ||||||
|  |      | ||||||
|  |     // Trigger resize when element is connected to DOM | ||||||
|  |     // This helps with dynamically added charts | ||||||
|  |     if (this.chart) { | ||||||
|  |       // Wait a frame for layout to settle | ||||||
|  |       await new Promise(resolve => requestAnimationFrame(resolve)); | ||||||
|  |       await this.resizeChart(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static styles = chartAreaStyles; | ||||||
|  |  | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     return renderChartArea(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public async firstUpdated() { | ||||||
|  |     await this.domtoolsPromise; | ||||||
|  |      | ||||||
|  |     // Wait for next animation frame to ensure layout is complete | ||||||
|  |     await new Promise(resolve => requestAnimationFrame(resolve)); | ||||||
|  |      | ||||||
|  |     // Get actual dimensions of the container | ||||||
|  |     const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox'); | ||||||
|  |     const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer'); | ||||||
|  |      | ||||||
|  |     if (!mainbox || !chartContainer) { | ||||||
|  |       console.error('Chart containers not found'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Calculate initial dimensions | ||||||
|  |     const styleChartContainer = window.getComputedStyle(chartContainer); | ||||||
|  |     const paddingTop = parseInt(styleChartContainer.paddingTop, 10); | ||||||
|  |     const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10); | ||||||
|  |     const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10); | ||||||
|  |     const paddingRight = parseInt(styleChartContainer.paddingRight, 10); | ||||||
|  |      | ||||||
|  |     const initialWidth = mainbox.clientWidth - paddingLeft - paddingRight; | ||||||
|  |     const initialHeight = mainbox.offsetHeight - paddingTop - paddingBottom; | ||||||
|  |      | ||||||
|  |     // Use provided series data or default demo data | ||||||
|  |     const chartSeries = this.series.length > 0 ? this.series : [ | ||||||
|  |       { | ||||||
|  |         name: 'cpu', | ||||||
|  |         data: [ | ||||||
|  |           { x: '2025-01-15T03:00:00', y: 25 }, | ||||||
|  |           { x: '2025-01-15T07:00:00', y: 30 }, | ||||||
|  |           { x: '2025-01-15T11:00:00', y: 20 }, | ||||||
|  |           { x: '2025-01-15T15:00:00', y: 35 }, | ||||||
|  |           { x: '2025-01-15T19:00:00', y: 25 }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'memory', | ||||||
|  |         data: [ | ||||||
|  |           { x: '2025-01-15T03:00:00', y: 10 }, | ||||||
|  |           { x: '2025-01-15T07:00:00', y: 12 }, | ||||||
|  |           { x: '2025-01-15T11:00:00', y: 10 }, | ||||||
|  |           { x: '2025-01-15T15:00:00', y: 30 }, | ||||||
|  |           { x: '2025-01-15T19:00:00', y: 40 }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |      | ||||||
|  |     // Store internal data | ||||||
|  |     this.internalChartData = chartSeries; | ||||||
|  |      | ||||||
|  |     // Get current theme | ||||||
|  |     const isDark = !this.goBright; | ||||||
|  |     const theme = isDark ? 'dark' : 'light'; | ||||||
|  |      | ||||||
|  |     var options: ApexCharts.ApexOptions = { | ||||||
|  |       series: chartSeries, | ||||||
|  |       chart: { | ||||||
|  |         width: initialWidth || 100, // Use actual width or fallback | ||||||
|  |         height: initialHeight || 100, // Use actual height or fallback | ||||||
|  |         type: 'area', | ||||||
|  |         background: 'transparent', // Transparent background to inherit from container | ||||||
|  |         toolbar: { | ||||||
|  |           show: false, // This line disables the toolbar | ||||||
|  |         }, | ||||||
|  |         animations: { | ||||||
|  |           enabled: !this.realtimeMode, // Disable animations in realtime mode | ||||||
|  |           speed: 400, | ||||||
|  |           animateGradually: { | ||||||
|  |             enabled: false, // Disable gradual animation for cleaner updates | ||||||
|  |             delay: 0 | ||||||
|  |           }, | ||||||
|  |           dynamicAnimation: { | ||||||
|  |             enabled: !this.realtimeMode, | ||||||
|  |             speed: 350 | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         zoom: { | ||||||
|  |           enabled: false, // Disable zoom for cleaner interaction | ||||||
|  |         }, | ||||||
|  |         selection: { | ||||||
|  |           enabled: false, // Disable selection | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       dataLabels: { | ||||||
|  |         enabled: false, | ||||||
|  |       }, | ||||||
|  |       stroke: { | ||||||
|  |         width: 2, | ||||||
|  |         curve: 'smooth', | ||||||
|  |       }, | ||||||
|  |       xaxis: { | ||||||
|  |         type: 'datetime', // Time-series data | ||||||
|  |         labels: { | ||||||
|  |           format: 'HH:mm:ss', // Time formatting with seconds | ||||||
|  |           datetimeUTC: false, | ||||||
|  |           style: { | ||||||
|  |             colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color | ||||||
|  |             fontSize: '12px', | ||||||
|  |             fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | ||||||
|  |             fontWeight: '400', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         axisBorder: { | ||||||
|  |           show: false, // Hide x-axis border | ||||||
|  |         }, | ||||||
|  |         axisTicks: { | ||||||
|  |           show: false, // Hide x-axis ticks | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       yaxis: { | ||||||
|  |         min: 0, | ||||||
|  |         max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax, | ||||||
|  |         labels: { | ||||||
|  |           formatter: this.yAxisFormatter, | ||||||
|  |           style: { | ||||||
|  |             colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], // Label color | ||||||
|  |             fontSize: '12px', | ||||||
|  |             fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | ||||||
|  |             fontWeight: '400', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         axisBorder: { | ||||||
|  |           show: false, // Hide y-axis border | ||||||
|  |         }, | ||||||
|  |         axisTicks: { | ||||||
|  |           show: false, // Hide y-axis ticks | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       tooltip: { | ||||||
|  |         shared: true, // Enables the tooltip to display across series | ||||||
|  |         intersect: false, // Allows hovering anywhere on the chart | ||||||
|  |         followCursor: true, // Makes tooltip follow mouse even between points | ||||||
|  |         x: { | ||||||
|  |           format: 'dd/MM/yy HH:mm', | ||||||
|  |         }, | ||||||
|  |         custom: ({ series, dataPointIndex, w }: any) => { | ||||||
|  |           // Iterate through each series and get its value | ||||||
|  |           // Note: We can't access component instance here, so we'll use w.config.theme.mode | ||||||
|  |           const currentTheme = w.config.theme.mode; | ||||||
|  |           const isDarkMode = currentTheme === 'dark'; | ||||||
|  |           const bgColor = isDarkMode ? 'hsl(0 0% 9%)' : 'hsl(0 0% 100%)'; | ||||||
|  |           const textColor = isDarkMode ? 'hsl(0 0% 95%)' : 'hsl(0 0% 9%)'; | ||||||
|  |           const borderColor = isDarkMode ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 89.8%)'; | ||||||
|  |            | ||||||
|  |           // Get formatter from chart config | ||||||
|  |           const formatter = w.config.yaxis[0]?.labels?.formatter || ((val: number) => val.toString()); | ||||||
|  |            | ||||||
|  |           let tooltipContent = `<div style="padding: 12px; background: ${bgColor}; color: ${textColor}; border-radius: 6px; box-shadow: 0 2px 8px 0 hsl(0 0% 0% / ${isDarkMode ? '0.2' : '0.1'}); border: 1px solid ${borderColor};font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 12px;">`; | ||||||
|  |  | ||||||
|  |           series.forEach((s: number[], index: number) => { | ||||||
|  |             const label = w.globals.seriesNames[index]; // Get series label | ||||||
|  |             const value = s[dataPointIndex]; // Get value at data point | ||||||
|  |             const color = w.globals.colors[index]; | ||||||
|  |             const formattedValue = formatter(value); | ||||||
|  |             tooltipContent += `<div style="display: flex; align-items: center; gap: 8px; margin: ${index > 0 ? '6px' : '0'} 0;"> | ||||||
|  |               <span style="display: inline-block; width: 10px; height: 10px; background: ${color}; border-radius: 2px;"></span> | ||||||
|  |               <span style="font-weight: 500;">${label}:</span> | ||||||
|  |               <span style="margin-left: auto; font-weight: 600;">${formattedValue}</span> | ||||||
|  |             </div>`; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           tooltipContent += `</div>`; | ||||||
|  |           return tooltipContent; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       grid: { | ||||||
|  |         xaxis: { | ||||||
|  |           lines: { | ||||||
|  |             show: false, // Hide vertical grid lines for cleaner look | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         yaxis: { | ||||||
|  |           lines: { | ||||||
|  |             show: true, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         borderColor: isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 94%)', // Very subtle grid lines | ||||||
|  |         strokeDashArray: 0, // Solid line | ||||||
|  |         padding: { | ||||||
|  |           top: 10, | ||||||
|  |           right: 20, | ||||||
|  |           bottom: 10, | ||||||
|  |           left: 20, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       fill: { | ||||||
|  |         type: 'gradient', // Gradient fill for the area | ||||||
|  |         gradient: { | ||||||
|  |           shade: isDark ? 'dark' : 'light', | ||||||
|  |           type: 'vertical', | ||||||
|  |           shadeIntensity: 0.1, | ||||||
|  |           opacityFrom: isDark ? 0.2 : 0.3, | ||||||
|  |           opacityTo: 0, | ||||||
|  |           stops: [0, 100], | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light, | ||||||
|  |       theme: { | ||||||
|  |         mode: theme, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     try { | ||||||
|  |       this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options); | ||||||
|  |       await this.chart.render(); | ||||||
|  |        | ||||||
|  |       // Give the chart a moment to fully initialize before resizing | ||||||
|  |       await new Promise(resolve => setTimeout(resolve, 100)); | ||||||
|  |       await this.resizeChart(); | ||||||
|  |        | ||||||
|  |       // Ensure resize observer is watching the mainbox | ||||||
|  |       const mainbox = this.shadowRoot.querySelector('.mainbox'); | ||||||
|  |       if (mainbox && this.resizeObserver) { | ||||||
|  |         // Disconnect any previous observations | ||||||
|  |         this.resizeObserver.disconnect(); | ||||||
|  |         // Start observing the mainbox | ||||||
|  |         this.resizeObserver.observe(mainbox); | ||||||
|  |         if (this.DEBUG_RESIZE) { | ||||||
|  |           console.log('DeesChartArea - ResizeObserver attached to mainbox'); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to initialize chart:', error); | ||||||
|  |       // Optionally, you could set an error state here | ||||||
|  |       // this.chartState = 'error'; | ||||||
|  |       // this.errorMessage = 'Failed to initialize chart'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updated(changedProperties: Map<string, any>) { | ||||||
|  |     super.updated(changedProperties); | ||||||
|  |      | ||||||
|  |     // Update chart theme when goBright changes | ||||||
|  |     if (changedProperties.has('goBright') && this.chart) { | ||||||
|  |       await this.updateChartTheme(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Update chart if series data changes | ||||||
|  |     if (changedProperties.has('series') && this.chart && this.series.length > 0) { | ||||||
|  |       await this.updateSeries(this.series); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Update y-axis formatter if it changes | ||||||
|  |     if (changedProperties.has('yAxisFormatter') && this.chart) { | ||||||
|  |       await this.chart.updateOptions({ | ||||||
|  |         yaxis: { | ||||||
|  |           labels: { | ||||||
|  |             formatter: this.yAxisFormatter, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Handle realtime mode changes | ||||||
|  |     if (changedProperties.has('realtimeMode') && this.chart) { | ||||||
|  |       await this.chart.updateOptions({ | ||||||
|  |         chart: { | ||||||
|  |           animations: { | ||||||
|  |             enabled: !this.realtimeMode, | ||||||
|  |             speed: 400, | ||||||
|  |             animateGradually: { | ||||||
|  |               enabled: false, | ||||||
|  |               delay: 0 | ||||||
|  |             }, | ||||||
|  |             dynamicAnimation: { | ||||||
|  |               enabled: !this.realtimeMode, | ||||||
|  |               speed: 350 | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       // Start/stop auto-scroll based on realtime mode | ||||||
|  |       if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) { | ||||||
|  |         this.startAutoScroll(); | ||||||
|  |       } else { | ||||||
|  |         this.stopAutoScroll(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Handle auto-scroll interval changes | ||||||
|  |     if (changedProperties.has('autoScrollInterval') && this.chart) { | ||||||
|  |       this.stopAutoScroll(); | ||||||
|  |       if (this.realtimeMode && this.rollingWindow > 0 && this.autoScrollInterval > 0) { | ||||||
|  |         this.startAutoScroll(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Handle y-axis scaling changes | ||||||
|  |     if ((changedProperties.has('yAxisScaling') || changedProperties.has('yAxisMax')) && this.chart) { | ||||||
|  |       await this.chart.updateOptions({ | ||||||
|  |         yaxis: { | ||||||
|  |           min: 0, | ||||||
|  |           max: this.yAxisScaling === 'dynamic' ? undefined : this.yAxisMax | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updateSeries(newSeries: ApexAxisChartSeries, animate: boolean = true) { | ||||||
|  |     if (!this.chart) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     try { | ||||||
|  |       // Store the new data first | ||||||
|  |       this.internalChartData = newSeries; | ||||||
|  |        | ||||||
|  |       // Handle rolling window if enabled | ||||||
|  |       if (this.rollingWindow > 0 && this.realtimeMode) { | ||||||
|  |         const now = Date.now(); | ||||||
|  |         const cutoffTime = now - this.rollingWindow; | ||||||
|  |          | ||||||
|  |         // Filter data to only include points within the rolling window | ||||||
|  |         const filteredSeries = newSeries.map(series => ({ | ||||||
|  |           name: series.name, | ||||||
|  |           data: (series.data as any[]).filter(point => { | ||||||
|  |             if (typeof point === 'object' && point !== null && 'x' in point) { | ||||||
|  |               return new Date(point.x).getTime() > cutoffTime; | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |           }) | ||||||
|  |         })); | ||||||
|  |          | ||||||
|  |         // Only update if we have data | ||||||
|  |         if (filteredSeries.some(s => s.data.length > 0)) { | ||||||
|  |           // Handle y-axis scaling first | ||||||
|  |           if (this.yAxisScaling === 'dynamic') { | ||||||
|  |             const allValues = filteredSeries.flatMap(s => (s.data as any[]).map(d => d.y)); | ||||||
|  |             if (allValues.length > 0) { | ||||||
|  |               const maxValue = Math.max(...allValues); | ||||||
|  |               const dynamicMax = Math.ceil(maxValue * 1.1); | ||||||
|  |               await this.chart.updateOptions({ | ||||||
|  |                 yaxis: { | ||||||
|  |                   min: 0, | ||||||
|  |                   max: dynamicMax | ||||||
|  |                 } | ||||||
|  |               }, false, false); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |            | ||||||
|  |           await this.chart.updateSeries(filteredSeries, false); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         await this.chart.updateSeries(newSeries, animate); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to update chart series:', error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Update just the x-axis for smooth scrolling in realtime mode | ||||||
|  |   // Public for advanced usage in demos, but typically handled automatically | ||||||
|  |   public async updateTimeWindow() { | ||||||
|  |     if (!this.chart || this.rollingWindow <= 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const now = Date.now(); | ||||||
|  |     const cutoffTime = now - this.rollingWindow; | ||||||
|  |      | ||||||
|  |     await this.chart.updateOptions({ | ||||||
|  |       xaxis: { | ||||||
|  |         min: cutoffTime, | ||||||
|  |         max: now, | ||||||
|  |         labels: { | ||||||
|  |           format: 'HH:mm:ss', | ||||||
|  |           datetimeUTC: false, | ||||||
|  |           style: { | ||||||
|  |             colors: [!this.goBright ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], | ||||||
|  |             fontSize: '12px', | ||||||
|  |             fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', | ||||||
|  |             fontWeight: '400', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         tickAmount: 6, | ||||||
|  |       } | ||||||
|  |     }, false, false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async appendData(newData: { data: any[] }[]) { | ||||||
|  |     if (!this.chart) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Use ApexCharts' appendData method for smoother real-time updates | ||||||
|  |     this.chart.appendData(newData); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   public async updateOptions(options: ApexCharts.ApexOptions, redrawPaths?: boolean, animate?: boolean) { | ||||||
|  |     if (!this.chart) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return this.chart.updateOptions(options, redrawPaths, animate); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async resizeChart() { | ||||||
|  |     if (!this.chart) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (this.DEBUG_RESIZE) { | ||||||
|  |       console.log('DeesChartArea - resizeChart called'); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     try { | ||||||
|  |       const mainbox: HTMLDivElement = this.shadowRoot.querySelector('.mainbox'); | ||||||
|  |       const chartContainer: HTMLDivElement = this.shadowRoot.querySelector('.chartContainer'); | ||||||
|  |        | ||||||
|  |       if (!mainbox || !chartContainer) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Force layout recalculation | ||||||
|  |       void mainbox.offsetHeight; | ||||||
|  |  | ||||||
|  |       // Get computed style of the element | ||||||
|  |       const styleChartContainer = window.getComputedStyle(chartContainer); | ||||||
|  |  | ||||||
|  |       // Extract padding values | ||||||
|  |       const paddingTop = parseInt(styleChartContainer.paddingTop, 10); | ||||||
|  |       const paddingBottom = parseInt(styleChartContainer.paddingBottom, 10); | ||||||
|  |       const paddingLeft = parseInt(styleChartContainer.paddingLeft, 10); | ||||||
|  |       const paddingRight = parseInt(styleChartContainer.paddingRight, 10); | ||||||
|  |  | ||||||
|  |       // Calculate the actual width and height to use, subtracting padding | ||||||
|  |       const actualWidth = mainbox.clientWidth - paddingLeft - paddingRight; | ||||||
|  |       const actualHeight = mainbox.offsetHeight - paddingTop - paddingBottom; | ||||||
|  |        | ||||||
|  |       // Validate dimensions | ||||||
|  |       if (actualWidth > 0 && actualHeight > 0) { | ||||||
|  |         if (this.DEBUG_RESIZE) { | ||||||
|  |           console.log('DeesChartArea - Updating chart dimensions:', { | ||||||
|  |             width: actualWidth, | ||||||
|  |             height: actualHeight | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         await this.chart.updateOptions({ | ||||||
|  |           chart: { | ||||||
|  |             width: actualWidth, | ||||||
|  |             height: actualHeight, | ||||||
|  |           }, | ||||||
|  |         }, true, false); // Redraw paths but don't animate | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to resize chart:', error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Manually trigger a chart resize. Useful when automatic detection doesn't work. | ||||||
|  |    * This is a convenience method that can be called from outside the component. | ||||||
|  |    */ | ||||||
|  |   public async forceResize() { | ||||||
|  |     await this.resizeChart(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private startAutoScroll() { | ||||||
|  |     if (this.autoScrollTimer) { | ||||||
|  |       return; // Already running | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     this.autoScrollTimer = window.setInterval(() => { | ||||||
|  |       this.updateTimeWindow(); | ||||||
|  |     }, this.autoScrollInterval); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private stopAutoScroll() { | ||||||
|  |     if (this.autoScrollTimer) { | ||||||
|  |       window.clearInterval(this.autoScrollTimer); | ||||||
|  |       this.autoScrollTimer = null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private async updateChartTheme() { | ||||||
|  |     if (!this.chart) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const isDark = !this.goBright; | ||||||
|  |     const theme = isDark ? 'dark' : 'light'; | ||||||
|  |      | ||||||
|  |     await this.chart.updateOptions({ | ||||||
|  |       theme: { | ||||||
|  |         mode: theme, | ||||||
|  |       }, | ||||||
|  |       colors: isDark ? this.CHART_COLORS.dark : this.CHART_COLORS.light, | ||||||
|  |       xaxis: { | ||||||
|  |         labels: { | ||||||
|  |           style: { | ||||||
|  |             colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       yaxis: { | ||||||
|  |         labels: { | ||||||
|  |           style: { | ||||||
|  |             colors: [isDark ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 20%)'], | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       grid: { | ||||||
|  |         borderColor: isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 94%)', | ||||||
|  |       }, | ||||||
|  |       fill: { | ||||||
|  |         gradient: { | ||||||
|  |           shade: isDark ? 'dark' : 'light', | ||||||
|  |           opacityFrom: isDark ? 0.2 : 0.3, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { html, css } from '@design.estate/dees-element'; | import { html, css, cssManager } from '@design.estate/dees-element'; | ||||||
| import type { DeesChartArea } from './dees-chart-area.js'; | import type { DeesChartArea } from './component.js'; | ||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  | import './component.js'; | ||||||
| 
 | 
 | ||||||
| export const demoFunc = () => { | export const demoFunc = () => { | ||||||
|   // Initial dataset values
 |   // Initial dataset values
 | ||||||
| @@ -402,7 +403,7 @@ export const demoFunc = () => { | |||||||
|         ${css` |         ${css` | ||||||
|         .demoBox { |         .demoBox { | ||||||
|           position: relative; |           position: relative; | ||||||
|           background: #000000; |           background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')}; | ||||||
|           height: 100%; |           height: 100%; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|           padding: 40px; |           padding: 40px; | ||||||
| @@ -425,9 +426,9 @@ export const demoFunc = () => { | |||||||
|         } |         } | ||||||
|          |          | ||||||
|         .info { |         .info { | ||||||
|           color: #666; |           color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; | ||||||
|           font-size: 11px; |           font-size: 12px; | ||||||
|           font-family: 'Geist Sans', sans-serif; |           font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif; | ||||||
|           text-align: center; |           text-align: center; | ||||||
|           margin-top: 8px; |           margin-top: 8px; | ||||||
|         } |         } | ||||||
							
								
								
									
										3
									
								
								ts_web/elements/dees-chart-area/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								ts_web/elements/dees-chart-area/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | export * from './component.js'; | ||||||
|  | export { chartAreaStyles } from './styles.js'; | ||||||
|  | export { renderChartArea } from './template.js'; | ||||||
							
								
								
									
										60
									
								
								ts_web/elements/dees-chart-area/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								ts_web/elements/dees-chart-area/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const chartAreaStyles = [ | ||||||
|  |     cssManager.defaultStyles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         font-weight: 400; | ||||||
|  |         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; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .chartTitle { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0; | ||||||
|  |         left: 0; | ||||||
|  |         width: 100%; | ||||||
|  |         text-align: left; | ||||||
|  |         padding: 16px 24px; | ||||||
|  |         z-index: 10; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-weight: 500; | ||||||
|  |         letter-spacing: -0.01em; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 63.9%)')}; | ||||||
|  |       } | ||||||
|  |       .chartContainer { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0px; | ||||||
|  |         left: 0px; | ||||||
|  |         bottom: 0px; | ||||||
|  |         right: 0px; | ||||||
|  |         padding: 44px 16px 16px 0px; | ||||||
|  |         overflow: hidden; | ||||||
|  |         background: transparent; /* Ensure container doesn't override chart background */ | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       /* ApexCharts theme overrides */ | ||||||
|  |       .apexcharts-canvas { | ||||||
|  |         background: transparent !important; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .apexcharts-inner { | ||||||
|  |         background: transparent !important; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .apexcharts-graphical { | ||||||
|  |         background: transparent !important; | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								ts_web/elements/dees-chart-area/template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								ts_web/elements/dees-chart-area/template.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | import { html, type TemplateResult } from '@design.estate/dees-element'; | ||||||
|  | import type { DeesChartArea } from './component.js'; | ||||||
|  |  | ||||||
|  | export const renderChartArea = (component: DeesChartArea): TemplateResult => { | ||||||
|  |       return html` | ||||||
|  |         <div class="mainbox"> | ||||||
|  |           <div class="chartTitle">${component.label}</div> | ||||||
|  |           <div class="chartContainer"></div> | ||||||
|  |         </div> | ||||||
|  |       `; | ||||||
|  |    | ||||||
|  | }; | ||||||
| @@ -53,17 +53,17 @@ export class DeesChartLog extends DeesElement { | |||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     css` |     css` | ||||||
|       :host { |       :host { | ||||||
|         font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace; |         font-family: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace; | ||||||
|         color: #ccc; |         color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; | ||||||
|         font-size: 12px; |         font-size: 12px; | ||||||
|         line-height: 1.4; |         line-height: 1.5; | ||||||
|       } |       } | ||||||
|       .mainbox { |       .mainbox { | ||||||
|         position: relative; |         position: relative; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 400px; |         height: 400px; | ||||||
|         background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; | ||||||
|         border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')}; |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
| @@ -71,9 +71,9 @@ export class DeesChartLog extends DeesElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .header { |       .header { | ||||||
|         background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')}; |         background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')}; | ||||||
|         padding: 8px 16px; |         padding: 12px 16px; | ||||||
|         border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')}; |         border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: space-between; |         justify-content: space-between; | ||||||
|         align-items: center; |         align-items: center; | ||||||
| @@ -81,8 +81,10 @@ export class DeesChartLog extends DeesElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .title { |       .title { | ||||||
|         font-weight: 600; |         font-weight: 500; | ||||||
|         color: ${cssManager.bdTheme('#212529', '#fff')}; |         font-size: 14px; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; | ||||||
|  |         font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .controls { |       .controls { | ||||||
| @@ -91,44 +93,48 @@ export class DeesChartLog extends DeesElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .control-button { |       .control-button { | ||||||
|         background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')}; |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')}; |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border-radius: 4px; |         border-radius: 6px; | ||||||
|         padding: 4px 8px; |         padding: 6px 12px; | ||||||
|         color: ${cssManager.bdTheme('#495057', '#ccc')}; |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|         font-size: 11px; |         font-size: 12px; | ||||||
|         transition: all 0.2s; |         font-weight: 500; | ||||||
|  |         transition: all 0.15s; | ||||||
|  |         font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .control-button:hover { |       .control-button:hover { | ||||||
|         background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')}; |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border-color: ${cssManager.bdTheme('#adb5bd', '#555')}; |         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%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .control-button.active { |       .control-button.active { | ||||||
|         background: ${cssManager.bdTheme('#007bff', '#4a4a4a')}; |         background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 93.9%)')}; | ||||||
|         color: ${cssManager.bdTheme('#fff', '#fff')}; |         color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .logContainer { |       .logContainer { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|         overflow-y: auto; |         overflow-y: auto; | ||||||
|         overflow-x: hidden; |         overflow-x: hidden; | ||||||
|         padding: 8px 16px; |         padding: 16px; | ||||||
|         font-size: 12px; |         font-size: 12px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .logEntry { |       .logEntry { | ||||||
|         margin-bottom: 2px; |         margin-bottom: 4px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         white-space: pre-wrap; |         white-space: pre-wrap; | ||||||
|         word-break: break-all; |         word-break: break-all; | ||||||
|  |         font-variant-numeric: tabular-nums; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .timestamp { |       .timestamp { | ||||||
|         color: ${cssManager.bdTheme('#6c757d', '#666')}; |         color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; | ||||||
|         margin-right: 8px; |         margin-right: 12px; | ||||||
|         flex-shrink: 0; |         flex-shrink: 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -143,38 +149,38 @@ export class DeesChartLog extends DeesElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .level.debug { |       .level.debug { | ||||||
|         color: ${cssManager.bdTheme('#6c757d', '#999')}; |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|         background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')}; |         background: ${cssManager.bdTheme('hsl(0 0% 45.1% / 0.1)', 'hsl(0 0% 63.9% / 0.1)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .level.info { |       .level.info { | ||||||
|         color: ${cssManager.bdTheme('#0066cc', '#4a9eff')}; |         color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; | ||||||
|         background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')}; |         background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .level.warn { |       .level.warn { | ||||||
|         color: ${cssManager.bdTheme('#ff8800', '#ffb84a')}; |         color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')}; | ||||||
|         background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')}; |         background: ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .level.error { |       .level.error { | ||||||
|         color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')}; |         color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')}; | ||||||
|         background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')}; |         background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .level.success { |       .level.success { | ||||||
|         color: ${cssManager.bdTheme('#28a745', '#4aff88')}; |         color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')}; | ||||||
|         background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')}; |         background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .source { |       .source { | ||||||
|         color: ${cssManager.bdTheme('#6c757d', '#888')}; |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|         margin-right: 8px; |         margin-right: 8px; | ||||||
|         flex-shrink: 0; |         flex-shrink: 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .message { |       .message { | ||||||
|         color: ${cssManager.bdTheme('#212529', '#ddd')}; |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; | ||||||
|         flex: 1; |         flex: 1; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -183,7 +189,7 @@ export class DeesChartLog extends DeesElement { | |||||||
|         align-items: center; |         align-items: center; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|         height: 100%; |         height: 100%; | ||||||
|         color: ${cssManager.bdTheme('#6c757d', '#666')}; |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|         font-style: italic; |         font-style: italic; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -193,16 +199,16 @@ export class DeesChartLog extends DeesElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .logContainer::-webkit-scrollbar-track { |       .logContainer::-webkit-scrollbar-track { | ||||||
|         background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')}; |         background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 10%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .logContainer::-webkit-scrollbar-thumb { |       .logContainer::-webkit-scrollbar-thumb { | ||||||
|         background: ${cssManager.bdTheme('#adb5bd', '#444')}; |         background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 30%)')}; | ||||||
|         border-radius: 4px; |         border-radius: 4px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .logContainer::-webkit-scrollbar-thumb:hover { |       .logContainer::-webkit-scrollbar-thumb:hover { | ||||||
|         background: ${cssManager.bdTheme('#6c757d', '#555')}; |         background: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 40%)')}; | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|   | |||||||
| @@ -1,41 +1,112 @@ | |||||||
| import { html } from '@design.estate/dees-element'; | import { html, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
| export const demoFunc = () => html` | export const demoFunc = () => html` | ||||||
|   <style> |   <style> | ||||||
|     .demoContainer { |     .demoContainer { | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       align-items: center; |       gap: 32px; | ||||||
|       justify-content: center; |       padding: 48px; | ||||||
|       height: 100%; |       background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; | ||||||
|       background: #222; |       min-height: 100vh; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .section { | ||||||
|  |       background: ${cssManager.bdTheme('#ffffff', '#18181b')}; | ||||||
|  |       border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |       border-radius: 8px; | ||||||
|  |       padding: 24px; | ||||||
|  |       box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .section-title { | ||||||
|  |       font-size: 18px; | ||||||
|  |       font-weight: 600; | ||||||
|  |       margin-bottom: 16px; | ||||||
|  |       color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .section-description { | ||||||
|  |       font-size: 14px; | ||||||
|  |       color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; | ||||||
|  |       margin-bottom: 16px; | ||||||
|     } |     } | ||||||
|   </style> |   </style> | ||||||
|   <div class="demoContainer"> |   <div class="demoContainer"> | ||||||
|     <dees-chips |     <div class="section"> | ||||||
|       selectionMode="none" |       <div class="section-title">Non-Selectable Chips</div> | ||||||
|       .selectableChips=${[ |       <div class="section-description">Basic chips without selection capability. Use for display-only tags.</div> | ||||||
|         { key: 'account1', value: 'Payment Account 1' }, |       <dees-chips | ||||||
|         { key: 'account2', value: 'PaymentAccount2' }, |         selectionMode="none" | ||||||
|         { key: 'account3', value: 'Payment Account 3' }, |         .selectableChips=${[ | ||||||
|       ]} |           { key: 'status', value: 'Active' }, | ||||||
|     ></dees-chips> |           { key: 'tier', value: 'Premium' }, | ||||||
|     <dees-chips |           { key: 'region', value: 'EU-West' }, | ||||||
|       selectionMode="single" |           { key: 'type', value: 'Enterprise' }, | ||||||
|       chipsAreRemovable |         ]} | ||||||
|       .selectableChips=${[ |       ></dees-chips> | ||||||
|         { key: 'account1', value: 'Payment Account 1' }, |     </div> | ||||||
|         { key: 'account2', value: 'PaymentAccount2' }, |      | ||||||
|         { key: 'account3', value: 'Payment Account 3' }, |     <div class="section"> | ||||||
|       ]} |       <div class="section-title">Single Selection Chips</div> | ||||||
|     ></dees-chips> |       <div class="section-description">Click to select one chip at a time. Useful for filters and options.</div> | ||||||
|     <dees-chips |       <dees-chips | ||||||
|       selectionMode="multiple" |         selectionMode="single" | ||||||
|       .selectableChips=${[ |         .selectableChips=${[ | ||||||
|         { key: 'account1', value: 'Payment Account 1' }, |           { key: 'all', value: 'All Projects' }, | ||||||
|         { key: 'account2', value: 'PaymentAccount2' }, |           { key: 'active', value: 'Active' }, | ||||||
|         { key: 'account3', value: 'Payment Account 3' }, |           { key: 'archived', value: 'Archived' }, | ||||||
|       ]} |           { key: 'drafts', value: 'Drafts' }, | ||||||
|     ></dees-chips> |         ]} | ||||||
|  |       ></dees-chips> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">Multiple Selection Chips</div> | ||||||
|  |       <div class="section-description">Select multiple chips simultaneously. Great for tag selection.</div> | ||||||
|  |       <dees-chips | ||||||
|  |         selectionMode="multiple" | ||||||
|  |         .selectableChips=${[ | ||||||
|  |           { key: 'js', value: 'JavaScript' }, | ||||||
|  |           { key: 'ts', value: 'TypeScript' }, | ||||||
|  |           { key: 'react', value: 'React' }, | ||||||
|  |           { key: 'vue', value: 'Vue' }, | ||||||
|  |           { key: 'angular', value: 'Angular' }, | ||||||
|  |           { key: 'node', value: 'Node.js' }, | ||||||
|  |         ]} | ||||||
|  |       ></dees-chips> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">Removable Chips with Keys</div> | ||||||
|  |       <div class="section-description">Chips with remove buttons and key-value pairs. Perfect for dynamic lists.</div> | ||||||
|  |       <dees-chips | ||||||
|  |         selectionMode="single" | ||||||
|  |         chipsAreRemovable | ||||||
|  |         .selectableChips=${[ | ||||||
|  |           { key: 'env', value: 'Production' }, | ||||||
|  |           { key: 'version', value: '2.4.1' }, | ||||||
|  |           { key: 'branch', value: 'main' }, | ||||||
|  |           { key: 'author', value: 'John Doe' }, | ||||||
|  |         ]} | ||||||
|  |       ></dees-chips> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">Mixed Content Example</div> | ||||||
|  |       <div class="section-description">Combining different chip types for complex UIs.</div> | ||||||
|  |       <dees-chips | ||||||
|  |         selectionMode="multiple" | ||||||
|  |         chipsAreRemovable | ||||||
|  |         .selectableChips=${[ | ||||||
|  |           { key: 'priority', value: 'High' }, | ||||||
|  |           { key: 'status', value: 'In Progress' }, | ||||||
|  |           { key: 'bug', value: 'Bug' }, | ||||||
|  |           { key: 'feature', value: 'Feature' }, | ||||||
|  |           { key: 'sprint', value: 'Sprint 23' }, | ||||||
|  |           { key: 'assignee', value: 'Alice' }, | ||||||
|  |         ]} | ||||||
|  |       ></dees-chips> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| `; | `; | ||||||
|   | |||||||
| @@ -60,52 +60,93 @@ export class DeesChips extends DeesElement { | |||||||
|  |  | ||||||
|       .mainbox { |       .mainbox { | ||||||
|         user-select: none; |         user-select: none; | ||||||
|  |         display: flex; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |         gap: 8px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .chip { |       .chip { | ||||||
|         border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #444')}; |         background: ${cssManager.bdTheme('#f4f4f5', '#27272a')}; | ||||||
|         background: #333333; |         border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#3f3f46')}; | ||||||
|         display: inline-flex; |         display: inline-flex; | ||||||
|         height: 20px; |         align-items: center; | ||||||
|         line-height: 20px; |         height: 32px; | ||||||
|         padding: 0px 8px; |         padding: 0px 12px; | ||||||
|         font-size: 12px; |         font-size: 14px; | ||||||
|         color: #fff; |         font-weight: 500; | ||||||
|         border-radius: 40px; |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|         margin-right: 4px; |         border-radius: 6px; | ||||||
|         margin-bottom: 4px; |  | ||||||
|         position: relative; |         position: relative; | ||||||
|         overflow: hidden; |         cursor: pointer; | ||||||
|         box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3); |         transition: all 0.15s ease; | ||||||
|  |         box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .chip:hover { |       .chip:hover { | ||||||
|         background: #666666; |         background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('#d1d5db', '#52525b')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .chip:active { | ||||||
|  |         transform: scale(0.98); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .chip.selected { |       .chip.selected { | ||||||
|         background: #00a3ff; |         background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         color: #ffffff; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .chip.selected:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#2563eb', '#2563eb')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('#2563eb', '#2563eb')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .chipKey { |       .chipKey { | ||||||
|         background: rgba(0, 0, 0, 0.3); |         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')}; | ||||||
|         height: 100%; |         height: 20px; | ||||||
|         display: inline-block; |         line-height: 20px; | ||||||
|  |         display: inline-flex; | ||||||
|  |         align-items: center; | ||||||
|         margin-left: -8px; |         margin-left: -8px; | ||||||
|         padding-left: 8px; |         padding: 0px 8px; | ||||||
|         padding-right: 8px; |  | ||||||
|         margin-right: 8px; |         margin-right: 8px; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 12px; | ||||||
|  |         font-weight: 600; | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .chip.selected .chipKey { | ||||||
|  |         background: rgba(255, 255, 255, 0.2); | ||||||
|  |         color: rgba(255, 255, 255, 0.9); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       dees-icon { |       dees-icon { | ||||||
|         padding: 0px 6px 0px 4px; |         display: flex; | ||||||
|         margin-left: 4px; |         align-items: center; | ||||||
|         margin-right: -8px; |         justify-content: center; | ||||||
|         background: rgba(0, 0, 0, 0.3); |         width: 16px; | ||||||
|  |         height: 16px; | ||||||
|  |         margin-left: 8px; | ||||||
|  |         margin-right: -6px; | ||||||
|  |         border-radius: 3px; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .chip.selected dees-icon { | ||||||
|  |         color: rgba(255, 255, 255, 0.8); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       dees-icon:hover { |       dees-icon:hover { | ||||||
|         background: #e4002b; |         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; | ||||||
|  |         color: ${cssManager.bdTheme('#ef4444', '#ef4444')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .chip.selected dees-icon:hover { | ||||||
|  |         background: rgba(255, 255, 255, 0.2); | ||||||
|  |         color: #ffffff; | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| @@ -127,7 +168,7 @@ export class DeesChips extends DeesElement { | |||||||
|                         event.stopPropagation(); // prevent the selectChip event from being triggered |                         event.stopPropagation(); // prevent the selectChip event from being triggered | ||||||
|                         this.removeChip(chip); |                         this.removeChip(chip); | ||||||
|                       }} |                       }} | ||||||
|                       .iconFA=${'xmark'} |                       .icon=${'fa:xmark'} | ||||||
|                     ></dees-icon> |                     ></dees-icon> | ||||||
|                   ` |                   ` | ||||||
|                 : html``} |                 : html``} | ||||||
| @@ -139,20 +180,26 @@ export class DeesChips extends DeesElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async firstUpdated() { |   public async firstUpdated() { | ||||||
|     if (!this.textContent) { |     // Component initialized | ||||||
|       this.textContent = 'Button'; |  | ||||||
|       this.performUpdate(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private isSelected(chip: Tag): boolean { |   private isSelected(chip: Tag): boolean { | ||||||
|     if (this.selectionMode === 'single') { |     if (this.selectionMode === 'single') { | ||||||
|       return this.selectedChip?.key === chip.key; |       return this.selectedChip ? this.isSameChip(this.selectedChip, chip) : false; | ||||||
|     } else { |     } else { | ||||||
|       return this.selectedChips.some((selected) => selected.key === chip.key); |       return this.selectedChips.some((selected) => this.isSameChip(selected, chip)); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   private isSameChip(chip1: Tag, chip2: Tag): boolean { | ||||||
|  |     // If both have keys, compare by key | ||||||
|  |     if (chip1.key && chip2.key) { | ||||||
|  |       return chip1.key === chip2.key; | ||||||
|  |     } | ||||||
|  |     // Otherwise compare by value (and key if present) | ||||||
|  |     return chip1.value === chip2.value && chip1.key === chip2.key; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public async selectChip(chip: Tag) { |   public async selectChip(chip: Tag) { | ||||||
|     if (this.selectionMode === 'none') { |     if (this.selectionMode === 'none') { | ||||||
|       return; |       return; | ||||||
| @@ -168,7 +215,7 @@ export class DeesChips extends DeesElement { | |||||||
|       } |       } | ||||||
|     } else if (this.selectionMode === 'multiple') { |     } else if (this.selectionMode === 'multiple') { | ||||||
|       if (this.isSelected(chip)) { |       if (this.isSelected(chip)) { | ||||||
|         this.selectedChips = this.selectedChips.filter((selected) => selected.key !== chip.key); |         this.selectedChips = this.selectedChips.filter((selected) => !this.isSameChip(selected, chip)); | ||||||
|       } else { |       } else { | ||||||
|         this.selectedChips = [...this.selectedChips, chip]; |         this.selectedChips = [...this.selectedChips, chip]; | ||||||
|       } |       } | ||||||
| @@ -179,13 +226,13 @@ export class DeesChips extends DeesElement { | |||||||
|  |  | ||||||
|   public removeChip(chipToRemove: Tag): void { |   public removeChip(chipToRemove: Tag): void { | ||||||
|     // Remove the chip from selectableChips |     // Remove the chip from selectableChips | ||||||
|     this.selectableChips = this.selectableChips.filter((chip) => chip.key !== chipToRemove.key); |     this.selectableChips = this.selectableChips.filter((chip) => !this.isSameChip(chip, chipToRemove)); | ||||||
|  |  | ||||||
|     // Remove the chip from selectedChips if present |     // Remove the chip from selectedChips if present | ||||||
|     this.selectedChips = this.selectedChips.filter((chip) => chip.key !== chipToRemove.key); |     this.selectedChips = this.selectedChips.filter((chip) => !this.isSameChip(chip, chipToRemove)); | ||||||
|  |  | ||||||
|     // If the removed chip was the selectedChip, set selectedChip to null |     // If the removed chip was the selectedChip, set selectedChip to null | ||||||
|     if (this.selectedChip && this.selectedChip.key === chipToRemove.key) { |     if (this.selectedChip && this.isSameChip(this.selectedChip, chipToRemove)) { | ||||||
|       this.selectedChip = null; |       this.selectedChip = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,139 +13,203 @@ export const demoFunc = () => html` | |||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     gap: 20px; |     gap: 20px; | ||||||
|     padding: 40px; |     padding: 20px; | ||||||
|     background: #f5f5f5; |  | ||||||
|     min-height: 400px; |     min-height: 400px; | ||||||
|   } |   } | ||||||
|   .demo-area { |   .demo-area { | ||||||
|     background: white; |  | ||||||
|     padding: 40px; |     padding: 40px; | ||||||
|     border-radius: 8px; |     border-radius: 8px; | ||||||
|     border: 1px solid #e0e0e0; |  | ||||||
|     text-align: center; |     text-align: center; | ||||||
|     cursor: context-menu; |     cursor: context-menu; | ||||||
|  |     transition: background 0.2s; | ||||||
|  |   } | ||||||
|  |   .demo-area:hover { | ||||||
|  |     background: rgba(0, 0, 0, 0.02); | ||||||
|   } |   } | ||||||
| </style> | </style> | ||||||
| <div class="demo-container"> | <div class="demo-container"> | ||||||
|   <div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => { |   <dees-panel heading="Basic Context Menu with Nested Submenus"> | ||||||
|     DeesContextmenu.openContextMenuWithOptions(eventArg, [ |     <div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => { | ||||||
|       { |       DeesContextmenu.openContextMenuWithOptions(eventArg, [ | ||||||
|         name: 'Cut', |         { | ||||||
|         iconName: 'scissors', |           name: 'File', | ||||||
|         shortcut: 'Cmd+X', |           iconName: 'fileText', | ||||||
|         action: async () => { |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|           console.log('Cut action'); |           submenu: [ | ||||||
|  |             { name: 'New', iconName: 'filePlus', shortcut: 'Cmd+N', action: async () => console.log('New file') }, | ||||||
|  |             { name: 'Open', iconName: 'folderOpen', shortcut: 'Cmd+O', action: async () => console.log('Open file') }, | ||||||
|  |             { name: 'Save', iconName: 'save', shortcut: 'Cmd+S', action: async () => console.log('Save') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'Export as PDF', iconName: 'download', action: async () => console.log('Export PDF') }, | ||||||
|  |             { name: 'Export as HTML', iconName: 'code', action: async () => console.log('Export HTML') }, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|       }, |         { | ||||||
|       { |           name: 'Edit', | ||||||
|         name: 'Copy', |           iconName: 'edit3', | ||||||
|         iconName: 'copy', |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|         shortcut: 'Cmd+C', |           submenu: [ | ||||||
|         action: async () => { |             { name: 'Cut', iconName: 'scissors', shortcut: 'Cmd+X', action: async () => console.log('Cut') }, | ||||||
|           console.log('Copy action'); |             { name: 'Copy', iconName: 'copy', shortcut: 'Cmd+C', action: async () => console.log('Copy') }, | ||||||
|  |             { name: 'Paste', iconName: 'clipboard', shortcut: 'Cmd+V', action: async () => console.log('Paste') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'Find', iconName: 'search', shortcut: 'Cmd+F', action: async () => console.log('Find') }, | ||||||
|  |             { name: 'Replace', iconName: 'repeat', shortcut: 'Cmd+H', action: async () => console.log('Replace') }, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|       }, |         { | ||||||
|       { |           name: 'View', | ||||||
|         name: 'Paste', |           iconName: 'eye', | ||||||
|         iconName: 'clipboard', |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|         shortcut: 'Cmd+V', |           submenu: [ | ||||||
|         action: async () => { |             { name: 'Zoom In', iconName: 'zoomIn', shortcut: 'Cmd++', action: async () => console.log('Zoom in') }, | ||||||
|           console.log('Paste action'); |             { name: 'Zoom Out', iconName: 'zoomOut', shortcut: 'Cmd+-', action: async () => console.log('Zoom out') }, | ||||||
|  |             { name: 'Reset Zoom', iconName: 'maximize2', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'Full Screen', iconName: 'maximize', shortcut: 'F11', action: async () => console.log('Full screen') }, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|       }, |         { divider: true }, | ||||||
|       { divider: true }, |         { | ||||||
|       { |           name: 'Settings', | ||||||
|         name: 'Delete', |           iconName: 'settings', | ||||||
|         iconName: 'trash2', |           action: async () => console.log('Settings') | ||||||
|         action: async () => { |  | ||||||
|           console.log('Delete action'); |  | ||||||
|         }, |         }, | ||||||
|       }, |         { | ||||||
|       { divider: true }, |           name: 'Help', | ||||||
|       { |           iconName: 'helpCircle', | ||||||
|         name: 'Select All', |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|         shortcut: 'Cmd+A', |           submenu: [ | ||||||
|         action: async () => { |             { name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') }, | ||||||
|           console.log('Select All action'); |             { name: 'Keyboard Shortcuts', iconName: 'keyboard', action: async () => console.log('Shortcuts') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'About', iconName: 'info', action: async () => console.log('About') }, | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ]); | ||||||
|  |     }}> | ||||||
|  |       <h3>Right-click anywhere in this area</h3> | ||||||
|  |       <p>A context menu with nested submenus will appear</p> | ||||||
|  |     </div> | ||||||
|  |   </dees-panel> | ||||||
|  |   <dees-panel heading="Component-Specific Context Menu"> | ||||||
|  |     <dees-button style="margin: 20px;" @contextmenu=${(eventArg: MouseEvent) => { | ||||||
|  |       DeesContextmenu.openContextMenuWithOptions(eventArg, [ | ||||||
|  |         { | ||||||
|  |           name: 'Button Actions', | ||||||
|  |           iconName: 'mousePointer', | ||||||
|  |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|  |           submenu: [ | ||||||
|  |             { name: 'Click', iconName: 'mouse', action: async () => console.log('Click action') }, | ||||||
|  |             { name: 'Double Click', iconName: 'zap', action: async () => console.log('Double click') }, | ||||||
|  |             { name: 'Long Press', iconName: 'clock', action: async () => console.log('Long press') }, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|       }, |         { | ||||||
|     ]); |           name: 'Button State', | ||||||
|   }}> |           iconName: 'toggleLeft', | ||||||
|     <h3>Right-click anywhere in this area</h3> |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|     <p>A context menu will appear with various options</p> |           submenu: [ | ||||||
|   </div> |             { name: 'Enable', iconName: 'checkCircle', action: async () => console.log('Enable') }, | ||||||
|  |             { name: 'Disable', iconName: 'xCircle', action: async () => console.log('Disable') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'Show', iconName: 'eye', action: async () => console.log('Show') }, | ||||||
|  |             { name: 'Hide', iconName: 'eyeOff', action: async () => console.log('Hide') }, | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         { divider: true }, | ||||||
|  |         { | ||||||
|  |           name: 'Disabled Action', | ||||||
|  |           iconName: 'ban', | ||||||
|  |           disabled: true, | ||||||
|  |           action: async () => console.log('This should not run'), | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           name: 'Properties', | ||||||
|  |           iconName: 'settings', | ||||||
|  |           action: async () => console.log('Button properties'), | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }}>Right-click on this button</dees-button> | ||||||
|  |   </dees-panel> | ||||||
|  |  | ||||||
|   <dees-button @contextmenu=${(eventArg: MouseEvent) => { |   <dees-panel heading="Advanced Context Menu Example"> | ||||||
|     DeesContextmenu.openContextMenuWithOptions(eventArg, [ |     <div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => { | ||||||
|       { |       DeesContextmenu.openContextMenuWithOptions(eventArg, [ | ||||||
|         name: 'Button Action 1', |         { | ||||||
|         iconName: 'play', |           name: 'Format', | ||||||
|         action: async () => { |           iconName: 'type', | ||||||
|           console.log('Button action 1'); |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|  |           submenu: [ | ||||||
|  |             { name: 'Bold', iconName: 'bold', shortcut: 'Cmd+B', action: async () => console.log('Bold') }, | ||||||
|  |             { name: 'Italic', iconName: 'italic', shortcut: 'Cmd+I', action: async () => console.log('Italic') }, | ||||||
|  |             { name: 'Underline', iconName: 'underline', shortcut: 'Cmd+U', action: async () => console.log('Underline') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'Font Size', iconName: 'type', action: async () => console.log('Font size menu') }, | ||||||
|  |             { name: 'Font Color', iconName: 'palette', action: async () => console.log('Font color menu') }, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|       }, |         { | ||||||
|       { |           name: 'Transform', | ||||||
|         name: 'Button Action 2', |           iconName: 'shuffle', | ||||||
|         iconName: 'pause', |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|         action: async () => { |           submenu: [ | ||||||
|           console.log('Button action 2'); |             { name: 'To Uppercase', iconName: 'arrowUp', action: async () => console.log('Uppercase') }, | ||||||
|  |             { name: 'To Lowercase', iconName: 'arrowDown', action: async () => console.log('Lowercase') }, | ||||||
|  |             { name: 'Capitalize', iconName: 'type', action: async () => console.log('Capitalize') }, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|       }, |         { divider: true }, | ||||||
|       { |         { | ||||||
|         name: 'Disabled Action', |           name: 'Delete', | ||||||
|         iconName: 'ban', |           iconName: 'trash2', | ||||||
|         disabled: true, |           action: async () => console.log('Delete') | ||||||
|         action: async () => { |         } | ||||||
|           console.log('This should not run'); |       ]); | ||||||
|         }, |     }}> | ||||||
|       }, |       <h3>Advanced Nested Menu Example</h3> | ||||||
|       { divider: true }, |       <p>This shows deeply nested submenus and various formatting options</p> | ||||||
|       { |     </div> | ||||||
|         name: 'Settings', |   </dees-panel> | ||||||
|         iconName: 'settings', |  | ||||||
|         action: async () => { |  | ||||||
|           console.log('Settings'); |  | ||||||
|         }, |  | ||||||
|       }, |  | ||||||
|     ]); |  | ||||||
|   }}>Right-click on this button for a different menu</dees-button> |  | ||||||
|    |    | ||||||
|   <div style="margin-top: 20px;"> |   <dees-panel heading="Static Context Menu (Always Visible)"> | ||||||
|     <h4>Static Context Menu (always visible):</h4> |  | ||||||
|     <dees-contextmenu |     <dees-contextmenu | ||||||
|       class="withMargin" |       class="withMargin" | ||||||
|       .menuItems=${[ |       .menuItems=${[ | ||||||
|         { |         { | ||||||
|           name: 'New File', |           name: 'Project', | ||||||
|           iconName: 'filePlus', |           iconName: 'folder', | ||||||
|           shortcut: 'Cmd+N', |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|           action: async () => console.log('New file'), |           submenu: [ | ||||||
|  |             { name: 'New Project', iconName: 'folderPlus', shortcut: 'Cmd+Shift+N', action: async () => console.log('New project') }, | ||||||
|  |             { name: 'Open Project', iconName: 'folderOpen', shortcut: 'Cmd+Shift+O', action: async () => console.log('Open project') }, | ||||||
|  |             { divider: true }, | ||||||
|  |             { name: 'Recent Projects', iconName: 'clock', action: async () => {}, submenu: [ | ||||||
|  |               { name: 'Project Alpha', action: async () => console.log('Open Alpha') }, | ||||||
|  |               { name: 'Project Beta', action: async () => console.log('Open Beta') }, | ||||||
|  |               { name: 'Project Gamma', action: async () => console.log('Open Gamma') }, | ||||||
|  |             ]}, | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: 'Open File', |           name: 'Tools', | ||||||
|           iconName: 'folderOpen', |           iconName: 'tool', | ||||||
|           shortcut: 'Cmd+O', |           action: async () => {}, // Parent items with submenus still need an action | ||||||
|           action: async () => console.log('Open file'), |           submenu: [ | ||||||
|         }, |             { name: 'Terminal', iconName: 'terminal', shortcut: 'Cmd+T', action: async () => console.log('Terminal') }, | ||||||
|         { |             { name: 'Console', iconName: 'monitor', shortcut: 'Cmd+K', action: async () => console.log('Console') }, | ||||||
|           name: 'Save', |             { divider: true }, | ||||||
|           iconName: 'save', |             { name: 'Extensions', iconName: 'package', action: async () => console.log('Extensions') }, | ||||||
|           shortcut: 'Cmd+S', |           ] | ||||||
|           action: async () => console.log('Save'), |  | ||||||
|         }, |         }, | ||||||
|         { divider: true }, |         { divider: true }, | ||||||
|         { |         { | ||||||
|           name: 'Export', |           name: 'Preferences', | ||||||
|           iconName: 'download', |           iconName: 'sliders', | ||||||
|           action: async () => console.log('Export'), |           action: async () => console.log('Preferences'), | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           name: 'Import', |  | ||||||
|           iconName: 'upload', |  | ||||||
|           action: async () => console.log('Import'), |  | ||||||
|         }, |         }, | ||||||
|       ]} |       ]} | ||||||
|     ></dees-contextmenu> |     ></dees-contextmenu> | ||||||
|   </div> |   </dees-panel> | ||||||
| </div> | </div> | ||||||
| `; | `; | ||||||
| @@ -31,7 +31,7 @@ export class DeesContextmenu extends DeesElement { | |||||||
|   // STATIC |   // STATIC | ||||||
|   // This will store all the accumulated menu items |   // This will store all the accumulated menu items | ||||||
|   public static contextMenuDeactivated = false; |   public static contextMenuDeactivated = false; | ||||||
|   public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] = []; |   public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[] = []; | ||||||
|  |  | ||||||
|   // Add a global event listener for the right-click context menu |   // Add a global event listener for the right-click context menu | ||||||
|   public static initializeGlobalListener() { |   public static initializeGlobalListener() { | ||||||
| @@ -41,16 +41,16 @@ export class DeesContextmenu extends DeesElement { | |||||||
|       } |       } | ||||||
|       event.preventDefault(); |       event.preventDefault(); | ||||||
|  |  | ||||||
|       // Get the target element of the right-click |  | ||||||
|       let target: EventTarget | null = event.target; |  | ||||||
|  |  | ||||||
|       // Clear previously accumulated items |       // Clear previously accumulated items | ||||||
|       DeesContextmenu.accumulatedMenuItems = []; |       DeesContextmenu.accumulatedMenuItems = []; | ||||||
|  |  | ||||||
|       // Traverse up the DOM tree to accumulate menu items |       // Use composedPath to properly traverse shadow DOM boundaries | ||||||
|       while (target) { |       const path = event.composedPath(); | ||||||
|         if ((target as any).getContextMenuItems) { |        | ||||||
|           const items = (target as any).getContextMenuItems(); |       // Traverse the composed path to accumulate menu items | ||||||
|  |       for (const element of path) { | ||||||
|  |         if ((element as any).getContextMenuItems) { | ||||||
|  |           const items = (element as any).getContextMenuItems(); | ||||||
|           if (items && items.length > 0) { |           if (items && items.length > 0) { | ||||||
|             if (DeesContextmenu.accumulatedMenuItems.length > 0) { |             if (DeesContextmenu.accumulatedMenuItems.length > 0) { | ||||||
|               DeesContextmenu.accumulatedMenuItems.push({ divider: true }); |               DeesContextmenu.accumulatedMenuItems.push({ divider: true }); | ||||||
| @@ -58,7 +58,6 @@ export class DeesContextmenu extends DeesElement { | |||||||
|             DeesContextmenu.accumulatedMenuItems.push(...items); |             DeesContextmenu.accumulatedMenuItems.push(...items); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         target = (target as Node).parentNode; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Open the context menu with the accumulated items |       // Open the context menu with the accumulated items | ||||||
| @@ -67,7 +66,7 @@ export class DeesContextmenu extends DeesElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // allows opening of a contextmenu with options |   // allows opening of a contextmenu with options | ||||||
|   public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]) { |   public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[]) { | ||||||
|     if (this.contextMenuDeactivated) { |     if (this.contextMenuDeactivated) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -80,8 +79,13 @@ export class DeesContextmenu extends DeesElement { | |||||||
|     contextMenu.style.transform = 'scale(0.95) translateY(-10px)'; |     contextMenu.style.transform = 'scale(0.95) translateY(-10px)'; | ||||||
|     contextMenu.menuItems = menuItemsArg; |     contextMenu.menuItems = menuItemsArg; | ||||||
|     contextMenu.windowLayer = await DeesWindowLayer.createAndShow(); |     contextMenu.windowLayer = await DeesWindowLayer.createAndShow(); | ||||||
|     contextMenu.windowLayer.addEventListener('click', async () => { |     contextMenu.windowLayer.addEventListener('click', async (event) => { | ||||||
|       await contextMenu.destroy(); |       // Check if click is on the context menu or its submenus | ||||||
|  |       const clickedElement = event.target as HTMLElement; | ||||||
|  |       const isContextMenu = clickedElement.closest('dees-contextmenu'); | ||||||
|  |       if (!isContextMenu) { | ||||||
|  |         await contextMenu.destroy(); | ||||||
|  |       } | ||||||
|     }) |     }) | ||||||
|     document.body.append(contextMenu); |     document.body.append(contextMenu); | ||||||
|      |      | ||||||
| @@ -123,9 +127,13 @@ export class DeesContextmenu extends DeesElement { | |||||||
|   @property({ |   @property({ | ||||||
|     type: Array, |     type: Array, | ||||||
|   }) |   }) | ||||||
|   public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = []; |   public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]; divider?: never } | { divider: true })[] = []; | ||||||
|   windowLayer: DeesWindowLayer; |   windowLayer: DeesWindowLayer; | ||||||
|    |    | ||||||
|  |   private submenu: DeesContextmenu | null = null; | ||||||
|  |   private submenuTimeout: any = null; | ||||||
|  |   private parentMenu: DeesContextmenu | null = null; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
|     this.tabIndex = 0; |     this.tabIndex = 0; | ||||||
| @@ -167,13 +175,22 @@ export class DeesContextmenu extends DeesElement { | |||||||
|         cursor: default; |         cursor: default; | ||||||
|         transition: background 0.1s; |         transition: background 0.1s; | ||||||
|         line-height: 1; |         line-height: 1; | ||||||
|  |         position: relative; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .menuitem:hover { |       .menuitem:hover { | ||||||
|         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')}; |         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')}; | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       .menuitem:active { |       .menuitem.has-submenu::after { | ||||||
|  |         content: '›'; | ||||||
|  |         position: absolute; | ||||||
|  |         right: 8px; | ||||||
|  |         font-size: 16px; | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .menuitem:active:not(.has-submenu) { | ||||||
|         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')}; |         background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')}; | ||||||
|       } |       } | ||||||
|        |        | ||||||
| @@ -215,14 +232,20 @@ export class DeesContextmenu extends DeesElement { | |||||||
|             return html`<div class="menu-divider"></div>`; |             return html`<div class="menu-divider"></div>`; | ||||||
|           } |           } | ||||||
|            |            | ||||||
|           const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }; |           const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: any }; | ||||||
|  |           const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0; | ||||||
|           return html` |           return html` | ||||||
|             <div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}> |             <div  | ||||||
|  |               class="menuitem ${menuItem.disabled ? 'disabled' : ''} ${hasSubmenu ? 'has-submenu' : ''}"  | ||||||
|  |               @click=${() => !menuItem.disabled && !hasSubmenu && this.handleClick(menuItem)} | ||||||
|  |               @mouseenter=${() => this.handleMenuItemHover(menuItem, hasSubmenu)} | ||||||
|  |               @mouseleave=${() => this.handleMenuItemLeave()} | ||||||
|  |             > | ||||||
|               ${menuItem.iconName ? html` |               ${menuItem.iconName ? html` | ||||||
|                 <dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon> |                 <dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon> | ||||||
|               ` : ''} |               ` : ''} | ||||||
|               <span class="menuitem-text">${menuItem.name}</span> |               <span class="menuitem-text">${menuItem.name}</span> | ||||||
|               ${menuItem.shortcut ? html` |               ${menuItem.shortcut && !hasSubmenu ? html` | ||||||
|                 <span class="menuitem-shortcut">${menuItem.shortcut}</span> |                 <span class="menuitem-shortcut">${menuItem.shortcut}</span> | ||||||
|               ` : ''} |               ` : ''} | ||||||
|             </div> |             </div> | ||||||
| @@ -282,17 +305,151 @@ export class DeesContextmenu extends DeesElement { | |||||||
|  |  | ||||||
|   public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) { |   public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) { | ||||||
|     menuItem.action(); |     menuItem.action(); | ||||||
|     await this.destroy(); |      | ||||||
|  |     // Close all menus in the chain (this menu and all parent menus) | ||||||
|  |     await this.destroyAll(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private async handleMenuItemHover(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }, hasSubmenu: boolean) { | ||||||
|  |     // Clear any existing timeout | ||||||
|  |     if (this.submenuTimeout) { | ||||||
|  |       clearTimeout(this.submenuTimeout); | ||||||
|  |       this.submenuTimeout = null; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Hide any existing submenu if hovering a different item | ||||||
|  |     if (this.submenu) { | ||||||
|  |       await this.hideSubmenu(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Show submenu if this item has one | ||||||
|  |     if (hasSubmenu && menuItem.submenu) { | ||||||
|  |       this.submenuTimeout = setTimeout(() => { | ||||||
|  |         this.showSubmenu(menuItem); | ||||||
|  |       }, 200); // Small delay to prevent accidental triggers | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private handleMenuItemLeave() { | ||||||
|  |     // Add a delay before hiding to allow moving to submenu | ||||||
|  |     if (this.submenuTimeout) { | ||||||
|  |       clearTimeout(this.submenuTimeout); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     this.submenuTimeout = setTimeout(() => { | ||||||
|  |       if (this.submenu && !this.submenu.matches(':hover')) { | ||||||
|  |         this.hideSubmenu(); | ||||||
|  |       } | ||||||
|  |     }, 300); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private async showSubmenu(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }) { | ||||||
|  |     if (!menuItem.submenu || menuItem.submenu.length === 0) return; | ||||||
|  |      | ||||||
|  |     // Find the menu item element | ||||||
|  |     const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem')); | ||||||
|  |     const menuItemElement = menuItems.find(el => el.querySelector('.menuitem-text')?.textContent === menuItem.name) as HTMLElement; | ||||||
|  |     if (!menuItemElement) return; | ||||||
|  |      | ||||||
|  |     // Create submenu | ||||||
|  |     this.submenu = new DeesContextmenu(); | ||||||
|  |     this.submenu.menuItems = menuItem.submenu; | ||||||
|  |     this.submenu.parentMenu = this; | ||||||
|  |     this.submenu.style.position = 'fixed'; | ||||||
|  |     this.submenu.style.zIndex = String(parseInt(this.style.zIndex) + 1); | ||||||
|  |     this.submenu.style.opacity = '0'; | ||||||
|  |     this.submenu.style.transform = 'scale(0.95)'; | ||||||
|  |      | ||||||
|  |     // Don't create a window layer for submenus | ||||||
|  |     document.body.append(this.submenu); | ||||||
|  |      | ||||||
|  |     // Position submenu | ||||||
|  |     await domtools.plugins.smartdelay.delayFor(0); | ||||||
|  |     const itemRect = menuItemElement.getBoundingClientRect(); | ||||||
|  |     const menuRect = this.getBoundingClientRect(); | ||||||
|  |     const submenuRect = this.submenu.getBoundingClientRect(); | ||||||
|  |     const windowWidth = window.innerWidth; | ||||||
|  |      | ||||||
|  |     let left = menuRect.right - 4; // Slight overlap | ||||||
|  |     let top = itemRect.top; | ||||||
|  |      | ||||||
|  |     // Check if submenu would go off right edge | ||||||
|  |     if (left + submenuRect.width > windowWidth - 10) { | ||||||
|  |       // Show on left side instead | ||||||
|  |       left = menuRect.left - submenuRect.width + 4; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Adjust vertical position if needed | ||||||
|  |     if (top + submenuRect.height > window.innerHeight - 10) { | ||||||
|  |       top = window.innerHeight - submenuRect.height - 10; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     this.submenu.style.left = `${left}px`; | ||||||
|  |     this.submenu.style.top = `${top}px`; | ||||||
|  |      | ||||||
|  |     // Animate in | ||||||
|  |     await domtools.plugins.smartdelay.delayFor(0); | ||||||
|  |     this.submenu.style.opacity = '1'; | ||||||
|  |     this.submenu.style.transform = 'scale(1)'; | ||||||
|  |      | ||||||
|  |     // Handle submenu hover | ||||||
|  |     this.submenu.addEventListener('mouseenter', () => { | ||||||
|  |       if (this.submenuTimeout) { | ||||||
|  |         clearTimeout(this.submenuTimeout); | ||||||
|  |         this.submenuTimeout = null; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     this.submenu.addEventListener('mouseleave', () => { | ||||||
|  |       this.handleMenuItemLeave(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   private async hideSubmenu() { | ||||||
|  |     if (!this.submenu) return; | ||||||
|  |      | ||||||
|  |     await this.submenu.destroy(); | ||||||
|  |     this.submenu = null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async destroy() { |   public async destroy() { | ||||||
|     if (this.windowLayer) { |     // Clear timeout | ||||||
|  |     if (this.submenuTimeout) { | ||||||
|  |       clearTimeout(this.submenuTimeout); | ||||||
|  |       this.submenuTimeout = null; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Destroy submenu first | ||||||
|  |     if (this.submenu) { | ||||||
|  |       await this.submenu.destroy(); | ||||||
|  |       this.submenu = null; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Only destroy window layer if this is not a submenu | ||||||
|  |     if (this.windowLayer && !this.parentMenu) { | ||||||
|       this.windowLayer.destroy(); |       this.windowLayer.destroy(); | ||||||
|     } |     } | ||||||
|  |      | ||||||
|     this.style.opacity = '0'; |     this.style.opacity = '0'; | ||||||
|     this.style.transform = 'scale(0.95) translateY(-10px)'; |     this.style.transform = 'scale(0.95) translateY(-10px)'; | ||||||
|     await domtools.plugins.smartdelay.delayFor(100); |     await domtools.plugins.smartdelay.delayFor(100); | ||||||
|     this.parentElement.removeChild(this); |      | ||||||
|  |     if (this.parentElement) { | ||||||
|  |       this.parentElement.removeChild(this); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Destroys this menu and all parent menus in the chain | ||||||
|  |    */ | ||||||
|  |   public async destroyAll() { | ||||||
|  |     // First destroy parent menus if they exist | ||||||
|  |     if (this.parentMenu) { | ||||||
|  |       await this.parentMenu.destroyAll(); | ||||||
|  |     } else { | ||||||
|  |       // If we're at the top level, just destroy this menu | ||||||
|  |       await this.destroy(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								ts_web/elements/dees-dashboardgrid/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								ts_web/elements/dees-dashboardgrid/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | # dees-dashboardgrid | ||||||
|  |  | ||||||
|  | `<dees-dashboardgrid>` renders a configurable dashboard layout with draggable and resizable tiles. The component is now grouped in its own folder alongside supporting utilities and styles. | ||||||
|  |  | ||||||
|  | ## Key Features | ||||||
|  |  | ||||||
|  | - Pointer-driven drag and resize interactions with keyboard fallbacks (arrow keys to move, `Shift` + arrows to resize). | ||||||
|  | - Collision-aware placement that swaps compatible tiles or displaces blocking tiles into the next free slot. | ||||||
|  | - Context menu (right-click on a tile header) that exposes destructive actions such as tile removal via `dees-contextmenu`. | ||||||
|  | - Layout persistence helpers via `getLayout()`, `setLayout(...)`, and the `layout-change` event. | ||||||
|  | - Responsive presets through the `layouts` map and `applyBreakpointLayout(...)` helper to hydrate per-breakpoint arrangements. | ||||||
|  |  | ||||||
|  | ## Public API Highlights | ||||||
|  |  | ||||||
|  | | Property | Description | | ||||||
|  | | --- | --- | | ||||||
|  | | `widgets` | Array of tile descriptors (`DashboardWidget`). | | ||||||
|  | | `columns` | Number of grid columns. | | ||||||
|  | | `layouts` | Optional record of named layout definitions. | | ||||||
|  | | `activeBreakpoint` | Name of the currently applied breakpoint layout. | | ||||||
|  | | `editable` | Toggles drag/resize affordances. | | ||||||
|  |  | ||||||
|  | | Method | Description | | ||||||
|  | | --- | --- | | ||||||
|  | | `addWidget(widget, autoPosition?)` | Adds a tile, optionally auto-placing it into the next free slot. | | ||||||
|  | | `removeWidget(id)` | Removes a tile and emits `widget-remove`. | | ||||||
|  | | `applyBreakpointLayout(name)` | Applies a layout from the `layouts` map. | | ||||||
|  | | `getLayout()` / `setLayout(layout)` | Retrieve or apply persisted layouts. | | ||||||
|  | | `compact(direction?)` | Densifies the grid vertically (default) or horizontally. | | ||||||
|  |  | ||||||
|  | | Event | Detail payload | | ||||||
|  | | --- | --- | | ||||||
|  | | `widget-move` | `{ widget, displaced, swappedWith }` | | ||||||
|  | | `widget-resize` | `{ widget, displaced, swappedWith }` | | ||||||
|  | | `widget-remove` | `{ widget }` | | ||||||
|  | | `layout-change` | `{ layout }` | | ||||||
|  |  | ||||||
|  | ## Usage Notes | ||||||
|  |  | ||||||
|  | - **Right-click** a tile header to open the contextual menu and delete the tile. | ||||||
|  | - When resizing, blocking tiles will automatically reflow into free space once the interaction completes. | ||||||
|  | - Listen to `layout-change` to persist layouts to storage; rehydrate using `setLayout` or the `layouts` map. | ||||||
|  | - For responsive dashboards, populate `grid.layouts = { base: [...], mobile: [...] }` and call `applyBreakpointLayout` based on your own breakpoint logic (see the co-located demo for an example). | ||||||
|  |  | ||||||
|  | ## Demo | ||||||
|  |  | ||||||
|  | The updated `dees-dashboardgrid.demo.ts` showcases live breakpoint switching, layout persistence, and the context menu. Run the demo gallery to explore the interactions end-to-end. | ||||||
							
								
								
									
										29
									
								
								ts_web/elements/dees-dashboardgrid/contextmenu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ts_web/elements/dees-dashboardgrid/contextmenu.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | import type { DashboardWidget } from './types.js'; | ||||||
|  | import { DeesContextmenu } from '../dees-contextmenu.js'; | ||||||
|  | import type { DeesDashboardgrid } from './dees-dashboardgrid.js'; | ||||||
|  | import * as plugins from '../00plugins.js'; | ||||||
|  |  | ||||||
|  | export interface WidgetContextMenuOptions { | ||||||
|  |   widget: DashboardWidget; | ||||||
|  |   host: DeesDashboardgrid; | ||||||
|  |   event: MouseEvent; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const openWidgetContextMenu = ({ | ||||||
|  |   widget, | ||||||
|  |   host, | ||||||
|  |   event, | ||||||
|  | }: WidgetContextMenuOptions) => { | ||||||
|  |   const items: (plugins.tsclass.website.IMenuItem | { divider: true })[] = [ | ||||||
|  |     { | ||||||
|  |       name: 'Delete tile', | ||||||
|  |       iconName: 'lucide:trash2' as any, | ||||||
|  |       action: async () => { | ||||||
|  |         host.removeWidget(widget.id); | ||||||
|  |         return null; | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   DeesContextmenu.openContextMenuWithOptions(event, items as any); | ||||||
|  | }; | ||||||
							
								
								
									
										405
									
								
								ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										405
									
								
								ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,405 @@ | |||||||
|  | import { html, css, cssManager } from '@design.estate/dees-element'; | ||||||
|  | import type { DeesDashboardgrid } from './dees-dashboardgrid.js'; | ||||||
|  | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  |  | ||||||
|  | export const demoFunc = () => { | ||||||
|  |   return html` | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       const grid = elementArg.querySelector('#dashboardGrid') as DeesDashboardgrid; | ||||||
|  |  | ||||||
|  |       const seedWidgets = [ | ||||||
|  |         { | ||||||
|  |           id: 'metrics1', | ||||||
|  |           x: 0, | ||||||
|  |           y: 0, | ||||||
|  |           w: 3, | ||||||
|  |           h: 2, | ||||||
|  |           title: 'Revenue', | ||||||
|  |           icon: 'lucide:dollarSign', | ||||||
|  |           content: html` | ||||||
|  |             <div style="padding: 20px;"> | ||||||
|  |               <div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">$124,563</div> | ||||||
|  |               <div style="color: #22c55e; font-size: 14px; margin-top: 8px;">↑ 12.5% from last month</div> | ||||||
|  |             </div> | ||||||
|  |           `, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           id: 'metrics2', | ||||||
|  |           x: 3, | ||||||
|  |           y: 0, | ||||||
|  |           w: 3, | ||||||
|  |           h: 2, | ||||||
|  |           title: 'Users', | ||||||
|  |           icon: 'lucide:users', | ||||||
|  |           content: html` | ||||||
|  |             <div style="padding: 20px;"> | ||||||
|  |               <div style="font-size: 32px; font-weight: 700; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">8,234</div> | ||||||
|  |               <div style="color: #3b82f6; font-size: 14px; margin-top: 8px;">↑ 5.2% from last week</div> | ||||||
|  |             </div> | ||||||
|  |           `, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           id: 'chart1', | ||||||
|  |           x: 6, | ||||||
|  |           y: 0, | ||||||
|  |           w: 6, | ||||||
|  |           h: 4, | ||||||
|  |           title: 'Analytics', | ||||||
|  |           icon: 'lucide:lineChart', | ||||||
|  |           content: html` | ||||||
|  |             <div style="padding: 20px; height: 100%; display: flex; align-items: center; justify-content: center;"> | ||||||
|  |               <div style="text-align: center; color: #71717a;"> | ||||||
|  |                 <dees-icon .icon=${'lucide:lineChart'} style="font-size: 48px; margin-bottom: 16px;"></dees-icon> | ||||||
|  |                 <div>Chart visualization area</div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           `, | ||||||
|  |         }, | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       grid.widgets = seedWidgets.map(widget => ({ ...widget })); | ||||||
|  |       grid.cellHeight = 80; | ||||||
|  |       grid.margin = { top: 10, right: 10, bottom: 10, left: 10 }; | ||||||
|  |       grid.enableAnimation = true; | ||||||
|  |       grid.showGridLines = false; | ||||||
|  |  | ||||||
|  |       const baseLayout = grid.getLayout().map(item => ({ ...item })); | ||||||
|  |       const mobileLayout = grid.widgets.map((widget, index) => ({ | ||||||
|  |         id: widget.id, | ||||||
|  |         x: 0, | ||||||
|  |         y: index === 0 ? 0 : grid.widgets.slice(0, index).reduce((acc, prev) => acc + prev.h, 0), | ||||||
|  |         w: grid.columns, | ||||||
|  |         h: widget.h, | ||||||
|  |       })); | ||||||
|  |  | ||||||
|  |       grid.layouts = { | ||||||
|  |         base: baseLayout, | ||||||
|  |         mobile: mobileLayout, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const statusEl = elementArg.querySelector('#dashboardLayoutStatus') as HTMLElement; | ||||||
|  |       const updateStatus = () => { | ||||||
|  |         const layout = grid.getLayout(); | ||||||
|  |         statusEl.textContent = `Active breakpoint: ${grid.activeBreakpoint} • Tiles: ${layout.length}`; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const mediaQuery = window.matchMedia('(max-width: 768px)'); | ||||||
|  |       const handleBreakpoint = () => { | ||||||
|  |         const target = mediaQuery.matches ? 'mobile' : 'base'; | ||||||
|  |         grid.applyBreakpointLayout(target); | ||||||
|  |         updateStatus(); | ||||||
|  |       }; | ||||||
|  |       if (typeof mediaQuery.addEventListener === 'function') { | ||||||
|  |         mediaQuery.addEventListener('change', handleBreakpoint); | ||||||
|  |       } else { | ||||||
|  |         (mediaQuery as MediaQueryList & { | ||||||
|  |           addListener?: (listener: (this: MediaQueryList, ev: MediaQueryListEvent) => void) => void; | ||||||
|  |         }).addListener?.(handleBreakpoint); | ||||||
|  |       } | ||||||
|  |       handleBreakpoint(); | ||||||
|  |  | ||||||
|  |       let widgetCounter = 4; | ||||||
|  |  | ||||||
|  |       const buttons = elementArg.querySelectorAll('dees-button'); | ||||||
|  |       buttons.forEach(button => { | ||||||
|  |         const text = button.textContent?.trim(); | ||||||
|  |  | ||||||
|  |         switch (text) { | ||||||
|  |           case 'Toggle Animation': | ||||||
|  |             button.addEventListener('click', () => { | ||||||
|  |               grid.enableAnimation = !grid.enableAnimation; | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case 'Toggle Grid Lines': | ||||||
|  |             button.addEventListener('click', () => { | ||||||
|  |               grid.showGridLines = !grid.showGridLines; | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case 'Add Widget': | ||||||
|  |             button.addEventListener('click', () => { | ||||||
|  |               const newWidget = { | ||||||
|  |                 id: `widget${widgetCounter++}`, | ||||||
|  |                 x: 0, | ||||||
|  |                 y: 0, | ||||||
|  |                 w: 3, | ||||||
|  |                 h: 2, | ||||||
|  |                 autoPosition: true, | ||||||
|  |                 title: `Widget ${widgetCounter - 1}`, | ||||||
|  |                 icon: 'lucide:package', | ||||||
|  |                 content: html` | ||||||
|  |                   <div style="padding: 20px; text-align: center;"> | ||||||
|  |                     <div style="color: #71717a;">New widget content</div> | ||||||
|  |                     <div style="margin-top: 8px; font-size: 24px; font-weight: 600; color: ${cssManager.bdTheme('#09090b', '#fafafa')};">${Math.floor( | ||||||
|  |                       Math.random() * 1000, | ||||||
|  |                     )}</div> | ||||||
|  |                   </div> | ||||||
|  |                 `, | ||||||
|  |               }; | ||||||
|  |               grid.addWidget(newWidget, true); | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case 'Compact Grid': | ||||||
|  |             button.addEventListener('click', () => { | ||||||
|  |               grid.compact(); | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case 'Toggle Edit Mode': | ||||||
|  |             button.addEventListener('click', () => { | ||||||
|  |               grid.editable = !grid.editable; | ||||||
|  |               button.textContent = grid.editable ? 'Lock Grid' : 'Unlock Grid'; | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case 'Reset Layout': | ||||||
|  |             button.addEventListener('click', () => { | ||||||
|  |               grid.applyBreakpointLayout(grid.activeBreakpoint); | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Enhanced logging for reflow events | ||||||
|  |       let lastPlaceholderPosition = null; | ||||||
|  |       let moveEventCounter = 0; | ||||||
|  |  | ||||||
|  |       // Helper function to log grid state | ||||||
|  |       const logGridState = (eventName: string, details?: any) => { | ||||||
|  |         const layout = grid.getLayout(); | ||||||
|  |         console.group(`🔄 ${eventName} [Event #${++moveEventCounter}]`); | ||||||
|  |         console.log('Timestamp:', new Date().toISOString()); | ||||||
|  |         console.log('Grid Configuration:', { | ||||||
|  |           columns: grid.columns, | ||||||
|  |           cellHeight: grid.cellHeight, | ||||||
|  |           margin: grid.margin, | ||||||
|  |           editable: grid.editable, | ||||||
|  |           activeBreakpoint: grid.activeBreakpoint | ||||||
|  |         }); | ||||||
|  |         console.log('Current Layout:', layout); | ||||||
|  |         console.log('Widget Count:', layout.length); | ||||||
|  |         console.log('Grid Bounds:', { | ||||||
|  |           totalWidgets: grid.widgets.length, | ||||||
|  |           maxY: Math.max(...layout.map(w => w.y + w.h)), | ||||||
|  |           occupied: layout.map(w => `${w.id}: (${w.x},${w.y}) ${w.w}x${w.h}`).join(', ') | ||||||
|  |         }); | ||||||
|  |         if (details) { | ||||||
|  |           console.log('Event Details:', details); | ||||||
|  |         } | ||||||
|  |         console.groupEnd(); | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // Monitor placeholder position changes using MutationObserver | ||||||
|  |       const placeholderObserver = new MutationObserver(() => { | ||||||
|  |         const placeholder = grid.shadowRoot?.querySelector('.placeholder') as HTMLElement; | ||||||
|  |         if (placeholder) { | ||||||
|  |           const currentPosition = { | ||||||
|  |             left: placeholder.style.left, | ||||||
|  |             top: placeholder.style.top, | ||||||
|  |             width: placeholder.style.width, | ||||||
|  |             height: placeholder.style.height | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           if (JSON.stringify(currentPosition) !== JSON.stringify(lastPlaceholderPosition)) { | ||||||
|  |             console.group('📍 Placeholder Position Changed'); | ||||||
|  |             console.log('Previous:', lastPlaceholderPosition); | ||||||
|  |             console.log('Current:', currentPosition); | ||||||
|  |  | ||||||
|  |             // Extract grid coordinates from style | ||||||
|  |             const gridInfo = grid.shadowRoot?.querySelector('.grid-container'); | ||||||
|  |             if (gridInfo) { | ||||||
|  |               console.log('Grid Container Dimensions:', { | ||||||
|  |                 width: gridInfo.clientWidth, | ||||||
|  |                 height: gridInfo.clientHeight | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |             console.groupEnd(); | ||||||
|  |             lastPlaceholderPosition = currentPosition; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Start observing the shadow DOM for placeholder changes | ||||||
|  |       if (grid.shadowRoot) { | ||||||
|  |         placeholderObserver.observe(grid.shadowRoot, { | ||||||
|  |           childList: true, | ||||||
|  |           subtree: true, | ||||||
|  |           attributes: true, | ||||||
|  |           attributeFilter: ['style'] | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Log initial state | ||||||
|  |       logGridState('Initial Grid State'); | ||||||
|  |  | ||||||
|  |       grid.addEventListener('widget-move', (e: CustomEvent) => { | ||||||
|  |         logGridState('Widget Move', { | ||||||
|  |           widget: e.detail.widget, | ||||||
|  |           displaced: e.detail.displaced, | ||||||
|  |           swappedWith: e.detail.swappedWith | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       grid.addEventListener('widget-resize', (e: CustomEvent) => { | ||||||
|  |         logGridState('Widget Resize', { | ||||||
|  |           widget: e.detail.widget, | ||||||
|  |           displaced: e.detail.displaced, | ||||||
|  |           swappedWith: e.detail.swappedWith | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       grid.addEventListener('widget-remove', (e: CustomEvent) => { | ||||||
|  |         logGridState('Widget Remove', { | ||||||
|  |           removedWidget: e.detail.widget | ||||||
|  |         }); | ||||||
|  |         updateStatus(); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       grid.addEventListener('layout-change', () => { | ||||||
|  |         logGridState('Layout Change'); | ||||||
|  |         updateStatus(); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Monitor during drag/resize operations using pointer events | ||||||
|  |       grid.addEventListener('pointerdown', (e: PointerEvent) => { | ||||||
|  |         const isHeader = (e.target as HTMLElement).closest('.widget-header'); | ||||||
|  |         const isResizeHandle = (e.target as HTMLElement).closest('.resize-handle'); | ||||||
|  |  | ||||||
|  |         if (isHeader || isResizeHandle) { | ||||||
|  |           console.group(`🎯 Interaction Started: ${isHeader ? 'Drag' : 'Resize'}`); | ||||||
|  |           console.log('Target Widget:', (e.target as HTMLElement).closest('.widget')?.getAttribute('data-widget-id')); | ||||||
|  |           console.log('Pointer Position:', { x: e.clientX, y: e.clientY }); | ||||||
|  |           console.groupEnd(); | ||||||
|  |  | ||||||
|  |           // Track pointer move during interaction | ||||||
|  |           const handlePointerMove = (moveEvent: PointerEvent) => { | ||||||
|  |             const widget = (e.target as HTMLElement).closest('.widget'); | ||||||
|  |             if (widget) { | ||||||
|  |               console.log(`↔️ Pointer Move:`, { | ||||||
|  |                 widgetId: widget.getAttribute('data-widget-id'), | ||||||
|  |                 position: { x: moveEvent.clientX, y: moveEvent.clientY }, | ||||||
|  |                 delta: { | ||||||
|  |                   x: moveEvent.clientX - e.clientX, | ||||||
|  |                   y: moveEvent.clientY - e.clientY | ||||||
|  |                 } | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           const handlePointerUp = () => { | ||||||
|  |             console.group('🏁 Interaction Ended'); | ||||||
|  |             logGridState('Final State After Interaction'); | ||||||
|  |             console.groupEnd(); | ||||||
|  |             document.removeEventListener('pointermove', handlePointerMove); | ||||||
|  |             document.removeEventListener('pointerup', handlePointerUp); | ||||||
|  |           }; | ||||||
|  |  | ||||||
|  |           document.addEventListener('pointermove', handlePointerMove); | ||||||
|  |           document.addEventListener('pointerup', handlePointerUp); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Log when widgets are added | ||||||
|  |       const originalAddWidget = grid.addWidget.bind(grid); | ||||||
|  |       grid.addWidget = (widget: any, autoPosition?: boolean) => { | ||||||
|  |         console.group('➕ Adding Widget'); | ||||||
|  |         console.log('New Widget:', widget); | ||||||
|  |         console.log('Auto Position:', autoPosition); | ||||||
|  |         const result = originalAddWidget(widget, autoPosition); | ||||||
|  |         logGridState('After Widget Added'); | ||||||
|  |         console.groupEnd(); | ||||||
|  |         return result; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       // Log compact operations | ||||||
|  |       const originalCompact = grid.compact.bind(grid); | ||||||
|  |       grid.compact = (direction?: string) => { | ||||||
|  |         console.group('🗜️ Compacting Grid'); | ||||||
|  |         console.log('Direction:', direction || 'vertical'); | ||||||
|  |         logGridState('Before Compact'); | ||||||
|  |         const result = originalCompact(direction); | ||||||
|  |         logGridState('After Compact'); | ||||||
|  |         console.groupEnd(); | ||||||
|  |         return result; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       updateStatus(); | ||||||
|  |     }}> | ||||||
|  |       <style> | ||||||
|  |         ${css` | ||||||
|  |           .demoBox { | ||||||
|  |             position: relative; | ||||||
|  |             background: ${cssManager.bdTheme('#f4f4f5', '#09090b')}; | ||||||
|  |             height: 100%; | ||||||
|  |             width: 100%; | ||||||
|  |             padding: 40px; | ||||||
|  |             box-sizing: border-box; | ||||||
|  |             display: flex; | ||||||
|  |             flex-direction: column; | ||||||
|  |             gap: 24px; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           .demo-controls { | ||||||
|  |             display: flex; | ||||||
|  |             flex-wrap: wrap; | ||||||
|  |             gap: 12px; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           .demo-controls dees-button { | ||||||
|  |             flex-shrink: 0; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           .grid-container-wrapper { | ||||||
|  |             flex: 1; | ||||||
|  |             min-height: 600px; | ||||||
|  |             position: relative; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           .info { | ||||||
|  |             color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |             font-size: 12px; | ||||||
|  |             font-family: 'Geist Sans', sans-serif; | ||||||
|  |             text-align: center; | ||||||
|  |             display: flex; | ||||||
|  |             flex-direction: column; | ||||||
|  |             gap: 6px; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           #dashboardLayoutStatus { | ||||||
|  |             font-weight: 600; | ||||||
|  |             color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; | ||||||
|  |           } | ||||||
|  |         `} | ||||||
|  |       </style> | ||||||
|  |       <div class="demoBox"> | ||||||
|  |         <div class="demo-controls"> | ||||||
|  |           <dees-button-group label="Animation:"> | ||||||
|  |             <dees-button>Toggle Animation</dees-button> | ||||||
|  |           </dees-button-group> | ||||||
|  |  | ||||||
|  |           <dees-button-group label="Display:"> | ||||||
|  |             <dees-button>Toggle Grid Lines</dees-button> | ||||||
|  |           </dees-button-group> | ||||||
|  |  | ||||||
|  |           <dees-button-group label="Actions:"> | ||||||
|  |             <dees-button>Add Widget</dees-button> | ||||||
|  |             <dees-button>Compact Grid</dees-button> | ||||||
|  |             <dees-button>Reset Layout</dees-button> | ||||||
|  |           </dees-button-group> | ||||||
|  |  | ||||||
|  |           <dees-button-group label="Mode:"> | ||||||
|  |             <dees-button>Toggle Edit Mode</dees-button> | ||||||
|  |           </dees-button-group> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="grid-container-wrapper"> | ||||||
|  |           <dees-dashboardgrid id="dashboardGrid"></dees-dashboardgrid> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="info"> | ||||||
|  |           <div>Drag to reposition, resize from handles, or right-click a header to delete a tile.</div> | ||||||
|  |           <div id="dashboardLayoutStatus"></div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |   `; | ||||||
|  | }; | ||||||
							
								
								
									
										796
									
								
								ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										796
									
								
								ts_web/elements/dees-dashboardgrid/dees-dashboardgrid.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,796 @@ | |||||||
|  | import { | ||||||
|  |   DeesElement, | ||||||
|  |   customElement, | ||||||
|  |   property, | ||||||
|  |   state, | ||||||
|  |   html, | ||||||
|  |   type TemplateResult, | ||||||
|  | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | import '../dees-icon.js'; | ||||||
|  | import '../dees-contextmenu.js'; | ||||||
|  | import { demoFunc } from './dees-dashboardgrid.demo.js'; | ||||||
|  | import { dashboardGridStyles } from './styles.js'; | ||||||
|  | import { | ||||||
|  |   resolveMargins, | ||||||
|  |   calculateCellMetrics, | ||||||
|  |   calculateGridHeight, | ||||||
|  |   findAvailablePosition, | ||||||
|  |   compactLayout, | ||||||
|  |   applyLayout, | ||||||
|  |   resolveWidgetPlacement, | ||||||
|  |   type PlacementResult, | ||||||
|  | } from './layout.js'; | ||||||
|  | import { | ||||||
|  |   computeGridCoordinates, | ||||||
|  |   computeResizeDimensions, | ||||||
|  |   type PointerPosition, | ||||||
|  | } from './interaction.js'; | ||||||
|  | import { openWidgetContextMenu } from './contextmenu.js'; | ||||||
|  | import type { | ||||||
|  |   DashboardWidget, | ||||||
|  |   DashboardMargin, | ||||||
|  |   DashboardResolvedMargins, | ||||||
|  |   GridCellMetrics, | ||||||
|  |   DashboardLayoutItem, | ||||||
|  |   LayoutDirection, | ||||||
|  |   CellHeightUnit, | ||||||
|  | } from './types.js'; | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     'dees-dashboardgrid': DeesDashboardgrid; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type DragState = { | ||||||
|  |   widgetId: string; | ||||||
|  |   pointerId: number; | ||||||
|  |   offsetX: number; | ||||||
|  |   offsetY: number; | ||||||
|  |   start: DashboardLayoutItem; | ||||||
|  |   previousPosition: DashboardLayoutItem; | ||||||
|  |   currentPointer: PointerPosition; | ||||||
|  |   lastPlacement: PlacementResult | null; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type ResizeState = { | ||||||
|  |   widgetId: string; | ||||||
|  |   pointerId: number; | ||||||
|  |   handler: 'e' | 's' | 'se'; | ||||||
|  |   startPointer: PointerPosition; | ||||||
|  |   start: DashboardLayoutItem; | ||||||
|  |   startWidth: number; | ||||||
|  |   startHeight: number; | ||||||
|  |   lastPlacement: PlacementResult | null; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @customElement('dees-dashboardgrid') | ||||||
|  | export class DeesDashboardgrid extends DeesElement { | ||||||
|  |   public static demo = demoFunc; | ||||||
|  |   public static styles = dashboardGridStyles; | ||||||
|  |  | ||||||
|  |   @property({ type: Array }) | ||||||
|  |   public widgets: DashboardWidget[] = []; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public cellHeight: number = 80; | ||||||
|  |  | ||||||
|  |   @property({ type: Object }) | ||||||
|  |   public margin: DashboardMargin = 10; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public columns: number = 12; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public editable: boolean = true; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true }) | ||||||
|  |   public enableAnimation: boolean = true; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public cellHeightUnit: CellHeightUnit = 'px'; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public rtl: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public showGridLines: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public layouts?: Record<string, DashboardLayoutItem[]>; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public activeBreakpoint: string = 'base'; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private placeholderPosition: DashboardLayoutItem | null = null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private metrics: GridCellMetrics | null = null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private resolvedMargins: DashboardResolvedMargins | null = null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private previewWidgets: DashboardWidget[] | null = null; | ||||||
|  |  | ||||||
|  |   private containerBounds: DOMRect | null = null; | ||||||
|  |   private dragState: DragState | null = null; | ||||||
|  |   private resizeState: ResizeState | null = null; | ||||||
|  |   private resizeObserver?: ResizeObserver; | ||||||
|  |   private interactionActive = false; | ||||||
|  |  | ||||||
|  |   public override async connectedCallback(): Promise<void> { | ||||||
|  |     await super.connectedCallback(); | ||||||
|  |     this.computeMetrics(); | ||||||
|  |     this.observeResize(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public override async disconnectedCallback(): Promise<void> { | ||||||
|  |     await super.disconnectedCallback(); | ||||||
|  |     this.disconnectResizeObserver(); | ||||||
|  |     this.releasePointerEvents(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected updated(changed: Map<string, unknown>): void { | ||||||
|  |     if ( | ||||||
|  |       changed.has('margin') || | ||||||
|  |       changed.has('columns') || | ||||||
|  |       changed.has('cellHeight') || | ||||||
|  |       changed.has('cellHeightUnit') | ||||||
|  |     ) { | ||||||
|  |       this.computeMetrics(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (changed.has('widgets') && !this.interactionActive) { | ||||||
|  |       this.notifyLayoutChange(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     const baseWidgets = this.widgets; | ||||||
|  |     if (baseWidgets.length === 0) { | ||||||
|  |       return html` | ||||||
|  |         <div class="empty-state"> | ||||||
|  |           <dees-icon .icon=${'lucide:layoutGrid'}></dees-icon> | ||||||
|  |           <div>No widgets configured</div> | ||||||
|  |           <div style="font-size: 14px; margin-top: 8px;">Add widgets to populate the dashboard</div> | ||||||
|  |         </div> | ||||||
|  |       `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const metrics = this.ensureMetrics(); | ||||||
|  |     const margins = this.resolvedMargins ?? resolveMargins(this.margin); | ||||||
|  |     const cellHeight = metrics.cellHeightPx; | ||||||
|  |     const layoutForHeight = this.previewWidgets ?? this.widgets; | ||||||
|  |     const gridHeight = calculateGridHeight(layoutForHeight, margins, cellHeight); | ||||||
|  |     const previewMap = this.previewWidgets ? new Map(this.previewWidgets.map(widget => [widget.id, widget])) : null; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="grid-container" style="height: ${gridHeight}px;"> | ||||||
|  |         ${this.showGridLines ? this.renderGridLines(metrics, gridHeight) : null} | ||||||
|  |         ${baseWidgets.map(widget => this.renderWidget(widget, metrics, margins, previewMap))} | ||||||
|  |         ${this.placeholderPosition ? this.renderPlaceholder(metrics, margins) : null} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderGridLines(metrics: GridCellMetrics, gridHeight: number): TemplateResult { | ||||||
|  |     const vertical: TemplateResult[] = []; | ||||||
|  |     const horizontal: TemplateResult[] = []; | ||||||
|  |     const cellPlusMarginX = metrics.cellWidthPx + metrics.marginHorizontalPx; | ||||||
|  |     const cellPlusMarginY = metrics.cellHeightPx + metrics.marginVerticalPx; | ||||||
|  |  | ||||||
|  |     for (let i = 0; i <= this.columns; i++) { | ||||||
|  |       const leftPx = i * cellPlusMarginX + metrics.marginHorizontalPx; | ||||||
|  |       const leftPercent = this.pxToPercent(leftPx, metrics.containerWidth); | ||||||
|  |       vertical.push(html`<div class="grid-line-vertical" style="left: ${leftPercent}%;"></div>`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const rows = Math.ceil(gridHeight / cellPlusMarginY); | ||||||
|  |     for (let row = 0; row <= rows; row++) { | ||||||
|  |       const top = row * cellPlusMarginY; | ||||||
|  |       horizontal.push(html`<div class="grid-line-horizontal" style="top: ${top}px;"></div>`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="grid-lines"> | ||||||
|  |         ${vertical} | ||||||
|  |         ${horizontal} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderWidget( | ||||||
|  |     widget: DashboardWidget, | ||||||
|  |     metrics: GridCellMetrics, | ||||||
|  |     margins: DashboardResolvedMargins, | ||||||
|  |     previewMap: Map<string, DashboardWidget> | null, | ||||||
|  |   ): TemplateResult { | ||||||
|  |     const isDragging = this.dragState?.widgetId === widget.id; | ||||||
|  |     const isResizing = this.resizeState?.widgetId === widget.id; | ||||||
|  |     const isLocked = widget.locked || !this.editable; | ||||||
|  |     const previewWidget = previewMap?.get(widget.id) ?? null; | ||||||
|  |     const layoutForRender = isDragging ? widget : previewWidget ?? widget; | ||||||
|  |     const rect = this.computeWidgetRect(layoutForRender, metrics, margins); | ||||||
|  |  | ||||||
|  |     const sideProperty = this.rtl ? 'right' : 'left'; | ||||||
|  |     const sideValue = this.pxToPercent(rect.left, metrics.containerWidth); | ||||||
|  |     const widthPercent = this.pxToPercent(rect.width, metrics.containerWidth); | ||||||
|  |  | ||||||
|  |     let transform = ''; | ||||||
|  |     if (isDragging && this.dragState?.currentPointer) { | ||||||
|  |       const pointer = this.dragState.currentPointer; | ||||||
|  |       const bounds = this.containerBounds ?? this.getBoundingClientRect(); | ||||||
|  |       const translateX = pointer.clientX - bounds.left - this.dragState.offsetX - rect.left; | ||||||
|  |       const translateY = pointer.clientY - bounds.top - this.dragState.offsetY - rect.top; | ||||||
|  |       transform = `transform: translate(${translateX}px, ${translateY}px);`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div | ||||||
|  |         class="grid-widget ${isDragging ? 'dragging' : ''} ${isResizing ? 'resizing' : ''}" | ||||||
|  |         style=" | ||||||
|  |           ${sideProperty}: ${sideValue}%; | ||||||
|  |           top: ${rect.top}px; | ||||||
|  |           width: ${widthPercent}%; | ||||||
|  |           height: ${rect.height}px; | ||||||
|  |           ${transform} | ||||||
|  |         " | ||||||
|  |         data-widget-id=${widget.id} | ||||||
|  |       > | ||||||
|  |         <div class="widget-content"> | ||||||
|  |           ${widget.title | ||||||
|  |             ? html` | ||||||
|  |                 <div | ||||||
|  |                   class="widget-header ${isLocked ? 'locked' : ''}" | ||||||
|  |                   @pointerdown=${!isLocked && !widget.noMove | ||||||
|  |                     ? (evt: PointerEvent) => this.startDrag(evt, widget) | ||||||
|  |                     : null} | ||||||
|  |                   @contextmenu=${(evt: MouseEvent) => this.handleWidgetContextMenu(evt, widget)} | ||||||
|  |                   tabindex=${!isLocked && !widget.noMove ? 0 : -1} | ||||||
|  |                   @keydown=${(evt: KeyboardEvent) => this.handleHeaderKeydown(evt, widget)} | ||||||
|  |                 > | ||||||
|  |                   ${widget.icon ? html`<dees-icon .icon=${widget.icon}></dees-icon>` : null} | ||||||
|  |                   ${widget.title} | ||||||
|  |                 </div> | ||||||
|  |               ` | ||||||
|  |             : null} | ||||||
|  |           <div class="widget-body ${widget.title ? 'has-header' : ''}"> | ||||||
|  |             ${widget.content} | ||||||
|  |           </div> | ||||||
|  |           ${!isLocked && !widget.noResize | ||||||
|  |             ? html` | ||||||
|  |                 <div | ||||||
|  |                   class="resize-handle resize-handle-e" | ||||||
|  |                   @pointerdown=${(evt: PointerEvent) => this.startResize(evt, widget, 'e')} | ||||||
|  |                 ></div> | ||||||
|  |                 <div | ||||||
|  |                   class="resize-handle resize-handle-s" | ||||||
|  |                   @pointerdown=${(evt: PointerEvent) => this.startResize(evt, widget, 's')} | ||||||
|  |                 ></div> | ||||||
|  |                 <div | ||||||
|  |                   class="resize-handle resize-handle-se" | ||||||
|  |                   @pointerdown=${(evt: PointerEvent) => this.startResize(evt, widget, 'se')} | ||||||
|  |                 ></div> | ||||||
|  |               ` | ||||||
|  |             : null} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderPlaceholder( | ||||||
|  |     metrics: GridCellMetrics, | ||||||
|  |     margins: DashboardResolvedMargins, | ||||||
|  |   ): TemplateResult { | ||||||
|  |     if (!this.placeholderPosition) { | ||||||
|  |       return html``; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const rect = this.computeWidgetRect(this.placeholderPosition, metrics, margins); | ||||||
|  |     const sideProperty = this.rtl ? 'right' : 'left'; | ||||||
|  |     const sideValue = this.pxToPercent(rect.left, metrics.containerWidth); | ||||||
|  |     const widthPercent = this.pxToPercent(rect.width, metrics.containerWidth); | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div | ||||||
|  |         class="grid-widget placeholder" | ||||||
|  |         style=" | ||||||
|  |           ${sideProperty}: ${sideValue}%; | ||||||
|  |           top: ${rect.top}px; | ||||||
|  |           width: ${widthPercent}%; | ||||||
|  |           height: ${rect.height}px; | ||||||
|  |         " | ||||||
|  |       > | ||||||
|  |         <div class="widget-content"></div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private startDrag(event: PointerEvent, widget: DashboardWidget): void { | ||||||
|  |     if (!this.editable || widget.noMove || widget.locked) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  |  | ||||||
|  |     const widgetElement = (event.currentTarget as HTMLElement).closest('.grid-widget') as HTMLElement | null; | ||||||
|  |     if (!widgetElement) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const widgetRect = widgetElement.getBoundingClientRect(); | ||||||
|  |     this.containerBounds = this.getBoundingClientRect(); | ||||||
|  |     this.ensureMetrics(); | ||||||
|  |  | ||||||
|  |     this.dragState = { | ||||||
|  |       widgetId: widget.id, | ||||||
|  |       pointerId: event.pointerId, | ||||||
|  |       offsetX: event.clientX - widgetRect.left, | ||||||
|  |       offsetY: event.clientY - widgetRect.top, | ||||||
|  |       start: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h }, | ||||||
|  |       previousPosition: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h }, | ||||||
|  |       currentPointer: { clientX: event.clientX, clientY: event.clientY }, | ||||||
|  |       lastPlacement: null, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.interactionActive = true; | ||||||
|  |     (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); | ||||||
|  |     document.addEventListener('pointermove', this.handleDragMove); | ||||||
|  |     document.addEventListener('pointerup', this.handleDragEnd); | ||||||
|  |  | ||||||
|  |     this.placeholderPosition = { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleDragMove = (event: PointerEvent): void => { | ||||||
|  |     if (!this.dragState) return; | ||||||
|  |     const metrics = this.ensureMetrics(); | ||||||
|  |     const activeWidgets = this.widgets; | ||||||
|  |     const widget = activeWidgets.find(item => item.id === this.dragState!.widgetId); | ||||||
|  |     if (!widget) return; | ||||||
|  |  | ||||||
|  |     event.preventDefault(); | ||||||
|  |  | ||||||
|  |     const previousPosition = this.dragState.previousPosition; | ||||||
|  |  | ||||||
|  |     const coords = computeGridCoordinates({ | ||||||
|  |       pointer: { clientX: event.clientX, clientY: event.clientY }, | ||||||
|  |       containerRect: this.containerBounds ?? this.getBoundingClientRect(), | ||||||
|  |       metrics, | ||||||
|  |       columns: this.columns, | ||||||
|  |       widget, | ||||||
|  |       rtl: this.rtl, | ||||||
|  |       dragOffsetX: this.dragState.offsetX, | ||||||
|  |       dragOffsetY: this.dragState.offsetY, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const placement = resolveWidgetPlacement( | ||||||
|  |       activeWidgets, | ||||||
|  |       widget.id, | ||||||
|  |       { x: coords.x, y: coords.y }, | ||||||
|  |       this.columns, | ||||||
|  |       previousPosition, | ||||||
|  |     ); | ||||||
|  |     if (placement) { | ||||||
|  |       const updatedWidget = placement.widgets.find(item => item.id === widget.id); | ||||||
|  |       this.dragState = { | ||||||
|  |         ...this.dragState, | ||||||
|  |         currentPointer: { clientX: event.clientX, clientY: event.clientY }, | ||||||
|  |         lastPlacement: placement, | ||||||
|  |         previousPosition: updatedWidget | ||||||
|  |           ? { id: updatedWidget.id, x: updatedWidget.x, y: updatedWidget.y, w: updatedWidget.w, h: updatedWidget.h } | ||||||
|  |           : { id: widget.id, x: coords.x, y: coords.y, w: widget.w, h: widget.h }, | ||||||
|  |       }; | ||||||
|  |       this.previewWidgets = placement.widgets; | ||||||
|  |       const previewWidget = placement.widgets.find(item => item.id === widget.id); | ||||||
|  |       if (previewWidget) { | ||||||
|  |         this.placeholderPosition = { | ||||||
|  |           id: previewWidget.id, | ||||||
|  |           x: previewWidget.x, | ||||||
|  |           y: previewWidget.y, | ||||||
|  |           w: previewWidget.w, | ||||||
|  |           h: previewWidget.h, | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         this.placeholderPosition = { id: widget.id, x: coords.x, y: coords.y, w: widget.w, h: widget.h }; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this.previewWidgets = null; | ||||||
|  |       this.placeholderPosition = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.requestUpdate(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private handleDragEnd = (event: PointerEvent): void => { | ||||||
|  |     const dragState = this.dragState; | ||||||
|  |     if (!dragState || event.pointerId !== dragState.pointerId) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const layoutSource = this.widgets; | ||||||
|  |     this.previewWidgets = null; | ||||||
|  |  | ||||||
|  |     // Always validate the final position, don't rely on lastPlacement from drag | ||||||
|  |     const target = this.placeholderPosition ?? dragState.start; | ||||||
|  |     const placement = resolveWidgetPlacement( | ||||||
|  |       layoutSource, | ||||||
|  |       dragState.widgetId, | ||||||
|  |       { x: target.x, y: target.y }, | ||||||
|  |       this.columns, | ||||||
|  |       dragState.previousPosition, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (placement) { | ||||||
|  |       // Verify that the placement doesn't result in overlapping widgets | ||||||
|  |       const finalWidget = placement.widgets.find(w => w.id === dragState.widgetId); | ||||||
|  |       if (finalWidget) { | ||||||
|  |         const hasOverlap = placement.widgets.some(w => { | ||||||
|  |           if (w.id === dragState.widgetId) return false; | ||||||
|  |           return ( | ||||||
|  |             finalWidget.x < w.x + w.w && | ||||||
|  |             finalWidget.x + finalWidget.w > w.x && | ||||||
|  |             finalWidget.y < w.y + w.h && | ||||||
|  |             finalWidget.y + finalWidget.h > w.y | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (!hasOverlap) { | ||||||
|  |           this.commitPlacement(placement, dragState.widgetId, 'widget-move'); | ||||||
|  |         } else { | ||||||
|  |           // Return to start position if overlap detected | ||||||
|  |           this.widgets = this.widgets.map(widget => | ||||||
|  |             widget.id === dragState.widgetId ? { ...widget, x: dragState.start.x, y: dragState.start.y } : widget, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // Return to start position if no valid placement | ||||||
|  |       this.widgets = this.widgets.map(widget => | ||||||
|  |         widget.id === dragState.widgetId ? { ...widget, x: dragState.start.x, y: dragState.start.y } : widget, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.placeholderPosition = null; | ||||||
|  |     this.dragState = null; | ||||||
|  |     this.interactionActive = false; | ||||||
|  |     this.releasePointerEvents(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private startResize(event: PointerEvent, widget: DashboardWidget, handler: 'e' | 's' | 'se'): void { | ||||||
|  |     if (!this.editable || widget.noResize || widget.locked) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  |  | ||||||
|  |     this.ensureMetrics(); | ||||||
|  |  | ||||||
|  |     this.resizeState = { | ||||||
|  |       widgetId: widget.id, | ||||||
|  |       pointerId: event.pointerId, | ||||||
|  |       handler, | ||||||
|  |       startPointer: { clientX: event.clientX, clientY: event.clientY }, | ||||||
|  |       start: { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h }, | ||||||
|  |       startWidth: widget.w, | ||||||
|  |       startHeight: widget.h, | ||||||
|  |       lastPlacement: null, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.interactionActive = true; | ||||||
|  |     (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); | ||||||
|  |     document.addEventListener('pointermove', this.handleResizeMove); | ||||||
|  |     document.addEventListener('pointerup', this.handleResizeEnd); | ||||||
|  |  | ||||||
|  |     this.placeholderPosition = { id: widget.id, x: widget.x, y: widget.y, w: widget.w, h: widget.h }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleResizeMove = (event: PointerEvent): void => { | ||||||
|  |     if (!this.resizeState) return; | ||||||
|  |     const metrics = this.ensureMetrics(); | ||||||
|  |     const activeWidgets = this.widgets; | ||||||
|  |     const widget = activeWidgets.find(item => item.id === this.resizeState!.widgetId); | ||||||
|  |     if (!widget) return; | ||||||
|  |  | ||||||
|  |     event.preventDefault(); | ||||||
|  |  | ||||||
|  |     const nextSize = computeResizeDimensions({ | ||||||
|  |       pointer: { clientX: event.clientX, clientY: event.clientY }, | ||||||
|  |       containerRect: this.containerBounds ?? this.getBoundingClientRect(), | ||||||
|  |       metrics, | ||||||
|  |       startWidth: this.resizeState.startWidth, | ||||||
|  |       startHeight: this.resizeState.startHeight, | ||||||
|  |       startPointer: this.resizeState.startPointer, | ||||||
|  |       handler: this.resizeState.handler, | ||||||
|  |       widget, | ||||||
|  |       columns: this.columns, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const placement = resolveWidgetPlacement( | ||||||
|  |       activeWidgets, | ||||||
|  |       widget.id, | ||||||
|  |       { x: widget.x, y: widget.y, w: nextSize.width, h: nextSize.height }, | ||||||
|  |       this.columns, | ||||||
|  |       this.resizeState.start, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (placement) { | ||||||
|  |       this.resizeState = { ...this.resizeState, lastPlacement: placement }; | ||||||
|  |       this.previewWidgets = placement.widgets; | ||||||
|  |       const previewWidget = placement.widgets.find(item => item.id === widget.id); | ||||||
|  |       if (previewWidget) { | ||||||
|  |         this.placeholderPosition = { | ||||||
|  |           id: previewWidget.id, | ||||||
|  |           x: previewWidget.x, | ||||||
|  |           y: previewWidget.y, | ||||||
|  |           w: previewWidget.w, | ||||||
|  |           h: previewWidget.h, | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         this.placeholderPosition = { | ||||||
|  |           id: widget.id, | ||||||
|  |           x: widget.x, | ||||||
|  |           y: widget.y, | ||||||
|  |           w: nextSize.width, | ||||||
|  |           h: nextSize.height, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this.previewWidgets = null; | ||||||
|  |       this.placeholderPosition = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.requestUpdate(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private handleResizeEnd = (event: PointerEvent): void => { | ||||||
|  |     const resizeState = this.resizeState; | ||||||
|  |     if (!resizeState || event.pointerId !== resizeState.pointerId) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const layoutSource = this.widgets; | ||||||
|  |     this.previewWidgets = null; | ||||||
|  |     const placement = | ||||||
|  |       resizeState.lastPlacement ?? | ||||||
|  |       resolveWidgetPlacement( | ||||||
|  |         layoutSource, | ||||||
|  |         resizeState.widgetId, | ||||||
|  |         { | ||||||
|  |           x: this.placeholderPosition?.x ?? resizeState.start.x, | ||||||
|  |           y: this.placeholderPosition?.y ?? resizeState.start.y, | ||||||
|  |           w: this.placeholderPosition?.w ?? resizeState.start.w, | ||||||
|  |           h: this.placeholderPosition?.h ?? resizeState.start.h, | ||||||
|  |         }, | ||||||
|  |         this.columns, | ||||||
|  |         resizeState.start, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     if (placement) { | ||||||
|  |       this.commitPlacement(placement, resizeState.widgetId, 'widget-resize'); | ||||||
|  |     } else { | ||||||
|  |       this.widgets = this.widgets.map(widget => | ||||||
|  |         widget.id === resizeState.widgetId ? { ...widget, w: resizeState.start.w, h: resizeState.start.h } : widget, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.placeholderPosition = null; | ||||||
|  |     this.resizeState = null; | ||||||
|  |     this.interactionActive = false; | ||||||
|  |     this.releasePointerEvents(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private handleHeaderKeydown(event: KeyboardEvent, widget: DashboardWidget): void { | ||||||
|  |     if (!this.editable || widget.noMove || widget.locked) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const key = event.key; | ||||||
|  |     const isResize = event.shiftKey; | ||||||
|  |     let placement: PlacementResult | null = null; | ||||||
|  |  | ||||||
|  |     if (isResize && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(key)) { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       const delta = key === 'ArrowRight' || key === 'ArrowDown' ? 1 : -1; | ||||||
|  |  | ||||||
|  |       if (key === 'ArrowLeft' || key === 'ArrowRight') { | ||||||
|  |         const maxWidth = widget.maxW ?? this.columns - widget.x; | ||||||
|  |         const nextWidth = Math.max(widget.minW ?? 1, Math.min(maxWidth, widget.w + delta)); | ||||||
|  |         placement = resolveWidgetPlacement( | ||||||
|  |           this.widgets, | ||||||
|  |           widget.id, | ||||||
|  |           { x: widget.x, y: widget.y, w: nextWidth, h: widget.h }, | ||||||
|  |           this.columns, | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         const maxHeight = widget.maxH ?? Number.POSITIVE_INFINITY; | ||||||
|  |         const nextHeight = Math.max(widget.minH ?? 1, Math.min(maxHeight, widget.h + delta)); | ||||||
|  |         placement = resolveWidgetPlacement( | ||||||
|  |           this.widgets, | ||||||
|  |           widget.id, | ||||||
|  |           { x: widget.x, y: widget.y, w: widget.w, h: nextHeight }, | ||||||
|  |           this.columns, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (placement) { | ||||||
|  |         this.commitPlacement(placement, widget.id, 'widget-resize'); | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const moveMap: Record<string, { dx: number; dy: number }> = { | ||||||
|  |       ArrowLeft: { dx: -1, dy: 0 }, | ||||||
|  |       ArrowRight: { dx: 1, dy: 0 }, | ||||||
|  |       ArrowUp: { dx: 0, dy: -1 }, | ||||||
|  |       ArrowDown: { dx: 0, dy: 1 }, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const delta = moveMap[key]; | ||||||
|  |     if (!delta) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     event.preventDefault(); | ||||||
|  |     const targetX = Math.max(0, Math.min(this.columns - widget.w, widget.x + delta.dx)); | ||||||
|  |     const targetY = Math.max(0, widget.y + delta.dy); | ||||||
|  |  | ||||||
|  |     placement = resolveWidgetPlacement(this.widgets, widget.id, { x: targetX, y: targetY }, this.columns); | ||||||
|  |     if (placement) { | ||||||
|  |       this.commitPlacement(placement, widget.id, 'widget-move'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleWidgetContextMenu(event: MouseEvent, widget: DashboardWidget): void { | ||||||
|  |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  |     openWidgetContextMenu({ widget, host: this, event }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private commitPlacement(result: PlacementResult, widgetId: string, type: 'widget-move' | 'widget-resize'): void { | ||||||
|  |     this.previewWidgets = null; | ||||||
|  |     this.widgets = result.widgets; | ||||||
|  |     const subject = this.widgets.find(item => item.id === widgetId); | ||||||
|  |     if (subject) { | ||||||
|  |       this.dispatchEvent( | ||||||
|  |         new CustomEvent(type, { | ||||||
|  |           detail: { | ||||||
|  |             widget: subject, | ||||||
|  |             displaced: result.movedWidgets.filter(id => id !== widgetId), | ||||||
|  |             swappedWith: result.swappedWith, | ||||||
|  |           }, | ||||||
|  |           bubbles: true, | ||||||
|  |           composed: true, | ||||||
|  |         }), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public removeWidget(widgetId: string): void { | ||||||
|  |     const target = this.widgets.find(widget => widget.id === widgetId); | ||||||
|  |     if (!target) return; | ||||||
|  |     this.widgets = this.widgets.filter(widget => widget.id !== widgetId); | ||||||
|  |     this.dispatchEvent( | ||||||
|  |       new CustomEvent('widget-remove', { | ||||||
|  |         detail: { widget: target }, | ||||||
|  |         bubbles: true, | ||||||
|  |         composed: true, | ||||||
|  |       }), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public updateWidget(widgetId: string, updates: Partial<DashboardWidget>): void { | ||||||
|  |     this.widgets = this.widgets.map(widget => (widget.id === widgetId ? { ...widget, ...updates } : widget)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getLayout(): DashboardLayoutItem[] { | ||||||
|  |     return this.widgets.map(({ id, x, y, w, h }) => ({ id, x, y, w, h })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public setLayout(layout: DashboardLayoutItem[]): void { | ||||||
|  |     this.widgets = applyLayout(this.widgets, layout); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public lockGrid(): void { | ||||||
|  |     this.editable = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public unlockGrid(): void { | ||||||
|  |     this.editable = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public addWidget(widget: DashboardWidget, autoPosition = false): void { | ||||||
|  |     const nextWidget = { ...widget }; | ||||||
|  |     if (autoPosition || nextWidget.autoPosition) { | ||||||
|  |       const position = findAvailablePosition(this.widgets, nextWidget.w, nextWidget.h, this.columns); | ||||||
|  |       nextWidget.x = position.x; | ||||||
|  |       nextWidget.y = position.y; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.widgets = [...this.widgets, nextWidget]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public compact(direction: LayoutDirection = 'vertical'): void { | ||||||
|  |     const nextWidgets = this.widgets.map(widget => ({ ...widget })); | ||||||
|  |     compactLayout(nextWidgets, direction); | ||||||
|  |     this.widgets = nextWidgets; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public applyBreakpointLayout(breakpoint: string): void { | ||||||
|  |     this.activeBreakpoint = breakpoint; | ||||||
|  |     const layout = this.layouts?.[breakpoint]; | ||||||
|  |     if (layout) { | ||||||
|  |       this.setLayout(layout); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public notifyLayoutChange(): void { | ||||||
|  |     this.dispatchEvent( | ||||||
|  |       new CustomEvent('layout-change', { | ||||||
|  |         detail: { layout: this.getLayout() }, | ||||||
|  |         bubbles: true, | ||||||
|  |         composed: true, | ||||||
|  |       }), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private ensureMetrics(): GridCellMetrics { | ||||||
|  |     if (!this.metrics) { | ||||||
|  |       this.computeMetrics(); | ||||||
|  |     } | ||||||
|  |     return this.metrics!; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private computeMetrics(): void { | ||||||
|  |     if (!this.isConnected) return; | ||||||
|  |     const bounds = this.getBoundingClientRect(); | ||||||
|  |     this.containerBounds = bounds; | ||||||
|  |     const margins = resolveMargins(this.margin); | ||||||
|  |     this.resolvedMargins = margins; | ||||||
|  |     this.metrics = calculateCellMetrics(bounds.width, this.columns, margins, this.cellHeight, this.cellHeightUnit); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private observeResize(): void { | ||||||
|  |     if (this.resizeObserver) return; | ||||||
|  |     this.resizeObserver = new ResizeObserver(() => { | ||||||
|  |       this.computeMetrics(); | ||||||
|  |     }); | ||||||
|  |     this.resizeObserver.observe(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private disconnectResizeObserver(): void { | ||||||
|  |     this.resizeObserver?.disconnect(); | ||||||
|  |     this.resizeObserver = undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private releasePointerEvents(): void { | ||||||
|  |     document.removeEventListener('pointermove', this.handleDragMove); | ||||||
|  |     document.removeEventListener('pointerup', this.handleDragEnd); | ||||||
|  |     document.removeEventListener('pointermove', this.handleResizeMove); | ||||||
|  |     document.removeEventListener('pointerup', this.handleResizeEnd); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private pxToPercent(value: number, container: number): number { | ||||||
|  |     if (!container) return 0; | ||||||
|  |     return Number(((value / container) * 100).toFixed(4)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private computeWidgetRect( | ||||||
|  |     widget: Pick<DashboardWidget, 'x' | 'y' | 'w' | 'h'>, | ||||||
|  |     metrics: GridCellMetrics, | ||||||
|  |     margins: DashboardResolvedMargins, | ||||||
|  |   ) { | ||||||
|  |     const cellWidth = metrics.cellWidthPx; | ||||||
|  |     const cellHeight = metrics.cellHeightPx; | ||||||
|  |     const left = widget.x * (cellWidth + margins.horizontal) + margins.horizontal; | ||||||
|  |     const top = widget.y * (cellHeight + margins.vertical) + margins.vertical; | ||||||
|  |     const width = widget.w * cellWidth + Math.max(0, widget.w - 1) * margins.horizontal; | ||||||
|  |     const height = widget.h * cellHeight + Math.max(0, widget.h - 1) * margins.vertical; | ||||||
|  |  | ||||||
|  |     return { left, top, width, height }; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								ts_web/elements/dees-dashboardgrid/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ts_web/elements/dees-dashboardgrid/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | export * from './dees-dashboardgrid.js'; | ||||||
|  | export * from './types.js'; | ||||||
							
								
								
									
										105
									
								
								ts_web/elements/dees-dashboardgrid/interaction.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								ts_web/elements/dees-dashboardgrid/interaction.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | |||||||
|  | import type { DashboardWidget, GridCellMetrics } from './types.js'; | ||||||
|  |  | ||||||
|  | export interface PointerPosition { | ||||||
|  |   clientX: number; | ||||||
|  |   clientY: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface DragComputationArgs { | ||||||
|  |   pointer: PointerPosition; | ||||||
|  |   containerRect: DOMRect; | ||||||
|  |   metrics: GridCellMetrics; | ||||||
|  |   columns: number; | ||||||
|  |   widget: DashboardWidget; | ||||||
|  |   rtl: boolean; | ||||||
|  |   dragOffsetX?: number; | ||||||
|  |   dragOffsetY?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const computeGridCoordinates = ({ | ||||||
|  |   pointer, | ||||||
|  |   containerRect, | ||||||
|  |   metrics, | ||||||
|  |   columns, | ||||||
|  |   widget, | ||||||
|  |   rtl, | ||||||
|  |   dragOffsetX = 0, | ||||||
|  |   dragOffsetY = 0, | ||||||
|  | }: DragComputationArgs): { x: number; y: number } => { | ||||||
|  |   const relativeX = pointer.clientX - containerRect.left - dragOffsetX; | ||||||
|  |   const relativeY = pointer.clientY - containerRect.top - dragOffsetY; | ||||||
|  |  | ||||||
|  |   const marginX = metrics.marginHorizontalPx; | ||||||
|  |   const marginY = metrics.marginVerticalPx; | ||||||
|  |   const cellWidth = metrics.cellWidthPx; | ||||||
|  |   const cellHeight = metrics.cellHeightPx; | ||||||
|  |  | ||||||
|  |   const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)); | ||||||
|  |  | ||||||
|  |   const adjustedX = clamp(relativeX - marginX, 0, containerRect.width - marginX); | ||||||
|  |   const adjustedY = clamp(relativeY - marginY, 0, Number.POSITIVE_INFINITY); | ||||||
|  |  | ||||||
|  |   const cellPlusMarginX = cellWidth + marginX; | ||||||
|  |   const cellPlusMarginY = cellHeight + marginY; | ||||||
|  |  | ||||||
|  |   let gridX = Math.round(adjustedX / cellPlusMarginX); | ||||||
|  |   if (rtl) { | ||||||
|  |     gridX = columns - widget.w - gridX; | ||||||
|  |   } | ||||||
|  |   gridX = clamp(gridX, 0, columns - widget.w); | ||||||
|  |  | ||||||
|  |   const gridY = clamp(Math.round(adjustedY / cellPlusMarginY), 0, Number.MAX_SAFE_INTEGER); | ||||||
|  |  | ||||||
|  |   return { x: gridX, y: gridY }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export interface ResizeComputationArgs { | ||||||
|  |   pointer: PointerPosition; | ||||||
|  |   containerRect: DOMRect; | ||||||
|  |   metrics: GridCellMetrics; | ||||||
|  |   startWidth: number; | ||||||
|  |   startHeight: number; | ||||||
|  |   startPointer: PointerPosition; | ||||||
|  |   handler: 'e' | 's' | 'se'; | ||||||
|  |   widget: DashboardWidget; | ||||||
|  |   columns: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const computeResizeDimensions = ({ | ||||||
|  |   pointer, | ||||||
|  |   containerRect, | ||||||
|  |   metrics, | ||||||
|  |   startWidth, | ||||||
|  |   startHeight, | ||||||
|  |   startPointer, | ||||||
|  |   handler, | ||||||
|  |   widget, | ||||||
|  |   columns, | ||||||
|  | }: ResizeComputationArgs): { width: number; height: number } => { | ||||||
|  |   const deltaX = pointer.clientX - startPointer.clientX; | ||||||
|  |   const deltaY = pointer.clientY - startPointer.clientY; | ||||||
|  |  | ||||||
|  |   let width = startWidth; | ||||||
|  |   let height = startHeight; | ||||||
|  |  | ||||||
|  |   const cellPlusMarginX = metrics.cellWidthPx + metrics.marginHorizontalPx; | ||||||
|  |   const cellPlusMarginY = metrics.cellHeightPx + metrics.marginVerticalPx; | ||||||
|  |  | ||||||
|  |   if (handler.includes('e')) { | ||||||
|  |     const deltaCols = Math.round(deltaX / cellPlusMarginX); | ||||||
|  |     width = startWidth + deltaCols; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (handler.includes('s')) { | ||||||
|  |     const deltaRows = Math.round(deltaY / cellPlusMarginY); | ||||||
|  |     height = startHeight + deltaRows; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const clampedWidth = Math.max(widget.minW || 1, Math.min(width, widget.maxW || columns - widget.x)); | ||||||
|  |   const clampedHeight = Math.max(widget.minH || 1, Math.min(height, widget.maxH || Number.MAX_SAFE_INTEGER)); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     width: clampedWidth, | ||||||
|  |     height: clampedHeight, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										246
									
								
								ts_web/elements/dees-dashboardgrid/layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								ts_web/elements/dees-dashboardgrid/layout.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,246 @@ | |||||||
|  | import type { | ||||||
|  |   DashboardResolvedMargins, | ||||||
|  |   DashboardMargin, | ||||||
|  |   DashboardWidget, | ||||||
|  |   DashboardLayoutItem, | ||||||
|  |   GridCellMetrics, | ||||||
|  |   LayoutDirection, | ||||||
|  | } from './types.js'; | ||||||
|  |  | ||||||
|  | export const DEFAULT_MARGIN = 10; | ||||||
|  |  | ||||||
|  | export const resolveMargins = (margin: DashboardMargin): DashboardResolvedMargins => { | ||||||
|  |   if (typeof margin === 'number') { | ||||||
|  |     return { | ||||||
|  |       horizontal: margin, | ||||||
|  |       vertical: margin, | ||||||
|  |       top: margin, | ||||||
|  |       right: margin, | ||||||
|  |       bottom: margin, | ||||||
|  |       left: margin, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const resolved = { | ||||||
|  |     top: margin.top ?? DEFAULT_MARGIN, | ||||||
|  |     right: margin.right ?? DEFAULT_MARGIN, | ||||||
|  |     bottom: margin.bottom ?? DEFAULT_MARGIN, | ||||||
|  |     left: margin.left ?? DEFAULT_MARGIN, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     ...resolved, | ||||||
|  |     horizontal: (resolved.left + resolved.right) / 2, | ||||||
|  |     vertical: (resolved.top + resolved.bottom) / 2, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const calculateCellMetrics = ( | ||||||
|  |   containerWidth: number, | ||||||
|  |   columns: number, | ||||||
|  |   margins: DashboardResolvedMargins, | ||||||
|  |   cellHeight: number, | ||||||
|  |   cellHeightUnit: string, | ||||||
|  | ): GridCellMetrics => { | ||||||
|  |   const totalMarginWidth = margins.horizontal * (columns + 1); | ||||||
|  |   const availableWidth = Math.max(containerWidth - totalMarginWidth, 0); | ||||||
|  |   const cellWidthPx = columns > 0 ? availableWidth / columns : 0; | ||||||
|  |   const cellHeightPx = cellHeightUnit === 'auto' ? cellWidthPx : cellHeight; | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     containerWidth, | ||||||
|  |     cellWidthPx, | ||||||
|  |     marginHorizontalPx: margins.horizontal, | ||||||
|  |     cellHeightPx, | ||||||
|  |     marginVerticalPx: margins.vertical, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const calculateGridHeight = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   margins: DashboardResolvedMargins, | ||||||
|  |   cellHeight: number, | ||||||
|  | ): number => { | ||||||
|  |   if (widgets.length === 0) return 0; | ||||||
|  |   const maxY = Math.max(...widgets.map(widget => widget.y + widget.h), 0); | ||||||
|  |   return maxY * cellHeight + (maxY + 1) * margins.vertical; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const overlaps = ( | ||||||
|  |   widget: DashboardWidget, | ||||||
|  |   x: number, | ||||||
|  |   y: number, | ||||||
|  |   w: number, | ||||||
|  |   h: number, | ||||||
|  | ) => x < widget.x + widget.w && x + w > widget.x && y < widget.y + widget.h && y + h > widget.y; | ||||||
|  |  | ||||||
|  | export const collectCollisions = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   target: DashboardWidget, | ||||||
|  |   nextX: number, | ||||||
|  |   nextY: number, | ||||||
|  |   nextW: number = target.w, | ||||||
|  |   nextH: number = target.h, | ||||||
|  | ): DashboardWidget[] => { | ||||||
|  |   return widgets.filter(widget => { | ||||||
|  |     if (widget.id === target.id) return false; | ||||||
|  |     return overlaps(widget, nextX, nextY, nextW, nextH); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const checkCollision = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   target: DashboardWidget, | ||||||
|  |   nextX: number, | ||||||
|  |   nextY: number, | ||||||
|  | ): boolean => collectCollisions(widgets, target, nextX, nextY).length > 0; | ||||||
|  |  | ||||||
|  | export const cloneWidget = (widget: DashboardWidget): DashboardWidget => ({ ...widget }); | ||||||
|  |  | ||||||
|  | export const cloneWidgets = (widgets: DashboardWidget[]): DashboardWidget[] => widgets.map(cloneWidget); | ||||||
|  |  | ||||||
|  | export const findAvailablePosition = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   width: number, | ||||||
|  |   height: number, | ||||||
|  |   columns: number, | ||||||
|  | ): { x: number; y: number } => { | ||||||
|  |   for (let y = 0; y < 200; y++) { | ||||||
|  |     for (let x = 0; x <= columns - width; x++) { | ||||||
|  |       const isFree = !widgets.some(widget => overlaps(widget, x, y, width, height)); | ||||||
|  |       if (isFree) { | ||||||
|  |         return { x, y }; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const maxY = widgets.reduce((acc, widget) => Math.max(acc, widget.y + widget.h), 0); | ||||||
|  |   return { x: 0, y: maxY }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export interface PlacementResult { | ||||||
|  |   widgets: DashboardWidget[]; | ||||||
|  |   movedWidgets: string[]; | ||||||
|  |   swappedWith?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const resolveWidgetPlacement = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   widgetId: string, | ||||||
|  |   next: { x: number; y: number; w?: number; h?: number }, | ||||||
|  |   columns: number, | ||||||
|  |   previousPosition?: DashboardLayoutItem, | ||||||
|  | ): PlacementResult | null => { | ||||||
|  |   const sourceWidgets = cloneWidgets(widgets); | ||||||
|  |   const moving = sourceWidgets.find(widget => widget.id === widgetId); | ||||||
|  |   const original = widgets.find(widget => widget.id === widgetId); | ||||||
|  |   if (!moving || !original) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const target = { | ||||||
|  |     x: next.x, | ||||||
|  |     y: next.y, | ||||||
|  |     w: next.w ?? moving.w, | ||||||
|  |     h: next.h ?? moving.h, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   moving.x = target.x; | ||||||
|  |   moving.y = target.y; | ||||||
|  |   moving.w = target.w; | ||||||
|  |   moving.h = target.h; | ||||||
|  |  | ||||||
|  |   const collisions = collectCollisions(sourceWidgets, moving, target.x, target.y, target.w, target.h); | ||||||
|  |  | ||||||
|  |   if (collisions.length === 0) { | ||||||
|  |     return { widgets: sourceWidgets, movedWidgets: [moving.id] }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (collisions.length === 1) { | ||||||
|  |     const other = collisions[0]; | ||||||
|  |     if (!other.locked && !other.noMove && other.w === moving.w && other.h === moving.h) { | ||||||
|  |       const otherClone = sourceWidgets.find(widget => widget.id === other.id); | ||||||
|  |       if (otherClone) { | ||||||
|  |         // Use the original position of the moving widget for a clean swap | ||||||
|  |         // This prevents the "snapping together" issue where both widgets end up at the same position | ||||||
|  |         const swapTarget = original; | ||||||
|  |         const previousOtherPosition = { x: otherClone.x, y: otherClone.y }; | ||||||
|  |         otherClone.x = swapTarget.x; | ||||||
|  |         otherClone.y = swapTarget.y; | ||||||
|  |  | ||||||
|  |         const swapValid = | ||||||
|  |           collectCollisions(sourceWidgets, moving, moving.x, moving.y, moving.w, moving.h).length === 0 && | ||||||
|  |           collectCollisions(sourceWidgets, otherClone, otherClone.x, otherClone.y, otherClone.w, otherClone.h).length === 0; | ||||||
|  |  | ||||||
|  |         if (swapValid) { | ||||||
|  |           return { widgets: sourceWidgets, movedWidgets: [moving.id, otherClone.id], swappedWith: otherClone.id }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         otherClone.x = previousOtherPosition.x; | ||||||
|  |         otherClone.y = previousOtherPosition.y; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // attempt displacement cascade | ||||||
|  |   const movedIds = new Set<string>([moving.id]); | ||||||
|  |   for (const offending of collisions) { | ||||||
|  |     if (offending.locked || offending.noMove) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     const clone = sourceWidgets.find(widget => widget.id === offending.id); | ||||||
|  |     if (!clone) continue; | ||||||
|  |     const remaining = sourceWidgets.filter(widget => widget.id !== offending.id); | ||||||
|  |     const position = findAvailablePosition(remaining, clone.w, clone.h, columns); | ||||||
|  |     clone.x = position.x; | ||||||
|  |     clone.y = position.y; | ||||||
|  |     movedIds.add(clone.id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // verify no overlaps remain | ||||||
|  |   const verify = collectCollisions(sourceWidgets, moving, moving.x, moving.y, moving.w, moving.h); | ||||||
|  |   if (verify.length > 0) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { widgets: sourceWidgets, movedWidgets: Array.from(movedIds) }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const compactLayout = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   direction: LayoutDirection = 'vertical', | ||||||
|  | ) => { | ||||||
|  |   const sorted = [...widgets].sort((a, b) => { | ||||||
|  |     if (direction === 'vertical') { | ||||||
|  |       if (a.y !== b.y) return a.y - b.y; | ||||||
|  |       return a.x - b.x; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (a.x !== b.x) return a.x - b.x; | ||||||
|  |     return a.y - b.y; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   for (const widget of sorted) { | ||||||
|  |     if (widget.locked || widget.noMove) continue; | ||||||
|  |  | ||||||
|  |     if (direction === 'vertical') { | ||||||
|  |       while (widget.y > 0 && !checkCollision(widgets, widget, widget.x, widget.y - 1)) { | ||||||
|  |         widget.y -= 1; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       while (widget.x > 0 && !checkCollision(widgets, widget, widget.x - 1, widget.y)) { | ||||||
|  |         widget.x -= 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const applyLayout = ( | ||||||
|  |   widgets: DashboardWidget[], | ||||||
|  |   layout: DashboardLayoutItem[], | ||||||
|  | ): DashboardWidget[] => { | ||||||
|  |   return widgets.map(widget => { | ||||||
|  |     const layoutItem = layout.find(item => item.id === widget.id); | ||||||
|  |     return layoutItem ? { ...widget, ...layoutItem } : widget; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										249
									
								
								ts_web/elements/dees-dashboardgrid/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								ts_web/elements/dees-dashboardgrid/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,249 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const dashboardGridStyles = [ | ||||||
|  |   cssManager.defaultStyles, | ||||||
|  |   css` | ||||||
|  |       :host { | ||||||
|  |         display: block; | ||||||
|  |         width: 100%; | ||||||
|  |         height: 100%; | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-container { | ||||||
|  |         position: relative; | ||||||
|  |         width: 100%; | ||||||
|  |         min-height: 400px; | ||||||
|  |         box-sizing: border-box; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-widget { | ||||||
|  |         position: absolute; | ||||||
|  |         will-change: auto; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       :host([enableanimation]) .grid-widget { | ||||||
|  |         transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-widget.dragging { | ||||||
|  |         z-index: 1000; | ||||||
|  |         transition: none !important; | ||||||
|  |         opacity: 0.8; | ||||||
|  |         cursor: grabbing; | ||||||
|  |         pointer-events: none; | ||||||
|  |         will-change: transform; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .grid-widget.placeholder { | ||||||
|  |         pointer-events: none; | ||||||
|  |         z-index: 1; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .grid-widget.placeholder .widget-content { | ||||||
|  |         background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')}; | ||||||
|  |         border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         box-shadow: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-widget.resizing { | ||||||
|  |         transition: none !important; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .widget-content { | ||||||
|  |         position: absolute; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|  |         top: 0; | ||||||
|  |         bottom: 0; | ||||||
|  |         overflow: hidden; | ||||||
|  |         background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         box-shadow: ${cssManager.bdTheme( | ||||||
|  |           '0 1px 3px rgba(0, 0, 0, 0.1)', | ||||||
|  |           '0 1px 3px rgba(0, 0, 0, 0.3)' | ||||||
|  |         )}; | ||||||
|  |         transition: box-shadow 0.2s ease; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-widget:hover .widget-content { | ||||||
|  |         box-shadow: ${cssManager.bdTheme( | ||||||
|  |           '0 4px 12px rgba(0, 0, 0, 0.15)', | ||||||
|  |           '0 4px 12px rgba(0, 0, 0, 0.4)' | ||||||
|  |         )}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-widget.dragging .widget-content { | ||||||
|  |         box-shadow: ${cssManager.bdTheme( | ||||||
|  |           '0 16px 48px rgba(0, 0, 0, 0.25)', | ||||||
|  |           '0 16px 48px rgba(0, 0, 0, 0.6)' | ||||||
|  |         )}; | ||||||
|  |         transform: scale(1.05); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .widget-header { | ||||||
|  |         padding: 12px 16px; | ||||||
|  |         border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-weight: 600; | ||||||
|  |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |         background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')}; | ||||||
|  |         cursor: grab; | ||||||
|  |         user-select: none; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .widget-header:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .widget-header:active { | ||||||
|  |         cursor: grabbing; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .widget-header.locked { | ||||||
|  |         cursor: default; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .widget-header.locked:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .widget-header dees-icon { | ||||||
|  |         font-size: 16px; | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .widget-body { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|  |         bottom: 0; | ||||||
|  |         overflow: auto; | ||||||
|  |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .widget-body.has-header { | ||||||
|  |         top: 45px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .resize-handle { | ||||||
|  |         position: absolute; | ||||||
|  |         background: transparent; | ||||||
|  |         z-index: 10; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .resize-handle:hover { | ||||||
|  |         background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         opacity: 0.3; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .resize-handle-e { | ||||||
|  |         cursor: ew-resize; | ||||||
|  |         width: 12px; | ||||||
|  |         right: -6px; | ||||||
|  |         top: 10%; | ||||||
|  |         height: 80%; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .resize-handle-s { | ||||||
|  |         cursor: ns-resize; | ||||||
|  |         height: 12px; | ||||||
|  |         width: 80%; | ||||||
|  |         bottom: -6px; | ||||||
|  |         left: 10%; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .resize-handle-se { | ||||||
|  |         cursor: se-resize; | ||||||
|  |         width: 20px; | ||||||
|  |         height: 20px; | ||||||
|  |         right: -2px; | ||||||
|  |         bottom: -2px; | ||||||
|  |         opacity: 0; | ||||||
|  |         transition: opacity 0.2s ease; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .resize-handle-se::after { | ||||||
|  |         content: ''; | ||||||
|  |         position: absolute; | ||||||
|  |         right: 4px; | ||||||
|  |         bottom: 4px; | ||||||
|  |         width: 6px; | ||||||
|  |         height: 6px; | ||||||
|  |         border-right: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         border-bottom: 2px solid ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-widget:hover .resize-handle-se { | ||||||
|  |         opacity: 0.7; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .resize-handle-se:hover { | ||||||
|  |         opacity: 1 !important; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .resize-handle-se:hover::after { | ||||||
|  |         border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .grid-placeholder { | ||||||
|  |         position: absolute; | ||||||
|  |         background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         opacity: 0.1; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|  |         transition: all 0.2s ease; | ||||||
|  |         pointer-events: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .empty-state { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         height: 400px; | ||||||
|  |         color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         text-align: center; | ||||||
|  |         padding: 32px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .empty-state dees-icon { | ||||||
|  |         font-size: 48px; | ||||||
|  |         margin-bottom: 16px; | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .grid-lines { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|  |         bottom: 0; | ||||||
|  |         pointer-events: none; | ||||||
|  |         z-index: -1; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .grid-line-vertical { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0; | ||||||
|  |         bottom: 0; | ||||||
|  |         width: 1px; | ||||||
|  |         background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         opacity: 0.3; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .grid-line-horizontal { | ||||||
|  |         position: absolute; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|  |         height: 1px; | ||||||
|  |         background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |         opacity: 0.3; | ||||||
|  |       } | ||||||
|  |   `, | ||||||
|  | ]; | ||||||
							
								
								
									
										53
									
								
								ts_web/elements/dees-dashboardgrid/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ts_web/elements/dees-dashboardgrid/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import type { TemplateResult } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export type CellHeightUnit = 'px' | 'em' | 'rem' | 'auto'; | ||||||
|  |  | ||||||
|  | export interface DashboardMarginObject { | ||||||
|  |   top?: number; | ||||||
|  |   right?: number; | ||||||
|  |   bottom?: number; | ||||||
|  |   left?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export type DashboardMargin = number | DashboardMarginObject; | ||||||
|  |  | ||||||
|  | export interface DashboardResolvedMargins { | ||||||
|  |   horizontal: number; | ||||||
|  |   vertical: number; | ||||||
|  |   top: number; | ||||||
|  |   right: number; | ||||||
|  |   bottom: number; | ||||||
|  |   left: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface DashboardLayoutItem { | ||||||
|  |   id: string; | ||||||
|  |   x: number; | ||||||
|  |   y: number; | ||||||
|  |   w: number; | ||||||
|  |   h: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface DashboardWidget extends DashboardLayoutItem { | ||||||
|  |   minW?: number; | ||||||
|  |   minH?: number; | ||||||
|  |   maxW?: number; | ||||||
|  |   maxH?: number; | ||||||
|  |   content: TemplateResult | string; | ||||||
|  |   title?: string; | ||||||
|  |   icon?: string; | ||||||
|  |   noMove?: boolean; | ||||||
|  |   noResize?: boolean; | ||||||
|  |   locked?: boolean; | ||||||
|  |   autoPosition?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export type LayoutDirection = 'vertical' | 'horizontal'; | ||||||
|  |  | ||||||
|  | export interface GridCellMetrics { | ||||||
|  |   containerWidth: number; | ||||||
|  |   cellWidthPx: number; | ||||||
|  |   marginHorizontalPx: number; | ||||||
|  |   cellHeightPx: number; | ||||||
|  |   marginVerticalPx: number; | ||||||
|  | } | ||||||
| @@ -1,18 +1,199 @@ | |||||||
| import { html } from '@design.estate/dees-element'; | import { html, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
| export const demoFunc = () => html` <style> | export const demoFunc = () => html` | ||||||
| .demoWrapper { |   <style> | ||||||
|   box-sizing: border-box; |     .demoWrapper { | ||||||
|   position: absolute; |       box-sizing: border-box; | ||||||
|   width: 100%; |       position: relative; | ||||||
|   height: 100%; |       width: 100%; | ||||||
|   padding: 20px; |       min-height: 100vh; | ||||||
|   background: none; |       padding: 48px; | ||||||
|  |       background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       gap: 32px; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .section { | ||||||
|  |       max-width: 900px; | ||||||
|  |       width: 100%; | ||||||
|  |       margin: 0 auto; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .section-title { | ||||||
|  |       font-size: 18px; | ||||||
|  |       font-weight: 600; | ||||||
|  |       margin-bottom: 16px; | ||||||
|  |       color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .section-description { | ||||||
|  |       font-size: 14px; | ||||||
|  |       color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; | ||||||
|  |       margin-bottom: 16px; | ||||||
|  |     } | ||||||
|  |   </style> | ||||||
|  |   <div class="demoWrapper"> | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">TypeScript Code Example</div> | ||||||
|  |       <div class="section-description">A comprehensive TypeScript code example with various syntax highlighting.</div> | ||||||
|  |       <dees-dataview-codebox proglang="typescript"> | ||||||
|  | interface User { | ||||||
|  |   id: number; | ||||||
|  |   name: string; | ||||||
|  |   email: string; | ||||||
|  |   isActive: boolean; | ||||||
| } | } | ||||||
| </style> |  | ||||||
| <div class="demoWrapper"> | class UserService { | ||||||
| <dees-dataview-codebox proglang="typescript"> |   private users: User[] = []; | ||||||
|   import * as text from './hello'; const hiThere = 'nice'; const myFunction = async () => { |    | ||||||
|   console.log('nice one'); } |   constructor(private apiUrl: string) { | ||||||
| </dees-dataview-codebox> |     console.log('UserService initialized'); | ||||||
| </div>` |   } | ||||||
|  |    | ||||||
|  |   async getUsers(): Promise<User[]> { | ||||||
|  |     try { | ||||||
|  |       const response = await fetch(this.apiUrl); | ||||||
|  |       const data = await response.json(); | ||||||
|  |       return data.users; | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to fetch users:', error); | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   addUser(user: User): void { | ||||||
|  |     this.users.push(user); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Usage example | ||||||
|  | const service = new UserService('https://api.example.com/users'); | ||||||
|  | const users = await service.getUsers(); | ||||||
|  | console.log('Found users:', users.length); | ||||||
|  |       </dees-dataview-codebox> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">JavaScript Example</div> | ||||||
|  |       <div class="section-description">Modern JavaScript with ES6+ features.</div> | ||||||
|  |       <dees-dataview-codebox proglang="javascript"> | ||||||
|  | // Array manipulation examples | ||||||
|  | const numbers = [1, 2, 3, 4, 5]; | ||||||
|  | const doubled = numbers.map(n => n * 2); | ||||||
|  | const filtered = numbers.filter(n => n > 3); | ||||||
|  |  | ||||||
|  | // Object destructuring | ||||||
|  | const user = { name: 'John', age: 30, city: 'New York' }; | ||||||
|  | const { name, age } = user; | ||||||
|  |  | ||||||
|  | // Promise handling | ||||||
|  | const fetchData = async (url) => { | ||||||
|  |   const response = await fetch(url); | ||||||
|  |   return response.json(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Modern syntax | ||||||
|  | const greet = (name = 'World') => \`Hello, \${name}!\`; | ||||||
|  | console.log(greet('ShadCN')); | ||||||
|  |       </dees-dataview-codebox> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">Python Example</div> | ||||||
|  |       <div class="section-description">Python code with classes and type hints.</div> | ||||||
|  |       <dees-dataview-codebox proglang="python"> | ||||||
|  | from typing import List, Optional | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
|  | class DataProcessor: | ||||||
|  |     """A simple data processor class""" | ||||||
|  |      | ||||||
|  |     def __init__(self, name: str): | ||||||
|  |         self.name = name | ||||||
|  |         self.data: List[dict] = [] | ||||||
|  |      | ||||||
|  |     async def process_data(self, items: List[dict]) -> List[dict]: | ||||||
|  |         """Process data items asynchronously""" | ||||||
|  |         results = [] | ||||||
|  |         for item in items: | ||||||
|  |             # Simulate async processing | ||||||
|  |             await asyncio.sleep(0.1) | ||||||
|  |             results.append({ | ||||||
|  |                 'id': item.get('id'), | ||||||
|  |                 'processed': True, | ||||||
|  |                 'processor': self.name | ||||||
|  |             }) | ||||||
|  |         return results | ||||||
|  |      | ||||||
|  |     def get_summary(self) -> dict: | ||||||
|  |         return { | ||||||
|  |             'processor': self.name, | ||||||
|  |             'items_processed': len(self.data) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  | # Usage | ||||||
|  | processor = DataProcessor("Main") | ||||||
|  | data = await processor.process_data([{'id': 1}, {'id': 2}]) | ||||||
|  |       </dees-dataview-codebox> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">CSS Example</div> | ||||||
|  |       <div class="section-description">Modern CSS with custom properties and animations. Note the shorter language label.</div> | ||||||
|  |       <dees-dataview-codebox proglang="css"> | ||||||
|  | /* Modern CSS with custom properties */ | ||||||
|  | :root { | ||||||
|  |   --primary-color: #3b82f6; | ||||||
|  |   --secondary-color: #10b981; | ||||||
|  |   --background: #ffffff; | ||||||
|  |   --text-color: #09090b; | ||||||
|  |   --border-radius: 6px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .card { | ||||||
|  |   background: var(--background); | ||||||
|  |   border: 1px solid #e5e7eb; | ||||||
|  |   border-radius: var(--border-radius); | ||||||
|  |   padding: 24px; | ||||||
|  |   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); | ||||||
|  |   transition: all 0.2s ease; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .card:hover { | ||||||
|  |   transform: translateY(-2px); | ||||||
|  |   box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes fadeIn { | ||||||
|  |   from { opacity: 0; transform: translateY(10px); } | ||||||
|  |   to { opacity: 1; transform: translateY(0); } | ||||||
|  | } | ||||||
|  |       </dees-dataview-codebox> | ||||||
|  |     </div> | ||||||
|  |      | ||||||
|  |     <div class="section"> | ||||||
|  |       <div class="section-title">JSON Example</div> | ||||||
|  |       <div class="section-description">JSON configuration with proper formatting.</div> | ||||||
|  |       <dees-dataview-codebox proglang="json"> | ||||||
|  | { | ||||||
|  |   "name": "@design.estate/dees-catalog", | ||||||
|  |   "version": "1.10.7", | ||||||
|  |   "description": "A comprehensive catalog of web components", | ||||||
|  |   "main": "dist_ts_web/index.js", | ||||||
|  |   "type": "module", | ||||||
|  |   "scripts": { | ||||||
|  |     "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production", | ||||||
|  |     "watch": "tswatch element", | ||||||
|  |     "test": "tstest test/ --web --verbose" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "@design.estate/dees-element": "^2.0.45", | ||||||
|  |     "highlight.js": "^11.9.0" | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |       </dees-dataview-codebox> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | ` | ||||||
| @@ -8,6 +8,7 @@ import { | |||||||
|   state, |   state, | ||||||
|   cssManager, |   cssManager, | ||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
|  | import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js'; | ||||||
|  |  | ||||||
| import hlight from 'highlight.js'; | import hlight from 'highlight.js'; | ||||||
|  |  | ||||||
| @@ -48,27 +49,27 @@ export class DeesDataviewCodebox extends DeesElement { | |||||||
|           display: block; |           display: block; | ||||||
|           text-align: left; |           text-align: left; | ||||||
|           font-size: 16px; |           font-size: 16px; | ||||||
|           font-family: 'Geist Sans', sans-serif; |           font-family: ${cssGeistFontFamily}; | ||||||
|         } |         } | ||||||
|         .mainbox { |         .mainbox { | ||||||
|           position: relative; |           position: relative; | ||||||
|           color: ${this.goBright ? '#333333' : '#ffffff'}; |           color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|           border-top: 1px solid ${this.goBright ? '#ffffff' : '#333333'}; |           border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|           box-shadow: 0px 0px 5px ${this.goBright ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.5)'}; |           box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); | ||||||
|           background: ${this.goBright ? '#ffffff' : '#191919'}; |           background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | ||||||
|           border-radius: 16px; |           border-radius: 6px; | ||||||
|           overflow: hidden; |           overflow: hidden; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .appbar { |         .appbar { | ||||||
|           position: relative; |           position: relative; | ||||||
|           color: ${cssManager.bdTheme('#333', '#ccc')}; |           color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; | ||||||
|           background: ${cssManager.bdTheme('#ffffff', '#161616')}; |           background: ${cssManager.bdTheme('#f9fafb', '#18181b')}; | ||||||
|           border-bottom: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222222')}; |           border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|           height: 24px; |           height: 32px; | ||||||
|           display: flex; |           display: flex; | ||||||
|           font-size: 12px; |           font-size: 13px; | ||||||
|           line-height: 24px; |           line-height: 32px; | ||||||
|           justify-content: center; |           justify-content: center; | ||||||
|           align-items: center; |           align-items: center; | ||||||
|         } |         } | ||||||
| @@ -81,31 +82,38 @@ export class DeesDataviewCodebox extends DeesElement { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         .bottomBar { |         .bottomBar { | ||||||
|           color: ${cssManager.bdTheme('#333', '#ccc')}; |           position: relative; | ||||||
|           background: ${cssManager.bdTheme('#ffffff', '#161616')}; |           color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; | ||||||
|           border-top: 1px solid ${cssManager.bdTheme('#eeeeeb', '#222222')}; |           background: ${cssManager.bdTheme('#f9fafb', '#18181b')}; | ||||||
|           height: 24px; |           border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|  |           height: 28px; | ||||||
|           font-size: 12px; |           font-size: 12px; | ||||||
|           line-height: 24px; |           line-height: 28px; | ||||||
|           text-align: right; |           display: flex; | ||||||
|           padding-right: 100px; |           justify-content: flex-end; | ||||||
|  |           align-items: stretch; | ||||||
|  |           overflow: hidden; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .spacesLabel { | ||||||
|  |           padding: 0 16px; | ||||||
|  |           display: flex; | ||||||
|  |           align-items: center; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .languageLabel { |         .languageLabel { | ||||||
|           color: ${cssManager.bdTheme('#333', '#ccc')}; |           color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')}; | ||||||
|           font-size: 12px; |           font-size: 12px; | ||||||
|           line-height: 24px; |           line-height: 28px; | ||||||
|           z-index: 10; |           background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')}; | ||||||
|           background: #6596ff20; |           padding: 0px 16px; | ||||||
|           display: inline-block; |           font-weight: 500; | ||||||
|           position: absolute; |           display: flex; | ||||||
|           bottom: 0px; |           align-items: center; | ||||||
|           right: 0px; |  | ||||||
|           padding: 0px 16px 0px 8px; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hljs-keyword { |         .hljs-keyword { | ||||||
|           color: #ff65ec; |           color: ${cssManager.bdTheme('#dc2626', '#f87171')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .codegrid { |         .codegrid { | ||||||
| @@ -115,10 +123,10 @@ export class DeesDataviewCodebox extends DeesElement { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         .lineNumbers { |         .lineNumbers { | ||||||
|           color: ${this.goBright ? '#acacac' : '#666666'}; |           color: ${cssManager.bdTheme('#71717a', '#52525b')}; | ||||||
|           padding: 30px 16px 0px 0px; |           padding: 24px 16px 0px 0px; | ||||||
|           text-align: right; |           text-align: right; | ||||||
|           border-right: 1px solid ${this.goBright ? '#eaeaea' : '#222222'}; |           border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .lineCounter:last-child { |         .lineCounter:last-child { | ||||||
| @@ -128,11 +136,11 @@ export class DeesDataviewCodebox extends DeesElement { | |||||||
|         pre { |         pre { | ||||||
|           overflow-x: auto; |           overflow-x: auto; | ||||||
|           margin: 0px; |           margin: 0px; | ||||||
|           padding: 30px 40px; |           padding: 24px 24px; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         code { |         code { | ||||||
|           font-weight: ${this.goBright ? '400' : '300'}; |           font-weight: 400; | ||||||
|           padding: 0px; |           padding: 0px; | ||||||
|           margin: 0px; |           margin: 0px; | ||||||
|         } |         } | ||||||
| @@ -142,27 +150,43 @@ export class DeesDataviewCodebox extends DeesElement { | |||||||
|         .lineNumbers { |         .lineNumbers { | ||||||
|           line-height: 1.4em; |           line-height: 1.4em; | ||||||
|           font-weight: 200; |           font-weight: 200; | ||||||
|           font-family: 'Intel One Mono', 'Geist Mono', 'monospace'; |           font-family: ${cssMonoFontFamily}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hljs-string { |         .hljs-string { | ||||||
|           color: #ffa465; |           color: ${cssManager.bdTheme('#059669', '#10b981')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hljs-built_in { |         .hljs-built_in { | ||||||
|           color: #65ff6a; |           color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hljs-function { |         .hljs-function { | ||||||
|           color: ${this.goBright ? '#2765DF' : '#6596ff'}; |           color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hljs-params { |         .hljs-params { | ||||||
|           color: ${this.goBright ? '#3DB420' : '#65d5ff'}; |           color: ${cssManager.bdTheme('#0891b2', '#06b6d4')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .hljs-comment { |         .hljs-comment { | ||||||
|           color: ${this.goBright ? '#EF9300' : '#ffd765'}; |           color: ${cssManager.bdTheme('#71717a', '#71717a')}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .hljs-number { | ||||||
|  |           color: ${cssManager.bdTheme('#ea580c', '#fb923c')}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .hljs-literal { | ||||||
|  |           color: ${cssManager.bdTheme('#dc2626', '#f87171')}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .hljs-attr { | ||||||
|  |           color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .hljs-variable { | ||||||
|  |           color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | ||||||
|         } |         } | ||||||
|       </style> |       </style> | ||||||
|       <div |       <div | ||||||
| @@ -197,7 +221,7 @@ export class DeesDataviewCodebox extends DeesElement { | |||||||
|           <pre><code></code></pre> |           <pre><code></code></pre> | ||||||
|         </div> |         </div> | ||||||
|         <div class="bottomBar"> |         <div class="bottomBar"> | ||||||
|           Spaces: 2 |           <div class="spacesLabel">Spaces: 2</div> | ||||||
|           <div class="languageLabel">${this.progLang}</div> |           <div class="languageLabel">${this.progLang}</div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|   | |||||||
| @@ -3,47 +3,162 @@ import * as tsclass from '@tsclass/tsclass'; | |||||||
|  |  | ||||||
| export const demoFunc = () => html` <style> | export const demoFunc = () => html` <style> | ||||||
|     .demo { |     .demo { | ||||||
|       background: ${cssManager.bdTheme('#eeeeeb', '#000000')}; |       background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')}; | ||||||
|       display: block; |       display: block; | ||||||
|       content: ''; |       content: ''; | ||||||
|       padding: 40px; |       padding: 40px; | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     .demo-grid { | ||||||
|  |       display: grid; | ||||||
|  |       gap: 24px; | ||||||
|  |       max-width: 800px; | ||||||
|  |       margin: 0 auto; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .demo-section { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       gap: 16px; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .demo-title { | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-weight: 600; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|  |       margin-bottom: 8px; | ||||||
|  |       font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .demo-note { | ||||||
|  |       font-size: 12px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|  |       margin-bottom: 24px; | ||||||
|  |       text-align: center; | ||||||
|  |       font-style: italic; | ||||||
|  |       font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | ||||||
|  |     } | ||||||
|   </style> |   </style> | ||||||
|   <div class="demo"> |   <div class="demo"> | ||||||
|     <dees-dataview-statusobject |     <div class="demo-note"> | ||||||
|       .statusObject=${{ |       Right-click on any detail row to copy the value, key, or key:value combination | ||||||
|         id: '1', |     </div> | ||||||
|         name: 'Demo Item', |     <div class="demo-grid"> | ||||||
|         combinedStatus: 'partly_ok', |       <div class="demo-section"> | ||||||
|         combinedStatusText: 'partly_ok', |         <div class="demo-title">Service Health Status</div> | ||||||
|         details: [ |         <dees-dataview-statusobject | ||||||
|           { |           .statusObject=${{ | ||||||
|             name: 'Detail 1', |             id: '1', | ||||||
|             value: 'Value 1', |             name: 'API Gateway Service', | ||||||
|             status: 'ok', |             combinedStatus: 'ok', | ||||||
|             statusText: 'OK', |             combinedStatusText: 'All systems operational', | ||||||
|           }, |             details: [ | ||||||
|           { |               { | ||||||
|             name: 'Detail 2', |                 name: 'Response Time', | ||||||
|             value: 'Value 2', |                 value: '45ms (avg)', | ||||||
|             status: 'partly_ok', |                 status: 'ok', | ||||||
|             statusText: 'partly_ok', |                 statusText: 'Within normal range', | ||||||
|           }, |               }, | ||||||
|           { |               { | ||||||
|             name: 'Detail 3', |                 name: 'Uptime', | ||||||
|             value: 'Value 3', |                 value: '99.99% (30 days)', | ||||||
|             status: 'not_ok', |                 status: 'ok', | ||||||
|             statusText: 'not_ok', |                 statusText: 'Excellent uptime', | ||||||
|           }, |               }, | ||||||
|           { |               { | ||||||
|             name: 'Detail 4', |                 name: 'Active Connections', | ||||||
|             value: |                 value: '1,234 / 10,000', | ||||||
|               'Value 4 jhdkfjhalskdfjhfdjskalsdkfjhfdjskalskdjfhjdkslaksjdhfjdkslaskdfjhfjdkslaskdjfhjdskalskdjhfdjskalskdjfhdjskl', |                 status: 'ok', | ||||||
|             status: 'ok', |                 statusText: 'Normal load', | ||||||
|             statusText: 'OK', |               }, | ||||||
|           }, |               { | ||||||
|         ], |                 name: 'SSL Certificate', | ||||||
|       } as tsclass.code.IStatusObject} |                 value: 'Valid until 2024-12-31', | ||||||
|     > |                 status: 'ok', | ||||||
|     </dees-dataview-statusobject> |                 statusText: 'Certificate valid', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |           } as tsclass.code.IStatusObject} | ||||||
|  |         > | ||||||
|  |         </dees-dataview-statusobject> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <div class="demo-section"> | ||||||
|  |         <div class="demo-title">Database Cluster Status</div> | ||||||
|  |         <dees-dataview-statusobject | ||||||
|  |           .statusObject=${{ | ||||||
|  |             id: '2', | ||||||
|  |             name: 'PostgreSQL Cluster', | ||||||
|  |             combinedStatus: 'partly_ok', | ||||||
|  |             combinedStatusText: 'Minor issues detected', | ||||||
|  |             details: [ | ||||||
|  |               { | ||||||
|  |                 name: 'Primary Node', | ||||||
|  |                 value: 'db-primary-01 (healthy)', | ||||||
|  |                 status: 'ok', | ||||||
|  |                 statusText: 'Operating normally', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Replica Lag', | ||||||
|  |                 value: '2.5 seconds', | ||||||
|  |                 status: 'partly_ok', | ||||||
|  |                 statusText: 'Slightly elevated', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Disk Usage', | ||||||
|  |                 value: '78% (312GB / 400GB)', | ||||||
|  |                 status: 'partly_ok', | ||||||
|  |                 statusText: 'Approaching threshold', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Connection Pool', | ||||||
|  |                 value: '89 / 100 connections', | ||||||
|  |                 status: 'ok', | ||||||
|  |                 statusText: 'Within limits', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |           } as tsclass.code.IStatusObject} | ||||||
|  |         > | ||||||
|  |         </dees-dataview-statusobject> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <div class="demo-section"> | ||||||
|  |         <div class="demo-title">Build Pipeline Status</div> | ||||||
|  |         <dees-dataview-statusobject | ||||||
|  |           .statusObject=${{ | ||||||
|  |             id: '3', | ||||||
|  |             name: 'CI/CD Pipeline', | ||||||
|  |             combinedStatus: 'not_ok', | ||||||
|  |             combinedStatusText: 'Build failure', | ||||||
|  |             details: [ | ||||||
|  |               { | ||||||
|  |                 name: 'Last Build', | ||||||
|  |                 value: 'Build #1234 - Failed', | ||||||
|  |                 status: 'not_ok', | ||||||
|  |                 statusText: 'Test failures', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Failed Tests', | ||||||
|  |                 value: '3 tests failed: auth.spec.ts, user.spec.ts, api.spec.ts', | ||||||
|  |                 status: 'not_ok', | ||||||
|  |                 statusText: 'Unit test failures', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Code Coverage', | ||||||
|  |                 value: '82.5% (target: 85%)', | ||||||
|  |                 status: 'partly_ok', | ||||||
|  |                 statusText: 'Below target', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Build Duration', | ||||||
|  |                 value: '12m 34s', | ||||||
|  |                 status: 'ok', | ||||||
|  |                 statusText: 'Normal duration', | ||||||
|  |               }, | ||||||
|  |             ], | ||||||
|  |           } as tsclass.code.IStatusObject} | ||||||
|  |         > | ||||||
|  |         </dees-dataview-statusobject> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|   </div>`; |   </div>`; | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import { | |||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
| import * as tsclass from '@tsclass/tsclass'; | import * as tsclass from '@tsclass/tsclass'; | ||||||
|  | import { DeesContextmenu } from './dees-contextmenu.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -31,109 +32,128 @@ export class DeesDataviewStatusobject extends DeesElement { | |||||||
|   public static styles = [ |   public static styles = [ | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
|     css` |     css` | ||||||
|  |       :host { | ||||||
|  |         font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       .mainbox { |       .mainbox { | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|         background: ${cssManager.bdTheme('#fff', '#1b1b1b')}; |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; | ||||||
|         box-shadow: 0px 1px 3px #00000030; |         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; |         min-height: 48px; | ||||||
|         color: ${cssManager.bdTheme('#000', '#fff')}; |         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; | ||||||
|         border-top: ${cssManager.bdTheme('none', '1px solid #ffffff10')}; |  | ||||||
|         cursor: default; |         cursor: default; | ||||||
|  |         overflow: hidden; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .heading { |       .heading { | ||||||
|         display: grid; |         display: grid; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         grid-template-columns: 40px auto 120px; |         grid-template-columns: 48px auto 100px; | ||||||
|  |         height: 56px; | ||||||
|  |         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 { |       h1 { | ||||||
|         display: block; |         display: block; | ||||||
|         margin: 0px; |         margin: 0px; | ||||||
|         padding: 0px; |         padding: 0px 12px; | ||||||
|         height: 48px; |         font-size: 14px; | ||||||
|         text-transform: uppercase; |         font-weight: 500; | ||||||
|         font-size: 12px; |         letter-spacing: -0.01em; | ||||||
|         line-height: 48px; |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .statusdot { |       .statusdot { | ||||||
|         height: 8px; |         height: 10px; | ||||||
|         width: 8px; |         width: 10px; | ||||||
|         border-radius: 6px; |         border-radius: 50%; | ||||||
|         background: grey; |         background: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; | ||||||
|         margin: auto; |         margin: auto; | ||||||
|  |         box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(0 0% 63.9% / 0.2)', 'hsl(0 0% 45.1% / 0.2)')}; | ||||||
|  |         transition: all 0.2s ease; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .copyMain { |       .copyMain { | ||||||
|         font-size: 10px; |         font-size: 12px; | ||||||
|         font-weight: 600; |         font-weight: 500; | ||||||
|         text-transform: uppercase; |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')}; |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|         padding: 4px; |         padding: 6px 12px; | ||||||
|         border-radius: 3px; |         border-radius: 6px; | ||||||
|         margin-right: 16px; |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|         color: ${cssManager.bdTheme('#333', '#ffffff80')}; |  | ||||||
|         user-select: none; |         user-select: none; | ||||||
|  |         cursor: pointer; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .copyMain:hover { |       .copyMain:hover { | ||||||
|         background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border: 1px solid ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)}; |         border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; | ||||||
|         color: #fff; |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .copyMain:active { |       .copyMain:active { | ||||||
|         background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; |         background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border: 1px solid ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)}; |         transform: scale(0.98); | ||||||
|         color: #fff; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .statusdot.ok { |       .statusdot.ok { | ||||||
|         background: green; |         background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')}; | ||||||
|  |         box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.2)', 'hsl(142.1 70.6% 45.3% / 0.2)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .statusdot.not_ok{ |       .statusdot.not_ok { | ||||||
|         background: red; |         background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')}; | ||||||
|  |         box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.2)', 'hsl(0 72.2% 50.6% / 0.2)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .statusdot.partly_ok { |       .statusdot.partly_ok { | ||||||
|         background: orange; |         background: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')}; | ||||||
|  |         box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(25 95% 53% / 0.2)', 'hsl(25 95% 63% / 0.2)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .detail { |       .detail { | ||||||
|         min-height: 60px; |         min-height: 60px; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|         display: grid; |         display: grid; | ||||||
|         grid-template-columns: 40px auto; |         grid-template-columns: 48px auto; | ||||||
|         border-top: 1px dotted ${cssManager.bdTheme('#e0e0e0', '#282828')}; |         border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         transition: all 0.2s; |         transition: background-color 0.15s ease; | ||||||
|  |         padding-right: 16px; | ||||||
|  |         cursor: context-menu; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .detail:hover { |       .detail:hover { | ||||||
|         background: #ffffff05; |         background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .detail:active { |       .detail:active { | ||||||
|         background: #ffffff10; |         background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .detail .detailsText { |       .detail .detailsText { | ||||||
|         padding-top: 8px; |         padding: 12px; | ||||||
|         padding-bottom: 8px; |  | ||||||
|         padding-right: 8px; |  | ||||||
|         word-break: break-all; |         word-break: break-all; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .detail .detailsText .label { |       .detail .detailsText .label { | ||||||
|         font-size: 12px; |         font-size: 12px; | ||||||
|         color: #ffffff80 |         font-weight: 500; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')} | ||||||
|  |         margin-bottom: 2px; | ||||||
|  |         letter-spacing: -0.01em; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .detail .detailsText .value { |       .detail .detailsText .value { | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|         font-family: 'Intel One Mono', 'Geist Mono'; |         font-family: 'Intel One Mono', 'Geist Mono', monospace; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; | ||||||
|  |         line-height: 1.5; | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| @@ -143,12 +163,40 @@ export class DeesDataviewStatusobject extends DeesElement { | |||||||
|       <div class="mainbox"> |       <div class="mainbox"> | ||||||
|         <div class="heading"> |         <div class="heading"> | ||||||
|           <div class="statusdot ${this.statusObject?.combinedStatus}"></div> |           <div class="statusdot ${this.statusObject?.combinedStatus}"></div> | ||||||
|           <h1>${this.statusObject?.name || 'no status object assigned'}</h1> |           <h1>${this.statusObject?.name || 'No status object assigned'}</h1> | ||||||
|           <div class="copyMain">Copy as JSON</div> |           <div class="copyMain" @click=${this.handleCopyAsJson}>Copy JSON</div> | ||||||
|         </div> |         </div> | ||||||
|         ${this.statusObject?.details?.map((detailArg) => { |         ${this.statusObject?.details?.map((detailArg) => { | ||||||
|           return html` |           return html` | ||||||
|             <div class="detail"> |             <div  | ||||||
|  |               class="detail" | ||||||
|  |               @contextmenu=${(event: MouseEvent) => { | ||||||
|  |                 event.preventDefault(); | ||||||
|  |                 DeesContextmenu.openContextMenuWithOptions(event, [ | ||||||
|  |                   { | ||||||
|  |                     name: 'Copy Value', | ||||||
|  |                     iconName: 'lucide:copy', | ||||||
|  |                     action: async () => { | ||||||
|  |                       await this.copyToClipboard(detailArg.value, 'Value'); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     name: 'Copy Key', | ||||||
|  |                     iconName: 'lucide:key', | ||||||
|  |                     action: async () => { | ||||||
|  |                       await this.copyToClipboard(detailArg.name, 'Key'); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     name: 'Copy Key:Value', | ||||||
|  |                     iconName: 'lucide:copy-plus', | ||||||
|  |                     action: async () => { | ||||||
|  |                       await this.copyToClipboard(`${detailArg.name}: ${detailArg.value}`, 'Key:Value'); | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 ]); | ||||||
|  |               }} | ||||||
|  |             > | ||||||
|               <div class="statusdot ${detailArg.status}"></div> |               <div class="statusdot ${detailArg.status}"></div> | ||||||
|               <div class="detailsText"> |               <div class="detailsText"> | ||||||
|                 <div class="label">${detailArg.name}</div> |                 <div class="label">${detailArg.name}</div> | ||||||
| @@ -162,4 +210,42 @@ export class DeesDataviewStatusobject extends DeesElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async firstUpdated() {} |   async firstUpdated() {} | ||||||
|  |  | ||||||
|  |   private async copyToClipboard(text: string, type: string = 'Text') { | ||||||
|  |     try { | ||||||
|  |       await navigator.clipboard.writeText(text); | ||||||
|  |       console.log(`${type} copied to clipboard`); | ||||||
|  |       // You could add visual feedback here if needed | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error(`Failed to copy ${type}:`, err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async handleCopyAsJson() { | ||||||
|  |     if (!this.statusObject) return; | ||||||
|  |      | ||||||
|  |     try { | ||||||
|  |       await navigator.clipboard.writeText(JSON.stringify(this.statusObject, null, 2)); | ||||||
|  |        | ||||||
|  |       // Show feedback | ||||||
|  |       const button = this.shadowRoot.querySelector('.copyMain') as HTMLElement; | ||||||
|  |       const originalText = button.textContent; | ||||||
|  |       button.textContent = 'Copied!'; | ||||||
|  |        | ||||||
|  |       // Apply success styles based on theme | ||||||
|  |       const isDark = !this.goBright; | ||||||
|  |       button.style.background = isDark ? 'hsl(142.1 70.6% 45.3% / 0.1)' : 'hsl(142.1 76.2% 36.3% / 0.1)'; | ||||||
|  |       button.style.borderColor = isDark ? 'hsl(142.1 70.6% 45.3%)' : 'hsl(142.1 76.2% 36.3%)'; | ||||||
|  |       button.style.color = isDark ? 'hsl(142.1 70.6% 45.3%)' : 'hsl(142.1 76.2% 36.3%)'; | ||||||
|  |        | ||||||
|  |       setTimeout(() => { | ||||||
|  |         button.textContent = originalText; | ||||||
|  |         button.style.background = ''; | ||||||
|  |         button.style.borderColor = ''; | ||||||
|  |         button.style.color = ''; | ||||||
|  |       }, 1500); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error('Failed to copy:', err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import { | |||||||
|   cssManager, |   cssManager, | ||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| import * as domtools from '@design.estate/dees-domtools'; | import * as domtools from '@design.estate/dees-domtools'; | ||||||
|  | import { MONACO_VERSION } from './version.js'; | ||||||
| 
 | 
 | ||||||
| import type * as monaco from 'monaco-editor'; | import type * as monaco from 'monaco-editor'; | ||||||
| 
 | 
 | ||||||
| @@ -80,10 +81,11 @@ export class DeesEditor extends DeesElement { | |||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     super.firstUpdated(_changedProperties); |     super.firstUpdated(_changedProperties); | ||||||
|     const container = this.shadowRoot.getElementById('container'); |     const container = this.shadowRoot.getElementById('container'); | ||||||
|  |     const monacoCdnBase = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}`; | ||||||
| 
 | 
 | ||||||
|     if (!DeesEditor.monacoDeferred) { |     if (!DeesEditor.monacoDeferred) { | ||||||
|       DeesEditor.monacoDeferred = domtools.plugins.smartpromise.defer(); |       DeesEditor.monacoDeferred = domtools.plugins.smartpromise.defer(); | ||||||
|       const scriptUrl = `https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/loader.js`; |       const scriptUrl = `${monacoCdnBase}/min/vs/loader.js`; | ||||||
|       const script = document.createElement('script'); |       const script = document.createElement('script'); | ||||||
|       script.src = scriptUrl; |       script.src = scriptUrl; | ||||||
|       script.onload = () => { |       script.onload = () => { | ||||||
| @@ -94,7 +96,7 @@ export class DeesEditor extends DeesElement { | |||||||
|     await DeesEditor.monacoDeferred.promise; |     await DeesEditor.monacoDeferred.promise; | ||||||
| 
 | 
 | ||||||
|     (window as any).require.config({ |     (window as any).require.config({ | ||||||
|       paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/vs' }, |       paths: { vs: `${monacoCdnBase}/min/vs` }, | ||||||
|     }); |     }); | ||||||
|     (window as any).require(['vs/editor/editor.main'], async () => { |     (window as any).require(['vs/editor/editor.main'], async () => { | ||||||
|       const editor = ((window as any).monaco.editor as typeof monaco.editor).create(container, { |       const editor = ((window as any).monaco.editor as typeof monaco.editor).create(container, { | ||||||
| @@ -109,7 +111,7 @@ export class DeesEditor extends DeesElement { | |||||||
|       this.editorDeferred.resolve(editor); |       this.editorDeferred.resolve(editor); | ||||||
|     }); |     }); | ||||||
|     const css = await ( |     const css = await ( | ||||||
|       await fetch('https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/editor/editor.main.css') |       await fetch(`${monacoCdnBase}/min/vs/editor/editor.main.css`) | ||||||
|     ).text(); |     ).text(); | ||||||
|     const styleElement = document.createElement('style'); |     const styleElement = document.createElement('style'); | ||||||
|     styleElement.textContent = css; |     styleElement.textContent = css; | ||||||
							
								
								
									
										2
									
								
								ts_web/elements/dees-editor/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ts_web/elements/dees-editor/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | export * from './dees-editor.js'; | ||||||
|  | export * from './version.js'; | ||||||
							
								
								
									
										2
									
								
								ts_web/elements/dees-editor/version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ts_web/elements/dees-editor/version.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | // Auto-generated by scripts/update-monaco-version.cjs | ||||||
|  | export const MONACO_VERSION = '0.52.2'; | ||||||
| @@ -57,9 +57,10 @@ export class DeesFormSubmit extends DeesElement { | |||||||
|     if (this.disabled) { |     if (this.disabled) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const parentElement: DeesForm = this.parentElement as DeesForm; |     // Walk up the DOM tree to find the nearest dees-form element | ||||||
|     if (parentElement && parentElement.gatherAndDispatch) { |     const parentFormElement = this.closest('dees-form') as DeesForm; | ||||||
|       parentElement.gatherAndDispatch(); |     if (parentFormElement && parentFormElement.gatherAndDispatch) { | ||||||
|  |       parentFormElement.gatherAndDispatch(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,40 +3,91 @@ import type { DeesForm } from './dees-form.js'; | |||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  |  | ||||||
| export const demoFunc = () => html` | export const demoFunc = () => html` | ||||||
|   <dees-demowrapper> |   <style> | ||||||
|     <style> |     ${css` | ||||||
|       ${css` |       .demo-container { | ||||||
|         .demo-container { |         display: flex; | ||||||
|           display: flex; |         flex-direction: column; | ||||||
|           flex-direction: column; |         gap: 24px; | ||||||
|           gap: 24px; |         padding: 24px; | ||||||
|           padding: 24px; |         max-width: 1200px; | ||||||
|           max-width: 1200px; |         margin: 0 auto; | ||||||
|           margin: 0 auto; |       } | ||||||
|         } |  | ||||||
|        |        | ||||||
|         dees-panel { |       dees-panel { | ||||||
|           margin-bottom: 24px; |         margin-bottom: 24px; | ||||||
|         } |       } | ||||||
|        |        | ||||||
|         dees-panel:last-child { |       dees-panel:last-child { | ||||||
|           margin-bottom: 0; |         margin-bottom: 0; | ||||||
|         } |       } | ||||||
|       `} |  | ||||||
|     </style> |  | ||||||
|        |        | ||||||
|     <div class="demo-container"> |       .form-output { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 12px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-family: monospace; | ||||||
|  |         white-space: pre-wrap; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .status-message { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 12px; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         font-size: 14px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .status-message.success { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.2)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(142.1 70.6% 35.3%)', 'hsl(142.1 70.6% 65.3%)')}; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .status-message.error { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 72.2% 50.6% / 0.2)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 72.2% 40.6%)', 'hsl(0 72.2% 60.6%)')}; | ||||||
|  |       } | ||||||
|  |     `} | ||||||
|  |   </style> | ||||||
|  |    | ||||||
|  |   <div class="demo-container"> | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       const form = elementArg.querySelector('dees-form') as DeesForm; | ||||||
|  |       const outputDiv = elementArg.querySelector('.form-output'); | ||||||
|  |        | ||||||
|  |       if (form && outputDiv) { | ||||||
|  |         form.addEventListener('formData', async (eventArg: CustomEvent) => { | ||||||
|  |           const data = eventArg.detail.data; | ||||||
|  |           console.log('Form submitted with data:', data); | ||||||
|  |            | ||||||
|  |           // Show processing state | ||||||
|  |           form.setStatus('pending', 'Processing your registration...'); | ||||||
|  |           outputDiv.innerHTML = `<strong>Submitted Data:</strong>\n${JSON.stringify(data, null, 2)}`; | ||||||
|  |            | ||||||
|  |           // Simulate API call | ||||||
|  |           await domtools.plugins.smartdelay.delayFor(2000); | ||||||
|  |            | ||||||
|  |           // Show success | ||||||
|  |           form.setStatus('success', 'Registration completed successfully!'); | ||||||
|  |            | ||||||
|  |           // Reset form after delay | ||||||
|  |           await domtools.plugins.smartdelay.delayFor(2000); | ||||||
|  |           form.reset(); | ||||||
|  |           outputDiv.innerHTML = '<em>Form has been reset</em>'; | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Track individual field changes | ||||||
|  |         const inputs = form.querySelectorAll('dees-input-text, dees-input-dropdown, dees-input-checkbox'); | ||||||
|  |         inputs.forEach((input) => { | ||||||
|  |           input.addEventListener('changeSubject', () => { | ||||||
|  |             console.log('Field changed:', input.getAttribute('key')); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .heading="Complete Form Example" .description="A comprehensive form with various input types, validation, and form submission handling"> |       <dees-panel .heading="Complete Form Example" .description="A comprehensive form with various input types, validation, and form submission handling"> | ||||||
|         <dees-form |         <dees-form> | ||||||
|           @formData=${async (eventArg) => { |  | ||||||
|             const form: DeesForm = eventArg.currentTarget; |  | ||||||
|             form.setStatus('pending', 'Processing...'); |  | ||||||
|             await domtools.plugins.smartdelay.delayFor(2000); |  | ||||||
|             form.setStatus('success', 'Form submitted successfully!'); |  | ||||||
|             await domtools.plugins.smartdelay.delayFor(2000); |  | ||||||
|             form.reset(); |  | ||||||
|           }} |  | ||||||
|         > |  | ||||||
|           <dees-input-text  |           <dees-input-text  | ||||||
|             .required=${true}  |             .required=${true}  | ||||||
|             key="firstName"  |             key="firstName"  | ||||||
| @@ -92,13 +143,47 @@ export const demoFunc = () => html` | |||||||
|            |            | ||||||
|           <dees-form-submit>Create Account</dees-form-submit> |           <dees-form-submit>Create Account</dees-form-submit> | ||||||
|         </dees-form> |         </dees-form> | ||||||
|       </dees-panel> |  | ||||||
|          |          | ||||||
|  |         <div class="form-output"> | ||||||
|  |           <em>Submit the form to see the collected data...</em> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       const form = elementArg.querySelector('dees-form') as DeesForm; | ||||||
|  |        | ||||||
|  |       if (form) { | ||||||
|  |         // Track horizontal layout behavior | ||||||
|  |         console.log('Horizontal form layout active'); | ||||||
|  |          | ||||||
|  |         // Monitor filter changes | ||||||
|  |         form.addEventListener('formData', (event: CustomEvent) => { | ||||||
|  |           const filters = event.detail.data; | ||||||
|  |           console.log('Filter applied:', filters); | ||||||
|  |            | ||||||
|  |           // Simulate search | ||||||
|  |           const resultsCount = Math.floor(Math.random() * 100) + 1; | ||||||
|  |           console.log(`Found ${resultsCount} results with filters:`, filters); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Setup real-time filter updates | ||||||
|  |         const inputs = form.querySelectorAll('[key]'); | ||||||
|  |         inputs.forEach((input) => { | ||||||
|  |           input.addEventListener('changeSubject', async () => { | ||||||
|  |             // Get current form data | ||||||
|  |             const formData = await form.collectFormData(); | ||||||
|  |             console.log('Live filter update:', formData); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .heading="Horizontal Form Layout" .description="Compact form with inputs arranged horizontally - perfect for filters and quick forms"> |       <dees-panel .heading="Horizontal Form Layout" .description="Compact form with inputs arranged horizontally - perfect for filters and quick forms"> | ||||||
|         <dees-form horizontal-layout> |         <dees-form horizontal-layout> | ||||||
|           <dees-input-text  |           <dees-input-text  | ||||||
|             key="search"  |             key="search"  | ||||||
|             label="Search" |             label="Search" | ||||||
|  |             placeholder="Enter keywords..." | ||||||
|           ></dees-input-text> |           ></dees-input-text> | ||||||
|            |            | ||||||
|           <dees-input-dropdown |           <dees-input-dropdown | ||||||
| @@ -132,16 +217,55 @@ export const demoFunc = () => html` | |||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|         </dees-form> |         </dees-form> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|      |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       const form = elementArg.querySelector('dees-form') as DeesForm; | ||||||
|  |       const statusDiv = elementArg.querySelector('#status-display'); | ||||||
|  |        | ||||||
|  |       if (form) { | ||||||
|  |         form.addEventListener('formData', async (eventArg: CustomEvent) => { | ||||||
|  |           const data = eventArg.detail.data; | ||||||
|  |           console.log('Advanced form data:', data); | ||||||
|  |            | ||||||
|  |           // Show validation in progress | ||||||
|  |           form.setStatus('pending', 'Validating your information...'); | ||||||
|  |            | ||||||
|  |           // Simulate validation | ||||||
|  |           await domtools.plugins.smartdelay.delayFor(1500); | ||||||
|  |            | ||||||
|  |           // Check IBAN validity (simple check) | ||||||
|  |           if (data.iban && data.iban.length > 15) { | ||||||
|  |             form.setStatus('success', 'Application submitted successfully!'); | ||||||
|  |              | ||||||
|  |             if (statusDiv) { | ||||||
|  |               statusDiv.className = 'status-message success'; | ||||||
|  |               statusDiv.textContent = '✓ Your application has been submitted. We will contact you soon.'; | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             form.setStatus('error', 'Please check your IBAN'); | ||||||
|  |              | ||||||
|  |             if (statusDiv) { | ||||||
|  |               statusDiv.className = 'status-message error'; | ||||||
|  |               statusDiv.textContent = '✗ Invalid IBAN format. Please check and try again.'; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |            | ||||||
|  |           console.log('Form data logged:', data); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Monitor file uploads | ||||||
|  |         const fileUpload = form.querySelector('dees-input-fileupload'); | ||||||
|  |         if (fileUpload) { | ||||||
|  |           fileUpload.addEventListener('change', (event: any) => { | ||||||
|  |             const files = event.detail?.files || []; | ||||||
|  |             console.log(`${files.length} file(s) selected for upload`); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .heading="Advanced Form Features" .description="Form with specialized input types and complex validation"> |       <dees-panel .heading="Advanced Form Features" .description="Form with specialized input types and complex validation"> | ||||||
|         <dees-form |         <dees-form> | ||||||
|           @formData=${async (eventArg) => { |  | ||||||
|             const form: DeesForm = eventArg.currentTarget; |  | ||||||
|             const data = eventArg.detail.data; |  | ||||||
|             console.log('Form data:', data); |  | ||||||
|             form.setStatus('success', 'Data logged to console!'); |  | ||||||
|           }} |  | ||||||
|         > |  | ||||||
|           <dees-input-iban  |           <dees-input-iban  | ||||||
|             key="iban" |             key="iban" | ||||||
|             label="IBAN" |             label="IBAN" | ||||||
| @@ -181,7 +305,9 @@ export const demoFunc = () => html` | |||||||
|            |            | ||||||
|           <dees-form-submit>Submit Application</dees-form-submit> |           <dees-form-submit>Submit Application</dees-form-submit> | ||||||
|         </dees-form> |         </dees-form> | ||||||
|  |          | ||||||
|  |         <div id="status-display"></div> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|     </div> |     </dees-demowrapper> | ||||||
|   </dees-demowrapper> |   </div> | ||||||
| `; | `; | ||||||
| @@ -9,22 +9,24 @@ import { | |||||||
| import * as domtools from '@design.estate/dees-domtools'; | import * as domtools from '@design.estate/dees-domtools'; | ||||||
|  |  | ||||||
| import { DeesInputCheckbox } from './dees-input-checkbox.js'; | import { DeesInputCheckbox } from './dees-input-checkbox.js'; | ||||||
|  | import { DeesInputDatepicker } from './dees-input-datepicker/index.js'; | ||||||
| import { DeesInputText } from './dees-input-text.js'; | import { DeesInputText } from './dees-input-text.js'; | ||||||
| import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; | import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; | ||||||
| import { DeesInputRadiogroup } from './dees-input-radiogroup.js'; | import { DeesInputRadiogroup } from './dees-input-radiogroup.js'; | ||||||
| import { DeesInputDropdown } from './dees-input-dropdown.js'; | import { DeesInputDropdown } from './dees-input-dropdown.js'; | ||||||
| import { DeesInputFileupload } from './dees-input-fileupload.js'; | import { DeesInputFileupload } from './dees-input-fileupload/index.js'; | ||||||
| import { DeesInputIban } from './dees-input-iban.js'; | import { DeesInputIban } from './dees-input-iban.js'; | ||||||
| import { DeesInputMultitoggle } from './dees-input-multitoggle.js'; | import { DeesInputMultitoggle } from './dees-input-multitoggle.js'; | ||||||
| import { DeesInputPhone } from './dees-input-phone.js'; | import { DeesInputPhone } from './dees-input-phone.js'; | ||||||
| import { DeesInputTypelist } from './dees-input-typelist.js'; | import { DeesInputTypelist } from './dees-input-typelist.js'; | ||||||
| import { DeesFormSubmit } from './dees-form-submit.js'; | import { DeesFormSubmit } from './dees-form-submit.js'; | ||||||
| import { DeesTable } from './dees-table.js'; | import { DeesTable } from './dees-table/index.js'; | ||||||
| import { demoFunc } from './dees-form.demo.js'; | import { demoFunc } from './dees-form.demo.js'; | ||||||
|  |  | ||||||
| // Unified set for form input types | // Unified set for form input types | ||||||
| const FORM_INPUT_TYPES = [ | const FORM_INPUT_TYPES = [ | ||||||
|   DeesInputCheckbox, |   DeesInputCheckbox, | ||||||
|  |   DeesInputDatepicker, | ||||||
|   DeesInputDropdown, |   DeesInputDropdown, | ||||||
|   DeesInputFileupload, |   DeesInputFileupload, | ||||||
|   DeesInputIban, |   DeesInputIban, | ||||||
| @@ -39,6 +41,7 @@ const FORM_INPUT_TYPES = [ | |||||||
|  |  | ||||||
| export type TFormInputElement = | export type TFormInputElement = | ||||||
|   | DeesInputCheckbox |   | DeesInputCheckbox | ||||||
|  |   | DeesInputDatepicker | ||||||
|   | DeesInputDropdown |   | DeesInputDropdown | ||||||
|   | DeesInputFileupload |   | DeesInputFileupload | ||||||
|   | DeesInputIban |   | DeesInputIban | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import { | |||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
| import { demoFunc } from './dees-heading.demo.js'; | import { demoFunc } from './dees-heading.demo.js'; | ||||||
|  | import { cssCalSansFontFamily } from './00fonts.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -39,7 +40,7 @@ export class DeesHeading extends DeesElement { | |||||||
|         font-weight: 600; |         font-weight: 600; | ||||||
|         color: ${cssManager.bdTheme('#000', '#fff')}; |         color: ${cssManager.bdTheme('#000', '#fff')}; | ||||||
|       } |       } | ||||||
|       h1 { font-size: 32px; font-family: 'Cal Sans'; letter-spacing: 0.025em;} |       h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;} | ||||||
|       h2 { font-size: 28px; } |       h2 { font-size: 28px; } | ||||||
|       h3 { font-size: 24px; } |       h3 { font-size: 24px; } | ||||||
|       h4 { font-size: 20px; } |       h4 { font-size: 20px; } | ||||||
|   | |||||||
| @@ -40,6 +40,26 @@ export const demoFunc = () => { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Define the functions in TS scope instead of script tags |   // Define the functions in TS scope instead of script tags | ||||||
|  |   const copyAllIconNames = () => { | ||||||
|  |     // Generate complete list of all icon names with prefixes | ||||||
|  |     const faIconsList = faIcons.map(name => `fa:${name}`); | ||||||
|  |     const lucideIconsListPrefixed = lucideIconsList.map(name => `lucide:${name}`); | ||||||
|  |     const allIcons = [...faIconsList, ...lucideIconsListPrefixed]; | ||||||
|  |     const textToCopy = allIcons.join('\n'); | ||||||
|  |      | ||||||
|  |     navigator.clipboard.writeText(textToCopy).then(() => { | ||||||
|  |       // Show feedback | ||||||
|  |       const currentEvent = window.event as MouseEvent; | ||||||
|  |       const button = currentEvent.currentTarget as HTMLElement; | ||||||
|  |       const originalText = button.textContent; | ||||||
|  |       button.textContent = `✓ Copied ${allIcons.length} icon names!`; | ||||||
|  |        | ||||||
|  |       setTimeout(() => { | ||||||
|  |         button.textContent = originalText; | ||||||
|  |       }, 2000); | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |    | ||||||
|   const searchIcons = (event: InputEvent) => { |   const searchIcons = (event: InputEvent) => { | ||||||
|     const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim(); |     const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim(); | ||||||
|     // Get the demo container first, then search within it |     // Get the demo container first, then search within it | ||||||
| @@ -111,6 +131,7 @@ export const demoFunc = () => { | |||||||
|       width: 100%; |       width: 100%; | ||||||
|       margin-bottom: 20px; |       margin-bottom: 20px; | ||||||
|       display: flex; |       display: flex; | ||||||
|  |       gap: 10px; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     #iconSearch { |     #iconSearch { | ||||||
| @@ -129,6 +150,27 @@ export const demoFunc = () => { | |||||||
|       border-color: #e4002b; |       border-color: #e4002b; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     .copy-all-button { | ||||||
|  |       padding: 12px 20px; | ||||||
|  |       font-size: 16px; | ||||||
|  |       border: none; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       background: #e4002b; | ||||||
|  |       color: #fff; | ||||||
|  |       cursor: pointer; | ||||||
|  |       transition: all 0.2s; | ||||||
|  |       white-space: nowrap; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .copy-all-button:hover { | ||||||
|  |       background: #c4001b; | ||||||
|  |       transform: translateY(-1px); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     .copy-all-button:active { | ||||||
|  |       transform: translateY(0); | ||||||
|  |     } | ||||||
|  |      | ||||||
|     dees-icon { |     dees-icon { | ||||||
|       transition: all 0.2s ease; |       transition: all 0.2s ease; | ||||||
|       color: #ffffff; |       color: #ffffff; | ||||||
| @@ -239,6 +281,7 @@ export const demoFunc = () => { | |||||||
|   <div class="demoContainer"> |   <div class="demoContainer"> | ||||||
|     <div class="search-container"> |     <div class="search-container"> | ||||||
|       <input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}> |       <input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}> | ||||||
|  |       <button class="copy-all-button" @click=${copyAllIconNames}>📋 Copy All Icon Names</button> | ||||||
|     </div> |     </div> | ||||||
|      |      | ||||||
|     <div class="api-note"> |     <div class="api-note"> | ||||||
| @@ -258,7 +301,7 @@ export const demoFunc = () => { | |||||||
|             return html` |             return html` | ||||||
|               <div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}> |               <div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}> | ||||||
|                 <dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon> |                 <dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon> | ||||||
|                 <div class="iconName">${iconName}</div> |                 <div class="iconName">fa:${iconName}</div> | ||||||
|                 <span class="copy-tooltip">Click to copy</span> |                 <span class="copy-tooltip">Click to copy</span> | ||||||
|               </div> |               </div> | ||||||
|             `; |             `; | ||||||
| @@ -279,7 +322,7 @@ export const demoFunc = () => { | |||||||
|             return html` |             return html` | ||||||
|               <div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}> |               <div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}> | ||||||
|                 <dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon> |                 <dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon> | ||||||
|                 <div class="iconName">${iconName}</div> |                 <div class="iconName">lucide:${iconName}</div> | ||||||
|                 <span class="copy-tooltip">Click to copy</span> |                 <span class="copy-tooltip">Click to copy</span> | ||||||
|               </div> |               </div> | ||||||
|             `; |             `; | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { html, css } from '@design.estate/dees-element'; | import { html, css, cssManager } from '@design.estate/dees-element'; | ||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  | import './dees-panel.js'; | ||||||
| import type { DeesInputCheckbox } from './dees-input-checkbox.js'; | import type { DeesInputCheckbox } from './dees-input-checkbox.js'; | ||||||
| import './dees-button.js'; | import './dees-button.js'; | ||||||
|  |  | ||||||
| @@ -41,62 +42,49 @@ export const demoFunc = () => html` | |||||||
|           margin: 0 auto; |           margin: 0 auto; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .demo-section { |         dees-panel { | ||||||
|           background: #f8f9fa; |           margin-bottom: 24px; | ||||||
|           border-radius: 8px; |  | ||||||
|           padding: 24px; |  | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         @media (prefers-color-scheme: dark) { |         dees-panel:last-child { | ||||||
|           .demo-section { |           margin-bottom: 0; | ||||||
|             background: #1a1a1a; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .demo-section h3 { |  | ||||||
|           margin-top: 0; |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|           color: #0069f2; |  | ||||||
|           font-size: 18px; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .demo-section p { |  | ||||||
|           margin-top: 0; |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|           color: #666; |  | ||||||
|           font-size: 14px; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         @media (prefers-color-scheme: dark) { |  | ||||||
|           .demo-section p { |  | ||||||
|             color: #999; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .horizontal-group { |  | ||||||
|           display: flex; |  | ||||||
|           align-items: center; |  | ||||||
|           gap: 16px; |  | ||||||
|           flex-wrap: wrap; |  | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .checkbox-group { |         .checkbox-group { | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: column; |           flex-direction: column; | ||||||
|           gap: 8px; |           gap: 12px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .feature-list { |         .horizontal-checkboxes { | ||||||
|           background: #f0f0f0; |           display: flex; | ||||||
|           border-radius: 4px; |           gap: 24px; | ||||||
|  |           flex-wrap: wrap; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .interactive-section { | ||||||
|  |           background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |           border-radius: 8px; | ||||||
|           padding: 16px; |           padding: 16px; | ||||||
|           margin-bottom: 16px; |           margin-top: 16px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         @media (prefers-color-scheme: dark) { |         .output-text { | ||||||
|           .feature-list { |           font-family: monospace; | ||||||
|             background: #0a0a0a; |           font-size: 13px; | ||||||
|           } |           color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(210 40% 80%)')}; | ||||||
|  |           padding: 8px; | ||||||
|  |           background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')}; | ||||||
|  |           border-radius: 4px; | ||||||
|  |           min-height: 24px; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .form-section { | ||||||
|  |           background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')}; | ||||||
|  |           border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |           border-radius: 8px; | ||||||
|  |           padding: 20px; | ||||||
|  |           margin-top: 16px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .button-group { |         .button-group { | ||||||
| @@ -104,70 +92,112 @@ export const demoFunc = () => html` | |||||||
|           gap: 8px; |           gap: 8px; | ||||||
|           margin-bottom: 16px; |           margin-bottom: 16px; | ||||||
|         } |         } | ||||||
|  |          | ||||||
|  |         .feature-list { | ||||||
|  |           background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')}; | ||||||
|  |           border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')}; | ||||||
|  |           border-radius: 6px; | ||||||
|  |           padding: 16px; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .section-title { | ||||||
|  |           font-size: 16px; | ||||||
|  |           font-weight: 600; | ||||||
|  |           margin-bottom: 16px; | ||||||
|  |           color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | ||||||
|  |         } | ||||||
|       `} |       `} | ||||||
|     </style> |     </style> | ||||||
|      |      | ||||||
|     <div class="demo-container"> |     <div class="demo-container"> | ||||||
|       <div class="demo-section"> |       <dees-panel .title=${'Basic Checkboxes'} .subtitle=${'Simple checkbox examples with various labels'}> | ||||||
|         <h3>Basic Checkboxes</h3> |         <div class="checkbox-group"> | ||||||
|         <p>Standard checkbox inputs for boolean selections</p> |           <dees-input-checkbox  | ||||||
|  |             .label=${'I agree to the Terms and Conditions'}  | ||||||
|  |             .value=${true} | ||||||
|  |             .key=${'terms'} | ||||||
|  |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|         <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|           .label=${'I agree to the Terms and Conditions'}  |             .label=${'Subscribe to newsletter'}  | ||||||
|           .value=${true} |             .value=${false} | ||||||
|           .key=${'terms'} |             .key=${'newsletter'} | ||||||
|         ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|         <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|           .label=${'Subscribe to newsletter'}  |             .label=${'Enable notifications'}  | ||||||
|           .value=${false} |             .value=${false} | ||||||
|           .key=${'newsletter'} |             .description=${'Receive email updates about your account'} | ||||||
|         ></dees-input-checkbox> |             .key=${'notifications'} | ||||||
|  |           ></dees-input-checkbox> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|        |        | ||||||
|         <dees-input-checkbox  |       <dees-panel .title=${'Checkbox States'} .subtitle=${'Different checkbox states and configurations'}> | ||||||
|           .label=${'Enable notifications'}  |         <div class="checkbox-group"> | ||||||
|           .required=${true} |           <dees-input-checkbox  | ||||||
|           .key=${'notifications'} |             .label=${'Default state'}  | ||||||
|         ></dees-input-checkbox> |             .value=${false} | ||||||
|       </div> |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|       <div class="demo-section"> |           <dees-input-checkbox  | ||||||
|         <h3>Horizontal Layout</h3> |             .label=${'Checked state'}  | ||||||
|         <p>Checkboxes arranged horizontally for compact forms</p> |             .value=${true} | ||||||
|  |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|         <div class="horizontal-group"> |           <dees-input-checkbox  | ||||||
|  |             .label=${'Disabled unchecked'}  | ||||||
|  |             .value=${false} | ||||||
|  |             .disabled=${true} | ||||||
|  |           ></dees-input-checkbox> | ||||||
|  |            | ||||||
|  |           <dees-input-checkbox  | ||||||
|  |             .label=${'Disabled checked'}  | ||||||
|  |             .value=${true} | ||||||
|  |             .disabled=${true} | ||||||
|  |           ></dees-input-checkbox> | ||||||
|  |            | ||||||
|  |           <dees-input-checkbox  | ||||||
|  |             .label=${'Required checkbox'}  | ||||||
|  |             .required=${true} | ||||||
|  |             .key=${'required'} | ||||||
|  |           ></dees-input-checkbox> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |        | ||||||
|  |       <dees-panel .title=${'Horizontal Layout'} .subtitle=${'Checkboxes arranged horizontally for compact forms'}> | ||||||
|  |         <div class="horizontal-checkboxes"> | ||||||
|           <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|             .label=${'Option A'}  |             .label=${'Option A'}  | ||||||
|  |             .value=${false} | ||||||
|             .layoutMode=${'horizontal'} |             .layoutMode=${'horizontal'} | ||||||
|             .key=${'optionA'} |             .key=${'optionA'} | ||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|           <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|             .label=${'Option B'}  |             .label=${'Option B'}  | ||||||
|             .layoutMode=${'horizontal'}  |  | ||||||
|             .value=${true} |             .value=${true} | ||||||
|  |             .layoutMode=${'horizontal'} | ||||||
|             .key=${'optionB'} |             .key=${'optionB'} | ||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|           <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|             .label=${'Option C'}  |             .label=${'Option C'}  | ||||||
|  |             .value=${false} | ||||||
|             .layoutMode=${'horizontal'} |             .layoutMode=${'horizontal'} | ||||||
|             .key=${'optionC'} |             .key=${'optionC'} | ||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|           <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|             .label=${'Option D'}  |             .label=${'Option D'}  | ||||||
|             .layoutMode=${'horizontal'} |  | ||||||
|             .value=${true} |             .value=${true} | ||||||
|  |             .layoutMode=${'horizontal'} | ||||||
|             .key=${'optionD'} |             .key=${'optionD'} | ||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </dees-panel> | ||||||
|        |  | ||||||
|       <div class="demo-section"> |  | ||||||
|         <h3>Feature Selection Example</h3> |  | ||||||
|         <p>Common use case for feature toggles with batch operations</p> |  | ||||||
|        |        | ||||||
|  |       <dees-panel .title=${'Feature Selection Example'} .subtitle=${'Common use case for feature toggles with batch operations'}> | ||||||
|         <div class="button-group"> |         <div class="button-group"> | ||||||
|           <dees-button id="select-all-btn" type="secondary">Select All</dees-button> |           <dees-button id="select-all-btn" type="secondary">Select All</dees-button> | ||||||
|           <dees-button id="clear-all-btn" type="secondary">Clear All</dees-button> |           <dees-button id="clear-all-btn" type="secondary">Clear All</dees-button> | ||||||
| @@ -206,62 +236,72 @@ export const demoFunc = () => html` | |||||||
|             ></dees-input-checkbox> |             ></dees-input-checkbox> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </dees-panel> | ||||||
|        |        | ||||||
|       <div class="demo-section"> |       <dees-panel .title=${'Privacy Settings Example'} .subtitle=${'Checkboxes in a typical form context'}> | ||||||
|         <h3>States</h3> |         <div class="form-section"> | ||||||
|         <p>Different checkbox states and configurations</p> |           <h4 class="section-title">Privacy Preferences</h4> | ||||||
|            |            | ||||||
|         <dees-input-checkbox  |           <div class="checkbox-group"> | ||||||
|           .label=${'Disabled Unchecked'}  |             <dees-input-checkbox  | ||||||
|           .disabled=${true} |               .label=${'Share analytics data'}  | ||||||
|           .key=${'disabled1'} |               .value=${true} | ||||||
|         ></dees-input-checkbox> |               .description=${'Help us improve by sharing anonymous usage data'} | ||||||
|  |             ></dees-input-checkbox> | ||||||
|              |              | ||||||
|         <dees-input-checkbox  |             <dees-input-checkbox  | ||||||
|           .label=${'Disabled Checked'}  |               .label=${'Personalized recommendations'}  | ||||||
|           .disabled=${true}  |               .value=${true} | ||||||
|           .value=${true} |               .description=${'Get suggestions based on your activity'} | ||||||
|           .key=${'disabled2'} |             ></dees-input-checkbox> | ||||||
|         ></dees-input-checkbox> |  | ||||||
|              |              | ||||||
|         <dees-input-checkbox  |             <dees-input-checkbox  | ||||||
|           .label=${'Required Checkbox'}  |               .label=${'Marketing communications'}  | ||||||
|           .required=${true} |               .value=${false} | ||||||
|           .key=${'required'} |               .description=${'Receive promotional emails and special offers'} | ||||||
|         ></dees-input-checkbox> |             ></dees-input-checkbox> | ||||||
|       </div> |  | ||||||
|              |              | ||||||
|       <div class="demo-section"> |             <dees-input-checkbox  | ||||||
|         <h3>Real-world Examples</h3> |               .label=${'Third-party integrations'}  | ||||||
|         <p>Common checkbox patterns in applications</p> |               .value=${false} | ||||||
|  |               .description=${'Allow approved partners to access your data'} | ||||||
|  |             ></dees-input-checkbox> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|        |        | ||||||
|  |       <dees-panel .title=${'Interactive Example'} .subtitle=${'Click checkboxes to see value changes'}> | ||||||
|         <div class="checkbox-group"> |         <div class="checkbox-group"> | ||||||
|           <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|             .label=${'Remember me on this device'}  |             .label=${'Feature toggle'}  | ||||||
|             .value=${true} |  | ||||||
|             .key=${'rememberMe'} |  | ||||||
|           ></dees-input-checkbox> |  | ||||||
|            |  | ||||||
|           <dees-input-checkbox  |  | ||||||
|             .label=${'Make my profile public'}  |  | ||||||
|             .value=${false} |             .value=${false} | ||||||
|             .key=${'publicProfile'} |             @changeSubject=${(event: CustomEvent) => { | ||||||
|  |               const output = document.querySelector('#checkbox-output'); | ||||||
|  |               if (output && event.detail) { | ||||||
|  |                 const isChecked = event.detail.getValue(); | ||||||
|  |                 output.textContent = `Feature is ${isChecked ? 'enabled' : 'disabled'}`; | ||||||
|  |               } | ||||||
|  |             }} | ||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|            |            | ||||||
|           <dees-input-checkbox  |           <dees-input-checkbox  | ||||||
|             .label=${'Allow others to find me by email'}  |             .label=${'Debug mode'}  | ||||||
|             .value=${false} |             .value=${false} | ||||||
|             .key=${'findByEmail'} |             @changeSubject=${(event: CustomEvent) => { | ||||||
|           ></dees-input-checkbox> |               const output = document.querySelector('#debug-output'); | ||||||
|            |               if (output && event.detail) { | ||||||
|           <dees-input-checkbox  |                 const isChecked = event.detail.getValue(); | ||||||
|             .label=${'Send me product updates and announcements'}  |                 output.textContent = `Debug mode: ${isChecked ? 'ON' : 'OFF'}`; | ||||||
|             .value=${true} |               } | ||||||
|             .key=${'productUpdates'} |             }} | ||||||
|           ></dees-input-checkbox> |           ></dees-input-checkbox> | ||||||
|         </div> |         </div> | ||||||
|       </div> |          | ||||||
|  |         <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> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|     </div> |     </div> | ||||||
|   </dees-demowrapper> |   </dees-demowrapper> | ||||||
| `; | `; | ||||||
| @@ -8,6 +8,7 @@ import { | |||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| import { DeesInputBase } from './dees-input-base.js'; | import { DeesInputBase } from './dees-input-base.js'; | ||||||
| import { demoFunc } from './dees-input-checkbox.demo.js'; | import { demoFunc } from './dees-input-checkbox.demo.js'; | ||||||
|  | import { cssGeistFontFamily } from './00fonts.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -27,6 +28,9 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> { | |||||||
|   }) |   }) | ||||||
|   public value: boolean = false; |   public value: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public indeterminate: boolean = false; | ||||||
|  |  | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
| @@ -44,120 +48,106 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> { | |||||||
|         :host { |         :host { | ||||||
|           position: relative; |           position: relative; | ||||||
|           cursor: default; |           cursor: default; | ||||||
|         } |           font-family: ${cssGeistFontFamily}; | ||||||
|         :host(:hover) { |  | ||||||
|           filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')}; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .maincontainer { |         .maincontainer { | ||||||
|           display: flex; |           display: inline-flex; | ||||||
|           align-items: center; |           align-items: flex-start; | ||||||
|           gap: 12px; |           gap: 8px; | ||||||
|           padding: 8px 0px; |  | ||||||
|           color: ${cssManager.bdTheme('#333', '#ccc')}; |  | ||||||
|           cursor: pointer; |           cursor: pointer; | ||||||
|           user-select: none; |           user-select: none; | ||||||
|           transition: all 0.2s; |           transition: all 0.15s ease; | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .maincontainer:hover { |  | ||||||
|           color: ${cssManager.bdTheme('#000', '#fff')}; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .maincontainer:hover .checkbox { |  | ||||||
|           border-color: ${cssManager.bdTheme('#999', '#888')}; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         input:focus { |  | ||||||
|           outline: none; |  | ||||||
|           border-bottom: 1px solid #e4002b; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .checkbox { |         .checkbox { | ||||||
|           transition: all 0.1s; |           position: relative; | ||||||
|           box-sizing: border-box; |           height: 18px; | ||||||
|           border: 1px solid ${cssManager.bdTheme('#CCC', '#999')}; |           width: 18px; | ||||||
|           border-radius: 2px; |  | ||||||
|           height: 24px; |  | ||||||
|           width: 24px; |  | ||||||
|           display: inline-block; |  | ||||||
|           background: ${cssManager.bdTheme('#fafafa', '#222')}; |  | ||||||
|           flex-shrink: 0; |           flex-shrink: 0; | ||||||
|  |           border-radius: 4px; | ||||||
|  |           border: 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%)')}; | ||||||
|  |           transition: all 0.15s ease; | ||||||
|  |           margin-top: 1px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .maincontainer:hover .checkbox { | ||||||
|  |           border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .checkbox.selected { |         .checkbox.selected { | ||||||
|           background: #0050b9; |           background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; | ||||||
|           border: 1px solid #0050b9; |           border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .checkbox.disabled { |         .checkbox:focus-visible { | ||||||
|           background: none; |           outline: none; | ||||||
|           border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; |           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)')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /* Checkmark using Lucide icon style */ | ||||||
|         .checkbox .checkmark { |         .checkbox .checkmark { | ||||||
|           display: inline-block; |  | ||||||
|           width: 22px; |  | ||||||
|           height: 22px; |  | ||||||
|           -ms-transform: rotate(45deg); /* IE 9 */ |  | ||||||
|           -webkit-transform: rotate(45deg); /* Chrome, Safari, Opera */ |  | ||||||
|           transform: rotate(45deg); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .checkbox .checkmark_stem { |  | ||||||
|           position: absolute; |           position: absolute; | ||||||
|           width: 3px; |           top: 50%; | ||||||
|           height: 9px; |           left: 50%; | ||||||
|           background-color: #fff; |           transform: translate(-50%, -50%); | ||||||
|           left: 11px; |           opacity: 0; | ||||||
|           top: 6px; |           transition: opacity 0.15s ease; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .checkbox .checkmark_kick { |         .checkbox.selected .checkmark { | ||||||
|           position: absolute; |           opacity: 1; | ||||||
|           width: 3px; |  | ||||||
|           height: 3px; |  | ||||||
|           background-color: #fff; |  | ||||||
|           left: 8px; |  | ||||||
|           top: 12px; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .checkbox.disabled .checkmark_stem, .checkbox.disabled .checkmark_kick { |         .checkbox .checkmark svg { | ||||||
|           background-color: ${cssManager.bdTheme('#333', '#fff')}; |           width: 12px; | ||||||
|         } |           height: 12px; | ||||||
|  |           stroke: white; | ||||||
|         img { |           stroke-width: 3; | ||||||
|           padding: 4px; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .checkbox-label { |  | ||||||
|           font-size: 14px; |  | ||||||
|           transition: color 0.2s ease; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .maincontainer:hover .checkbox-label { |  | ||||||
|           color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')}; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         /* Disabled state */ | ||||||
|         .maincontainer.disabled { |         .maincontainer.disabled { | ||||||
|           cursor: not-allowed; |           cursor: not-allowed; | ||||||
|           opacity: 0.5; |           opacity: 0.5; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .maincontainer.disabled:hover { |         .checkbox.disabled { | ||||||
|           color: ${cssManager.bdTheme('#333', '#ccc')}; |           background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |           border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .maincontainer.disabled:hover .checkbox { |         /* Label */ | ||||||
|           border-color: ${cssManager.bdTheme('#ccc', '#333')}; |         .label-container { | ||||||
|  |           display: flex; | ||||||
|  |           flex-direction: column; | ||||||
|  |           gap: 2px; | ||||||
|  |           flex: 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         .checkbox-label { | ||||||
|  |           font-size: 14px; | ||||||
|  |           font-weight: 500; | ||||||
|  |           line-height: 20px; | ||||||
|  |           color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; | ||||||
|  |           transition: color 0.15s ease; | ||||||
|  |           letter-spacing: -0.01em; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .maincontainer:hover .checkbox-label { | ||||||
|  |           color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .maincontainer.disabled:hover .checkbox-label { | ||||||
|  |           color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /* Description */ | ||||||
|         .description-text { |         .description-text { | ||||||
|           font-size: 12px; |           font-size: 12px; | ||||||
|           color: ${cssManager.bdTheme('#666', '#999')}; |           color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|           margin-top: 4px; |           line-height: 1.5; | ||||||
|           line-height: 1.4; |  | ||||||
|           padding-left: 36px; |  | ||||||
|         } |         } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| @@ -166,21 +156,34 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> { | |||||||
|     return html` |     return html` | ||||||
|       <div class="input-wrapper"> |       <div class="input-wrapper"> | ||||||
|         <div class="maincontainer ${this.disabled ? 'disabled' : ''}" @click="${this.toggleSelected}"> |         <div class="maincontainer ${this.disabled ? 'disabled' : ''}" @click="${this.toggleSelected}"> | ||||||
|           <div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0"> |           <div  | ||||||
|  |             class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}"  | ||||||
|  |             tabindex="${this.disabled ? '-1' : '0'}" | ||||||
|  |             @keydown="${this.handleKeydown}" | ||||||
|  |           > | ||||||
|             ${this.value |             ${this.value | ||||||
|               ? html` |               ? html` | ||||||
|                   <span class="checkmark"> |                   <span class="checkmark"> | ||||||
|                     <div class="checkmark_stem"></div> |                     <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||
|                     <div class="checkmark_kick"></div> |                       <path d="M20 6L9 17L4 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/> | ||||||
|  |                     </svg> | ||||||
|                   </span> |                   </span> | ||||||
|                 ` |                 ` | ||||||
|               : html``} |               : this.indeterminate | ||||||
|  |                 ? html` | ||||||
|  |                     <span class="checkmark"> | ||||||
|  |                       <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |                         <path d="M5 12H19" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/> | ||||||
|  |                       </svg> | ||||||
|  |                     </span> | ||||||
|  |                   ` | ||||||
|  |                 : html``} | ||||||
|  |           </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>` : ''} | ||||||
|           </div> |           </div> | ||||||
|           ${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''} |  | ||||||
|         </div> |         </div> | ||||||
|         ${this.description ? html` |  | ||||||
|           <div class="description-text">${this.description}</div> |  | ||||||
|         ` : ''} |  | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -213,4 +216,11 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> { | |||||||
|       (checkboxDiv as any).focus(); |       (checkboxDiv as any).focus(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private handleKeydown(event: KeyboardEvent) { | ||||||
|  |     if (event.key === ' ' || event.key === 'Enter') { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.toggleSelected(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										624
									
								
								ts_web/elements/dees-input-datepicker/component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										624
									
								
								ts_web/elements/dees-input-datepicker/component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,624 @@ | |||||||
|  | import { | ||||||
|  |   customElement, | ||||||
|  |   type TemplateResult, | ||||||
|  |   property, | ||||||
|  |   state, | ||||||
|  | } from '@design.estate/dees-element'; | ||||||
|  | import { DeesInputBase } from '../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 '../dees-icon.js'; | ||||||
|  | import '../dees-label.js'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     'dees-input-datepicker': DeesInputDatepicker; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @customElement('dees-input-datepicker') | ||||||
|  | export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> { | ||||||
|  |   public static demo = demoFunc; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public value: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public enableTime: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public timeFormat: '24h' | '12h' = '24h'; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public minuteIncrement: number = 1; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public dateFormat: string = 'YYYY-MM-DD'; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public minDate: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public maxDate: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ type: Array }) | ||||||
|  |   public disabledDates: string[] = []; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public weekStartsOn: 0 | 1 = 1; // Default to Monday | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public placeholder: string = 'YYYY-MM-DD'; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public enableTimezone: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||||||
|  |  | ||||||
|  |   @property({ type: Array }) | ||||||
|  |   public events: IDateEvent[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public isOpened: boolean = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public opensToTop: boolean = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public selectedDate: Date | null = null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public viewDate: Date = new Date(); | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public selectedHour: number = 0; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public selectedMinute: number = 0; | ||||||
|  |  | ||||||
|  |   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); | ||||||
|  |         if (!isNaN(date.getTime())) { | ||||||
|  |           this.selectedDate = date; | ||||||
|  |           this.viewDate = new Date(date); | ||||||
|  |           this.selectedHour = date.getHours(); | ||||||
|  |           this.selectedMinute = date.getMinutes(); | ||||||
|  |         } | ||||||
|  |       } catch { | ||||||
|  |         // Invalid date | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       const now = new Date(); | ||||||
|  |       this.viewDate = new Date(now); | ||||||
|  |       this.selectedHour = now.getHours(); | ||||||
|  |       this.selectedMinute = 0; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public formatDate(isoString: string): string { | ||||||
|  |     if (!isoString) return ''; | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const date = new Date(isoString); | ||||||
|  |       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; | ||||||
|  |         const minutes = date.getMinutes().toString().padStart(2, '0'); | ||||||
|  |         const ampm = hours24 >= 12 ? 'PM' : 'AM'; | ||||||
|  |  | ||||||
|  |         if (this.timeFormat === '12h') { | ||||||
|  |           formatted += ` ${hours12}:${minutes} ${ampm}`; | ||||||
|  |         } else { | ||||||
|  |           formatted += ` ${hours24.toString().padStart(2, '0')}:${minutes}`; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Timezone formatting if enabled | ||||||
|  |       if (this.enableTimezone) { | ||||||
|  |         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}`; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return formatted; | ||||||
|  |     } catch { | ||||||
|  |       return ''; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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; | ||||||
|  |  | ||||||
|  |       // Add click outside listener | ||||||
|  |       setTimeout(() => { | ||||||
|  |         document.addEventListener('click', this.handleClickOutside); | ||||||
|  |       }, 0); | ||||||
|  |     } else { | ||||||
|  |       document.removeEventListener('click', this.handleClickOutside); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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); | ||||||
|  |     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 { | ||||||
|  |     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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public nextMonth(): void { | ||||||
|  |     this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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)); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     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')}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public handleKeydown(e: KeyboardEvent): void { | ||||||
|  |     if (e.key === 'Enter' || e.key === ' ') { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       this.toggleCalendar(); | ||||||
|  |     } else if (e.key === 'Escape' && this.isOpened) { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       this.isOpened = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public clearValue(e: Event): void { | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     this.value = ''; | ||||||
|  |     this.selectedDate = null; | ||||||
|  |     this.changeSubject.next(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |       this.selectedHour = parsedDate.getHours(); | ||||||
|  |       this.selectedMinute = parsedDate.getMinutes(); | ||||||
|  |       this.changeSubject.next(this); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public handleInputBlur(e: FocusEvent): void { | ||||||
|  |     const input = e.target as HTMLInputElement; | ||||||
|  |     const inputValue = input.value.trim(); | ||||||
|  |      | ||||||
|  |     if (!inputValue) { | ||||||
|  |       this.value = ''; | ||||||
|  |       this.selectedDate = null; | ||||||
|  |       this.changeSubject.next(this); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const parsedDate = this.parseManualDate(inputValue); | ||||||
|  |     if (parsedDate && !isNaN(parsedDate.getTime())) { | ||||||
|  |       this.value = parsedDate.toISOString(); | ||||||
|  |       this.selectedDate = parsedDate; | ||||||
|  |       this.viewDate = new Date(parsedDate); | ||||||
|  |       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); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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) { | ||||||
|  |         const [_, day, month, year] = euMatch; | ||||||
|  |         parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Format 3: MM/DD/YYYY (US) | ||||||
|  |     if (!parsedDate) { | ||||||
|  |       const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); | ||||||
|  |       if (usMatch) { | ||||||
|  |         const [_, month, day, year] = usMatch; | ||||||
|  |         parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // If no date was parsed, 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) { | ||||||
|  |         const [_, hours, minutes] = timeMatch; | ||||||
|  |         parsedDate.setHours(parseInt(hours)); | ||||||
|  |         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()); | ||||||
|  |       parsedDate.setSeconds(0); | ||||||
|  |       parsedDate.setMilliseconds(0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return parsedDate; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getValue(): string { | ||||||
|  |     return this.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public setValue(value: string): void { | ||||||
|  |     this.value = value; | ||||||
|  |     if (value) { | ||||||
|  |       try { | ||||||
|  |         const date = new Date(value); | ||||||
|  |         if (!isNaN(date.getTime())) { | ||||||
|  |           this.selectedDate = date; | ||||||
|  |           this.viewDate = new Date(date); | ||||||
|  |           this.selectedHour = date.getHours(); | ||||||
|  |           this.selectedMinute = date.getMinutes(); | ||||||
|  |         } | ||||||
|  |       } catch { | ||||||
|  |         // Invalid date | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										410
									
								
								ts_web/elements/dees-input-datepicker/demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								ts_web/elements/dees-input-datepicker/demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,410 @@ | |||||||
|  | import { html, css } from '@design.estate/dees-element'; | ||||||
|  | import '@design.estate/dees-wcctools/demotools'; | ||||||
|  | import '../dees-panel.js'; | ||||||
|  | import './component.js'; | ||||||
|  | import type { DeesInputDatepicker } from './component.js'; | ||||||
|  |  | ||||||
|  | export const demoFunc = () => html` | ||||||
|  |   <style> | ||||||
|  |     ${css` | ||||||
|  |       .demo-container { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         gap: 24px; | ||||||
|  |         padding: 24px; | ||||||
|  |         max-width: 1200px; | ||||||
|  |         margin: 0 auto; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       dees-panel { | ||||||
|  |         margin-bottom: 24px; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       dees-panel:last-child { | ||||||
|  |         margin-bottom: 0; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .demo-output { | ||||||
|  |         margin-top: 16px; | ||||||
|  |         padding: 12px; | ||||||
|  |         background: rgba(0, 105, 242, 0.1); | ||||||
|  |         border-radius: 4px; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-family: monospace; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       .date-group { | ||||||
|  |         display: flex; | ||||||
|  |         gap: 16px; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |       } | ||||||
|  |     `} | ||||||
|  |   </style> | ||||||
|  |    | ||||||
|  |   <div class="demo-container"> | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate basic date picker functionality | ||||||
|  |       const datePicker = elementArg.querySelector('dees-input-datepicker'); | ||||||
|  |        | ||||||
|  |       if (datePicker) { | ||||||
|  |         datePicker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           console.log('Basic date selected:', (event.target as DeesInputDatepicker).value); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Basic Date Picker'} .subtitle=${'Simple date selection without time'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Select Date" | ||||||
|  |           description="Choose a date from the calendar" | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate date and time picker | ||||||
|  |       const dateTimePicker = elementArg.querySelector('dees-input-datepicker[label="Event Date & Time"]'); | ||||||
|  |       const appointmentPicker = elementArg.querySelector('dees-input-datepicker[label="Appointment"]'); | ||||||
|  |        | ||||||
|  |       if (dateTimePicker) { | ||||||
|  |         dateTimePicker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           const value = (event.target as DeesInputDatepicker).value; | ||||||
|  |           console.log('24h format datetime:', value); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (appointmentPicker) { | ||||||
|  |         appointmentPicker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           const value = (event.target as DeesInputDatepicker).value; | ||||||
|  |           console.log('12h format datetime:', value); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Date and Time Selection'} .subtitle=${'Date pickers with time selection in different formats'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Event Date & Time" | ||||||
|  |           description="Select both date and time (24-hour format)" | ||||||
|  |           .enableTime=${true} | ||||||
|  |           timeFormat="24h" | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |          | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Appointment" | ||||||
|  |           description="Date and time with AM/PM selector (15-minute increments)" | ||||||
|  |           .enableTime=${true} | ||||||
|  |           timeFormat="12h" | ||||||
|  |           .minuteIncrement=${15} | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate timezone functionality | ||||||
|  |       const timezonePickers = elementArg.querySelectorAll('dees-input-datepicker'); | ||||||
|  |        | ||||||
|  |       timezonePickers.forEach((picker) => { | ||||||
|  |         picker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           const target = event.target as DeesInputDatepicker; | ||||||
|  |           console.log(`${target.label} value:`, target.value); | ||||||
|  |           const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement; | ||||||
|  |           if (input) { | ||||||
|  |             console.log(`${target.label} formatted:`, input.value); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Timezone Support'} .subtitle=${'Date and time selection with timezone awareness'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Meeting Time (with Timezone)" | ||||||
|  |           description="Select a date/time and timezone for the meeting" | ||||||
|  |           .enableTime=${true} | ||||||
|  |           .enableTimezone=${true} | ||||||
|  |           timeFormat="24h" | ||||||
|  |           timezone="America/New_York" | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |          | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Global Event Schedule" | ||||||
|  |           description="Schedule an event across different timezones" | ||||||
|  |           .enableTime=${true} | ||||||
|  |           .enableTimezone=${true} | ||||||
|  |           timeFormat="12h" | ||||||
|  |           timezone="Europe/London" | ||||||
|  |           .minuteIncrement=${30} | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate date constraints | ||||||
|  |       const futureDatePicker = elementArg.querySelector('dees-input-datepicker'); | ||||||
|  |        | ||||||
|  |       if (futureDatePicker) { | ||||||
|  |         // Show the min/max constraints in action | ||||||
|  |         futureDatePicker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           const value = (event.target as DeesInputDatepicker).value; | ||||||
|  |           if (value) { | ||||||
|  |             const selectedDate = new Date(value); | ||||||
|  |             const today = new Date(); | ||||||
|  |             const daysDiff = Math.floor((selectedDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); | ||||||
|  |             console.log(`Selected date is ${daysDiff} days from today`); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Date Range Constraints'} .subtitle=${'Limit selectable dates with min and max values'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Future Date Only" | ||||||
|  |           description="Can only select dates from today to 90 days in the future" | ||||||
|  |           .minDate=${new Date().toISOString()} | ||||||
|  |           .maxDate=${new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString()} | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate different date formats | ||||||
|  |       const formatters = { | ||||||
|  |         'DD/MM/YYYY': 'European', | ||||||
|  |         'MM/DD/YYYY': 'US', | ||||||
|  |         'YYYY-MM-DD': 'ISO' | ||||||
|  |       }; | ||||||
|  |        | ||||||
|  |       const datePickers = elementArg.querySelectorAll('dees-input-datepicker'); | ||||||
|  |       datePickers.forEach((picker) => { | ||||||
|  |         picker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           const target = event.target as DeesInputDatepicker; | ||||||
|  |           // Log the formatted value that's displayed in the input | ||||||
|  |           const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement; | ||||||
|  |           if (input) { | ||||||
|  |             console.log(`${target.label} format:`, input.value); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Date Formats'} .subtitle=${'Different date display formats for various regions'}> | ||||||
|  |         <div class="date-group"> | ||||||
|  |           <dees-input-datepicker | ||||||
|  |             label="European Format" | ||||||
|  |             dateFormat="DD/MM/YYYY" | ||||||
|  |             .value=${new Date().toISOString()} | ||||||
|  |           ></dees-input-datepicker> | ||||||
|  |            | ||||||
|  |           <dees-input-datepicker | ||||||
|  |             label="US Format" | ||||||
|  |             dateFormat="MM/DD/YYYY" | ||||||
|  |             .value=${new Date().toISOString()} | ||||||
|  |           ></dees-input-datepicker> | ||||||
|  |            | ||||||
|  |           <dees-input-datepicker | ||||||
|  |             label="ISO Format" | ||||||
|  |             dateFormat="YYYY-MM-DD" | ||||||
|  |             .value=${new Date().toISOString()} | ||||||
|  |           ></dees-input-datepicker> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate required field validation | ||||||
|  |       const requiredPicker = elementArg.querySelector('dees-input-datepicker[required]'); | ||||||
|  |        | ||||||
|  |       if (requiredPicker) { | ||||||
|  |         // Monitor blur events for validation | ||||||
|  |         requiredPicker.addEventListener('blur', () => { | ||||||
|  |           const picker = requiredPicker as DeesInputDatepicker; | ||||||
|  |           const value = picker.getValue(); | ||||||
|  |           if (!value) { | ||||||
|  |             console.log('Required date field is empty'); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Form States'} .subtitle=${'Required and disabled states'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Birth Date" | ||||||
|  |           description="This field is required" | ||||||
|  |           .required=${true} | ||||||
|  |           placeholder="Select your birth date" | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |          | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Disabled Date" | ||||||
|  |           description="This field cannot be edited" | ||||||
|  |           .disabled=${true} | ||||||
|  |           .value=${new Date().toISOString()} | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate week start customization | ||||||
|  |       const usPicker = elementArg.querySelector('dees-input-datepicker[label="US Calendar"]'); | ||||||
|  |       const euPicker = elementArg.querySelector('dees-input-datepicker[label="EU Calendar"]'); | ||||||
|  |        | ||||||
|  |       if (usPicker) { | ||||||
|  |         console.log('US Calendar starts on Sunday (0)'); | ||||||
|  |       } | ||||||
|  |       if (euPicker) { | ||||||
|  |         console.log('EU Calendar starts on Monday (1)'); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Calendar Customization'} .subtitle=${'Different week start days for various regions'}> | ||||||
|  |         <div class="date-group"> | ||||||
|  |           <dees-input-datepicker | ||||||
|  |             label="US Calendar" | ||||||
|  |             description="Week starts on Sunday" | ||||||
|  |             .weekStartsOn=${0} | ||||||
|  |           ></dees-input-datepicker> | ||||||
|  |            | ||||||
|  |           <dees-input-datepicker | ||||||
|  |             label="EU Calendar" | ||||||
|  |             description="Week starts on Monday" | ||||||
|  |             .weekStartsOn=${1} | ||||||
|  |           ></dees-input-datepicker> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Generate weekend dates for the current month | ||||||
|  |       const generateWeekends = () => { | ||||||
|  |         const weekends = []; | ||||||
|  |         const now = new Date(); | ||||||
|  |         const year = now.getFullYear(); | ||||||
|  |         const month = now.getMonth(); | ||||||
|  |          | ||||||
|  |         // Get all weekends for current month | ||||||
|  |         const date = new Date(year, month, 1); | ||||||
|  |         while (date.getMonth() === month) { | ||||||
|  |           if (date.getDay() === 0 || date.getDay() === 6) { | ||||||
|  |             weekends.push(new Date(date).toISOString()); | ||||||
|  |           } | ||||||
|  |           date.setDate(date.getDate() + 1); | ||||||
|  |         } | ||||||
|  |         return weekends; | ||||||
|  |       }; | ||||||
|  |        | ||||||
|  |       const picker = elementArg.querySelector('dees-input-datepicker'); | ||||||
|  |       if (picker) { | ||||||
|  |         picker.disabledDates = generateWeekends(); | ||||||
|  |         console.log('Disabled weekend dates for current month'); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Disabled Dates'} .subtitle=${'Calendar with specific dates disabled (weekends in current month)'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Availability Calendar" | ||||||
|  |           description="Weekends are disabled for the current month" | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Generate sample events for the calendar | ||||||
|  |       const today = new Date(); | ||||||
|  |       const currentMonth = today.getMonth(); | ||||||
|  |       const currentYear = today.getFullYear(); | ||||||
|  |        | ||||||
|  |       const sampleEvents = [ | ||||||
|  |         // Current week events | ||||||
|  |         { | ||||||
|  |           date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}`, | ||||||
|  |           title: "Team Meeting", | ||||||
|  |           type: "info" as const, | ||||||
|  |           count: 2 | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 1).toString().padStart(2, '0')}`, | ||||||
|  |           title: "Project Deadline", | ||||||
|  |           type: "warning" as const | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 2).toString().padStart(2, '0')}`, | ||||||
|  |           title: "Release Day", | ||||||
|  |           type: "success" as const | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 5).toString().padStart(2, '0')}`, | ||||||
|  |           title: "Urgent Fix Required", | ||||||
|  |           type: "error" as const | ||||||
|  |         }, | ||||||
|  |         // Multiple events on one day | ||||||
|  |         { | ||||||
|  |           date: `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${(today.getDate() + 7).toString().padStart(2, '0')}`, | ||||||
|  |           title: "Multiple Events Today", | ||||||
|  |           type: "info" as const, | ||||||
|  |           count: 5 | ||||||
|  |         }, | ||||||
|  |         // Next month event | ||||||
|  |         { | ||||||
|  |           date: `${currentYear}-${(currentMonth + 2).toString().padStart(2, '0')}-15`, | ||||||
|  |           title: "Future Planning Session", | ||||||
|  |           type: "info" as const | ||||||
|  |         } | ||||||
|  |       ]; | ||||||
|  |        | ||||||
|  |       const picker = elementArg.querySelector('dees-input-datepicker'); | ||||||
|  |       if (picker) { | ||||||
|  |         picker.events = sampleEvents; | ||||||
|  |         console.log('Calendar events loaded:', sampleEvents); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Calendar with Events'} .subtitle=${'Visual feedback for scheduled events'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Event Calendar" | ||||||
|  |           description="Days with colored dots have events. Hover to see details." | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |          | ||||||
|  |         <div class="demo-output" style="margin-top: 16px;"> | ||||||
|  |           <strong>Event Legend:</strong><br> | ||||||
|  |           <span style="color: #0969da;">● Info</span> |  | ||||||
|  |           <span style="color: #d29922;">● Warning</span> |  | ||||||
|  |           <span style="color: #2ea043;">● Success</span> |  | ||||||
|  |           <span style="color: #cf222e;">● Error</span><br> | ||||||
|  |           <em>Days with more than 3 events show a count badge</em> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Interactive event demonstration | ||||||
|  |       const picker = elementArg.querySelector('dees-input-datepicker'); | ||||||
|  |       const output = elementArg.querySelector('#event-output'); | ||||||
|  |        | ||||||
|  |       if (picker && output) { | ||||||
|  |         picker.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           const target = event.target as DeesInputDatepicker; | ||||||
|  |           const value = target.value; | ||||||
|  |           if (value) { | ||||||
|  |             const date = new Date(value); | ||||||
|  |             // Get the formatted value from the input element | ||||||
|  |             const input = target.shadowRoot?.querySelector('.date-input') as HTMLInputElement; | ||||||
|  |             const formattedValue = input?.value || 'N/A'; | ||||||
|  |             output.innerHTML = ` | ||||||
|  |               <strong>Event triggered!</strong><br> | ||||||
|  |               ISO Value: ${value}<br> | ||||||
|  |               Formatted: ${formattedValue}<br> | ||||||
|  |               Date object: ${date.toLocaleString()} | ||||||
|  |             `; | ||||||
|  |           } else { | ||||||
|  |             output.innerHTML = '<em>Date cleared</em>'; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         picker.addEventListener('blur', () => { | ||||||
|  |           console.log('Datepicker lost focus'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|  |       <dees-panel .title=${'Event Handling'} .subtitle=${'Interactive demonstration of change events'}> | ||||||
|  |         <dees-input-datepicker | ||||||
|  |           label="Event Demo" | ||||||
|  |           description="Select a date to see the event details" | ||||||
|  |         ></dees-input-datepicker> | ||||||
|  |          | ||||||
|  |         <div id="event-output" class="demo-output"> | ||||||
|  |           <em>Select a date to see event details...</em> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |   </div> | ||||||
|  | `; | ||||||
							
								
								
									
										4
									
								
								ts_web/elements/dees-input-datepicker/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								ts_web/elements/dees-input-datepicker/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | export * from './component.js'; | ||||||
|  | export { datepickerStyles } from './styles.js'; | ||||||
|  | export { renderDatepicker } from './template.js'; | ||||||
|  | export type { IDateEvent } from './types.js'; | ||||||
							
								
								
									
										514
									
								
								ts_web/elements/dees-input-datepicker/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								ts_web/elements/dees-input-datepicker/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,514 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  | import { DeesInputBase } from '../dees-input-base.js'; | ||||||
|  |  | ||||||
|  | export const datepickerStyles = [ | ||||||
|  |     ...DeesInputBase.baseStyles, | ||||||
|  |     cssManager.defaultStyles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         display: block; | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .input-container { | ||||||
|  |         position: relative; | ||||||
|  |         width: 100%; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .date-input { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 40px; | ||||||
|  |         padding: 0 12px; | ||||||
|  |         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%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         font-size: 14px; | ||||||
|  |         line-height: 1.5; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; | ||||||
|  |         cursor: pointer; | ||||||
|  |         transition: all 0.2s ease; | ||||||
|  |         outline: none; | ||||||
|  |         font-family: inherit; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .date-input::placeholder { | ||||||
|  |         color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .date-input:hover:not(:disabled) { | ||||||
|  |         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%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .date-input:focus, | ||||||
|  |       .date-input.open { | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; | ||||||
|  |         outline: 2px solid transparent; | ||||||
|  |         outline-offset: 2px; | ||||||
|  |         box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}, | ||||||
|  |                     0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .date-input:disabled { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Icon container using flexbox for better positioning */ | ||||||
|  |       .icon-container { | ||||||
|  |         position: absolute; | ||||||
|  |         right: 0; | ||||||
|  |         top: 0; | ||||||
|  |         bottom: 0; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 4px; | ||||||
|  |         padding: 0 12px; | ||||||
|  |         pointer-events: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .icon-container > * { | ||||||
|  |         pointer-events: auto; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .calendar-icon { | ||||||
|  |         color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         pointer-events: none; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .clear-button { | ||||||
|  |         width: 20px; | ||||||
|  |         height: 20px; | ||||||
|  |         border: none; | ||||||
|  |         background: transparent; | ||||||
|  |         cursor: pointer; | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         transition: opacity 0.2s ease, background-color 0.2s ease; | ||||||
|  |         padding: 0; | ||||||
|  |         flex-shrink: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .clear-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%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .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)')}; | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
							
								
								
									
										179
									
								
								ts_web/elements/dees-input-datepicker/template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								ts_web/elements/dees-input-datepicker/template.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | |||||||
|  | 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> | ||||||
|  |           <div class="input-container"> | ||||||
|  |             <input | ||||||
|  |               type="text" | ||||||
|  |               class="date-input ${component.isOpened ? 'open' : ''}" | ||||||
|  |               .value=${component.formatDate(component.value)} | ||||||
|  |               .placeholder=${component.placeholder} | ||||||
|  |               ?disabled=${component.disabled} | ||||||
|  |               @click=${component.toggleCalendar} | ||||||
|  |               @keydown=${component.handleKeydown} | ||||||
|  |               @input=${component.handleManualInput} | ||||||
|  |               @blur=${component.handleInputBlur} | ||||||
|  |               style="padding-right: ${component.value ? '64px' : '40px'}" | ||||||
|  |             /> | ||||||
|  |             <div class="icon-container"> | ||||||
|  |               ${component.value && !component.disabled ? html` | ||||||
|  |                 <button class="clear-button" @click=${component.clearValue} title="Clear"> | ||||||
|  |                   <dees-icon icon="lucide:x" iconSize="14"></dees-icon> | ||||||
|  |                 </button> | ||||||
|  |               ` : ''} | ||||||
|  |               <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> | ||||||
|  |         </div> | ||||||
|  |       `; | ||||||
|  |    | ||||||
|  | }; | ||||||
							
								
								
									
										7
									
								
								ts_web/elements/dees-input-datepicker/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ts_web/elements/dees-input-datepicker/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | export interface IDateEvent { | ||||||
|  |   date: string; // ISO date string (YYYY-MM-DD) | ||||||
|  |   title?: string; | ||||||
|  |   description?: string; | ||||||
|  |   type?: 'info' | 'warning' | 'success' | 'error'; | ||||||
|  |   count?: number; // Number of events on this day | ||||||
|  | } | ||||||
| @@ -5,45 +5,63 @@ import './dees-form.js'; | |||||||
| import './dees-form-submit.js'; | import './dees-form-submit.js'; | ||||||
|  |  | ||||||
| export const demoFunc = () => html` | export const demoFunc = () => html` | ||||||
|   <dees-demowrapper> |   <style> | ||||||
|     <style> |     ${css` | ||||||
|       ${css` |       .demo-container { | ||||||
|         .demo-container { |         display: flex; | ||||||
|           display: flex; |         flex-direction: column; | ||||||
|           flex-direction: column; |         gap: 24px; | ||||||
|           gap: 24px; |         padding: 24px; | ||||||
|           padding: 24px; |         max-width: 1200px; | ||||||
|           max-width: 1200px; |         margin: 0 auto; | ||||||
|           margin: 0 auto; |       } | ||||||
|         } |  | ||||||
|        |        | ||||||
|         dees-panel { |       dees-panel { | ||||||
|           margin-bottom: 24px; |         margin-bottom: 24px; | ||||||
|         } |       } | ||||||
|        |        | ||||||
|         dees-panel:last-child { |       dees-panel:last-child { | ||||||
|           margin-bottom: 0; |         margin-bottom: 0; | ||||||
|         } |       } | ||||||
|        |        | ||||||
|         .horizontal-group { |       .horizontal-group { | ||||||
|           display: flex; |         display: flex; | ||||||
|           align-items: center; |         align-items: center; | ||||||
|           gap: 16px; |         gap: 16px; | ||||||
|           flex-wrap: wrap; |         flex-wrap: wrap; | ||||||
|         } |       } | ||||||
|        |        | ||||||
|         .spacer { |       .spacer { | ||||||
|           height: 200px; |         height: 200px; | ||||||
|           display: flex; |         display: flex; | ||||||
|           align-items: center; |         align-items: center; | ||||||
|           justify-content: center; |         justify-content: center; | ||||||
|           color: #999; |         color: #999; | ||||||
|           font-size: 14px; |         font-size: 14px; | ||||||
|         } |       } | ||||||
|       `} |     `} | ||||||
|     </style> |   </style> | ||||||
|    |    | ||||||
|     <div class="demo-container"> |   <div class="demo-container"> | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate programmatic interaction with basic dropdowns | ||||||
|  |       const countryDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Country"]'); | ||||||
|  |       const roleDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Role"]'); | ||||||
|  |        | ||||||
|  |       // Log when country changes | ||||||
|  |       if (countryDropdown) { | ||||||
|  |         countryDropdown.addEventListener('selectedOption', (event: CustomEvent) => { | ||||||
|  |           console.log('Country selected:', event.detail); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Log when role changes | ||||||
|  |       if (roleDropdown) { | ||||||
|  |         roleDropdown.addEventListener('selectedOption', (event: CustomEvent) => { | ||||||
|  |           console.log('Role selected:', event.detail); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}> |       <dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}> | ||||||
|         <dees-input-dropdown |         <dees-input-dropdown | ||||||
|           .label=${'Select Country'} |           .label=${'Select Country'} | ||||||
| @@ -70,7 +88,18 @@ export const demoFunc = () => html` | |||||||
|           ]} |           ]} | ||||||
|         ></dees-input-dropdown> |         ></dees-input-dropdown> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|      |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate simpler dropdown without search | ||||||
|  |       const priorityDropdown = elementArg.querySelector('dees-input-dropdown'); | ||||||
|  |        | ||||||
|  |       if (priorityDropdown) { | ||||||
|  |         priorityDropdown.addEventListener('selectedOption', (event: CustomEvent) => { | ||||||
|  |           console.log(`Priority changed to: ${event.detail.option}`); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}> |       <dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}> | ||||||
|         <dees-input-dropdown |         <dees-input-dropdown | ||||||
|           .label=${'Priority Level'} |           .label=${'Priority Level'} | ||||||
| @@ -83,7 +112,20 @@ export const demoFunc = () => html` | |||||||
|           .selectedOption=${{ option: 'Medium', key: 'medium' }} |           .selectedOption=${{ option: 'Medium', key: 'medium' }} | ||||||
|         ></dees-input-dropdown> |         ></dees-input-dropdown> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|      |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate horizontal layout with multiple dropdowns | ||||||
|  |       const dropdowns = elementArg.querySelectorAll('dees-input-dropdown'); | ||||||
|  |        | ||||||
|  |       // Log all changes from horizontal dropdowns | ||||||
|  |       dropdowns.forEach((dropdown) => { | ||||||
|  |         dropdown.addEventListener('selectedOption', (event: CustomEvent) => { | ||||||
|  |           const label = dropdown.getAttribute('label'); | ||||||
|  |           console.log(`${label}: ${event.detail.option}`); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}> |       <dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}> | ||||||
|         <div class="horizontal-group"> |         <div class="horizontal-group"> | ||||||
|           <dees-input-dropdown |           <dees-input-dropdown | ||||||
| @@ -120,7 +162,19 @@ export const demoFunc = () => html` | |||||||
|           ></dees-input-dropdown> |           ></dees-input-dropdown> | ||||||
|         </div> |         </div> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|      |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate state handling | ||||||
|  |       const requiredDropdown = elementArg.querySelector('dees-input-dropdown[required]'); | ||||||
|  |        | ||||||
|  |       if (requiredDropdown) { | ||||||
|  |         // Show validation state changes | ||||||
|  |         requiredDropdown.addEventListener('blur', () => { | ||||||
|  |           console.log('Required dropdown lost focus'); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}> |       <dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}> | ||||||
|         <dees-input-dropdown |         <dees-input-dropdown | ||||||
|           .label=${'Required Field'} |           .label=${'Required Field'} | ||||||
| @@ -141,11 +195,25 @@ export const demoFunc = () => html` | |||||||
|           .selectedOption=${{ option: 'Cannot Select', key: 'disabled' }} |           .selectedOption=${{ option: 'Cannot Select', key: 'disabled' }} | ||||||
|         ></dees-input-dropdown> |         ></dees-input-dropdown> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|      |      | ||||||
|       <div class="spacer"> |     <div class="spacer"> | ||||||
|         (Spacer to test dropdown positioning) |       (Spacer to test dropdown positioning) | ||||||
|       </div> |     </div> | ||||||
|      |      | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // This dropdown demonstrates automatic positioning | ||||||
|  |       const dropdown = elementArg.querySelector('dees-input-dropdown'); | ||||||
|  |        | ||||||
|  |       if (dropdown) { | ||||||
|  |         dropdown.addEventListener('selectedOption', (event: CustomEvent) => { | ||||||
|  |           console.log('Bottom dropdown selected:', event.detail); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // Note: The dropdown automatically detects available space | ||||||
|  |         // and opens upward when near the bottom of the viewport | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}> |       <dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}> | ||||||
|         <dees-input-dropdown |         <dees-input-dropdown | ||||||
|           .label=${'Opens Upward'} |           .label=${'Opens Upward'} | ||||||
| @@ -158,7 +226,30 @@ export const demoFunc = () => html` | |||||||
|           ]} |           ]} | ||||||
|         ></dees-input-dropdown> |         ></dees-input-dropdown> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Setup the interactive payload display | ||||||
|  |       const dropdown = elementArg.querySelector('dees-input-dropdown'); | ||||||
|  |       const output = elementArg.querySelector('#selection-output'); | ||||||
|  |        | ||||||
|  |       if (dropdown && output) { | ||||||
|  |         // Initialize output | ||||||
|  |         output.innerHTML = '<em>Select a product to see details...</em>'; | ||||||
|  |          | ||||||
|  |         // Handle dropdown changes | ||||||
|  |         dropdown.addEventListener('change', (event: CustomEvent) => { | ||||||
|  |           if (event.detail.value) { | ||||||
|  |             output.innerHTML = ` | ||||||
|  |               <strong>Selected:</strong> ${event.detail.value.option}<br> | ||||||
|  |               <strong>Key:</strong> ${event.detail.value.key}<br> | ||||||
|  |               <strong>Price:</strong> $${event.detail.value.payload?.price || 'N/A'}<br> | ||||||
|  |               <strong>Features:</strong> ${event.detail.value.payload?.features?.join(', ') || 'N/A'} | ||||||
|  |             `; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}> |       <dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}> | ||||||
|         <dees-input-dropdown |         <dees-input-dropdown | ||||||
|           .label=${'Select Product'} |           .label=${'Select Product'} | ||||||
| @@ -167,24 +258,35 @@ export const demoFunc = () => html` | |||||||
|             { option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } }, |             { option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } }, | ||||||
|             { option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } } |             { option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } } | ||||||
|           ]} |           ]} | ||||||
|           @change=${(e: CustomEvent) => { |  | ||||||
|             const output = document.querySelector('#selection-output'); |  | ||||||
|             if (output && e.detail.value) { |  | ||||||
|               output.innerHTML = ` |  | ||||||
|                 <strong>Selected:</strong> ${e.detail.value.option}<br> |  | ||||||
|                 <strong>Key:</strong> ${e.detail.value.key}<br> |  | ||||||
|                 <strong>Price:</strong> $${e.detail.value.payload?.price || 'N/A'}<br> |  | ||||||
|                 <strong>Features:</strong> ${e.detail.value.payload?.features?.join(', ') || 'N/A'} |  | ||||||
|               `; |  | ||||||
|             } |  | ||||||
|           }} |  | ||||||
|         ></dees-input-dropdown> |         ></dees-input-dropdown> | ||||||
|          |          | ||||||
|         <div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;"> |         <div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;"></div> | ||||||
|           <em>Select a product to see details...</em> |  | ||||||
|         </div> |  | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|  |     </dees-demowrapper> | ||||||
|  |  | ||||||
|  |     <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { | ||||||
|  |       // Demonstrate form integration and validation | ||||||
|  |       const form = elementArg.querySelector('dees-form'); | ||||||
|  |       const projectTypeDropdown = elementArg.querySelector('dees-input-dropdown[key="projectType"]'); | ||||||
|  |       const frameworkDropdown = elementArg.querySelector('dees-input-dropdown[key="framework"]'); | ||||||
|  |        | ||||||
|  |       if (form) { | ||||||
|  |         form.addEventListener('formData', (event: CustomEvent) => { | ||||||
|  |           console.log('Form submitted with data:', event.detail.data); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (projectTypeDropdown && frameworkDropdown) { | ||||||
|  |         // Filter frameworks based on project type | ||||||
|  |         projectTypeDropdown.addEventListener('selectedOption', (event: CustomEvent) => { | ||||||
|  |           const selectedType = event.detail.key; | ||||||
|  |           console.log(`Project type changed to: ${selectedType}`); | ||||||
|  |            | ||||||
|  |           // In a real app, you could filter the framework options based on project type | ||||||
|  |           // For demo purposes, we just log the change | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }}> | ||||||
|       <dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}> |       <dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}> | ||||||
|         <dees-form> |         <dees-form> | ||||||
|           <dees-input-dropdown |           <dees-input-dropdown | ||||||
| @@ -216,6 +318,6 @@ export const demoFunc = () => html` | |||||||
|           <dees-form-submit .text=${'Create Project'}></dees-form-submit> |           <dees-form-submit .text=${'Create Project'}></dees-form-submit> | ||||||
|         </dees-form> |         </dees-form> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|     </div> |     </dees-demowrapper> | ||||||
|   </dees-demowrapper> |   </div> | ||||||
| ` | ` | ||||||
| @@ -9,8 +9,8 @@ import { | |||||||
| } from '@design.estate/dees-element'; | } from '@design.estate/dees-element'; | ||||||
| import * as domtools from '@design.estate/dees-domtools'; | import * as domtools from '@design.estate/dees-domtools'; | ||||||
| import { demoFunc } from './dees-input-dropdown.demo.js'; | import { demoFunc } from './dees-input-dropdown.demo.js'; | ||||||
| import { DeesWindowLayer } from './dees-windowlayer.js'; |  | ||||||
| import { DeesInputBase } from './dees-input-base.js'; | import { DeesInputBase } from './dees-input-base.js'; | ||||||
|  | import { cssGeistFontFamily } from './00fonts.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -39,13 +39,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> { | |||||||
|     this.selectedOption = val; |     this.selectedOption = val; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   @property({ |   @property({ | ||||||
|     type: Boolean, |     type: Boolean, | ||||||
|   }) |   }) | ||||||
|   public enableSearch: boolean = true; |   public enableSearch: boolean = true; | ||||||
|  |  | ||||||
|  |  | ||||||
|   @state() |   @state() | ||||||
|   public opensToTop: boolean = false; |   public opensToTop: boolean = false; | ||||||
|  |  | ||||||
| @@ -58,6 +56,9 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> { | |||||||
|   @state() |   @state() | ||||||
|   public isOpened = false; |   public isOpened = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   private searchValue: string = ''; | ||||||
|  |  | ||||||
|   public static styles = [ |   public static styles = [ | ||||||
|     ...DeesInputBase.baseStyles, |     ...DeesInputBase.baseStyles, | ||||||
|     cssManager.defaultStyles, |     cssManager.defaultStyles, | ||||||
| @@ -67,123 +68,201 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       :host { |       :host { | ||||||
|         font-family: Roboto; |         font-family: ${cssGeistFontFamily}; | ||||||
|         position: relative; |         position: relative; | ||||||
|         color: ${cssManager.bdTheme('#222', '#fff')}; |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .maincontainer { |       .maincontainer { | ||||||
|         display: block; |         display: block; | ||||||
|  |         position: relative; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |  | ||||||
|       .selectedBox { |       .selectedBox { | ||||||
|         user-select: none; |         user-select: none; | ||||||
|         position: relative; |         position: relative; | ||||||
|         max-width: 420px; |         width: 100%; | ||||||
|         height: 40px; |         height: 40px; | ||||||
|         line-height: 40px; |         line-height: 38px; | ||||||
|         padding: 0px 8px; |         padding: 0 40px 0 12px; | ||||||
|         background: ${cssManager.bdTheme('#fafafa', '#222222')}; |         background: transparent; | ||||||
|         box-shadow: ${cssManager.bdTheme('0px 1px 4px rgba(0,0,0,0.3)', 'none')}; |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         border-radius: 3px; |         border-radius: 6px; | ||||||
|         border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')}; |         transition: all 0.15s ease; | ||||||
|         border-bottom: ${cssManager.bdTheme('1px solid #CCC', '1px solid #222')}; |         font-size: 14px; | ||||||
|         transition: all 0.2s ease; |         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; | ||||||
|         font-size: 16px; |         cursor: pointer; | ||||||
|         color: ${cssManager.bdTheme('#222', '#ccc')}; |         overflow: hidden; | ||||||
|  |         text-overflow: ellipsis; | ||||||
|  |         white-space: nowrap; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .selectedBox:hover { |       .selectedBox:hover:not(.disabled) { | ||||||
|         filter: ${cssManager.bdTheme('brightness(0.95)', 'brightness(1.1)')}; |         border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .accentBottom { |       .selectedBox:focus-visible { | ||||||
|         filter: none !important; |         outline: none; | ||||||
|  |         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)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .accentTop { |       .selectedBox.disabled { | ||||||
|         filter: none !important; |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Dropdown arrow */ | ||||||
|  |       .selectedBox::after { | ||||||
|  |         content: ''; | ||||||
|  |         position: absolute; | ||||||
|  |         right: 12px; | ||||||
|  |         top: 50%; | ||||||
|  |         transform: translateY(-50%); | ||||||
|  |         width: 0; | ||||||
|  |         height: 0; | ||||||
|  |         border-left: 4px solid transparent; | ||||||
|  |         border-right: 4px solid transparent; | ||||||
|  |         border-top: 4px solid ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|  |         transition: transform 0.15s ease; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .selectedBox.open::after { | ||||||
|  |         transform: translateY(-50%) rotate(180deg); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .selectionBox { |       .selectionBox { | ||||||
|         will-change: transform; |         will-change: transform, opacity; | ||||||
|         pointer-events: none; |         pointer-events: none; | ||||||
|         transition: all 0.2s ease; |         transition: all 0.15s ease; | ||||||
|         opacity: 0; |         opacity: 0; | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#222222')}; |         transform: translateY(-8px) scale(0.98); | ||||||
|         max-width: 420px; |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; | ||||||
|         box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2); |         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; |         min-height: 40px; | ||||||
|         border-radius: 8px; |         max-height: 300px; | ||||||
|         padding: 4px 8px; |         overflow: hidden; | ||||||
|  |         border-radius: 6px; | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         user-select: none; |         user-select: none; | ||||||
|         margin: 3px 0px 0px 0px; |         margin-top: 4px; | ||||||
|         border-top: ${cssManager.bdTheme('1px solid #CCC', '1px solid #ffffff10')}; |         z-index: 50; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|       } |       } | ||||||
|  |        | ||||||
|       .selectionBox.top { |       .selectionBox.top { | ||||||
|         transform: translate(0px, 4px); |         bottom: calc(100% + 4px); | ||||||
|  |         top: auto; | ||||||
|  |         margin-top: 0; | ||||||
|  |         margin-bottom: 4px; | ||||||
|  |         transform: translateY(8px) scale(0.98); | ||||||
|       } |       } | ||||||
|  |        | ||||||
|       .selectionBox.bottom { |       .selectionBox.bottom { | ||||||
|         transform: translate(0px, -4px); |         top: 100%; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .selectionBox.show { |       .selectionBox.show { | ||||||
|         pointer-events: all; |         pointer-events: all; | ||||||
|         transform: scale(1, 1) translate(0px, 0px); |         transform: translateY(0) scale(1); | ||||||
|         opacity: 1; |         opacity: 1; | ||||||
|         box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.8); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       /* Options container */ | ||||||
|  |       .options-container { | ||||||
|  |         max-height: 250px; | ||||||
|  |         overflow-y: auto; | ||||||
|  |         padding: 4px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       /* Options */ | ||||||
|       .option { |       .option { | ||||||
|         transition: all 0.1s; |         transition: all 0.15s ease; | ||||||
|         line-height: 32px; |         line-height: 32px; | ||||||
|         padding: 0px 4px; |         padding: 0 8px; | ||||||
|         border-radius: 3px; |         border-radius: 4px; | ||||||
|         margin: 4px 0px; |         margin: 2px 0; | ||||||
|  |         cursor: pointer; | ||||||
|  |         font-size: 14px; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .option.highlighted { |       .option.highlighted { | ||||||
|         border-left: 2px solid #0069f2; |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         padding-left: 6px; |  | ||||||
|         background: #ffffff20; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .option:hover { |       .option:hover { | ||||||
|         color: #fff; |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|         padding-left: 8px; |         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; | ||||||
|         background: #0069f2; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .search.top { |       /* No options message */ | ||||||
|         padding-top: 4px; |       .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 { |       .search.bottom { | ||||||
|         padding-bottom: 4px; |         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 { |       .search input { | ||||||
|         display: block; |         display: block; | ||||||
|         background: none; |  | ||||||
|         border: none; |  | ||||||
|         height: 24px; |  | ||||||
|         color: inherit; |  | ||||||
|         text-align: left; |  | ||||||
|         font-size: 12px; |  | ||||||
|         font-weight: 600; |  | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         margin: auto; |         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.top input { |       .search input::placeholder { | ||||||
|         border-bottom: 1px dotted #333; |         color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')}; | ||||||
|       } |  | ||||||
|       .search.bottom input { |  | ||||||
|         border-top: 1px dotted #333; |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .search input:focus { |       .search input:focus { | ||||||
|         outline: none; |         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 { | ||||||
|  |         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%)')}; | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| @@ -191,61 +270,78 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> { | |||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <div class="input-wrapper"> |       <div class="input-wrapper"> | ||||||
|         <dees-label .label=${this.label}></dees-label> |         <dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label> | ||||||
|         <div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}"> |         <div class="maincontainer"> | ||||||
|           <div class="selectionBox"> |           <div | ||||||
|           ${this.enableSearch && !this.opensToTop |             class="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}" | ||||||
|             ? html` |             @click="${() => !this.disabled && this.toggleSelectionBox()}" | ||||||
|                 <div class="search top"> |             tabindex="${this.disabled ? '-1' : '0'}" | ||||||
|                   <input type="text" placeholder="Search" @input="${this.handleSearch}" /> |             @keydown="${this.handleSelectedBoxKeydown}" | ||||||
|                 </div> |           > | ||||||
|               ` |             ${this.selectedOption?.option || 'Select an option'} | ||||||
|             : null} |           </div> | ||||||
|           ${this.filteredOptions.map((option, index) => { |           <div class="selectionBox ${this.isOpened ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}"> | ||||||
|             const isHighlighted = this.highlightedIndex === index; |             ${this.enableSearch | ||||||
|             return html` |               ? html` | ||||||
|               <div |                   <div class="search"> | ||||||
|                 class="option ${isHighlighted ? 'highlighted' : ''}" |                     <input  | ||||||
|                 @click=${() => { |                       type="text"  | ||||||
|                   this.updateSelection(option); |                       placeholder="Search options..."  | ||||||
|                 }} |                       .value="${this.searchValue}" | ||||||
|               > |                       @input="${this.handleSearch}" | ||||||
|                 ${option.option} |                       @click="${(e: Event) => e.stopPropagation()}" | ||||||
|               </div> |                       @keydown="${this.handleSearchKeydown}" | ||||||
|             `; |                     /> | ||||||
|           })} |                   </div> | ||||||
|           ${this.enableSearch && this.opensToTop |                 ` | ||||||
|             ? html` |               : null} | ||||||
|                 <div class="search bottom"> |             <div class="options-container"> | ||||||
|                   <input type="text" placeholder="Search" @input="${this.handleSearch}" /> |               ${this.filteredOptions.length === 0 | ||||||
|                 </div> |                 ? html`<div class="no-options">No options found</div>` | ||||||
|               ` |                 : this.filteredOptions.map((option, index) => { | ||||||
|             : null} |                     const isHighlighted = this.highlightedIndex === index; | ||||||
|         </div> |                     return html` | ||||||
|         <div |                       <div | ||||||
|           class="selectedBox" |                         class="option ${isHighlighted ? 'highlighted' : ''}" | ||||||
|           @click="${(event) => { |                         @click="${() => this.updateSelection(option)}" | ||||||
|             if (!this.isElevated) { |                         @mouseenter="${() => this.highlightedIndex = index}" | ||||||
|               this.toggleSelectionBox(); |                       > | ||||||
|             } else { |                         ${option.option} | ||||||
|               this.updateSelection(this.selectedOption); |                       </div> | ||||||
|             } |                     `; | ||||||
|           }}" |                   }) | ||||||
|         > |               } | ||||||
|           ${this.selectedOption?.option || 'Select...'}  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   firstUpdated() { |   async connectedCallback() { | ||||||
|     this.selectedOption = this.selectedOption || null; |     super.connectedCallback(); | ||||||
|     this.filteredOptions = this.options; // Initialize filteredOptions |     this.handleClickOutside = this.handleClickOutside.bind(this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async updateSelection(selectedOption) { |   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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) { | ||||||
|     this.selectedOption = selectedOption; |     this.selectedOption = selectedOption; | ||||||
|  |     this.isOpened = false; | ||||||
|  |     this.searchValue = ''; | ||||||
|  |     this.filteredOptions = this.options; | ||||||
|  |     this.highlightedIndex = 0; | ||||||
|  |  | ||||||
|     this.dispatchEvent( |     this.dispatchEvent( | ||||||
|       new CustomEvent('selectedOption', { |       new CustomEvent('selectedOption', { | ||||||
| @@ -253,135 +349,105 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> { | |||||||
|         bubbles: true, |         bubbles: true, | ||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|     if (this.isElevated) { |      | ||||||
|       this.toggleSelectionBox(); |  | ||||||
|     } |  | ||||||
|     this.changeSubject.next(this); |     this.changeSubject.next(this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private isElevated: boolean = false; |   private handleClickOutside = (event: MouseEvent) => { | ||||||
|   private windowOverlay: DeesWindowLayer; |     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() { |   public async toggleSelectionBox() { | ||||||
|     this.isOpened = !this.isOpened; |     this.isOpened = !this.isOpened; | ||||||
|     const domtoolsInstance = await this.domtoolsPromise; |  | ||||||
|     const selectedBox: HTMLElement = this.shadowRoot.querySelector('.selectedBox'); |  | ||||||
|     const selectionBox: HTMLElement = this.shadowRoot.querySelector('.selectionBox'); |  | ||||||
|     if (!this.isElevated) { |  | ||||||
|       this.windowOverlay = await DeesWindowLayer.createAndShow({ |  | ||||||
|         blur: false, |  | ||||||
|       }); |  | ||||||
|       const elevatedDropdown = new DeesInputDropdown(); |  | ||||||
|       elevatedDropdown.isElevated = true; |  | ||||||
|       elevatedDropdown.label = this.label; |  | ||||||
|       elevatedDropdown.enableSearch = this.enableSearch; |  | ||||||
|       elevatedDropdown.required = this.required; |  | ||||||
|       elevatedDropdown.disabled = this.disabled; |  | ||||||
|       elevatedDropdown.style.position = 'fixed'; |  | ||||||
|       elevatedDropdown.style.top = this.getBoundingClientRect().top + 'px'; |  | ||||||
|       elevatedDropdown.style.left = this.getBoundingClientRect().left + 'px'; |  | ||||||
|       elevatedDropdown.style.width = this.clientWidth + 'px'; |  | ||||||
|      |      | ||||||
|       // Get z-index from registry for the elevated dropdown |     if (this.isOpened) { | ||||||
|       const dropdownZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex(); |       // Check available space and set position | ||||||
|       elevatedDropdown.style.zIndex = dropdownZIndex.toString(); |       const selectedBox = this.shadowRoot.querySelector('.selectedBox') as HTMLElement; | ||||||
|       (await import('./00zindex.js')).zIndexRegistry.register(elevatedDropdown, dropdownZIndex); |       const rect = selectedBox.getBoundingClientRect(); | ||||||
|       elevatedDropdown.options = this.options; |       const spaceBelow = window.innerHeight - rect.bottom; | ||||||
|       elevatedDropdown.selectedOption = this.selectedOption; |       const spaceAbove = rect.top; | ||||||
|       elevatedDropdown.highlightedIndex = elevatedDropdown.selectedOption ? elevatedDropdown.options.indexOf( |  | ||||||
|         elevatedDropdown.options.find((option) => option.key === this.selectedOption.key) |  | ||||||
|       ) : -1; |  | ||||||
|       console.log(elevatedDropdown.options); |  | ||||||
|       console.log(elevatedDropdown.selectedOption); |  | ||||||
|       console.log(elevatedDropdown.highlightedIndex); |  | ||||||
|       this.windowOverlay.appendChild(elevatedDropdown); |  | ||||||
|        |        | ||||||
|       // Prevent clicks on the dropdown from closing it |       // Determine if we should open upwards | ||||||
|       elevatedDropdown.addEventListener('click', (e: Event) => { |       this.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow; | ||||||
|         e.stopPropagation(); |  | ||||||
|       }); |  | ||||||
|        |        | ||||||
|       await domtoolsInstance.convenience.smartdelay.delayFor(0); |       // Focus search input if present | ||||||
|       elevatedDropdown.toggleSelectionBox(); |       await this.updateComplete; | ||||||
|       const destroyOverlay = async () => { |       const searchInput = this.shadowRoot.querySelector('.search input') as HTMLInputElement; | ||||||
|         (elevatedDropdown.shadowRoot.querySelector('.selectionBox') as HTMLElement).style.opacity = |       if (searchInput) { | ||||||
|           '0'; |         searchInput.focus(); | ||||||
|         elevatedDropdown.removeEventListener('selectedOption', handleSelection); |  | ||||||
|         this.windowOverlay.removeEventListener('clicked', destroyOverlay); |  | ||||||
|          |  | ||||||
|         // Unregister elevated dropdown from z-index registry |  | ||||||
|         (await import('./00zindex.js')).zIndexRegistry.unregister(elevatedDropdown); |  | ||||||
|          |  | ||||||
|         this.windowOverlay.destroy(); |  | ||||||
|       }; |  | ||||||
|       const handleSelection = async () => { |  | ||||||
|         await this.updateSelection(elevatedDropdown.selectedOption); |  | ||||||
|         destroyOverlay(); |  | ||||||
|       }; |  | ||||||
|       elevatedDropdown.addEventListener('selectedOption', handleSelection); |  | ||||||
|       this.windowOverlay.addEventListener('clicked', destroyOverlay); |  | ||||||
|     } else { |  | ||||||
|       if (!selectionBox.classList.contains('show')) { |  | ||||||
|         selectionBox.style.width = selectedBox.clientWidth + 'px'; |  | ||||||
|         const spaceData = selectedBox.getBoundingClientRect(); |  | ||||||
|         if (300 > window.innerHeight - spaceData.bottom) { |  | ||||||
|           this.opensToTop = true; |  | ||||||
|           selectedBox.classList.add('accentTop'); |  | ||||||
|           selectionBox.classList.add('top'); |  | ||||||
|           selectionBox.style.bottom = selectedBox.clientHeight + 2 + 'px'; |  | ||||||
|         } else { |  | ||||||
|           selectedBox.classList.add('accentBottom'); |  | ||||||
|           selectionBox.classList.add('bottom'); |  | ||||||
|           this.opensToTop = false; |  | ||||||
|           const labelOffset = this.label ? 24 : 0; |  | ||||||
|           selectionBox.style.top = selectedBox.clientHeight + labelOffset + 'px'; |  | ||||||
|         } |  | ||||||
|         await domtoolsInstance.convenience.smartdelay.delayFor(0); |  | ||||||
|         const searchInput = selectionBox.querySelector('input'); |  | ||||||
|         searchInput?.focus(); |  | ||||||
|          |  | ||||||
|         // Get z-index from registry for the selection box |  | ||||||
|         const selectionBoxZIndex = (await import('./00zindex.js')).zIndexRegistry.getNextZIndex(); |  | ||||||
|         selectionBox.style.zIndex = selectionBoxZIndex.toString(); |  | ||||||
|         (await import('./00zindex.js')).zIndexRegistry.register(selectionBox as HTMLElement, selectionBoxZIndex); |  | ||||||
|          |  | ||||||
|         selectionBox.classList.add('show'); |  | ||||||
|       } else { |  | ||||||
|         selectedBox.style.pointerEvents = 'none'; |  | ||||||
|         selectionBox.classList.remove('show'); |  | ||||||
|          |  | ||||||
|         // Unregister selection box from z-index registry |  | ||||||
|         (await import('./00zindex.js')).zIndexRegistry.unregister(selectionBox as HTMLElement); |  | ||||||
|          |  | ||||||
|         // selectedBox.style.opacity = '0'; |  | ||||||
|       } |       } | ||||||
|  |        | ||||||
|  |       // Add click outside listener | ||||||
|  |       setTimeout(() => { | ||||||
|  |         document.addEventListener('click', this.handleClickOutside); | ||||||
|  |       }, 0); | ||||||
|  |     } else { | ||||||
|  |       // Cleanup | ||||||
|  |       this.searchValue = ''; | ||||||
|  |       this.filteredOptions = this.options; | ||||||
|  |       document.removeEventListener('click', this.handleClickOutside); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private handleSearch(event: Event): void { |   private handleSearch(event: Event): void { | ||||||
|     const searchTerm = (event.target as HTMLInputElement).value.toLowerCase(); |     const searchTerm = (event.target as HTMLInputElement).value; | ||||||
|  |     this.searchValue = searchTerm; | ||||||
|  |     const searchLower = searchTerm.toLowerCase(); | ||||||
|     this.filteredOptions = this.options.filter((option) => |     this.filteredOptions = this.options.filter((option) => | ||||||
|       option.option.toLowerCase().includes(searchTerm) |       option.option.toLowerCase().includes(searchLower) | ||||||
|     ); |     ); | ||||||
|     this.highlightedIndex = 0; // Reset highlighted index |     this.highlightedIndex = 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private handleKeyDown(event: KeyboardEvent): void { |   private handleKeyDown(event: KeyboardEvent): void { | ||||||
|     if (!this.isOpened) { |  | ||||||
|       console.log('discarded key event. Check why this function is called.'); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     const key = event.key; |     const key = event.key; | ||||||
|     const maxIndex = this.filteredOptions.length - 1; |     const maxIndex = this.filteredOptions.length - 1; | ||||||
|  |  | ||||||
|     if (key === 'ArrowDown') { |     if (key === 'ArrowDown') { | ||||||
|  |       event.preventDefault(); | ||||||
|       this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1; |       this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1; | ||||||
|       event.preventDefault(); |  | ||||||
|     } else if (key === 'ArrowUp') { |     } else if (key === 'ArrowUp') { | ||||||
|  |       event.preventDefault(); | ||||||
|       this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1; |       this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1; | ||||||
|       event.preventDefault(); |  | ||||||
|     } else if (key === 'Enter') { |     } else if (key === 'Enter') { | ||||||
|       this.updateSelection(this.filteredOptions[this.highlightedIndex]); |  | ||||||
|       event.preventDefault(); |       event.preventDefault(); | ||||||
|  |       if (this.filteredOptions[this.highlightedIndex]) { | ||||||
|  |         this.updateSelection(this.filteredOptions[this.highlightedIndex]); | ||||||
|  |       } | ||||||
|  |     } else if (key === 'Escape') { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.isOpened = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleSearchKeydown(event: KeyboardEvent): void { | ||||||
|  |     if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') { | ||||||
|  |       this.handleKeyDown(event); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleSelectedBoxKeydown(event: KeyboardEvent) { | ||||||
|  |     if (this.disabled) return; | ||||||
|  |      | ||||||
|  |     if (event.key === 'Enter' || event.key === ' ') { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.toggleSelectionBox(); | ||||||
|  |     } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       if (!this.isOpened) { | ||||||
|  |         this.toggleSelectionBox(); | ||||||
|  |       } | ||||||
|  |     } else if (event.key === 'Escape') { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       if (this.isOpened) { | ||||||
|  |         this.isOpened = false; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -392,4 +458,9 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> { | |||||||
|   public setValue(value: { option: string; key: string; payload?: any }): void { |   public setValue(value: { option: string; key: string; payload?: any }): void { | ||||||
|     this.selectedOption = value; |     this.selectedOption = value; | ||||||
|   } |   } | ||||||
|  |    | ||||||
|  |   async disconnectedCallback() { | ||||||
|  |     await super.disconnectedCallback(); | ||||||
|  |     document.removeEventListener('click', this.handleClickOutside); | ||||||
|  |   } | ||||||
| } | } | ||||||
| @@ -1,204 +0,0 @@ | |||||||
| import { html, css, cssManager } from '@design.estate/dees-element'; |  | ||||||
|  |  | ||||||
| export const demoFunc = () => html` |  | ||||||
|   <dees-demowrapper> |  | ||||||
|     <style> |  | ||||||
|       ${css` |  | ||||||
|         .demo-container { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: column; |  | ||||||
|           gap: 24px; |  | ||||||
|           padding: 24px; |  | ||||||
|           max-width: 1200px; |  | ||||||
|           margin: 0 auto; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .upload-grid { |  | ||||||
|           display: grid; |  | ||||||
|           grid-template-columns: 1fr 1fr; |  | ||||||
|           gap: 24px; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         @media (max-width: 768px) { |  | ||||||
|           .upload-grid { |  | ||||||
|             grid-template-columns: 1fr; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .upload-box { |  | ||||||
|           padding: 16px; |  | ||||||
|           background: ${cssManager.bdTheme('#fff', '#2a2a2a')}; |  | ||||||
|           border-radius: 4px; |  | ||||||
|           border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444')}; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .upload-box h4 { |  | ||||||
|           margin-top: 0; |  | ||||||
|           margin-bottom: 16px; |  | ||||||
|           color: ${cssManager.bdTheme('#333', '#fff')}; |  | ||||||
|           font-size: 16px; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         .info-section { |  | ||||||
|           margin-top: 32px; |  | ||||||
|           padding: 16px; |  | ||||||
|           background: ${cssManager.bdTheme('#fff3cd', '#332701')}; |  | ||||||
|           border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#664400')}; |  | ||||||
|           border-radius: 4px; |  | ||||||
|           color: ${cssManager.bdTheme('#856404', '#ffecb5')}; |  | ||||||
|         } |  | ||||||
|       `} |  | ||||||
|     </style> |  | ||||||
|      |  | ||||||
|     <div class="demo-container"> |  | ||||||
|       <dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}> |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Attachments'} |  | ||||||
|           .description=${'Upload any files by clicking or dragging them here'} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|          |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Single File Only'} |  | ||||||
|           .description=${'Only one file can be uploaded at a time'} |  | ||||||
|           .multiple=${false} |  | ||||||
|           .buttonText=${'Choose File'} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|       </dees-panel> |  | ||||||
|        |  | ||||||
|       <dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}> |  | ||||||
|         <div class="upload-grid"> |  | ||||||
|           <div class="upload-box"> |  | ||||||
|             <h4>Images Only</h4> |  | ||||||
|             <dees-input-fileupload |  | ||||||
|               .label=${'Profile Picture'} |  | ||||||
|               .description=${'JPG, PNG or GIF (max 5MB)'} |  | ||||||
|               .accept=${'image/jpeg,image/png,image/gif'} |  | ||||||
|               .maxSize=${5 * 1024 * 1024} |  | ||||||
|               .multiple=${false} |  | ||||||
|               .buttonText=${'Select Image'} |  | ||||||
|             ></dees-input-fileupload> |  | ||||||
|           </div> |  | ||||||
|            |  | ||||||
|           <div class="upload-box"> |  | ||||||
|             <h4>Documents Only</h4> |  | ||||||
|             <dees-input-fileupload |  | ||||||
|               .label=${'Resume'} |  | ||||||
|               .description=${'PDF or Word documents only'} |  | ||||||
|               .accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"} |  | ||||||
|               .buttonText=${'Select Document'} |  | ||||||
|             ></dees-input-fileupload> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </dees-panel> |  | ||||||
|        |  | ||||||
|       <dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}> |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Small Files Only'} |  | ||||||
|           .description=${'Maximum file size: 1MB'} |  | ||||||
|           .maxSize=${1024 * 1024} |  | ||||||
|           .buttonText=${'Upload Small File'} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|          |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Limited Upload'} |  | ||||||
|           .description=${'Maximum 3 files, each up to 2MB'} |  | ||||||
|           .maxFiles=${3} |  | ||||||
|           .maxSize=${2 * 1024 * 1024} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|          |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Required Upload'} |  | ||||||
|           .description=${'This field is required'} |  | ||||||
|           .required=${true} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|       </dees-panel> |  | ||||||
|        |  | ||||||
|       <dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}> |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Disabled Upload'} |  | ||||||
|           .description=${'File upload is currently disabled'} |  | ||||||
|           .disabled=${true} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|          |  | ||||||
|         <dees-input-fileupload |  | ||||||
|           .label=${'Pre-filled Example'} |  | ||||||
|           .description=${'Component with pre-loaded files'} |  | ||||||
|           .value=${[ |  | ||||||
|             new File(['Hello World'], 'example.txt', { type: 'text/plain' }), |  | ||||||
|             new File(['Test Data'], 'data.json', { type: 'application/json' }) |  | ||||||
|           ]} |  | ||||||
|         ></dees-input-fileupload> |  | ||||||
|       </dees-panel> |  | ||||||
|        |  | ||||||
|       <dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}> |  | ||||||
|         <dees-form> |  | ||||||
|           <h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3> |  | ||||||
|            |  | ||||||
|           <dees-input-text  |  | ||||||
|             .label=${'Full Name'}  |  | ||||||
|             .required=${true} |  | ||||||
|             .key=${'fullName'} |  | ||||||
|           ></dees-input-text> |  | ||||||
|            |  | ||||||
|           <dees-input-text  |  | ||||||
|             .label=${'Email'}  |  | ||||||
|             .inputType=${'email'}  |  | ||||||
|             .required=${true} |  | ||||||
|             .key=${'email'} |  | ||||||
|           ></dees-input-text> |  | ||||||
|            |  | ||||||
|           <dees-input-fileupload |  | ||||||
|             .label=${'Resume'} |  | ||||||
|             .description=${'Required: PDF format only (max 10MB)'} |  | ||||||
|             .required=${true} |  | ||||||
|             .accept=${'application/pdf'} |  | ||||||
|             .maxSize=${10 * 1024 * 1024} |  | ||||||
|             .multiple=${false} |  | ||||||
|             .key=${'resume'} |  | ||||||
|           ></dees-input-fileupload> |  | ||||||
|            |  | ||||||
|           <dees-input-fileupload |  | ||||||
|             .label=${'Portfolio'} |  | ||||||
|             .description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'} |  | ||||||
|             .accept=${'image/*,application/pdf'} |  | ||||||
|             .maxFiles=${5} |  | ||||||
|             .maxSize=${5 * 1024 * 1024} |  | ||||||
|             .key=${'portfolio'} |  | ||||||
|           ></dees-input-fileupload> |  | ||||||
|            |  | ||||||
|           <dees-input-fileupload |  | ||||||
|             .label=${'References'} |  | ||||||
|             .description=${'Upload reference letters (optional)'} |  | ||||||
|             .accept=${".pdf,.doc,.docx"} |  | ||||||
|             .key=${'references'} |  | ||||||
|           ></dees-input-fileupload> |  | ||||||
|            |  | ||||||
|           <dees-input-text |  | ||||||
|             .label=${'Additional Comments'} |  | ||||||
|             .inputType=${'textarea'} |  | ||||||
|             .description=${'Any additional information you would like to share'} |  | ||||||
|             .key=${'comments'} |  | ||||||
|           ></dees-input-text> |  | ||||||
|            |  | ||||||
|           <dees-form-submit .text=${'Submit Application'}></dees-form-submit> |  | ||||||
|         </dees-form> |  | ||||||
|          |  | ||||||
|         <div class="info-section"> |  | ||||||
|           <h4 style="margin-top: 0;">Enhanced Features:</h4> |  | ||||||
|           <ul style="margin: 0; padding-left: 20px;"> |  | ||||||
|             <li>Drag & drop with visual feedback</li> |  | ||||||
|             <li>File type restrictions via accept attribute</li> |  | ||||||
|             <li>File size validation with custom limits</li> |  | ||||||
|             <li>Maximum file count restrictions</li> |  | ||||||
|             <li>Image preview thumbnails</li> |  | ||||||
|             <li>File type-specific icons</li> |  | ||||||
|             <li>Clear all button for multiple files</li> |  | ||||||
|             <li>Proper validation states and messages</li> |  | ||||||
|             <li>Keyboard accessible</li> |  | ||||||
|             <li>Single or multiple file modes</li> |  | ||||||
|           </ul> |  | ||||||
|         </div> |  | ||||||
|       </dees-panel> |  | ||||||
|     </div> |  | ||||||
|   </dees-demowrapper> |  | ||||||
| `; |  | ||||||
| @@ -1,642 +0,0 @@ | |||||||
| import * as colors from './00colors.js'; |  | ||||||
| import * as plugins from './00plugins.js'; |  | ||||||
|  |  | ||||||
| import { DeesContextmenu } from './dees-contextmenu.js'; |  | ||||||
| import { DeesInputBase } from './dees-input-base.js'; |  | ||||||
| import { demoFunc } from './dees-input-fileupload.demo.js'; |  | ||||||
|  |  | ||||||
| import { |  | ||||||
|   customElement, |  | ||||||
|   DeesElement, |  | ||||||
|   type TemplateResult, |  | ||||||
|   property, |  | ||||||
|   html, |  | ||||||
|   css, |  | ||||||
|   unsafeCSS, |  | ||||||
|   cssManager, |  | ||||||
|   type CSSResult, |  | ||||||
|   domtools, |  | ||||||
| } from '@design.estate/dees-element'; |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     'dees-input-fileupload': DeesInputFileupload; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement('dees-input-fileupload') |  | ||||||
| export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> { |  | ||||||
|   public static demo = demoFunc; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   @property({ |  | ||||||
|     attribute: false, |  | ||||||
|   }) |  | ||||||
|   public value: File[] = []; |  | ||||||
|  |  | ||||||
|   @property() |  | ||||||
|   public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle'; |  | ||||||
|  |  | ||||||
|   @property({ |  | ||||||
|     type: String, |  | ||||||
|   }) |  | ||||||
|   public buttonText: string = 'Upload File...'; |  | ||||||
|  |  | ||||||
|   @property({ type: String }) |  | ||||||
|   public accept: string = ''; |  | ||||||
|  |  | ||||||
|   @property({ type: Boolean }) |  | ||||||
|   public multiple: boolean = true; |  | ||||||
|  |  | ||||||
|   @property({ type: Number }) |  | ||||||
|   public maxSize: number = 0; // 0 means no limit |  | ||||||
|  |  | ||||||
|   @property({ type: Number }) |  | ||||||
|   public maxFiles: number = 0; // 0 means no limit |  | ||||||
|  |  | ||||||
|   @property({ type: String, reflect: true }) |  | ||||||
|   public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null; |  | ||||||
|  |  | ||||||
|   constructor() { |  | ||||||
|     super(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public static styles = [ |  | ||||||
|     ...DeesInputBase.baseStyles, |  | ||||||
|     cssManager.defaultStyles, |  | ||||||
|     css` |  | ||||||
|       :host { |  | ||||||
|         position: relative; |  | ||||||
|         display: block; |  | ||||||
|         color: ${cssManager.bdTheme('#333', '#ccc')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .hidden { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .input-wrapper { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         gap: 8px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .maincontainer { |  | ||||||
|         position: relative; |  | ||||||
|         border-radius: 8px; |  | ||||||
|         padding: 16px; |  | ||||||
|         background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; |  | ||||||
|         color: ${cssManager.bdTheme('#333', '#ccc')}; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; |  | ||||||
|         transition: all 0.2s ease; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .maincontainer:hover { |  | ||||||
|         border-color: ${cssManager.bdTheme('#ccc', '#444')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([disabled]) .maincontainer { |  | ||||||
|         opacity: 0.5; |  | ||||||
|         cursor: not-allowed; |  | ||||||
|         pointer-events: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([validationState="invalid"]) .maincontainer { |  | ||||||
|         border-color: #e74c3c; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([validationState="valid"]) .maincontainer { |  | ||||||
|         border-color: #27ae60; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([validationState="warn"]) .maincontainer { |  | ||||||
|         border-color: #f39c12; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .maincontainer::after { |  | ||||||
|         top: 2px; |  | ||||||
|         right: 2px; |  | ||||||
|         left: 2px; |  | ||||||
|         bottom: 2px; |  | ||||||
|         transform: scale3d(0.98, 0.9, 1); |  | ||||||
|         position: absolute; |  | ||||||
|         content: ''; |  | ||||||
|         display: block; |  | ||||||
|         border: 2px dashed transparent; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         transition: all 0.3s ease; |  | ||||||
|         pointer-events: none; |  | ||||||
|         background: transparent; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .maincontainer.dragOver { |  | ||||||
|         border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')}; |  | ||||||
|         background: ${cssManager.bdTheme('#f0f8ff', '#001933')}; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .maincontainer.dragOver::after { |  | ||||||
|         transform: scale3d(1, 1, 1); |  | ||||||
|         border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton { |  | ||||||
|         position: relative; |  | ||||||
|         padding: 12px 24px; |  | ||||||
|         background: ${cssManager.bdTheme('#0084ff', '#0084ff')}; |  | ||||||
|         color: white; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         text-align: center; |  | ||||||
|         font-size: 14px; |  | ||||||
|         font-weight: 500; |  | ||||||
|         cursor: pointer; |  | ||||||
|         transition: all 0.2s ease; |  | ||||||
|         border: none; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         gap: 8px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton:hover { |  | ||||||
|         background: ${cssManager.bdTheme('#0073e6', '#0073e6')}; |  | ||||||
|         transform: translateY(-1px); |  | ||||||
|         box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton:active { |  | ||||||
|         transform: translateY(0); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton dees-icon { |  | ||||||
|         font-size: 16px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .files-container { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         gap: 8px; |  | ||||||
|         margin-bottom: 12px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate { |  | ||||||
|         display: grid; |  | ||||||
|         grid-template-columns: 40px 1fr auto; |  | ||||||
|         background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')}; |  | ||||||
|         padding: 12px; |  | ||||||
|         text-align: left; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         color: ${cssManager.bdTheme('#333', '#ccc')}; |  | ||||||
|         cursor: default; |  | ||||||
|         transition: all 0.2s; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')}; |  | ||||||
|         position: relative; |  | ||||||
|         overflow: hidden; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate:hover { |  | ||||||
|         background: ${cssManager.bdTheme('#f5f5f5', '#333')}; |  | ||||||
|         border-color: ${cssManager.bdTheme('#ccc', '#444')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate .icon { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         font-size: 20px; |  | ||||||
|         color: ${cssManager.bdTheme('#666', '#999')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate.image-file .icon { |  | ||||||
|         color: #4CAF50; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .uploadCandidate.pdf-file .icon { |  | ||||||
|         color: #f44336; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .uploadCandidate.doc-file .icon { |  | ||||||
|         color: #2196F3; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate .info { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         gap: 2px; |  | ||||||
|         min-width: 0; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .uploadCandidate .filename { |  | ||||||
|         font-weight: 500; |  | ||||||
|         font-size: 14px; |  | ||||||
|         white-space: nowrap; |  | ||||||
|         overflow: hidden; |  | ||||||
|         text-overflow: ellipsis; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .uploadCandidate .filesize { |  | ||||||
|         font-size: 12px; |  | ||||||
|         color: ${cssManager.bdTheme('#666', '#999')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate .actions { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         gap: 8px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .remove-button { |  | ||||||
|         width: 32px; |  | ||||||
|         height: 32px; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         background: transparent; |  | ||||||
|         border: none; |  | ||||||
|         cursor: pointer; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         transition: all 0.2s; |  | ||||||
|         color: ${cssManager.bdTheme('#666', '#999')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .remove-button:hover { |  | ||||||
|         background: ${cssManager.bdTheme('#fee', '#4a1c1c')}; |  | ||||||
|         color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .clear-all-button { |  | ||||||
|         margin-bottom: 8px; |  | ||||||
|         text-align: right; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .clear-all-button button { |  | ||||||
|         background: none; |  | ||||||
|         border: none; |  | ||||||
|         color: ${cssManager.bdTheme('#666', '#999')}; |  | ||||||
|         cursor: pointer; |  | ||||||
|         font-size: 12px; |  | ||||||
|         padding: 4px 8px; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         transition: all 0.2s; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .clear-all-button button:hover { |  | ||||||
|         background: ${cssManager.bdTheme('#fee', '#4a1c1c')}; |  | ||||||
|         color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .validation-message { |  | ||||||
|         font-size: 12px; |  | ||||||
|         margin-top: 4px; |  | ||||||
|         color: #e74c3c; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .drop-hint { |  | ||||||
|         text-align: center; |  | ||||||
|         padding: 40px 20px; |  | ||||||
|         color: ${cssManager.bdTheme('#999', '#666')}; |  | ||||||
|         font-size: 14px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .drop-hint dees-icon { |  | ||||||
|         font-size: 48px; |  | ||||||
|         margin-bottom: 16px; |  | ||||||
|         opacity: 0.3; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .image-preview { |  | ||||||
|         width: 40px; |  | ||||||
|         height: 40px; |  | ||||||
|         object-fit: cover; |  | ||||||
|         border-radius: 4px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .description-text { |  | ||||||
|         font-size: 12px; |  | ||||||
|         color: ${cssManager.bdTheme('#666', '#999')}; |  | ||||||
|         margin-top: 4px; |  | ||||||
|         line-height: 1.4; |  | ||||||
|       } |  | ||||||
|     `, |  | ||||||
|   ]; |  | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |  | ||||||
|     const hasFiles = this.value.length > 0; |  | ||||||
|     const showClearAll = hasFiles && this.value.length > 1; |  | ||||||
|      |  | ||||||
|     return html` |  | ||||||
|       <div class="input-wrapper"> |  | ||||||
|         ${this.label ? html` |  | ||||||
|           <dees-label .label=${this.label}></dees-label> |  | ||||||
|         ` : ''} |  | ||||||
|         <div class="hidden"> |  | ||||||
|           <input  |  | ||||||
|             type="file"  |  | ||||||
|             ?multiple=${this.multiple} |  | ||||||
|             accept="${this.accept}" |  | ||||||
|           > |  | ||||||
|         </div> |  | ||||||
|         <div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}"> |  | ||||||
|           ${hasFiles ? html` |  | ||||||
|             ${showClearAll ? html` |  | ||||||
|               <div class="clear-all-button"> |  | ||||||
|                 <button @click=${this.clearAll}>Clear All</button> |  | ||||||
|               </div> |  | ||||||
|             ` : ''} |  | ||||||
|             <div class="files-container"> |  | ||||||
|               ${this.value.map((fileArg) => { |  | ||||||
|                 const fileType = this.getFileType(fileArg); |  | ||||||
|                 const isImage = fileType === 'image'; |  | ||||||
|                 return html` |  | ||||||
|                   <div class="uploadCandidate ${fileType}-file"> |  | ||||||
|                     <div class="icon"> |  | ||||||
|                       ${isImage && this.canShowPreview(fileArg) ? html` |  | ||||||
|                         <img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}"> |  | ||||||
|                       ` : html` |  | ||||||
|                         <dees-icon .iconName=${this.getFileIcon(fileArg)}></dees-icon> |  | ||||||
|                       `} |  | ||||||
|                     </div> |  | ||||||
|                     <div class="info"> |  | ||||||
|                       <div class="filename" title="${fileArg.name}">${fileArg.name}</div> |  | ||||||
|                       <div class="filesize">${this.formatFileSize(fileArg.size)}</div> |  | ||||||
|                     </div> |  | ||||||
|                     <div class="actions"> |  | ||||||
|                       <button  |  | ||||||
|                         class="remove-button"  |  | ||||||
|                         @click=${() => this.removeFile(fileArg)} |  | ||||||
|                         title="Remove file" |  | ||||||
|                       > |  | ||||||
|                         <dees-icon .iconName=${'lucide:x'}></dees-icon> |  | ||||||
|                       </button> |  | ||||||
|                     </div> |  | ||||||
|                   </div> |  | ||||||
|                 `; |  | ||||||
|               })} |  | ||||||
|             </div> |  | ||||||
|           ` : html` |  | ||||||
|             <div class="drop-hint"> |  | ||||||
|               <dees-icon .iconName=${'lucide:cloud-upload'}></dees-icon> |  | ||||||
|               <div>Drag files here or click to browse</div> |  | ||||||
|             </div> |  | ||||||
|           `} |  | ||||||
|           <div class="uploadButton" @click=${this.openFileSelector}> |  | ||||||
|             <dees-icon .iconName=${'lucide:upload'}></dees-icon> |  | ||||||
|             ${this.buttonText} |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|         ${this.description ? html` |  | ||||||
|           <div class="description-text">${this.description}</div> |  | ||||||
|         ` : ''} |  | ||||||
|         ${this.validationState === 'invalid' && this.validationMessage ? html` |  | ||||||
|           <div class="validation-message">${this.validationMessage}</div> |  | ||||||
|         ` : ''} |  | ||||||
|       </div> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private validationMessage: string = ''; |  | ||||||
|  |  | ||||||
|   // Utility methods |  | ||||||
|   private formatFileSize(bytes: number): string { |  | ||||||
|     const sizes = ['Bytes', 'KB', 'MB', 'GB']; |  | ||||||
|     if (bytes === 0) return '0 Bytes'; |  | ||||||
|     const i = Math.floor(Math.log(bytes) / Math.log(1024)); |  | ||||||
|     return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private getFileType(file: File): string { |  | ||||||
|     const type = file.type.toLowerCase(); |  | ||||||
|     if (type.startsWith('image/')) return 'image'; |  | ||||||
|     if (type === 'application/pdf') return 'pdf'; |  | ||||||
|     if (type.includes('word') || type.includes('document')) return 'doc'; |  | ||||||
|     if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet'; |  | ||||||
|     if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation'; |  | ||||||
|     if (type.startsWith('video/')) return 'video'; |  | ||||||
|     if (type.startsWith('audio/')) return 'audio'; |  | ||||||
|     if (type.includes('zip') || type.includes('compressed')) return 'archive'; |  | ||||||
|     return 'file'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private getFileIcon(file: File): string { |  | ||||||
|     const type = this.getFileType(file); |  | ||||||
|     const iconMap = { |  | ||||||
|       'image': 'lucide:image', |  | ||||||
|       'pdf': 'lucide:file-text', |  | ||||||
|       'doc': 'lucide:file-text', |  | ||||||
|       'spreadsheet': 'lucide:table', |  | ||||||
|       'presentation': 'lucide:presentation', |  | ||||||
|       'video': 'lucide:video', |  | ||||||
|       'audio': 'lucide:music', |  | ||||||
|       'archive': 'lucide:archive', |  | ||||||
|       'file': 'lucide:file' |  | ||||||
|     }; |  | ||||||
|     return iconMap[type] || 'lucide:file'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private canShowPreview(file: File): boolean { |  | ||||||
|     return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private validateFile(file: File): boolean { |  | ||||||
|     // Check file size |  | ||||||
|     if (this.maxSize > 0 && file.size > this.maxSize) { |  | ||||||
|       this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`; |  | ||||||
|       this.validationState = 'invalid'; |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Check file type |  | ||||||
|     if (this.accept) { |  | ||||||
|       const acceptedTypes = this.accept.split(',').map(s => s.trim()); |  | ||||||
|       let isAccepted = false; |  | ||||||
|        |  | ||||||
|       for (const acceptType of acceptedTypes) { |  | ||||||
|         if (acceptType.startsWith('.')) { |  | ||||||
|           // Extension check |  | ||||||
|           if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) { |  | ||||||
|             isAccepted = true; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } else if (acceptType.endsWith('/*')) { |  | ||||||
|           // MIME type wildcard check |  | ||||||
|           const mimePrefix = acceptType.slice(0, -2); |  | ||||||
|           if (file.type.startsWith(mimePrefix)) { |  | ||||||
|             isAccepted = true; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } else if (file.type === acceptType) { |  | ||||||
|           // Exact MIME type check |  | ||||||
|           isAccepted = true; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       if (!isAccepted) { |  | ||||||
|         this.validationMessage = `File type not accepted. Please upload: ${this.accept}`; |  | ||||||
|         this.validationState = 'invalid'; |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async openFileSelector() { |  | ||||||
|     if (this.disabled) return; |  | ||||||
|     const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); |  | ||||||
|     inputFile.click(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private removeFile(file: File) { |  | ||||||
|     const index = this.value.indexOf(file); |  | ||||||
|     if (index > -1) { |  | ||||||
|       this.value.splice(index, 1); |  | ||||||
|       this.requestUpdate(); |  | ||||||
|       this.validate(); |  | ||||||
|       this.changeSubject.next(this); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private clearAll() { |  | ||||||
|     this.value = []; |  | ||||||
|     this.requestUpdate(); |  | ||||||
|     this.validate(); |  | ||||||
|     this.changeSubject.next(this); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async updateValue(eventArg: Event) { |  | ||||||
|     const target: any = eventArg.target; |  | ||||||
|     this.value = target.value; |  | ||||||
|     this.changeSubject.next(this); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) { |  | ||||||
|     super.firstUpdated(_changedProperties); |  | ||||||
|     const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); |  | ||||||
|     inputFile.addEventListener('change', async (event: Event) => { |  | ||||||
|       const target = event.target as HTMLInputElement; |  | ||||||
|       const newFiles = Array.from(target.files); |  | ||||||
|       await this.addFiles(newFiles); |  | ||||||
|       // Reset the input value to allow selecting the same file again if needed |  | ||||||
|       target.value = ''; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // Handle drag and drop |  | ||||||
|     const dropArea = this.shadowRoot.querySelector('.maincontainer'); |  | ||||||
|     const handlerFunction = async (eventArg: DragEvent) => { |  | ||||||
|       eventArg.preventDefault(); |  | ||||||
|       eventArg.stopPropagation(); |  | ||||||
|        |  | ||||||
|       switch (eventArg.type) { |  | ||||||
|         case 'dragenter': |  | ||||||
|         case 'dragover': |  | ||||||
|           this.state = 'dragOver'; |  | ||||||
|           break; |  | ||||||
|         case 'dragleave': |  | ||||||
|           // Check if we're actually leaving the drop area |  | ||||||
|           const rect = dropArea.getBoundingClientRect(); |  | ||||||
|           const x = eventArg.clientX; |  | ||||||
|           const y = eventArg.clientY; |  | ||||||
|           if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) { |  | ||||||
|             this.state = 'idle'; |  | ||||||
|           } |  | ||||||
|           break; |  | ||||||
|         case 'drop': |  | ||||||
|           this.state = 'idle'; |  | ||||||
|           const files = Array.from(eventArg.dataTransfer.files); |  | ||||||
|           await this.addFiles(files); |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     dropArea.addEventListener('dragenter', handlerFunction, false); |  | ||||||
|     dropArea.addEventListener('dragleave', handlerFunction, false); |  | ||||||
|     dropArea.addEventListener('dragover', handlerFunction, false); |  | ||||||
|     dropArea.addEventListener('drop', handlerFunction, false); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async addFiles(files: File[]) { |  | ||||||
|     const filesToAdd: File[] = []; |  | ||||||
|      |  | ||||||
|     for (const file of files) { |  | ||||||
|       if (this.validateFile(file)) { |  | ||||||
|         filesToAdd.push(file); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (filesToAdd.length === 0) return; |  | ||||||
|  |  | ||||||
|     // Check max files limit |  | ||||||
|     if (this.maxFiles > 0) { |  | ||||||
|       const totalFiles = this.value.length + filesToAdd.length; |  | ||||||
|       if (totalFiles > this.maxFiles) { |  | ||||||
|         const allowedCount = this.maxFiles - this.value.length; |  | ||||||
|         if (allowedCount <= 0) { |  | ||||||
|           this.validationMessage = `Maximum ${this.maxFiles} files allowed`; |  | ||||||
|           this.validationState = 'invalid'; |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         filesToAdd.splice(allowedCount); |  | ||||||
|         this.validationMessage = `Only ${allowedCount} more file(s) can be added`; |  | ||||||
|         this.validationState = 'warn'; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Add files |  | ||||||
|     if (!this.multiple && filesToAdd.length > 0) { |  | ||||||
|       this.value = [filesToAdd[0]]; |  | ||||||
|     } else { |  | ||||||
|       this.value.push(...filesToAdd); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.requestUpdate(); |  | ||||||
|     this.validate(); |  | ||||||
|     this.changeSubject.next(this); |  | ||||||
|      |  | ||||||
|     // Update button text |  | ||||||
|     if (this.value.length > 0) { |  | ||||||
|       this.buttonText = this.multiple ? 'Add more files' : 'Replace file'; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async validate(): Promise<boolean> { |  | ||||||
|     this.validationMessage = ''; |  | ||||||
|      |  | ||||||
|     if (this.required && this.value.length === 0) { |  | ||||||
|       this.validationState = 'invalid'; |  | ||||||
|       this.validationMessage = 'Please select at least one file'; |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Validate all files |  | ||||||
|     for (const file of this.value) { |  | ||||||
|       if (!this.validateFile(file)) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     this.validationState = 'valid'; |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public getValue(): File[] { |  | ||||||
|     return this.value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public setValue(value: File[]): void { |  | ||||||
|     this.value = value; |  | ||||||
|     this.requestUpdate(); |  | ||||||
|     if (value.length > 0) { |  | ||||||
|       this.buttonText = this.multiple ? 'Add more files' : 'Replace file'; |  | ||||||
|     } else { |  | ||||||
|       this.buttonText = 'Upload File...'; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public updated(changedProperties: Map<string, any>) { |  | ||||||
|     super.updated(changedProperties); |  | ||||||
|      |  | ||||||
|     if (changedProperties.has('value')) { |  | ||||||
|       this.validate(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										619
									
								
								ts_web/elements/dees-input-fileupload/component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										619
									
								
								ts_web/elements/dees-input-fileupload/component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,619 @@ | |||||||
|  | import { DeesInputBase } from '../dees-input-base.js'; | ||||||
|  | import { demoFunc } from './demo.js'; | ||||||
|  | import { fileuploadStyles } from './styles.js'; | ||||||
|  | import '../dees-icon.js'; | ||||||
|  | import '../dees-label.js'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   customElement, | ||||||
|  |   html, | ||||||
|  |   property, | ||||||
|  |   state, | ||||||
|  |   type TemplateResult, | ||||||
|  | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     'dees-input-fileupload': DeesInputFileupload; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @customElement('dees-input-fileupload') | ||||||
|  | export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> { | ||||||
|  |   public static demo = demoFunc; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public value: File[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public state: 'idle' | 'dragOver' | 'dropped' | 'uploading' | 'completed' = 'idle'; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public isLoading: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public buttonText: string = 'Select files'; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public accept: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public multiple: boolean = true; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public maxSize: number = 0; // 0 means no limit | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public maxFiles: number = 0; // 0 means no limit | ||||||
|  |  | ||||||
|  |   @property({ type: String, reflect: true }) | ||||||
|  |   public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null; | ||||||
|  |  | ||||||
|  |   public validationMessage: string = ''; | ||||||
|  |  | ||||||
|  |   private previewUrlMap: WeakMap<File, string> = new WeakMap(); | ||||||
|  |   private dropArea: HTMLElement | null = null; | ||||||
|  |  | ||||||
|  |   public static styles = fileuploadStyles; | ||||||
|  |  | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     const acceptedSummary = this.getAcceptedSummary(); | ||||||
|  |     const metaEntries: string[] = [ | ||||||
|  |       this.multiple ? 'Multiple files supported' : 'Single file only', | ||||||
|  |       this.maxSize > 0 ? `Max ${this.formatFileSize(this.maxSize)}` : 'No size limit', | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     if (acceptedSummary) { | ||||||
|  |       metaEntries.push(`Accepts ${acceptedSummary}`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="input-wrapper"> | ||||||
|  |         <dees-label | ||||||
|  |           .label=${this.label} | ||||||
|  |           .description=${this.description} | ||||||
|  |           .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" | ||||||
|  |           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" | ||||||
|  |             style="position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; top: 0; left: 0; overflow: hidden;" | ||||||
|  |             type="file" | ||||||
|  |             ?multiple=${this.multiple} | ||||||
|  |             accept=${this.accept || ''} | ||||||
|  |             ?disabled=${this.disabled} | ||||||
|  |             @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> | ||||||
|  |           ${this.renderFileList()} | ||||||
|  |         </div> | ||||||
|  |         ${this.validationMessage | ||||||
|  |           ? html`<div class="validation-message" aria-live="polite">${this.validationMessage}</div>` | ||||||
|  |           : html``} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderFileList(): TemplateResult { | ||||||
|  |     if (this.value.length === 0) { | ||||||
|  |       return html``; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="file-list"> | ||||||
|  |         <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))} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private renderFileRow(file: File): TemplateResult { | ||||||
|  |     const fileType = this.getFileType(file); | ||||||
|  |     const previewUrl = this.canShowPreview(file) ? this.getPreviewUrl(file) : null; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="file-row ${fileType}-file"> | ||||||
|  |         <div class="file-thumb" aria-hidden="true"> | ||||||
|  |           ${previewUrl | ||||||
|  |             ? html`<img class="thumb-image" src=${previewUrl} alt=${`Preview of ${file.name}`}>` | ||||||
|  |             : html`<dees-icon icon=${this.getFileIcon(file)}></dees-icon>`} | ||||||
|  |         </div> | ||||||
|  |         <div class="file-meta"> | ||||||
|  |           <div class="file-name" title=${file.name}>${file.name}</div> | ||||||
|  |           <div class="file-details"> | ||||||
|  |             <span class="file-size">${this.formatFileSize(file.size)}</span> | ||||||
|  |             ${fileType !== 'file' ? html`<span class="file-type">${fileType}</span>` : html``} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="file-actions"> | ||||||
|  |           <button | ||||||
|  |             type="button" | ||||||
|  |             class="remove-button" | ||||||
|  |             @click=${() => this.removeFile(file)} | ||||||
|  |             aria-label=${`Remove ${file.name}`} | ||||||
|  |           > | ||||||
|  |             <dees-icon icon="lucide:X"></dees-icon> | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleFileInputChange = async (event: Event) => { | ||||||
|  |     this.isLoading = false; | ||||||
|  |     const target = event.target as HTMLInputElement; | ||||||
|  |     const files = Array.from(target.files ?? []); | ||||||
|  |     if (files.length > 0) { | ||||||
|  |       await this.addFiles(files); | ||||||
|  |     } | ||||||
|  |     target.value = ''; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private handleDropzoneClick = (event: MouseEvent) => { | ||||||
|  |     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')) { | ||||||
|  |       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; | ||||||
|  |     } | ||||||
|  |     if (event.key === 'Enter' || event.key === ' ') { | ||||||
|  |       event.preventDefault(); | ||||||
|  |       this.openFileSelector(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private handleClearAll = (event: MouseEvent) => { | ||||||
|  |     event.preventDefault(); | ||||||
|  |     this.clearAll(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private handleDragEvent = async (event: DragEvent) => { | ||||||
|  |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  |  | ||||||
|  |     if (this.disabled) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (event.type === 'dragenter' || event.type === 'dragover') { | ||||||
|  |       if (event.dataTransfer) { | ||||||
|  |         event.dataTransfer.dropEffect = 'copy'; | ||||||
|  |       } | ||||||
|  |       this.state = 'dragOver'; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (event.type === 'dragleave') { | ||||||
|  |       if (!this.dropArea) { | ||||||
|  |         this.state = 'idle'; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const rect = this.dropArea.getBoundingClientRect(); | ||||||
|  |       const { clientX = 0, clientY = 0 } = event; | ||||||
|  |       if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) { | ||||||
|  |         this.state = 'idle'; | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (event.type === 'drop') { | ||||||
|  |       this.state = 'idle'; | ||||||
|  |       const files = Array.from(event.dataTransfer?.files ?? []); | ||||||
|  |       if (files.length > 0) { | ||||||
|  |         await this.addFiles(files); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private attachDropListeners(): void { | ||||||
|  |     if (!this.dropArea) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => { | ||||||
|  |       this.dropArea!.addEventListener(eventName, this.handleDragEvent); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private detachDropListeners(): void { | ||||||
|  |     if (!this.dropArea) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => { | ||||||
|  |       this.dropArea!.removeEventListener(eventName, this.handleDragEvent); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private rebindInteractiveElements(): void { | ||||||
|  |     const newDropArea = this.shadowRoot?.querySelector('.dropzone') as HTMLElement | null; | ||||||
|  |  | ||||||
|  |     if (newDropArea !== this.dropArea) { | ||||||
|  |       this.detachDropListeners(); | ||||||
|  |       this.dropArea = newDropArea; | ||||||
|  |       this.attachDropListeners(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public formatFileSize(bytes: number): string { | ||||||
|  |     const units = ['Bytes', 'KB', 'MB', 'GB']; | ||||||
|  |     if (bytes === 0) return '0 Bytes'; | ||||||
|  |     const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); | ||||||
|  |     const size = bytes / Math.pow(1024, exponent); | ||||||
|  |     return `${Math.round(size * 100) / 100} ${units[exponent]}`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getFileType(file: File): string { | ||||||
|  |     const type = file.type.toLowerCase(); | ||||||
|  |     if (type.startsWith('image/')) return 'image'; | ||||||
|  |     if (type === 'application/pdf') return 'pdf'; | ||||||
|  |     if (type.includes('word') || type.includes('document')) return 'doc'; | ||||||
|  |     if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet'; | ||||||
|  |     if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation'; | ||||||
|  |     if (type.startsWith('video/')) return 'video'; | ||||||
|  |     if (type.startsWith('audio/')) return 'audio'; | ||||||
|  |     if (type.includes('zip') || type.includes('compressed')) return 'archive'; | ||||||
|  |     return 'file'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getFileIcon(file: File): string { | ||||||
|  |     const fileType = this.getFileType(file); | ||||||
|  |     const iconMap: Record<string, string> = { | ||||||
|  |       image: 'lucide:FileImage', | ||||||
|  |       pdf: 'lucide:FileText', | ||||||
|  |       doc: 'lucide:FileText', | ||||||
|  |       spreadsheet: 'lucide:FileSpreadsheet', | ||||||
|  |       presentation: 'lucide:FileBarChart', | ||||||
|  |       video: 'lucide:FileVideo', | ||||||
|  |       audio: 'lucide:FileAudio', | ||||||
|  |       archive: 'lucide:FileArchive', | ||||||
|  |       file: 'lucide:File', | ||||||
|  |     }; | ||||||
|  |     return iconMap[fileType] ?? 'lucide:File'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public canShowPreview(file: File): boolean { | ||||||
|  |     return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private validateFile(file: File): boolean { | ||||||
|  |     if (this.maxSize > 0 && file.size > this.maxSize) { | ||||||
|  |       this.validationMessage = `File "${file.name}" exceeds the maximum size of ${this.formatFileSize(this.maxSize)}`; | ||||||
|  |       this.validationState = 'invalid'; | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.accept) { | ||||||
|  |       const acceptedTypes = this.accept | ||||||
|  |         .split(',') | ||||||
|  |         .map((entry) => entry.trim()) | ||||||
|  |         .filter((entry) => entry.length > 0); | ||||||
|  |  | ||||||
|  |       if (acceptedTypes.length > 0) { | ||||||
|  |         let isAccepted = false; | ||||||
|  |         for (const acceptType of acceptedTypes) { | ||||||
|  |           if (acceptType.startsWith('.')) { | ||||||
|  |             if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) { | ||||||
|  |               isAccepted = true; | ||||||
|  |               break; | ||||||
|  |             } | ||||||
|  |           } else if (acceptType.endsWith('/*')) { | ||||||
|  |             const prefix = acceptType.slice(0, -2); | ||||||
|  |             if (file.type.startsWith(prefix)) { | ||||||
|  |               isAccepted = true; | ||||||
|  |               break; | ||||||
|  |             } | ||||||
|  |           } else if (file.type === acceptType) { | ||||||
|  |             isAccepted = true; | ||||||
|  |             break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (!isAccepted) { | ||||||
|  |           this.validationMessage = `File type not accepted. Allowed: ${acceptedTypes.join(', ')}`; | ||||||
|  |           this.validationState = 'invalid'; | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getPreviewUrl(file: File): string { | ||||||
|  |     let url = this.previewUrlMap.get(file); | ||||||
|  |     if (!url) { | ||||||
|  |       url = URL.createObjectURL(file); | ||||||
|  |       this.previewUrlMap.set(file, url); | ||||||
|  |     } | ||||||
|  |     return url; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private releasePreview(file: File): void { | ||||||
|  |     const url = this.previewUrlMap.get(file); | ||||||
|  |     if (url) { | ||||||
|  |       URL.revokeObjectURL(url); | ||||||
|  |       this.previewUrlMap.delete(file); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getAcceptedSummary(): string | null { | ||||||
|  |     if (!this.accept) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const formatted = Array.from( | ||||||
|  |       new Set( | ||||||
|  |         this.accept | ||||||
|  |           .split(',') | ||||||
|  |           .map((token) => token.trim()) | ||||||
|  |           .filter((token) => token.length > 0) | ||||||
|  |           .map((token) => this.formatAcceptToken(token)) | ||||||
|  |       ) | ||||||
|  |     ).filter(Boolean); | ||||||
|  |  | ||||||
|  |     if (formatted.length === 0) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (formatted.length === 1) { | ||||||
|  |       return formatted[0]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (formatted.length === 2) { | ||||||
|  |       return `${formatted[0]}, ${formatted[1]}`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return `${formatted.slice(0, 2).join(', ')}…`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private formatAcceptToken(token: string): string { | ||||||
|  |     if (token === '*/*') { | ||||||
|  |       return 'All files'; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (token.endsWith('/*')) { | ||||||
|  |       const family = token.split('/')[0]; | ||||||
|  |       if (!family) { | ||||||
|  |         return 'All files'; | ||||||
|  |       } | ||||||
|  |       return `${family.charAt(0).toUpperCase()}${family.slice(1)} files`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (token.startsWith('.')) { | ||||||
|  |       return token.slice(1).toUpperCase(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (token.includes('pdf')) return 'PDF'; | ||||||
|  |     if (token.includes('zip')) return 'ZIP'; | ||||||
|  |     if (token.includes('json')) return 'JSON'; | ||||||
|  |     if (token.includes('msword')) return 'DOC'; | ||||||
|  |     if (token.includes('wordprocessingml')) return 'DOCX'; | ||||||
|  |     if (token.includes('excel')) return 'XLS'; | ||||||
|  |     if (token.includes('presentation')) return 'PPT'; | ||||||
|  |  | ||||||
|  |     const segments = token.split('/'); | ||||||
|  |     const lastSegment = segments.pop() ?? token; | ||||||
|  |     return lastSegment.toUpperCase(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private attachLifecycleListeners(): void { | ||||||
|  |     this.rebindInteractiveElements(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public firstUpdated(changedProperties: Map<string, unknown>) { | ||||||
|  |     super.firstUpdated(changedProperties); | ||||||
|  |     this.attachLifecycleListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public updated(changedProperties: Map<string, unknown>) { | ||||||
|  |     super.updated(changedProperties); | ||||||
|  |     if (changedProperties.has('value')) { | ||||||
|  |       void this.validate(); | ||||||
|  |     } | ||||||
|  |     this.rebindInteractiveElements(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async disconnectedCallback(): Promise<void> { | ||||||
|  |     this.detachDropListeners(); | ||||||
|  |     this.value.forEach((file) => this.releasePreview(file)); | ||||||
|  |     this.previewUrlMap = new WeakMap(); | ||||||
|  |     await super.disconnectedCallback(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async openFileSelector() { | ||||||
|  |     if (this.disabled || this.isLoading) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.isLoading = true; | ||||||
|  |  | ||||||
|  |     // Ensure we have the latest reference to the file input | ||||||
|  |     const inputFile = this.shadowRoot?.querySelector('.file-input') as HTMLInputElement | null; | ||||||
|  |  | ||||||
|  |     if (!inputFile) { | ||||||
|  |       this.isLoading = false; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const handleFocus = () => { | ||||||
|  |       setTimeout(() => { | ||||||
|  |         if (!inputFile.files || inputFile.files.length === 0) { | ||||||
|  |           this.isLoading = false; | ||||||
|  |         } | ||||||
|  |         window.removeEventListener('focus', handleFocus); | ||||||
|  |       }, 300); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     window.addEventListener('focus', handleFocus); | ||||||
|  |  | ||||||
|  |     // Click the input to open file selector | ||||||
|  |     inputFile.click(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public removeFile(file: File) { | ||||||
|  |     const index = this.value.indexOf(file); | ||||||
|  |     if (index > -1) { | ||||||
|  |       this.releasePreview(file); | ||||||
|  |       this.value.splice(index, 1); | ||||||
|  |       this.requestUpdate('value'); | ||||||
|  |       void this.validate(); | ||||||
|  |       this.changeSubject.next(this); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public clearAll() { | ||||||
|  |     const existingFiles = [...this.value]; | ||||||
|  |     this.value = []; | ||||||
|  |     existingFiles.forEach((file) => this.releasePreview(file)); | ||||||
|  |     this.requestUpdate('value'); | ||||||
|  |     void this.validate(); | ||||||
|  |     this.changeSubject.next(this); | ||||||
|  |     this.buttonText = 'Select files'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updateValue(eventArg: Event) { | ||||||
|  |     const target = eventArg.target as HTMLInputElement; | ||||||
|  |     this.value = Array.from(target.files ?? []); | ||||||
|  |     this.changeSubject.next(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public setValue(value: File[]): void { | ||||||
|  |     this.value.forEach((file) => this.releasePreview(file)); | ||||||
|  |     this.value = value; | ||||||
|  |     if (value.length > 0) { | ||||||
|  |       this.buttonText = this.multiple ? 'Add more files' : 'Replace file'; | ||||||
|  |     } else { | ||||||
|  |       this.buttonText = 'Select files'; | ||||||
|  |     } | ||||||
|  |     this.requestUpdate('value'); | ||||||
|  |     void this.validate(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getValue(): File[] { | ||||||
|  |     return this.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async addFiles(files: File[]) { | ||||||
|  |     const filesToAdd: File[] = []; | ||||||
|  |  | ||||||
|  |     for (const file of files) { | ||||||
|  |       if (this.validateFile(file)) { | ||||||
|  |         filesToAdd.push(file); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (filesToAdd.length === 0) { | ||||||
|  |       this.isLoading = false; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.maxFiles > 0) { | ||||||
|  |       const totalFiles = this.value.length + filesToAdd.length; | ||||||
|  |       if (totalFiles > this.maxFiles) { | ||||||
|  |         const allowedCount = this.maxFiles - this.value.length; | ||||||
|  |         if (allowedCount <= 0) { | ||||||
|  |           this.validationMessage = `Maximum ${this.maxFiles} files allowed`; | ||||||
|  |           this.validationState = 'invalid'; | ||||||
|  |           this.isLoading = false; | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         filesToAdd.splice(allowedCount); | ||||||
|  |         this.validationMessage = `Only ${allowedCount} more file(s) can be added`; | ||||||
|  |         this.validationState = 'warn'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!this.multiple && filesToAdd.length > 0) { | ||||||
|  |       this.value.forEach((file) => this.releasePreview(file)); | ||||||
|  |       this.value = [filesToAdd[0]]; | ||||||
|  |     } else { | ||||||
|  |       this.value.push(...filesToAdd); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.validationMessage = ''; | ||||||
|  |     this.validationState = null; | ||||||
|  |     this.requestUpdate('value'); | ||||||
|  |     await this.validate(); | ||||||
|  |     this.changeSubject.next(this); | ||||||
|  |     this.isLoading = false; | ||||||
|  |  | ||||||
|  |     if (this.value.length > 0) { | ||||||
|  |       this.buttonText = this.multiple ? 'Add more files' : 'Replace file'; | ||||||
|  |     } else { | ||||||
|  |       this.buttonText = 'Select files'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async validate(): Promise<boolean> { | ||||||
|  |     this.validationMessage = ''; | ||||||
|  |  | ||||||
|  |     if (this.required && this.value.length === 0) { | ||||||
|  |       this.validationState = 'invalid'; | ||||||
|  |       this.validationMessage = 'Please select at least one file'; | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const file of this.value) { | ||||||
|  |       if (!this.validateFile(file)) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.validationState = this.value.length > 0 ? 'valid' : null; | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										159
									
								
								ts_web/elements/dees-input-fileupload/demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								ts_web/elements/dees-input-fileupload/demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | import { css, cssManager, html } from '@design.estate/dees-element'; | ||||||
|  | import './component.js'; | ||||||
|  | import '../dees-panel.js'; | ||||||
|  |  | ||||||
|  | export const demoFunc = () => html` | ||||||
|  |   <dees-demowrapper> | ||||||
|  |     <style> | ||||||
|  |       ${css` | ||||||
|  |         .demo-shell { | ||||||
|  |           display: flex; | ||||||
|  |           flex-direction: column; | ||||||
|  |           gap: 32px; | ||||||
|  |           padding: 24px; | ||||||
|  |           max-width: 1160px; | ||||||
|  |           margin: 0 auto; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .demo-grid { | ||||||
|  |           display: grid; | ||||||
|  |           gap: 24px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @media (min-width: 960px) { | ||||||
|  |           .demo-grid--two { | ||||||
|  |             grid-template-columns: repeat(2, minmax(0, 1fr)); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .demo-stack { | ||||||
|  |           display: flex; | ||||||
|  |           flex-direction: column; | ||||||
|  |           gap: 18px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .demo-note { | ||||||
|  |           margin-top: 16px; | ||||||
|  |           padding: 16px; | ||||||
|  |           border-radius: 12px; | ||||||
|  |           border: 1px solid ${cssManager.bdTheme('hsl(217 91% 90%)', 'hsl(215 20% 26%)')}; | ||||||
|  |           background: ${cssManager.bdTheme('hsl(213 100% 97%)', 'hsl(215 20% 12%)')}; | ||||||
|  |           color: ${cssManager.bdTheme('hsl(215 25% 32%)', 'hsl(215 20% 82%)')}; | ||||||
|  |           font-size: 13px; | ||||||
|  |           line-height: 1.55; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .demo-note strong { | ||||||
|  |           color: ${cssManager.bdTheme('hsl(217 91% 45%)', 'hsl(213 93% 68%)')}; | ||||||
|  |           font-weight: 600; | ||||||
|  |         } | ||||||
|  |       `} | ||||||
|  |     </style> | ||||||
|  |  | ||||||
|  |     <div class="demo-shell"> | ||||||
|  |       <dees-panel | ||||||
|  |         .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-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> | ||||||
|  |           </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-panel> | ||||||
|  |  | ||||||
|  |       <dees-panel | ||||||
|  |         .title=${'Form integration'} | ||||||
|  |         .subtitle=${'Combine file uploads with the rest of the DEES form ecosystem'} | ||||||
|  |       > | ||||||
|  |         <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?'} | ||||||
|  |                 .required=${true} | ||||||
|  |                 .key=${'projectName'} | ||||||
|  |               ></dees-input-text> | ||||||
|  |  | ||||||
|  |               <dees-input-text | ||||||
|  |                 .label=${'Contact email'} | ||||||
|  |                 .inputType=${'email'} | ||||||
|  |                 .required=${true} | ||||||
|  |                 .key=${'contactEmail'} | ||||||
|  |               ></dees-input-text> | ||||||
|  |  | ||||||
|  |               <dees-input-fileupload | ||||||
|  |                 .label=${'Statement of work'} | ||||||
|  |                 .description=${'Upload a signed statement of work (PDF, max 15MB)'} | ||||||
|  |                 .required=${true} | ||||||
|  |                 .accept=${'application/pdf'} | ||||||
|  |                 .maxSize=${15 * 1024 * 1024} | ||||||
|  |                 .multiple=${false} | ||||||
|  |                 .key=${'sow'} | ||||||
|  |               ></dees-input-fileupload> | ||||||
|  |  | ||||||
|  |               <dees-input-fileupload | ||||||
|  |                 .label=${'Creative references'} | ||||||
|  |                 .description=${'Optional. Upload up to five visual references'} | ||||||
|  |                 .accept=${'image/*'} | ||||||
|  |                 .maxFiles=${5} | ||||||
|  |                 .maxSize=${8 * 1024 * 1024} | ||||||
|  |                 .key=${'references'} | ||||||
|  |               ></dees-input-fileupload> | ||||||
|  |  | ||||||
|  |               <dees-input-text | ||||||
|  |                 .label=${'Notes'} | ||||||
|  |                 .description=${'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"> | ||||||
|  |             <strong>Good to know:</strong> | ||||||
|  |             <ul> | ||||||
|  |               <li>Drag & drop highlights the dropzone and supports keyboard activation.</li> | ||||||
|  |               <li>Accepted file types are summarised automatically from the <code>accept</code> attribute.</li> | ||||||
|  |               <li>Image uploads show live previews generated via <code>URL.createObjectURL</code>.</li> | ||||||
|  |               <li>File size and file-count limits surface inline validation messages.</li> | ||||||
|  |               <li>The component stays compatible with <code>dees-form</code> value accessors.</li> | ||||||
|  |             </ul> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </dees-panel> | ||||||
|  |     </div> | ||||||
|  |   </dees-demowrapper> | ||||||
|  | `; | ||||||
							
								
								
									
										2
									
								
								ts_web/elements/dees-input-fileupload/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ts_web/elements/dees-input-fileupload/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | export * from './component.js'; | ||||||
|  | export { fileuploadStyles } from './styles.js'; | ||||||
							
								
								
									
										313
									
								
								ts_web/elements/dees-input-fileupload/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								ts_web/elements/dees-input-fileupload/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  | import { DeesInputBase } from '../dees-input-base.js'; | ||||||
|  |  | ||||||
|  | export const fileuploadStyles = [ | ||||||
|  |   cssManager.defaultStyles, | ||||||
|  |   ...DeesInputBase.baseStyles, | ||||||
|  |   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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone--has-files { | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(215 20% 11%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone--disabled { | ||||||
|  |       opacity: 0.6; | ||||||
|  |       pointer-events: none; | ||||||
|  |       cursor: not-allowed; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone__body { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 16px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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__icon dees-icon { | ||||||
|  |       font-size: 22px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone__loader { | ||||||
|  |       width: 20px; | ||||||
|  |       height: 20px; | ||||||
|  |       border-radius: 999px; | ||||||
|  |       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%)')}; | ||||||
|  |       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 { | ||||||
|  |       font-size: 13px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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-weight: 600; | ||||||
|  |       cursor: pointer; | ||||||
|  |       text-decoration: none; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone__browse:hover { | ||||||
|  |       text-decoration: underline; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone__browse:disabled { | ||||||
|  |       cursor: not-allowed; | ||||||
|  |       opacity: 0.6; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .dropzone__meta { | ||||||
|  |       margin-top: 14px; | ||||||
|  |       display: flex; | ||||||
|  |       flex-wrap: wrap; | ||||||
|  |       gap: 8px; | ||||||
|  |       font-size: 12px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 72%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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 { | ||||||
|  |       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 { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       font-size: 13px; | ||||||
|  |       font-weight: 500; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 68%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-list__clear { | ||||||
|  |       appearance: none; | ||||||
|  |       border: none; | ||||||
|  |       background: none; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; | ||||||
|  |       cursor: pointer; | ||||||
|  |       font-weight: 500; | ||||||
|  |       font-size: 13px; | ||||||
|  |       padding: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-row:hover { | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.8)', 'hsl(215 20% 16% / 0.8)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-thumb { | ||||||
|  |       width: 36px; | ||||||
|  |       height: 36px; | ||||||
|  |       border-radius: 8px; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 32% 18%)')}; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       overflow: hidden; | ||||||
|  |       flex-shrink: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-thumb dees-icon { | ||||||
|  |       font-size: 18px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 70%)')}; | ||||||
|  |       display: block; | ||||||
|  |       width: 18px; | ||||||
|  |       height: 18px; | ||||||
|  |       line-height: 1; | ||||||
|  |       flex-shrink: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     .thumb-image { | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |       object-fit: cover; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-meta { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       gap: 4px; | ||||||
|  |       min-width: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-name { | ||||||
|  |       font-weight: 600; | ||||||
|  |       font-size: 14px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')}; | ||||||
|  |       white-space: nowrap; | ||||||
|  |       overflow: hidden; | ||||||
|  |       text-overflow: ellipsis; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-details { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 8px; | ||||||
|  |       flex-wrap: wrap; | ||||||
|  |       font-size: 12px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 46%)', 'hsl(215 16% 70%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-size { | ||||||
|  |       font-variant-numeric: tabular-nums; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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%)')}; | ||||||
|  |       text-transform: uppercase; | ||||||
|  |       letter-spacing: 0.08em; | ||||||
|  |       line-height: 1; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .file-actions { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 8px; | ||||||
|  |       margin-left: auto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .remove-button { | ||||||
|  |       width: 28px; | ||||||
|  |       height: 28px; | ||||||
|  |       border-radius: 6px; | ||||||
|  |       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%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .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%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .remove-button:active { | ||||||
|  |       transform: scale(0.96); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .remove-button dees-icon { | ||||||
|  |       display: block; | ||||||
|  |       width: 14px; | ||||||
|  |       height: 14px; | ||||||
|  |       font-size: 14px; | ||||||
|  |       line-height: 1; | ||||||
|  |       flex-shrink: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .validation-message { | ||||||
|  |       font-size: 13px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')}; | ||||||
|  |       line-height: 1.5; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @keyframes loader-spin { | ||||||
|  |       to { | ||||||
|  |         transform: rotate(360deg); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   `, | ||||||
|  | ]; | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user