Compare commits
	
		
			31 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
							
								
								
									
										174
									
								
								CLAUDE.md
									
									
									
									
									
								
							
							
						
						
									
										174
									
								
								CLAUDE.md
									
									
									
									
									
								
							| @@ -1,174 +0,0 @@ | |||||||
| # CLAUDE.md |  | ||||||
|  |  | ||||||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |  | ||||||
|  |  | ||||||
| ## Project Overview |  | ||||||
|  |  | ||||||
| @design.estate/dees-catalog is a comprehensive web components library built with TypeScript and LitElement. It provides a large collection of UI components for building modern web applications with consistent design and behavior. |  | ||||||
|  |  | ||||||
| ## Build and Development Commands |  | ||||||
|  |  | ||||||
| ```bash |  | ||||||
| # Install dependencies |  | ||||||
| pnpm install |  | ||||||
|  |  | ||||||
| # Build the project |  | ||||||
| pnpm run build |  | ||||||
| # This runs: tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild |  | ||||||
|  |  | ||||||
| # Run development watch mode |  | ||||||
| pnpm run watch |  | ||||||
| # This runs: tswatch element |  | ||||||
|  |  | ||||||
| # Run tests (browser tests) |  | ||||||
| pnpm test |  | ||||||
| # This runs: tstest test/ --web --verbose --timeout 30 --logfile |  | ||||||
|  |  | ||||||
| # Run a specific test file |  | ||||||
| tsx test/test.wysiwyg-basic.browser.ts --verbose |  | ||||||
|  |  | ||||||
| # Build documentation |  | ||||||
| pnpm run buildDocs |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Testing Notes |  | ||||||
| - Test files follow the pattern: `test.*.browser.ts`, `test.*.node.ts`, or `test.*.both.ts` |  | ||||||
| - Browser tests run in a headless browser environment |  | ||||||
| - Use `--logfile` option to store logs in `.nogit/testlogs/` |  | ||||||
| - For debugging, create files in `.nogit/debug/` and run with `tsx` |  | ||||||
|  |  | ||||||
| ## Architecture Overview |  | ||||||
|  |  | ||||||
| ### Component Structure |  | ||||||
| The library is organized into several categories: |  | ||||||
|  |  | ||||||
| 1. **Core UI Components** (`dees-button`, `dees-badge`, `dees-icon`, etc.) |  | ||||||
|    - Basic building blocks with consistent theming |  | ||||||
|    - All support light/dark themes via `cssManager.bdTheme()` |  | ||||||
|  |  | ||||||
| 2. **Form Components** (`dees-form`, `dees-input-*`) |  | ||||||
|    - Complete form system with validation |  | ||||||
|    - Base class `DeesInputBase` provides common functionality |  | ||||||
|    - Form data collection via `DeesForm` container |  | ||||||
|  |  | ||||||
| 3. **Layout Components** (`dees-appui-*`) |  | ||||||
|    - Application shell components |  | ||||||
|    - `DeesAppuiBase` orchestrates the entire layout |  | ||||||
|    - Grid-based responsive design |  | ||||||
|  |  | ||||||
| 4. **Data Display** (`dees-table`, `dees-dataview-*`, `dees-statsgrid`) |  | ||||||
|    - Complex data visualization components |  | ||||||
|    - Interactive tables with sorting/filtering |  | ||||||
|    - Chart components using ApexCharts |  | ||||||
|  |  | ||||||
| 5. **Overlays** (`dees-modal`, `dees-contextmenu`, `dees-toast`) |  | ||||||
|    - Managed by central z-index registry |  | ||||||
|    - Window layer system for proper stacking |  | ||||||
|  |  | ||||||
| ### Key Architectural Patterns |  | ||||||
|  |  | ||||||
| #### Z-Index Management |  | ||||||
| All overlay components use a centralized z-index registry system: |  | ||||||
| - Definition in `ts_web/elements/00zindex.ts` |  | ||||||
| - Dynamic z-index assignment via `ZIndexRegistry` class |  | ||||||
| - Components get z-index from registry when showing |  | ||||||
| - Ensures proper stacking order (dropdowns above modals, etc.) |  | ||||||
|  |  | ||||||
| #### Theme System |  | ||||||
| - All components support light/dark themes |  | ||||||
| - Use `cssManager.bdTheme(lightValue, darkValue)` for theme-aware colors |  | ||||||
| - Consistent color palette defined in `00colors.ts` |  | ||||||
|  |  | ||||||
| #### Component Demo System |  | ||||||
| - Each component has a static `demo` property |  | ||||||
| - Demo functions in separate `.demo.ts` files |  | ||||||
| - Showcase pages aggregate demos (e.g., `input-showcase.ts`) |  | ||||||
|  |  | ||||||
| #### WYSIWYG Editor Architecture |  | ||||||
| The WYSIWYG editor uses a sophisticated architecture with separated concerns: |  | ||||||
| - **Main Component**: `dees-input-wysiwyg.ts` - Orchestrates the editor |  | ||||||
| - **Handler Classes**: |  | ||||||
|   - `WysiwygInputHandler` - Handles text input and block transformations |  | ||||||
|   - `WysiwygKeyboardHandler` - Manages keyboard shortcuts and navigation |  | ||||||
|   - `WysiwygDragDropHandler` - Manages block reordering |  | ||||||
|   - `WysiwygModalManager` - Shows configuration modals |  | ||||||
|   - `WysiwygBlockOperations` - Core block manipulation logic |  | ||||||
| - **Global Menus**:  |  | ||||||
|   - `DeesSlashMenu` and `DeesFormattingMenu` render globally to avoid focus issues |  | ||||||
|   - Singleton pattern ensures single instance |  | ||||||
| - **Programmatic Rendering**: Uses manual DOM manipulation to prevent focus loss |  | ||||||
|  |  | ||||||
| ### Component Communication |  | ||||||
| - Custom events for parent-child communication |  | ||||||
| - Form components emit standardized events (`change`, `blur`, etc.) |  | ||||||
| - Complex components like `DeesAppuiBase` re-emit child events |  | ||||||
|  |  | ||||||
| ### Build System |  | ||||||
| - TypeScript compilation with decorators support |  | ||||||
| - Web component bundling with esbuild |  | ||||||
| - Element exports in `ts_web/elements/index.ts` |  | ||||||
| - Distribution builds in `dist_ts_web/` |  | ||||||
|  |  | ||||||
| ## Important Implementation Details |  | ||||||
|  |  | ||||||
| ### When Creating New Components |  | ||||||
| 1. Extend `DeesElement` from `@design.estate/dees-element` |  | ||||||
| 2. Use `@customElement('dees-componentname')` decorator |  | ||||||
| 3. Implement theme support with `cssManager.bdTheme()` |  | ||||||
| 4. Create a demo function in a separate `.demo.ts` file |  | ||||||
| 5. Export from `elements/index.ts` |  | ||||||
|  |  | ||||||
| ### Form Input Components |  | ||||||
| 1. Extend `DeesInputBase` for form inputs |  | ||||||
| 2. Implement `getValue()` and `setValue()` methods |  | ||||||
| 3. Use `changeSubject.next(this)` to emit changes |  | ||||||
| 4. Support `disabled` and `required` properties |  | ||||||
|  |  | ||||||
| ### Overlay Components |  | ||||||
| 1. Import z-index from `00zindex.ts` |  | ||||||
| 2. Get z-index from registry when showing: `zIndexRegistry.getNextZIndex()` |  | ||||||
| 3. Register/unregister with the registry |  | ||||||
| 4. Use `DeesWindowLayer` for backdrop if needed |  | ||||||
|  |  | ||||||
| ### Testing Components |  | ||||||
| 1. Create test files in `test/` directory |  | ||||||
| 2. Use `@git.zone/tstest` with tap-bundle |  | ||||||
| 3. Test in browser environment for web components |  | ||||||
| 4. Use proper async/await for component lifecycle |  | ||||||
|  |  | ||||||
| ## Common Patterns and Pitfalls |  | ||||||
|  |  | ||||||
| ### Focus Management |  | ||||||
| - WYSIWYG editor uses programmatic rendering to prevent focus loss |  | ||||||
| - Use `requestAnimationFrame` for timing-sensitive focus operations |  | ||||||
| - Avoid reactive re-renders during user input |  | ||||||
|  |  | ||||||
| ### Event Handling |  | ||||||
| - Prevent event bubbling in nested interactive components |  | ||||||
| - Use `pointer-events: none/auto` for click-through behavior |  | ||||||
| - Handle both mouse and keyboard events for accessibility |  | ||||||
|  |  | ||||||
| ### Performance Considerations |  | ||||||
| - Large components (editor, terminal) use lazy loading |  | ||||||
| - Charts use debounced resize observers |  | ||||||
| - Tables implement virtual scrolling for large datasets |  | ||||||
|  |  | ||||||
| ## File Organization |  | ||||||
| ``` |  | ||||||
| ts_web/ |  | ||||||
| ├── elements/           # All component files |  | ||||||
| │   ├── 00*.ts         # Shared utilities (colors, z-index, plugins) |  | ||||||
| │   ├── dees-*.ts      # Component implementations |  | ||||||
| │   ├── dees-*.demo.ts # Component demos |  | ||||||
| │   ├── interfaces/    # Shared TypeScript interfaces |  | ||||||
| │   ├── helperclasses/ # Utility classes (FormController) |  | ||||||
| │   └── wysiwyg/       # WYSIWYG editor subsystem |  | ||||||
| ├── pages/             # Demo showcase pages |  | ||||||
| └── index.ts           # Main export file |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Recent Major Changes |  | ||||||
| - Z-Index Registry System (2025-12-24): Dynamic stacking order management |  | ||||||
| - WYSIWYG Refactoring (2025-06-24): Complete architecture overhaul with separated concerns |  | ||||||
| - Form System Enhancement: Unified validation and data collection |  | ||||||
| - Theme System: Consistent light/dark theme support across all components |  | ||||||
							
								
								
									
										39
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,42 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 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) | ## 2025-09-18 - 1.12.0 - feat(dees-stepper) | ||||||
| Revamp dees-stepper: modern styling, new steps and improved navigation/validation | Revamp dees-stepper: modern styling, new steps and improved navigation/validation | ||||||
|  |  | ||||||
| @@ -231,7 +268,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. |  | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@design.estate/dees-catalog", |   "name": "@design.estate/dees-catalog", | ||||||
|   "version": "1.12.0", |   "version": "1.12.5", | ||||||
|   "private": false, |   "private": false, | ||||||
|   "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", |   "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", | ||||||
|   "main": "dist_ts_web/index.js", |   "main": "dist_ts_web/index.js", | ||||||
| @@ -18,7 +18,7 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@design.estate/dees-domtools": "^2.3.3", |     "@design.estate/dees-domtools": "^2.3.3", | ||||||
|     "@design.estate/dees-element": "^2.1.2", |     "@design.estate/dees-element": "^2.1.2", | ||||||
|     "@design.estate/dees-wcctools": "^1.1.1", |     "@design.estate/dees-wcctools": "^1.2.0", | ||||||
|     "@fortawesome/fontawesome-svg-core": "^7.0.1", |     "@fortawesome/fontawesome-svg-core": "^7.0.1", | ||||||
|     "@fortawesome/free-brands-svg-icons": "^7.0.1", |     "@fortawesome/free-brands-svg-icons": "^7.0.1", | ||||||
|     "@fortawesome/free-regular-svg-icons": "^7.0.1", |     "@fortawesome/free-regular-svg-icons": "^7.0.1", | ||||||
| @@ -37,6 +37,7 @@ | |||||||
|     "apexcharts": "^5.3.5", |     "apexcharts": "^5.3.5", | ||||||
|     "highlight.js": "11.11.1", |     "highlight.js": "11.11.1", | ||||||
|     "ibantools": "^4.5.1", |     "ibantools": "^4.5.1", | ||||||
|  |     "lit": "^3.3.1", | ||||||
|     "lucide": "^0.544.0", |     "lucide": "^0.544.0", | ||||||
|     "monaco-editor": "0.52.2", |     "monaco-editor": "0.52.2", | ||||||
|     "pdfjs-dist": "^4.10.38", |     "pdfjs-dist": "^4.10.38", | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										51
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -15,8 +15,8 @@ importers: | |||||||
|         specifier: ^2.1.2 |         specifier: ^2.1.2 | ||||||
|         version: 2.1.2 |         version: 2.1.2 | ||||||
|       '@design.estate/dees-wcctools': |       '@design.estate/dees-wcctools': | ||||||
|         specifier: ^1.1.1 |         specifier: ^1.2.0 | ||||||
|         version: 1.1.1 |         version: 1.2.0 | ||||||
|       '@fortawesome/fontawesome-svg-core': |       '@fortawesome/fontawesome-svg-core': | ||||||
|         specifier: ^7.0.1 |         specifier: ^7.0.1 | ||||||
|         version: 7.0.1 |         version: 7.0.1 | ||||||
| @@ -71,6 +71,9 @@ importers: | |||||||
|       ibantools: |       ibantools: | ||||||
|         specifier: ^4.5.1 |         specifier: ^4.5.1 | ||||||
|         version: 4.5.1 |         version: 4.5.1 | ||||||
|  |       lit: | ||||||
|  |         specifier: ^3.3.1 | ||||||
|  |         version: 3.3.1 | ||||||
|       lucide: |       lucide: | ||||||
|         specifier: ^0.544.0 |         specifier: ^0.544.0 | ||||||
|         version: 0.544.0 |         version: 0.544.0 | ||||||
| @@ -450,8 +453,8 @@ packages: | |||||||
|   '@design.estate/dees-element@2.1.2': |   '@design.estate/dees-element@2.1.2': | ||||||
|     resolution: {integrity: sha512-ZiwvE411RJPHaYio26asQLnSmtJ6G1HRLYWbxW/HvCMbFtrcrXysP1y4PQ9KjdNfiQ4yoWPjTtwYMJjLE0NcbA==} |     resolution: {integrity: sha512-ZiwvE411RJPHaYio26asQLnSmtJ6G1HRLYWbxW/HvCMbFtrcrXysP1y4PQ9KjdNfiQ4yoWPjTtwYMJjLE0NcbA==} | ||||||
|  |  | ||||||
|   '@design.estate/dees-wcctools@1.1.1': |   '@design.estate/dees-wcctools@1.2.0': | ||||||
|     resolution: {integrity: sha512-oT0gPQ9suaCi0D2jNHPjE0ugn0xUm43yPfQt7vQgrOZZ6EOQ3zWkYVOp8NbGOVwKTvMvZKyjdDmqJG4NFHPvcg==} |     resolution: {integrity: sha512-E01IPNzGJ1TtCxsBiJAaDkIkveu1VwDv24CLfBt+UzjJnZGOJqDKYJlgE3XV1aBs4G/cI5bQ8j8rGqdGwp2FCg==} | ||||||
|  |  | ||||||
|   '@emnapi/core@1.4.3': |   '@emnapi/core@1.4.3': | ||||||
|     resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} |     resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} | ||||||
| @@ -853,15 +856,9 @@ packages: | |||||||
|   '@leichtgewicht/ip-codec@2.0.5': |   '@leichtgewicht/ip-codec@2.0.5': | ||||||
|     resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} |     resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} | ||||||
|  |  | ||||||
|   '@lit-labs/ssr-dom-shim@1.3.0': |  | ||||||
|     resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==} |  | ||||||
|  |  | ||||||
|   '@lit-labs/ssr-dom-shim@1.4.0': |   '@lit-labs/ssr-dom-shim@1.4.0': | ||||||
|     resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} |     resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} | ||||||
|  |  | ||||||
|   '@lit/reactive-element@2.1.0': |  | ||||||
|     resolution: {integrity: sha512-L2qyoZSQClcBmq0qajBVbhYEcG6iK0XfLn66ifLe/RfC0/ihpc+pl0Wdn8bJ8o+hj38cG0fGXRgSS20MuXn7qA==} |  | ||||||
|  |  | ||||||
|   '@lit/reactive-element@2.1.1': |   '@lit/reactive-element@2.1.1': | ||||||
|     resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} |     resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} | ||||||
|  |  | ||||||
| @@ -3850,9 +3847,6 @@ packages: | |||||||
|   linkifyjs@4.3.1: |   linkifyjs@4.3.1: | ||||||
|     resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==} |     resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==} | ||||||
|  |  | ||||||
|   lit-element@4.2.0: |  | ||||||
|     resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==} |  | ||||||
|  |  | ||||||
|   lit-element@4.2.1: |   lit-element@4.2.1: | ||||||
|     resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} |     resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} | ||||||
|  |  | ||||||
| @@ -3862,9 +3856,6 @@ packages: | |||||||
|   lit-html@3.3.1: |   lit-html@3.3.1: | ||||||
|     resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} |     resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} | ||||||
|  |  | ||||||
|   lit@3.3.0: |  | ||||||
|     resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==} |  | ||||||
|  |  | ||||||
|   lit@3.3.1: |   lit@3.3.1: | ||||||
|     resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} |     resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} | ||||||
|  |  | ||||||
| @@ -6466,7 +6457,7 @@ snapshots: | |||||||
|       '@push.rocks/websetup': 3.0.19 |       '@push.rocks/websetup': 3.0.19 | ||||||
|       '@push.rocks/webstore': 2.0.20 |       '@push.rocks/webstore': 2.0.20 | ||||||
|       lenis: 1.3.4 |       lenis: 1.3.4 | ||||||
|       lit: 3.3.0 |       lit: 3.3.1 | ||||||
|       sweet-scroll: 4.0.0 |       sweet-scroll: 4.0.0 | ||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@nuxt/kit' |       - '@nuxt/kit' | ||||||
| @@ -6486,7 +6477,7 @@ snapshots: | |||||||
|       - supports-color |       - supports-color | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@design.estate/dees-wcctools@1.1.1': |   '@design.estate/dees-wcctools@1.2.0': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@design.estate/dees-domtools': 2.3.3 |       '@design.estate/dees-domtools': 2.3.3 | ||||||
|       '@design.estate/dees-element': 2.1.2 |       '@design.estate/dees-element': 2.1.2 | ||||||
| @@ -6810,8 +6801,10 @@ snapshots: | |||||||
|     transitivePeerDependencies: |     transitivePeerDependencies: | ||||||
|       - '@nuxt/kit' |       - '@nuxt/kit' | ||||||
|       - '@swc/helpers' |       - '@swc/helpers' | ||||||
|  |       - bufferutil | ||||||
|       - react |       - react | ||||||
|       - supports-color |       - supports-color | ||||||
|  |       - utf-8-validate | ||||||
|       - vue |       - vue | ||||||
|  |  | ||||||
|   '@hapi/bourne@3.0.0': {} |   '@hapi/bourne@3.0.0': {} | ||||||
| @@ -6864,14 +6857,8 @@ snapshots: | |||||||
|  |  | ||||||
|   '@leichtgewicht/ip-codec@2.0.5': {} |   '@leichtgewicht/ip-codec@2.0.5': {} | ||||||
|  |  | ||||||
|   '@lit-labs/ssr-dom-shim@1.3.0': {} |  | ||||||
|  |  | ||||||
|   '@lit-labs/ssr-dom-shim@1.4.0': {} |   '@lit-labs/ssr-dom-shim@1.4.0': {} | ||||||
|  |  | ||||||
|   '@lit/reactive-element@2.1.0': |  | ||||||
|     dependencies: |  | ||||||
|       '@lit-labs/ssr-dom-shim': 1.3.0 |  | ||||||
|  |  | ||||||
|   '@lit/reactive-element@2.1.1': |   '@lit/reactive-element@2.1.1': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@lit-labs/ssr-dom-shim': 1.4.0 |       '@lit-labs/ssr-dom-shim': 1.4.0 | ||||||
| @@ -6992,7 +6979,7 @@ snapshots: | |||||||
|   '@open-wc/scoped-elements@3.0.5': |   '@open-wc/scoped-elements@3.0.5': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@open-wc/dedupe-mixin': 1.4.0 |       '@open-wc/dedupe-mixin': 1.4.0 | ||||||
|       lit: 3.3.0 |       lit: 3.3.1 | ||||||
|  |  | ||||||
|   '@open-wc/semantic-dom-diff@0.20.1': |   '@open-wc/semantic-dom-diff@0.20.1': | ||||||
|     dependencies: |     dependencies: | ||||||
| @@ -7006,7 +6993,7 @@ snapshots: | |||||||
|   '@open-wc/testing-helpers@3.0.1': |   '@open-wc/testing-helpers@3.0.1': | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@open-wc/scoped-elements': 3.0.5 |       '@open-wc/scoped-elements': 3.0.5 | ||||||
|       lit: 3.3.0 |       lit: 3.3.1 | ||||||
|       lit-html: 3.3.0 |       lit-html: 3.3.0 | ||||||
|  |  | ||||||
|   '@open-wc/testing@4.0.0': |   '@open-wc/testing@4.0.0': | ||||||
| @@ -10987,12 +10974,6 @@ snapshots: | |||||||
|  |  | ||||||
|   linkifyjs@4.3.1: {} |   linkifyjs@4.3.1: {} | ||||||
|  |  | ||||||
|   lit-element@4.2.0: |  | ||||||
|     dependencies: |  | ||||||
|       '@lit-labs/ssr-dom-shim': 1.3.0 |  | ||||||
|       '@lit/reactive-element': 2.1.0 |  | ||||||
|       lit-html: 3.3.0 |  | ||||||
|  |  | ||||||
|   lit-element@4.2.1: |   lit-element@4.2.1: | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@lit-labs/ssr-dom-shim': 1.4.0 |       '@lit-labs/ssr-dom-shim': 1.4.0 | ||||||
| @@ -11007,12 +10988,6 @@ snapshots: | |||||||
|     dependencies: |     dependencies: | ||||||
|       '@types/trusted-types': 2.0.7 |       '@types/trusted-types': 2.0.7 | ||||||
|  |  | ||||||
|   lit@3.3.0: |  | ||||||
|     dependencies: |  | ||||||
|       '@lit/reactive-element': 2.1.0 |  | ||||||
|       lit-element: 4.2.0 |  | ||||||
|       lit-html: 3.3.0 |  | ||||||
|  |  | ||||||
|   lit@3.3.1: |   lit@3.3.1: | ||||||
|     dependencies: |     dependencies: | ||||||
|       '@lit/reactive-element': 2.1.1 |       '@lit/reactive-element': 2.1.1 | ||||||
|   | |||||||
| @@ -1,2 +1,4 @@ | |||||||
| onlyBuiltDependencies: | onlyBuiltDependencies: | ||||||
|  |   - esbuild | ||||||
|  |   - mongodb-memory-server | ||||||
|   - puppeteer |   - puppeteer | ||||||
|   | |||||||
							
								
								
									
										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.
										
									
								
							| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@design.estate/dees-catalog', |   name: '@design.estate/dees-catalog', | ||||||
|   version: '1.12.0', |   version: '1.12.5', | ||||||
|   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.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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> | ||||||
|  |       `; | ||||||
|  |    | ||||||
|  | }; | ||||||
| @@ -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'; | ||||||
|   | |||||||
| @@ -1,16 +1,15 @@ | |||||||
| import { | import { | ||||||
|   DeesElement, |   DeesElement, | ||||||
|   css, |  | ||||||
|   cssManager, |  | ||||||
|   customElement, |   customElement, | ||||||
|   html, |  | ||||||
|   property, |   property, | ||||||
|   state, |   state, | ||||||
|   type TemplateResult, |   type TemplateResult, | ||||||
| } 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-chart-area.demo.js'; | import { demoFunc } from './demo.js'; | ||||||
|  | import { chartAreaStyles } from './styles.js'; | ||||||
|  | import { renderChartArea } from './template.js'; | ||||||
| 
 | 
 | ||||||
| import ApexCharts from 'apexcharts'; | import ApexCharts from 'apexcharts'; | ||||||
| 
 | 
 | ||||||
| @@ -141,73 +140,14 @@ export class DeesChartArea extends DeesElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public static styles = [ |   public static styles = 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; |  | ||||||
|       } |  | ||||||
|     `,
 |  | ||||||
|   ]; |  | ||||||
| 
 | 
 | ||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|     return html` |     return renderChartArea(this); | ||||||
|       <div class="mainbox"> |  | ||||||
|         <div class="chartTitle">${this.label}</div> |  | ||||||
|         <div class="chartContainer"></div> |  | ||||||
|       </div> |  | ||||||
|     `;
 |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   public async firstUpdated() { |   public async firstUpdated() { | ||||||
|     await this.domtoolsPromise; |     await this.domtoolsPromise; | ||||||
|      |      | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { html, css, cssManager } 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
 | ||||||
							
								
								
									
										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> | ||||||
|  |       `; | ||||||
|  |    | ||||||
|  | }; | ||||||
							
								
								
									
										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'; | ||||||
| @@ -9,18 +9,18 @@ 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.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/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 | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										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 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { html, css } from '@design.estate/dees-element'; | import { html, css } from '@design.estate/dees-element'; | ||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
| import './dees-panel.js'; | import '../dees-panel.js'; | ||||||
| import './dees-input-datepicker.js'; | import './component.js'; | ||||||
| import type { DeesInputDatepicker } from './dees-input-datepicker.js'; | import type { DeesInputDatepicker } from './component.js'; | ||||||
| 
 | 
 | ||||||
| export const demoFunc = () => html` | export const demoFunc = () => html` | ||||||
|   <style> |   <style> | ||||||
							
								
								
									
										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 | ||||||
|  | } | ||||||
| @@ -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,721 +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: Boolean }) |  | ||||||
|   private isLoading: boolean = false; |  | ||||||
|  |  | ||||||
|   @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('hsl(0 0% 15%)', 'hsl(0 0% 90%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .hidden { |  | ||||||
|         display: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .input-wrapper { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         gap: 8px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .maincontainer { |  | ||||||
|         position: relative; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         padding: 16px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 11.8%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')}; |  | ||||||
|         transition: all 0.15s ease; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .maincontainer:hover { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(215 20.2% 55.1%)', 'hsl(215 20.2% 45.1%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([disabled]) .maincontainer { |  | ||||||
|         opacity: 0.5; |  | ||||||
|         cursor: not-allowed; |  | ||||||
|         pointer-events: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([validationState="invalid"]) .maincontainer { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([validationState="valid"]) .maincontainer { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([validationState="warn"]) .maincontainer { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .maincontainer::after { |  | ||||||
|         top: 1px; |  | ||||||
|         right: 1px; |  | ||||||
|         left: 1px; |  | ||||||
|         bottom: 1px; |  | ||||||
|         transform: scale3d(0.98, 0.95, 1); |  | ||||||
|         position: absolute; |  | ||||||
|         content: ''; |  | ||||||
|         display: block; |  | ||||||
|         border: 2px dashed transparent; |  | ||||||
|         border-radius: 5px; |  | ||||||
|         transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |  | ||||||
|         pointer-events: none; |  | ||||||
|         background: transparent; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .maincontainer.dragOver { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8% / 0.05)', 'hsl(213.1 93.9% 67.8% / 0.05)')}; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .maincontainer.dragOver::after { |  | ||||||
|         transform: scale3d(1, 1, 1); |  | ||||||
|         border: 2px dashed ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton { |  | ||||||
|         position: relative; |  | ||||||
|         padding: 10px 20px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 7.8%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         text-align: center; |  | ||||||
|         font-size: 14px; |  | ||||||
|         font-weight: 500; |  | ||||||
|         cursor: pointer; |  | ||||||
|         transition: all 0.15s ease; |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         gap: 8px; |  | ||||||
|         line-height: 20px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton:hover { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton:active { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 11%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .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('hsl(0 0% 100%)', 'hsl(215 20.2% 16.8%)')}; |  | ||||||
|         padding: 12px; |  | ||||||
|         text-align: left; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; |  | ||||||
|         cursor: default; |  | ||||||
|         transition: all 0.15s ease; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         position: relative; |  | ||||||
|         overflow: hidden; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate:hover { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(215 20.2% 20.8%)')}; |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate .icon { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         font-size: 20px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadCandidate.image-file .icon { |  | ||||||
|         color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')}; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .uploadCandidate.pdf-file .icon { |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       .uploadCandidate.doc-file .icon { |  | ||||||
|         color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .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('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .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.15s ease; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .remove-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-all-button { |  | ||||||
|         margin-bottom: 8px; |  | ||||||
|         text-align: right; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .clear-all-button button { |  | ||||||
|         background: none; |  | ||||||
|         border: none; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; |  | ||||||
|         cursor: pointer; |  | ||||||
|         font-size: 12px; |  | ||||||
|         padding: 4px 8px; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         transition: all 0.15s ease; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .clear-all-button 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%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .validation-message { |  | ||||||
|         font-size: 13px; |  | ||||||
|         margin-top: 6px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; |  | ||||||
|         line-height: 1.5; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .drop-hint { |  | ||||||
|         text-align: center; |  | ||||||
|         padding: 40px 20px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; |  | ||||||
|         font-size: 14px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .drop-hint dees-icon { |  | ||||||
|         font-size: 48px; |  | ||||||
|         margin-bottom: 16px; |  | ||||||
|         opacity: 0.2; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .image-preview { |  | ||||||
|         width: 40px; |  | ||||||
|         height: 40px; |  | ||||||
|         object-fit: cover; |  | ||||||
|         border-radius: 4px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .description-text { |  | ||||||
|         font-size: 13px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; |  | ||||||
|         margin-top: 6px; |  | ||||||
|         line-height: 1.5; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       /* Loading state styles */ |  | ||||||
|       .uploadButton.loading { |  | ||||||
|         pointer-events: none; |  | ||||||
|         opacity: 0.8; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton .button-content { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         gap: 8px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .loading-spinner { |  | ||||||
|         width: 16px; |  | ||||||
|         height: 16px; |  | ||||||
|         border: 2px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; |  | ||||||
|         border-top-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; |  | ||||||
|         border-radius: 50%; |  | ||||||
|         animation: spin 0.6s linear infinite; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       @keyframes spin { |  | ||||||
|         to { |  | ||||||
|           transform: rotate(360deg); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       @keyframes pulse { |  | ||||||
|         0% { |  | ||||||
|           transform: scale(1); |  | ||||||
|           opacity: 1; |  | ||||||
|         } |  | ||||||
|         50% { |  | ||||||
|           transform: scale(1.02); |  | ||||||
|           opacity: 0.9; |  | ||||||
|         } |  | ||||||
|         100% { |  | ||||||
|           transform: scale(1); |  | ||||||
|           opacity: 1; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .uploadButton.loading { |  | ||||||
|         animation: pulse 1s ease-in-out infinite; |  | ||||||
|       } |  | ||||||
|     `, |  | ||||||
|   ]; |  | ||||||
|  |  | ||||||
|   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 .icon=${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 .icon=${'lucide:x'}></dees-icon> |  | ||||||
|                       </button> |  | ||||||
|                     </div> |  | ||||||
|                   </div> |  | ||||||
|                 `; |  | ||||||
|               })} |  | ||||||
|             </div> |  | ||||||
|           ` : html` |  | ||||||
|             <div class="drop-hint"> |  | ||||||
|               <dees-icon .icon=${'lucide:cloud-upload'}></dees-icon> |  | ||||||
|               <div>Drag files here or click to browse</div> |  | ||||||
|             </div> |  | ||||||
|           `} |  | ||||||
|           <div class="uploadButton ${this.isLoading ? 'loading' : ''}" @click=${this.openFileSelector}> |  | ||||||
|             <div class="button-content"> |  | ||||||
|               ${this.isLoading ? html` |  | ||||||
|                 <div class="loading-spinner"></div> |  | ||||||
|                 <span>Opening...</span> |  | ||||||
|               ` : html` |  | ||||||
|                 <dees-icon .icon=${'lucide:upload'}></dees-icon> |  | ||||||
|                 ${this.buttonText} |  | ||||||
|               `} |  | ||||||
|             </div> |  | ||||||
|           </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 || this.isLoading) return; |  | ||||||
|      |  | ||||||
|     // Set loading state |  | ||||||
|     this.isLoading = true; |  | ||||||
|      |  | ||||||
|     const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); |  | ||||||
|      |  | ||||||
|     // Set up a focus handler to detect when the dialog is closed without selection |  | ||||||
|     const handleFocus = () => { |  | ||||||
|       setTimeout(() => { |  | ||||||
|         // Check if no file was selected |  | ||||||
|         if (!inputFile.files || inputFile.files.length === 0) { |  | ||||||
|           this.isLoading = false; |  | ||||||
|         } |  | ||||||
|         window.removeEventListener('focus', handleFocus); |  | ||||||
|       }, 300); |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     window.addEventListener('focus', handleFocus); |  | ||||||
|     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); |  | ||||||
|        |  | ||||||
|       // Always reset loading state when file dialog interaction completes |  | ||||||
|       this.isLoading = false; |  | ||||||
|        |  | ||||||
|       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); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   `, | ||||||
|  | ]; | ||||||
| @@ -1,714 +0,0 @@ | |||||||
| import * as colors from './00colors.js'; |  | ||||||
| import { DeesInputBase } from './dees-input-base.js'; |  | ||||||
| import { demoFunc } from './dees-input-richtext.demo.js'; |  | ||||||
| import './dees-icon.js'; |  | ||||||
|  |  | ||||||
| import { |  | ||||||
|   customElement, |  | ||||||
|   type TemplateResult, |  | ||||||
|   property, |  | ||||||
|   html, |  | ||||||
|   css, |  | ||||||
|   cssManager, |  | ||||||
|   state, |  | ||||||
|   query, |  | ||||||
| } from '@design.estate/dees-element'; |  | ||||||
| import * as domtools from '@design.estate/dees-domtools'; |  | ||||||
|  |  | ||||||
| import { Editor } from '@tiptap/core'; |  | ||||||
| import StarterKit from '@tiptap/starter-kit'; |  | ||||||
| import Underline from '@tiptap/extension-underline'; |  | ||||||
| import TextAlign from '@tiptap/extension-text-align'; |  | ||||||
| import Link from '@tiptap/extension-link'; |  | ||||||
| import Typography from '@tiptap/extension-typography'; |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     'dees-input-richtext': DeesInputRichtext; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface IToolbarButton { |  | ||||||
|   name: string; |  | ||||||
|   icon?: string; |  | ||||||
|   action?: () => void; |  | ||||||
|   isActive?: () => boolean; |  | ||||||
|   title: string; |  | ||||||
|   isDivider?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement('dees-input-richtext') |  | ||||||
| export class DeesInputRichtext extends DeesInputBase<string> { |  | ||||||
|   public static demo = demoFunc; |  | ||||||
|  |  | ||||||
|   // INSTANCE |  | ||||||
|   @property({ |  | ||||||
|     type: String, |  | ||||||
|     reflect: true, |  | ||||||
|   }) |  | ||||||
|   public value: string = ''; |  | ||||||
|  |  | ||||||
|   @property({ |  | ||||||
|     type: String, |  | ||||||
|   }) |  | ||||||
|   public placeholder: string = ''; |  | ||||||
|  |  | ||||||
|   @property({ |  | ||||||
|     type: Boolean, |  | ||||||
|   }) |  | ||||||
|   public showWordCount: boolean = true; |  | ||||||
|  |  | ||||||
|   @property({ |  | ||||||
|     type: Number, |  | ||||||
|   }) |  | ||||||
|   public minHeight: number = 200; |  | ||||||
|  |  | ||||||
|   @state() |  | ||||||
|   private showLinkInput: boolean = false; |  | ||||||
|  |  | ||||||
|   @state() |  | ||||||
|   private wordCount: number = 0; |  | ||||||
|  |  | ||||||
|   @query('.editor-content') |  | ||||||
|   private editorElement: HTMLElement; |  | ||||||
|  |  | ||||||
|   @query('.link-input input') |  | ||||||
|   private linkInputElement: HTMLInputElement; |  | ||||||
|  |  | ||||||
|   private editor: Editor; |  | ||||||
|  |  | ||||||
|   public static styles = [ |  | ||||||
|     ...DeesInputBase.baseStyles, |  | ||||||
|     cssManager.defaultStyles, |  | ||||||
|     css` |  | ||||||
|       :host { |  | ||||||
|         display: block; |  | ||||||
|         position: relative; |  | ||||||
|         font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .input-wrapper { |  | ||||||
|         position: relative; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .label { |  | ||||||
|         display: block; |  | ||||||
|         margin-bottom: 8px; |  | ||||||
|         font-size: 14px; |  | ||||||
|         font-weight: 500; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-container { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: column; |  | ||||||
|         min-height: ${cssManager.bdTheme('200px', '200px')}; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; |  | ||||||
|         overflow: hidden; |  | ||||||
|         transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-container:hover { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-container.focused { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-toolbar { |  | ||||||
|         display: flex; |  | ||||||
|         flex-wrap: wrap; |  | ||||||
|         gap: 4px; |  | ||||||
|         padding: 8px 12px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         align-items: center; |  | ||||||
|         position: relative; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .toolbar-button { |  | ||||||
|         display: flex; |  | ||||||
|         align-items: center; |  | ||||||
|         justify-content: center; |  | ||||||
|         width: 32px; |  | ||||||
|         height: 32px; |  | ||||||
|         border: none; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         background: transparent; |  | ||||||
|         cursor: pointer; |  | ||||||
|         font-size: 14px; |  | ||||||
|         font-weight: 500; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; |  | ||||||
|         transition: all 0.15s ease; |  | ||||||
|         user-select: none; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .toolbar-button dees-icon { |  | ||||||
|         width: 16px; |  | ||||||
|         height: 16px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .toolbar-button:hover { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .toolbar-button.active { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .toolbar-button:disabled { |  | ||||||
|         opacity: 0.5; |  | ||||||
|         cursor: not-allowed; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .toolbar-divider { |  | ||||||
|         width: 1px; |  | ||||||
|         height: 24px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         margin: 0 4px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content { |  | ||||||
|         flex: 1; |  | ||||||
|         padding: 16px; |  | ||||||
|         overflow-y: auto; |  | ||||||
|         min-height: var(--min-height, 200px); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror { |  | ||||||
|         outline: none; |  | ||||||
|         line-height: 1.6; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         min-height: 100%; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror p { |  | ||||||
|         margin: 0.5em 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror p:first-child { |  | ||||||
|         margin-top: 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror p:last-child { |  | ||||||
|         margin-bottom: 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror h1 { |  | ||||||
|         font-size: 2em; |  | ||||||
|         font-weight: bold; |  | ||||||
|         margin: 1em 0 0.5em 0; |  | ||||||
|         line-height: 1.2; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror h2 { |  | ||||||
|         font-size: 1.5em; |  | ||||||
|         font-weight: bold; |  | ||||||
|         margin: 1em 0 0.5em 0; |  | ||||||
|         line-height: 1.3; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror h3 { |  | ||||||
|         font-size: 1.25em; |  | ||||||
|         font-weight: bold; |  | ||||||
|         margin: 1em 0 0.5em 0; |  | ||||||
|         line-height: 1.4; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror ul, |  | ||||||
|       .editor-content .ProseMirror ol { |  | ||||||
|         padding-left: 1.5em; |  | ||||||
|         margin: 0.5em 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror li { |  | ||||||
|         margin: 0.25em 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror blockquote { |  | ||||||
|         border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         margin: 1em 0; |  | ||||||
|         padding-left: 1em; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; |  | ||||||
|         font-style: italic; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror code { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-radius: 3px; |  | ||||||
|         padding: 0.2em 0.4em; |  | ||||||
|         font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; |  | ||||||
|         font-size: 0.9em; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror pre { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')}; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         padding: 1em; |  | ||||||
|         margin: 1em 0; |  | ||||||
|         overflow-x: auto; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror pre code { |  | ||||||
|         background: none; |  | ||||||
|         color: inherit; |  | ||||||
|         padding: 0; |  | ||||||
|         border-radius: 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror a { |  | ||||||
|         color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; |  | ||||||
|         text-decoration: underline; |  | ||||||
|         cursor: pointer; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-content .ProseMirror a:hover { |  | ||||||
|         color: ${cssManager.bdTheme('hsl(222.2 47.4% 41.2%)', 'hsl(217.2 91.2% 69.8%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .editor-footer { |  | ||||||
|         padding: 8px 12px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         font-size: 12px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; |  | ||||||
|         display: flex; |  | ||||||
|         justify-content: space-between; |  | ||||||
|         align-items: center; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .word-count { |  | ||||||
|         font-weight: 500; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input { |  | ||||||
|         display: none; |  | ||||||
|         position: absolute; |  | ||||||
|         top: 100%; |  | ||||||
|         left: 0; |  | ||||||
|         right: 0; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |  | ||||||
|         padding: 12px; |  | ||||||
|         z-index: 1000; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input.show { |  | ||||||
|         display: block; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input input { |  | ||||||
|         width: 100%; |  | ||||||
|         padding: 8px 12px; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-radius: 6px; |  | ||||||
|         outline: none; |  | ||||||
|         font-size: 14px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input input:focus { |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input-buttons { |  | ||||||
|         display: flex; |  | ||||||
|         gap: 8px; |  | ||||||
|         margin-top: 8px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input-buttons button { |  | ||||||
|         padding: 6px 12px; |  | ||||||
|         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; |  | ||||||
|         cursor: pointer; |  | ||||||
|         font-size: 12px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; |  | ||||||
|         transition: all 0.15s ease; |  | ||||||
|         font-weight: 500; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input-buttons button:hover { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input-buttons button.primary { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .link-input-buttons button.primary:hover { |  | ||||||
|         background: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; |  | ||||||
|         border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .description { |  | ||||||
|         margin-top: 8px; |  | ||||||
|         font-size: 12px; |  | ||||||
|         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; |  | ||||||
|         line-height: 1.4; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([disabled]) .editor-container { |  | ||||||
|         opacity: 0.6; |  | ||||||
|         cursor: not-allowed; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       :host([disabled]) .toolbar-button, |  | ||||||
|       :host([disabled]) .editor-content { |  | ||||||
|         pointer-events: none; |  | ||||||
|       } |  | ||||||
|     `, |  | ||||||
|   ]; |  | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |  | ||||||
|     return html` |  | ||||||
|       <div class="input-wrapper"> |  | ||||||
|         ${this.label ? html`<label class="label">${this.label}</label>` : ''} |  | ||||||
|         <div class="editor-container ${this.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${this.minHeight}px"> |  | ||||||
|           <div class="editor-toolbar"> |  | ||||||
|             ${this.renderToolbar()} |  | ||||||
|             <div class="link-input ${this.showLinkInput ? 'show' : ''}"> |  | ||||||
|               <input type="url" placeholder="Enter URL..." @keydown=${this.handleLinkInputKeydown} /> |  | ||||||
|               <div class="link-input-buttons"> |  | ||||||
|                 <button class="primary" @click=${this.saveLink}>Save</button> |  | ||||||
|                 <button @click=${this.removeLink}>Remove</button> |  | ||||||
|                 <button @click=${this.hideLinkInput}>Cancel</button> |  | ||||||
|               </div> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|           <div class="editor-content"></div> |  | ||||||
|           ${this.showWordCount |  | ||||||
|             ? html` |  | ||||||
|                 <div class="editor-footer"> |  | ||||||
|                   <span class="word-count">${this.wordCount} word${this.wordCount !== 1 ? 's' : ''}</span> |  | ||||||
|                 </div> |  | ||||||
|               ` |  | ||||||
|             : ''} |  | ||||||
|         </div> |  | ||||||
|         ${this.description ? html`<div class="description">${this.description}</div>` : ''} |  | ||||||
|       </div> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private renderToolbar(): TemplateResult { |  | ||||||
|     const buttons: IToolbarButton[] = this.getToolbarButtons(); |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       ${buttons.map((button) => { |  | ||||||
|         if (button.isDivider) { |  | ||||||
|           return html`<div class="toolbar-divider"></div>`; |  | ||||||
|         } |  | ||||||
|         return html` |  | ||||||
|           <button |  | ||||||
|             class="toolbar-button ${button.isActive?.() ? 'active' : ''}" |  | ||||||
|             @click=${button.action} |  | ||||||
|             title=${button.title} |  | ||||||
|             ?disabled=${this.disabled || !this.editor} |  | ||||||
|           > |  | ||||||
|             <dees-icon .icon=${button.icon}></dees-icon> |  | ||||||
|           </button> |  | ||||||
|         `; |  | ||||||
|       })} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private getToolbarButtons(): IToolbarButton[] { |  | ||||||
|     if (!this.editor) return []; |  | ||||||
|  |  | ||||||
|     return [ |  | ||||||
|       { |  | ||||||
|         name: 'bold', |  | ||||||
|         icon: 'lucide:bold', |  | ||||||
|         title: 'Bold (Ctrl+B)', |  | ||||||
|         action: () => this.editor.chain().focus().toggleBold().run(), |  | ||||||
|         isActive: () => this.editor.isActive('bold'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'italic', |  | ||||||
|         icon: 'lucide:italic', |  | ||||||
|         title: 'Italic (Ctrl+I)', |  | ||||||
|         action: () => this.editor.chain().focus().toggleItalic().run(), |  | ||||||
|         isActive: () => this.editor.isActive('italic'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'underline', |  | ||||||
|         icon: 'lucide:underline', |  | ||||||
|         title: 'Underline (Ctrl+U)', |  | ||||||
|         action: () => this.editor.chain().focus().toggleUnderline().run(), |  | ||||||
|         isActive: () => this.editor.isActive('underline'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'strike', |  | ||||||
|         icon: 'lucide:strikethrough', |  | ||||||
|         title: 'Strikethrough', |  | ||||||
|         action: () => this.editor.chain().focus().toggleStrike().run(), |  | ||||||
|         isActive: () => this.editor.isActive('strike'), |  | ||||||
|       }, |  | ||||||
|       { name: 'divider1', title: '', isDivider: true }, |  | ||||||
|       { |  | ||||||
|         name: 'h1', |  | ||||||
|         icon: 'lucide:heading1', |  | ||||||
|         title: 'Heading 1', |  | ||||||
|         action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(), |  | ||||||
|         isActive: () => this.editor.isActive('heading', { level: 1 }), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'h2', |  | ||||||
|         icon: 'lucide:heading2', |  | ||||||
|         title: 'Heading 2', |  | ||||||
|         action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(), |  | ||||||
|         isActive: () => this.editor.isActive('heading', { level: 2 }), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'h3', |  | ||||||
|         icon: 'lucide:heading3', |  | ||||||
|         title: 'Heading 3', |  | ||||||
|         action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(), |  | ||||||
|         isActive: () => this.editor.isActive('heading', { level: 3 }), |  | ||||||
|       }, |  | ||||||
|       { name: 'divider2', title: '', isDivider: true }, |  | ||||||
|       { |  | ||||||
|         name: 'bulletList', |  | ||||||
|         icon: 'lucide:list', |  | ||||||
|         title: 'Bullet List', |  | ||||||
|         action: () => this.editor.chain().focus().toggleBulletList().run(), |  | ||||||
|         isActive: () => this.editor.isActive('bulletList'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'orderedList', |  | ||||||
|         icon: 'lucide:listOrdered', |  | ||||||
|         title: 'Numbered List', |  | ||||||
|         action: () => this.editor.chain().focus().toggleOrderedList().run(), |  | ||||||
|         isActive: () => this.editor.isActive('orderedList'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'blockquote', |  | ||||||
|         icon: 'lucide:quote', |  | ||||||
|         title: 'Quote', |  | ||||||
|         action: () => this.editor.chain().focus().toggleBlockquote().run(), |  | ||||||
|         isActive: () => this.editor.isActive('blockquote'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'code', |  | ||||||
|         icon: 'lucide:code', |  | ||||||
|         title: 'Code', |  | ||||||
|         action: () => this.editor.chain().focus().toggleCode().run(), |  | ||||||
|         isActive: () => this.editor.isActive('code'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'codeBlock', |  | ||||||
|         icon: 'lucide:fileCode', |  | ||||||
|         title: 'Code Block', |  | ||||||
|         action: () => this.editor.chain().focus().toggleCodeBlock().run(), |  | ||||||
|         isActive: () => this.editor.isActive('codeBlock'), |  | ||||||
|       }, |  | ||||||
|       { name: 'divider3', title: '', isDivider: true }, |  | ||||||
|       { |  | ||||||
|         name: 'link', |  | ||||||
|         icon: 'lucide:link', |  | ||||||
|         title: 'Add Link', |  | ||||||
|         action: () => this.toggleLink(), |  | ||||||
|         isActive: () => this.editor.isActive('link'), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'alignLeft', |  | ||||||
|         icon: 'lucide:alignLeft', |  | ||||||
|         title: 'Align Left', |  | ||||||
|         action: () => this.editor.chain().focus().setTextAlign('left').run(), |  | ||||||
|         isActive: () => this.editor.isActive({ textAlign: 'left' }), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'alignCenter', |  | ||||||
|         icon: 'lucide:alignCenter', |  | ||||||
|         title: 'Align Center', |  | ||||||
|         action: () => this.editor.chain().focus().setTextAlign('center').run(), |  | ||||||
|         isActive: () => this.editor.isActive({ textAlign: 'center' }), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'alignRight', |  | ||||||
|         icon: 'lucide:alignRight', |  | ||||||
|         title: 'Align Right', |  | ||||||
|         action: () => this.editor.chain().focus().setTextAlign('right').run(), |  | ||||||
|         isActive: () => this.editor.isActive({ textAlign: 'right' }), |  | ||||||
|       }, |  | ||||||
|       { name: 'divider4', title: '', isDivider: true }, |  | ||||||
|       { |  | ||||||
|         name: 'undo', |  | ||||||
|         icon: 'lucide:undo', |  | ||||||
|         title: 'Undo (Ctrl+Z)', |  | ||||||
|         action: () => this.editor.chain().focus().undo().run(), |  | ||||||
|       }, |  | ||||||
|       { |  | ||||||
|         name: 'redo', |  | ||||||
|         icon: 'lucide:redo', |  | ||||||
|         title: 'Redo (Ctrl+Y)', |  | ||||||
|         action: () => this.editor.chain().focus().redo().run(), |  | ||||||
|       }, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async firstUpdated() { |  | ||||||
|     await this.updateComplete; |  | ||||||
|     this.initializeEditor(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private initializeEditor(): void { |  | ||||||
|     if (this.disabled) return; |  | ||||||
|  |  | ||||||
|     this.editor = new Editor({ |  | ||||||
|       element: this.editorElement, |  | ||||||
|       extensions: [ |  | ||||||
|         StarterKit.configure({ |  | ||||||
|           heading: { |  | ||||||
|             levels: [1, 2, 3], |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|         Underline, |  | ||||||
|         TextAlign.configure({ |  | ||||||
|           types: ['heading', 'paragraph'], |  | ||||||
|         }), |  | ||||||
|         Link.configure({ |  | ||||||
|           openOnClick: false, |  | ||||||
|           HTMLAttributes: { |  | ||||||
|             class: 'editor-link', |  | ||||||
|           }, |  | ||||||
|         }), |  | ||||||
|         Typography, |  | ||||||
|       ], |  | ||||||
|       content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''), |  | ||||||
|       onUpdate: ({ editor }) => { |  | ||||||
|         this.value = editor.getHTML(); |  | ||||||
|         this.updateWordCount(); |  | ||||||
|         this.dispatchEvent( |  | ||||||
|           new CustomEvent('input', { |  | ||||||
|             detail: { value: this.value }, |  | ||||||
|             bubbles: true, |  | ||||||
|             composed: true, |  | ||||||
|           }) |  | ||||||
|         ); |  | ||||||
|         this.dispatchEvent( |  | ||||||
|           new CustomEvent('change', { |  | ||||||
|             detail: { value: this.value }, |  | ||||||
|             bubbles: true, |  | ||||||
|             composed: true, |  | ||||||
|           }) |  | ||||||
|         ); |  | ||||||
|       }, |  | ||||||
|       onSelectionUpdate: () => { |  | ||||||
|         this.requestUpdate(); |  | ||||||
|       }, |  | ||||||
|       onFocus: () => { |  | ||||||
|         this.requestUpdate(); |  | ||||||
|       }, |  | ||||||
|       onBlur: () => { |  | ||||||
|         this.requestUpdate(); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this.updateWordCount(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private updateWordCount(): void { |  | ||||||
|     if (!this.editor) return; |  | ||||||
|     const text = this.editor.getText(); |  | ||||||
|     this.wordCount = text.trim() ? text.trim().split(/\s+/).length : 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private toggleLink(): void { |  | ||||||
|     if (!this.editor) return; |  | ||||||
|  |  | ||||||
|     if (this.editor.isActive('link')) { |  | ||||||
|       const href = this.editor.getAttributes('link').href; |  | ||||||
|       this.showLinkInput = true; |  | ||||||
|       requestAnimationFrame(() => { |  | ||||||
|         if (this.linkInputElement) { |  | ||||||
|           this.linkInputElement.value = href || ''; |  | ||||||
|           this.linkInputElement.focus(); |  | ||||||
|           this.linkInputElement.select(); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } else { |  | ||||||
|       this.showLinkInput = true; |  | ||||||
|       requestAnimationFrame(() => { |  | ||||||
|         if (this.linkInputElement) { |  | ||||||
|           this.linkInputElement.value = ''; |  | ||||||
|           this.linkInputElement.focus(); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private saveLink(): void { |  | ||||||
|     if (!this.editor || !this.linkInputElement) return; |  | ||||||
|  |  | ||||||
|     const url = this.linkInputElement.value; |  | ||||||
|     if (url) { |  | ||||||
|       this.editor.chain().focus().setLink({ href: url }).run(); |  | ||||||
|     } |  | ||||||
|     this.hideLinkInput(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private removeLink(): void { |  | ||||||
|     if (!this.editor) return; |  | ||||||
|     this.editor.chain().focus().unsetLink().run(); |  | ||||||
|     this.hideLinkInput(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private hideLinkInput(): void { |  | ||||||
|     this.showLinkInput = false; |  | ||||||
|     this.editor?.commands.focus(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private handleLinkInputKeydown(e: KeyboardEvent): void { |  | ||||||
|     if (e.key === 'Enter') { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       this.saveLink(); |  | ||||||
|     } else if (e.key === 'Escape') { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       this.hideLinkInput(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public setValue(value: string): void { |  | ||||||
|     this.value = value; |  | ||||||
|     if (this.editor && value !== this.editor.getHTML()) { |  | ||||||
|       this.editor.commands.setContent(value); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public getValue(): string { |  | ||||||
|     return this.value; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public clear(): void { |  | ||||||
|     this.setValue(''); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public focus(): void { |  | ||||||
|     this.editor?.commands.focus(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public async disconnectedCallback(): Promise<void> { |  | ||||||
|     await super.disconnectedCallback(); |  | ||||||
|     if (this.editor) { |  | ||||||
|       this.editor.destroy(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										384
									
								
								ts_web/elements/dees-input-richtext/component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								ts_web/elements/dees-input-richtext/component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,384 @@ | |||||||
|  | import { DeesInputBase } from '../dees-input-base.js'; | ||||||
|  | import { demoFunc } from './demo.js'; | ||||||
|  | import { richtextStyles } from './styles.js'; | ||||||
|  | import { renderRichtext } from './template.js'; | ||||||
|  | import type { IToolbarButton } from './types.js'; | ||||||
|  | import '../dees-icon.js'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   customElement, | ||||||
|  |   type TemplateResult, | ||||||
|  |   property, | ||||||
|  |   html, | ||||||
|  |   state, | ||||||
|  |   query, | ||||||
|  | } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | import { Editor } from '@tiptap/core'; | ||||||
|  | import StarterKit from '@tiptap/starter-kit'; | ||||||
|  | import Underline from '@tiptap/extension-underline'; | ||||||
|  | import TextAlign from '@tiptap/extension-text-align'; | ||||||
|  | import Link from '@tiptap/extension-link'; | ||||||
|  | import Typography from '@tiptap/extension-typography'; | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     'dees-input-richtext': DeesInputRichtext; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @customElement('dees-input-richtext') | ||||||
|  | export class DeesInputRichtext extends DeesInputBase<string> { | ||||||
|  |   public static demo = demoFunc; | ||||||
|  |  | ||||||
|  |   // INSTANCE | ||||||
|  |   @property({ | ||||||
|  |     type: String, | ||||||
|  |     reflect: true, | ||||||
|  |   }) | ||||||
|  |   public value: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ | ||||||
|  |     type: String, | ||||||
|  |   }) | ||||||
|  |   public placeholder: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ | ||||||
|  |     type: Boolean, | ||||||
|  |   }) | ||||||
|  |   public showWordCount: boolean = true; | ||||||
|  |  | ||||||
|  |   @property({ | ||||||
|  |     type: Number, | ||||||
|  |   }) | ||||||
|  |   public minHeight: number = 200; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public showLinkInput: boolean = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   public wordCount: number = 0; | ||||||
|  |  | ||||||
|  |   @query('.editor-content') | ||||||
|  |   private editorElement: HTMLElement; | ||||||
|  |  | ||||||
|  |   @query('.link-input input') | ||||||
|  |   private linkInputElement: HTMLInputElement; | ||||||
|  |  | ||||||
|  |   public editor: Editor; | ||||||
|  |  | ||||||
|  |   public static styles = richtextStyles; | ||||||
|  |  | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     return renderRichtext(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   public renderToolbar(): TemplateResult { | ||||||
|  |     const buttons: IToolbarButton[] = this.getToolbarButtons(); | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       ${buttons.map((button) => { | ||||||
|  |         if (button.isDivider) { | ||||||
|  |           return html`<div class="toolbar-divider"></div>`; | ||||||
|  |         } | ||||||
|  |         return html` | ||||||
|  |           <button | ||||||
|  |             class="toolbar-button ${button.isActive?.() ? 'active' : ''}" | ||||||
|  |             @click=${button.action} | ||||||
|  |             title=${button.title} | ||||||
|  |             ?disabled=${this.disabled || !this.editor} | ||||||
|  |           > | ||||||
|  |             <dees-icon .icon=${button.icon}></dees-icon> | ||||||
|  |           </button> | ||||||
|  |         `; | ||||||
|  |       })} | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getToolbarButtons(): IToolbarButton[] { | ||||||
|  |     if (!this.editor) return []; | ||||||
|  |  | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         name: 'bold', | ||||||
|  |         icon: 'lucide:bold', | ||||||
|  |         title: 'Bold (Ctrl+B)', | ||||||
|  |         action: () => this.editor.chain().focus().toggleBold().run(), | ||||||
|  |         isActive: () => this.editor.isActive('bold'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'italic', | ||||||
|  |         icon: 'lucide:italic', | ||||||
|  |         title: 'Italic (Ctrl+I)', | ||||||
|  |         action: () => this.editor.chain().focus().toggleItalic().run(), | ||||||
|  |         isActive: () => this.editor.isActive('italic'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'underline', | ||||||
|  |         icon: 'lucide:underline', | ||||||
|  |         title: 'Underline (Ctrl+U)', | ||||||
|  |         action: () => this.editor.chain().focus().toggleUnderline().run(), | ||||||
|  |         isActive: () => this.editor.isActive('underline'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'strike', | ||||||
|  |         icon: 'lucide:strikethrough', | ||||||
|  |         title: 'Strikethrough', | ||||||
|  |         action: () => this.editor.chain().focus().toggleStrike().run(), | ||||||
|  |         isActive: () => this.editor.isActive('strike'), | ||||||
|  |       }, | ||||||
|  |       { name: 'divider1', title: '', isDivider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'h1', | ||||||
|  |         icon: 'lucide:heading1', | ||||||
|  |         title: 'Heading 1', | ||||||
|  |         action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(), | ||||||
|  |         isActive: () => this.editor.isActive('heading', { level: 1 }), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'h2', | ||||||
|  |         icon: 'lucide:heading2', | ||||||
|  |         title: 'Heading 2', | ||||||
|  |         action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(), | ||||||
|  |         isActive: () => this.editor.isActive('heading', { level: 2 }), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'h3', | ||||||
|  |         icon: 'lucide:heading3', | ||||||
|  |         title: 'Heading 3', | ||||||
|  |         action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(), | ||||||
|  |         isActive: () => this.editor.isActive('heading', { level: 3 }), | ||||||
|  |       }, | ||||||
|  |       { name: 'divider2', title: '', isDivider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'bulletList', | ||||||
|  |         icon: 'lucide:list', | ||||||
|  |         title: 'Bullet List', | ||||||
|  |         action: () => this.editor.chain().focus().toggleBulletList().run(), | ||||||
|  |         isActive: () => this.editor.isActive('bulletList'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'orderedList', | ||||||
|  |         icon: 'lucide:listOrdered', | ||||||
|  |         title: 'Numbered List', | ||||||
|  |         action: () => this.editor.chain().focus().toggleOrderedList().run(), | ||||||
|  |         isActive: () => this.editor.isActive('orderedList'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'blockquote', | ||||||
|  |         icon: 'lucide:quote', | ||||||
|  |         title: 'Quote', | ||||||
|  |         action: () => this.editor.chain().focus().toggleBlockquote().run(), | ||||||
|  |         isActive: () => this.editor.isActive('blockquote'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'code', | ||||||
|  |         icon: 'lucide:code', | ||||||
|  |         title: 'Code', | ||||||
|  |         action: () => this.editor.chain().focus().toggleCode().run(), | ||||||
|  |         isActive: () => this.editor.isActive('code'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'codeBlock', | ||||||
|  |         icon: 'lucide:fileCode', | ||||||
|  |         title: 'Code Block', | ||||||
|  |         action: () => this.editor.chain().focus().toggleCodeBlock().run(), | ||||||
|  |         isActive: () => this.editor.isActive('codeBlock'), | ||||||
|  |       }, | ||||||
|  |       { name: 'divider3', title: '', isDivider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'link', | ||||||
|  |         icon: 'lucide:link', | ||||||
|  |         title: 'Add Link', | ||||||
|  |         action: () => this.toggleLink(), | ||||||
|  |         isActive: () => this.editor.isActive('link'), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'alignLeft', | ||||||
|  |         icon: 'lucide:alignLeft', | ||||||
|  |         title: 'Align Left', | ||||||
|  |         action: () => this.editor.chain().focus().setTextAlign('left').run(), | ||||||
|  |         isActive: () => this.editor.isActive({ textAlign: 'left' }), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'alignCenter', | ||||||
|  |         icon: 'lucide:alignCenter', | ||||||
|  |         title: 'Align Center', | ||||||
|  |         action: () => this.editor.chain().focus().setTextAlign('center').run(), | ||||||
|  |         isActive: () => this.editor.isActive({ textAlign: 'center' }), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'alignRight', | ||||||
|  |         icon: 'lucide:alignRight', | ||||||
|  |         title: 'Align Right', | ||||||
|  |         action: () => this.editor.chain().focus().setTextAlign('right').run(), | ||||||
|  |         isActive: () => this.editor.isActive({ textAlign: 'right' }), | ||||||
|  |       }, | ||||||
|  |       { name: 'divider4', title: '', isDivider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'undo', | ||||||
|  |         icon: 'lucide:undo', | ||||||
|  |         title: 'Undo (Ctrl+Z)', | ||||||
|  |         action: () => this.editor.chain().focus().undo().run(), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'redo', | ||||||
|  |         icon: 'lucide:redo', | ||||||
|  |         title: 'Redo (Ctrl+Y)', | ||||||
|  |         action: () => this.editor.chain().focus().redo().run(), | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async firstUpdated() { | ||||||
|  |     await this.updateComplete; | ||||||
|  |     this.initializeEditor(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private initializeEditor(): void { | ||||||
|  |     if (this.disabled) return; | ||||||
|  |  | ||||||
|  |     this.editor = new Editor({ | ||||||
|  |       element: this.editorElement, | ||||||
|  |       extensions: [ | ||||||
|  |         StarterKit.configure({ | ||||||
|  |           heading: { | ||||||
|  |             levels: [1, 2, 3], | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |         Underline, | ||||||
|  |         TextAlign.configure({ | ||||||
|  |           types: ['heading', 'paragraph'], | ||||||
|  |         }), | ||||||
|  |         Link.configure({ | ||||||
|  |           openOnClick: false, | ||||||
|  |           HTMLAttributes: { | ||||||
|  |             class: 'editor-link', | ||||||
|  |           }, | ||||||
|  |         }), | ||||||
|  |         Typography, | ||||||
|  |       ], | ||||||
|  |       content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''), | ||||||
|  |       onUpdate: ({ editor }) => { | ||||||
|  |         this.value = editor.getHTML(); | ||||||
|  |         this.updateWordCount(); | ||||||
|  |         this.dispatchEvent( | ||||||
|  |           new CustomEvent('input', { | ||||||
|  |             detail: { value: this.value }, | ||||||
|  |             bubbles: true, | ||||||
|  |             composed: true, | ||||||
|  |           }) | ||||||
|  |         ); | ||||||
|  |         this.dispatchEvent( | ||||||
|  |           new CustomEvent('change', { | ||||||
|  |             detail: { value: this.value }, | ||||||
|  |             bubbles: true, | ||||||
|  |             composed: true, | ||||||
|  |           }) | ||||||
|  |         ); | ||||||
|  |       }, | ||||||
|  |       onSelectionUpdate: () => { | ||||||
|  |         this.requestUpdate(); | ||||||
|  |       }, | ||||||
|  |       onFocus: () => { | ||||||
|  |         this.requestUpdate(); | ||||||
|  |       }, | ||||||
|  |       onBlur: () => { | ||||||
|  |         this.requestUpdate(); | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this.updateWordCount(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private updateWordCount(): void { | ||||||
|  |     if (!this.editor) return; | ||||||
|  |     const text = this.editor.getText(); | ||||||
|  |     this.wordCount = text.trim() ? text.trim().split(/\s+/).length : 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private toggleLink(): void { | ||||||
|  |     if (!this.editor) return; | ||||||
|  |  | ||||||
|  |     if (this.editor.isActive('link')) { | ||||||
|  |       const href = this.editor.getAttributes('link').href; | ||||||
|  |       this.showLinkInput = true; | ||||||
|  |       requestAnimationFrame(() => { | ||||||
|  |         if (this.linkInputElement) { | ||||||
|  |           this.linkInputElement.value = href || ''; | ||||||
|  |           this.linkInputElement.focus(); | ||||||
|  |           this.linkInputElement.select(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       this.showLinkInput = true; | ||||||
|  |       requestAnimationFrame(() => { | ||||||
|  |         if (this.linkInputElement) { | ||||||
|  |           this.linkInputElement.value = ''; | ||||||
|  |           this.linkInputElement.focus(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public saveLink(): void { | ||||||
|  |     if (!this.editor || !this.linkInputElement) return; | ||||||
|  |  | ||||||
|  |     const url = this.linkInputElement.value; | ||||||
|  |     if (url) { | ||||||
|  |       this.editor.chain().focus().setLink({ href: url }).run(); | ||||||
|  |     } | ||||||
|  |     this.hideLinkInput(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public removeLink(): void { | ||||||
|  |     if (!this.editor) return; | ||||||
|  |     this.editor.chain().focus().unsetLink().run(); | ||||||
|  |     this.hideLinkInput(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public hideLinkInput(): void { | ||||||
|  |     this.showLinkInput = false; | ||||||
|  |     this.editor?.commands.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public handleLinkInputKeydown(e: KeyboardEvent): void { | ||||||
|  |     if (e.key === 'Enter') { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       this.saveLink(); | ||||||
|  |     } else if (e.key === 'Escape') { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       this.hideLinkInput(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public setValue(value: string): void { | ||||||
|  |     this.value = value; | ||||||
|  |     if (this.editor && value !== this.editor.getHTML()) { | ||||||
|  |       this.editor.commands.setContent(value); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public getValue(): string { | ||||||
|  |     return this.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public clear(): void { | ||||||
|  |     this.setValue(''); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public focus(): void { | ||||||
|  |     this.editor?.commands.focus(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async disconnectedCallback(): Promise<void> { | ||||||
|  |     await super.disconnectedCallback(); | ||||||
|  |     if (this.editor) { | ||||||
|  |       this.editor.destroy(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { html, css } from '@design.estate/dees-element'; | import { html, css } from '@design.estate/dees-element'; | ||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
| import './dees-panel.js'; | import './component.js'; | ||||||
|  | import '../dees-panel.js'; | ||||||
| 
 | 
 | ||||||
| export const demoFunc = () => html` | export const demoFunc = () => html` | ||||||
|   <dees-demowrapper> |   <dees-demowrapper> | ||||||
							
								
								
									
										4
									
								
								ts_web/elements/dees-input-richtext/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								ts_web/elements/dees-input-richtext/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | export * from './component.js'; | ||||||
|  | export { richtextStyles } from './styles.js'; | ||||||
|  | export { renderRichtext } from './template.js'; | ||||||
|  | export type { IToolbarButton } from './types.js'; | ||||||
							
								
								
									
										303
									
								
								ts_web/elements/dees-input-richtext/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								ts_web/elements/dees-input-richtext/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,303 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  | import { DeesInputBase } from '../dees-input-base.js'; | ||||||
|  |  | ||||||
|  | export const richtextStyles = [ | ||||||
|  |     ...DeesInputBase.baseStyles, | ||||||
|  |     cssManager.defaultStyles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         display: block; | ||||||
|  |         position: relative; | ||||||
|  |         font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .input-wrapper { | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .label { | ||||||
|  |         display: block; | ||||||
|  |         margin-bottom: 8px; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-weight: 500; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-container { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         min-height: ${cssManager.bdTheme('200px', '200px')}; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; | ||||||
|  |         overflow: hidden; | ||||||
|  |         transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-container:hover { | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-container.focused { | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-toolbar { | ||||||
|  |         display: flex; | ||||||
|  |         flex-wrap: wrap; | ||||||
|  |         gap: 4px; | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         align-items: center; | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .toolbar-button { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         justify-content: center; | ||||||
|  |         width: 32px; | ||||||
|  |         height: 32px; | ||||||
|  |         border: none; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         background: transparent; | ||||||
|  |         cursor: pointer; | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-weight: 500; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|  |         user-select: none; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .toolbar-button dees-icon { | ||||||
|  |         width: 16px; | ||||||
|  |         height: 16px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .toolbar-button:hover { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .toolbar-button.active { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .toolbar-button:disabled { | ||||||
|  |         opacity: 0.5; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .toolbar-divider { | ||||||
|  |         width: 1px; | ||||||
|  |         height: 24px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         margin: 0 4px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content { | ||||||
|  |         flex: 1; | ||||||
|  |         padding: 16px; | ||||||
|  |         overflow-y: auto; | ||||||
|  |         min-height: var(--min-height, 200px); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror { | ||||||
|  |         outline: none; | ||||||
|  |         line-height: 1.6; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         min-height: 100%; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror p { | ||||||
|  |         margin: 0.5em 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror p:first-child { | ||||||
|  |         margin-top: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror p:last-child { | ||||||
|  |         margin-bottom: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror h1 { | ||||||
|  |         font-size: 2em; | ||||||
|  |         font-weight: bold; | ||||||
|  |         margin: 1em 0 0.5em 0; | ||||||
|  |         line-height: 1.2; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror h2 { | ||||||
|  |         font-size: 1.5em; | ||||||
|  |         font-weight: bold; | ||||||
|  |         margin: 1em 0 0.5em 0; | ||||||
|  |         line-height: 1.3; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror h3 { | ||||||
|  |         font-size: 1.25em; | ||||||
|  |         font-weight: bold; | ||||||
|  |         margin: 1em 0 0.5em 0; | ||||||
|  |         line-height: 1.4; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror ul, | ||||||
|  |       .editor-content .ProseMirror ol { | ||||||
|  |         padding-left: 1.5em; | ||||||
|  |         margin: 0.5em 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror li { | ||||||
|  |         margin: 0.25em 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror blockquote { | ||||||
|  |         border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         margin: 1em 0; | ||||||
|  |         padding-left: 1em; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         font-style: italic; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror code { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-radius: 3px; | ||||||
|  |         padding: 0.2em 0.4em; | ||||||
|  |         font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; | ||||||
|  |         font-size: 0.9em; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror pre { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         padding: 1em; | ||||||
|  |         margin: 1em 0; | ||||||
|  |         overflow-x: auto; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror pre code { | ||||||
|  |         background: none; | ||||||
|  |         color: inherit; | ||||||
|  |         padding: 0; | ||||||
|  |         border-radius: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror a { | ||||||
|  |         color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')}; | ||||||
|  |         text-decoration: underline; | ||||||
|  |         cursor: pointer; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-content .ProseMirror a:hover { | ||||||
|  |         color: ${cssManager.bdTheme('hsl(222.2 47.4% 41.2%)', 'hsl(217.2 91.2% 69.8%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .editor-footer { | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         font-size: 12px; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: space-between; | ||||||
|  |         align-items: center; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .word-count { | ||||||
|  |         font-weight: 500; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input { | ||||||
|  |         display: none; | ||||||
|  |         position: absolute; | ||||||
|  |         top: 100%; | ||||||
|  |         left: 0; | ||||||
|  |         right: 0; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | ||||||
|  |         padding: 12px; | ||||||
|  |         z-index: 1000; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input.show { | ||||||
|  |         display: block; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input input { | ||||||
|  |         width: 100%; | ||||||
|  |         padding: 8px 12px; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-radius: 6px; | ||||||
|  |         outline: none; | ||||||
|  |         font-size: 14px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input input:focus { | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input-buttons { | ||||||
|  |         display: flex; | ||||||
|  |         gap: 8px; | ||||||
|  |         margin-top: 8px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input-buttons button { | ||||||
|  |         padding: 6px 12px; | ||||||
|  |         border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         border-radius: 4px; | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; | ||||||
|  |         cursor: pointer; | ||||||
|  |         font-size: 12px; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')}; | ||||||
|  |         transition: all 0.15s ease; | ||||||
|  |         font-weight: 500; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input-buttons button:hover { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input-buttons button.primary { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .link-input-buttons button.primary:hover { | ||||||
|  |         background: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; | ||||||
|  |         border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .description { | ||||||
|  |         margin-top: 8px; | ||||||
|  |         font-size: 12px; | ||||||
|  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | ||||||
|  |         line-height: 1.4; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([disabled]) .editor-container { | ||||||
|  |         opacity: 0.6; | ||||||
|  |         cursor: not-allowed; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([disabled]) .toolbar-button, | ||||||
|  |       :host([disabled]) .editor-content { | ||||||
|  |         pointer-events: none; | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
							
								
								
									
										33
									
								
								ts_web/elements/dees-input-richtext/template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								ts_web/elements/dees-input-richtext/template.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | import { html, type TemplateResult } from '@design.estate/dees-element'; | ||||||
|  | import type { DeesInputRichtext } from './component.js'; | ||||||
|  |  | ||||||
|  | export const renderRichtext = (component: DeesInputRichtext): TemplateResult => { | ||||||
|  |       return html` | ||||||
|  |         <div class="input-wrapper"> | ||||||
|  |           ${component.label ? html`<label class="label">${component.label}</label>` : ''} | ||||||
|  |           <div class="editor-container ${component.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${component.minHeight}px"> | ||||||
|  |             <div class="editor-toolbar"> | ||||||
|  |               ${component.renderToolbar()} | ||||||
|  |               <div class="link-input ${component.showLinkInput ? 'show' : ''}"> | ||||||
|  |                 <input type="url" placeholder="Enter URL..." @keydown=${component.handleLinkInputKeydown} /> | ||||||
|  |                 <div class="link-input-buttons"> | ||||||
|  |                   <button class="primary" @click=${component.saveLink}>Save</button> | ||||||
|  |                   <button @click=${component.removeLink}>Remove</button> | ||||||
|  |                   <button @click=${component.hideLinkInput}>Cancel</button> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |             <div class="editor-content"></div> | ||||||
|  |             ${component.showWordCount | ||||||
|  |               ? html` | ||||||
|  |                   <div class="editor-footer"> | ||||||
|  |                     <span class="word-count">${component.wordCount} word${component.wordCount !== 1 ? 's' : ''}</span> | ||||||
|  |                   </div> | ||||||
|  |                 ` | ||||||
|  |               : ''} | ||||||
|  |           </div> | ||||||
|  |           ${component.description ? html`<div class="description">${component.description}</div>` : ''} | ||||||
|  |         </div> | ||||||
|  |       `; | ||||||
|  |    | ||||||
|  | }; | ||||||
							
								
								
									
										8
									
								
								ts_web/elements/dees-input-richtext/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								ts_web/elements/dees-input-richtext/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | export interface IToolbarButton { | ||||||
|  |   name: string; | ||||||
|  |   icon?: string; | ||||||
|  |   action?: () => void; | ||||||
|  |   isActive?: () => boolean; | ||||||
|  |   title: string; | ||||||
|  |   isDivider?: boolean; | ||||||
|  | } | ||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { html, css, type TemplateResult } from '@design.estate/dees-element'; | import { html, css, type TemplateResult } from '@design.estate/dees-element'; | ||||||
| import '@design.estate/dees-wcctools/demotools'; | import '@design.estate/dees-wcctools/demotools'; | ||||||
| import './dees-panel.js'; | import './dees-panel.js'; | ||||||
| import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js'; | import type { DeesInputWysiwyg } from './dees-input-wysiwyg/dees-input-wysiwyg.js'; | ||||||
| import type { IBlock } from './wysiwyg/wysiwyg.types.js'; | import type { IBlock } from './dees-input-wysiwyg/wysiwyg.types.js'; | ||||||
|  |  | ||||||
| interface IDemoEditor { | interface IDemoEditor { | ||||||
|   basic: DeesInputWysiwyg; |   basic: DeesInputWysiwyg; | ||||||
| @@ -250,6 +250,30 @@ const setupExportDemo = (container: HTMLElement, editor: DeesInputWysiwyg) => { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const setupOutputFormatDemo = ( | ||||||
|  |   container: HTMLElement, | ||||||
|  |   htmlEditor?: DeesInputWysiwyg, | ||||||
|  |   markdownEditor?: DeesInputWysiwyg, | ||||||
|  | ) => { | ||||||
|  |   const htmlBtn = container.querySelector('#btn-show-html-output') as HTMLButtonElement | null; | ||||||
|  |   const htmlPreview = container.querySelector('#output-preview-html') as HTMLElement | null; | ||||||
|  |   if (htmlBtn && htmlPreview && htmlEditor) { | ||||||
|  |     htmlBtn.addEventListener('click', () => { | ||||||
|  |       htmlPreview.textContent = htmlEditor.getValue(); | ||||||
|  |       htmlPreview.classList.add('visible'); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const markdownBtn = container.querySelector('#btn-show-markdown-output') as HTMLButtonElement | null; | ||||||
|  |   const markdownPreview = container.querySelector('#output-preview-markdown') as HTMLElement | null; | ||||||
|  |   if (markdownBtn && markdownPreview && markdownEditor) { | ||||||
|  |     markdownBtn.addEventListener('click', () => { | ||||||
|  |       markdownPreview.textContent = markdownEditor.getValue(); | ||||||
|  |       markdownPreview.classList.add('visible'); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| const populateInitialContent = (editors: IDemoEditor) => { | const populateInitialContent = (editors: IDemoEditor) => { | ||||||
|   // Article editor content |   // Article editor content | ||||||
|   if (editors.article) { |   if (editors.article) { | ||||||
| @@ -360,6 +384,9 @@ export const demoFunc = (): TemplateResult => html` | |||||||
|       setupExportDemo(elementArg, editors.exportDemo); |       setupExportDemo(elementArg, editors.exportDemo); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Setup output format preview buttons | ||||||
|  |     setupOutputFormatDemo(elementArg, editors.meeting, editors.recipe); | ||||||
|  |  | ||||||
|     // Populate initial content |     // Populate initial content | ||||||
|     populateInitialContent(editors); |     populateInitialContent(editors); | ||||||
|      |      | ||||||
| @@ -488,11 +515,46 @@ export const demoFunc = (): TemplateResult => html` | |||||||
|  |  | ||||||
|       .output-grid { |       .output-grid { | ||||||
|         display: grid; |         display: grid; | ||||||
|         grid-template-columns: 1fr 1fr; |         grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); | ||||||
|         gap: 24px; |         gap: 24px; | ||||||
|         margin-top: 24px; |         margin-top: 24px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       .output-card { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         gap: 12px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .output-actions { | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: flex-end; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .output-preview { | ||||||
|  |         display: none; | ||||||
|  |         background: rgba(15, 23, 42, 0.04); | ||||||
|  |         color: var(--dees-color-text, #0f172a); | ||||||
|  |         border: 1px solid rgba(15, 23, 42, 0.1); | ||||||
|  |         border-radius: 8px; | ||||||
|  |         padding: 16px; | ||||||
|  |         white-space: pre-wrap; | ||||||
|  |         font-family: 'Geist Mono', 'Fira Code', monospace; | ||||||
|  |         font-size: 13px; | ||||||
|  |         max-height: 280px; | ||||||
|  |         overflow: auto; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([theme='dark']) .output-preview { | ||||||
|  |         background: rgba(250, 250, 250, 0.06); | ||||||
|  |         border-color: rgba(250, 250, 250, 0.15); | ||||||
|  |         color: var(--dees-color-text, #f4f4f5); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .output-preview.visible { | ||||||
|  |         display: block; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       @media (max-width: 768px) { |       @media (max-width: 768px) { | ||||||
|         .output-grid { |         .output-grid { | ||||||
|           grid-template-columns: 1fr; |           grid-template-columns: 1fr; | ||||||
| @@ -858,7 +920,7 @@ git merge feature-branch | |||||||
|         </p> |         </p> | ||||||
|          |          | ||||||
|         <div class="output-grid"> |         <div class="output-grid"> | ||||||
|           <div> |           <div class="output-card"> | ||||||
|             <dees-input-wysiwyg  |             <dees-input-wysiwyg  | ||||||
|               id="editor-meeting" |               id="editor-meeting" | ||||||
|               label="Meeting Notes"  |               label="Meeting Notes"  | ||||||
| @@ -866,9 +928,13 @@ git merge feature-branch | |||||||
|               outputFormat="html" |               outputFormat="html" | ||||||
|               value="<h2>Q4 Planning Meeting</h2><p><strong>Date:</strong> December 15, 2024<br><strong>Attendees:</strong> Product Team, Engineering, Design</p><h3>Agenda Items</h3><ol><li>Review Q3 achievements</li><li>Set Q4 objectives</li><li>Resource allocation</li><li>Timeline discussion</li></ol><h3>Key Decisions</h3><ul><li>Launch new dashboard feature by end of January</li><li>Increase engineering team by 2 developers</li><li>Implement weekly design reviews</li></ul><blockquote>"Focus on user experience improvements based on Q3 feedback" - Product Manager</blockquote><h3>Action Items</h3><ul><li>Sarah: Create detailed project timeline</li><li>Mike: Draft technical requirements</li><li>Lisa: Schedule user research sessions</li></ul><hr><p>Next meeting: January 5, 2025</p>" |               value="<h2>Q4 Planning Meeting</h2><p><strong>Date:</strong> December 15, 2024<br><strong>Attendees:</strong> Product Team, Engineering, Design</p><h3>Agenda Items</h3><ol><li>Review Q3 achievements</li><li>Set Q4 objectives</li><li>Resource allocation</li><li>Timeline discussion</li></ol><h3>Key Decisions</h3><ul><li>Launch new dashboard feature by end of January</li><li>Increase engineering team by 2 developers</li><li>Implement weekly design reviews</li></ul><blockquote>"Focus on user experience improvements based on Q3 feedback" - Product Manager</blockquote><h3>Action Items</h3><ul><li>Sarah: Create detailed project timeline</li><li>Mike: Draft technical requirements</li><li>Lisa: Schedule user research sessions</li></ul><hr><p>Next meeting: January 5, 2025</p>" | ||||||
|             ></dees-input-wysiwyg> |             ></dees-input-wysiwyg> | ||||||
|  |             <div class="output-actions"> | ||||||
|  |               <button id="btn-show-html-output" class="demo-button">Show HTML Output</button> | ||||||
|  |             </div> | ||||||
|  |             <pre id="output-preview-html" class="output-preview" aria-live="polite"></pre> | ||||||
|           </div> |           </div> | ||||||
|            |            | ||||||
|           <div> |           <div class="output-card"> | ||||||
|             <dees-input-wysiwyg  |             <dees-input-wysiwyg  | ||||||
|               id="editor-recipe" |               id="editor-recipe" | ||||||
|               label="Recipe Blog Post"  |               label="Recipe Blog Post"  | ||||||
| @@ -927,6 +993,10 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab | |||||||
|  |  | ||||||
| **Yield:** About 5 dozen cookies" | **Yield:** About 5 dozen cookies" | ||||||
|             ></dees-input-wysiwyg> |             ></dees-input-wysiwyg> | ||||||
|  |             <div class="output-actions"> | ||||||
|  |               <button id="btn-show-markdown-output" class="demo-button">Show Markdown Output</button> | ||||||
|  |             </div> | ||||||
|  |             <pre id="output-preview-markdown" class="output-preview" aria-live="polite"></pre> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dees-panel> |       </dees-panel> | ||||||
|   | |||||||
| @@ -1,2 +1,3 @@ | |||||||
| // Re-export the modular component from the wysiwyg directory | // Re-export the component and related helpers from the dedicated subdirectory | ||||||
| export { DeesInputWysiwyg } from './wysiwyg/dees-input-wysiwyg.js'; | export { DeesInputWysiwyg } from './dees-input-wysiwyg/dees-input-wysiwyg.js'; | ||||||
|  | export * from './dees-input-wysiwyg/index.js'; | ||||||
|   | |||||||
| @@ -118,6 +118,17 @@ export class DeesInputWysiwyg extends DeesInputBase<string> { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async firstUpdated() { |   async firstUpdated() { | ||||||
|  |     if (this.value && this.value.trim().length > 0) { | ||||||
|  |       const parsedBlocks = | ||||||
|  |         this.outputFormat === 'html' | ||||||
|  |           ? WysiwygConverters.parseHtmlToBlocks(this.value) | ||||||
|  |           : WysiwygConverters.parseMarkdownToBlocks(this.value); | ||||||
|  | 
 | ||||||
|  |       if (parsedBlocks.length > 0) { | ||||||
|  |         this.blocks = parsedBlocks; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     this.updateValue(); |     this.updateValue(); | ||||||
|     this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; |     this.editorContentRef = this.shadowRoot!.querySelector('.editor-content') as HTMLDivElement; | ||||||
|      |      | ||||||
							
								
								
									
										537
									
								
								ts_web/elements/dees-pdf-preview/component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										537
									
								
								ts_web/elements/dees-pdf-preview/component.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,537 @@ | |||||||
|  | import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element'; | ||||||
|  | import { PdfManager } from '../dees-pdf-shared/PdfManager.js'; | ||||||
|  | import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js'; | ||||||
|  | import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js'; | ||||||
|  | import { previewStyles } from './styles.js'; | ||||||
|  | import { demo as demoFunc } from './demo.js'; | ||||||
|  | import '../dees-icon.js'; | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     'dees-pdf-preview': DeesPdfPreview; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @customElement('dees-pdf-preview') | ||||||
|  | export class DeesPdfPreview extends DeesElement { | ||||||
|  |   public static demo = demoFunc; | ||||||
|  |   public static styles = previewStyles; | ||||||
|  |  | ||||||
|  |   @property({ type: String }) | ||||||
|  |   public pdfUrl: string = ''; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   public currentPreviewPage: number = 1; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   public clickable: boolean = true; | ||||||
|  |  | ||||||
|  |   @property({ type: Number }) | ||||||
|  |   private pageCount: number = 0; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   private loading: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   private rendered: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   private error: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   private isHovering: boolean = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) | ||||||
|  |   private isA4Format: boolean = true; | ||||||
|  |  | ||||||
|  |   private renderPagesTask: Promise<void> | null = null; | ||||||
|  |   private renderPagesQueued: boolean = false; | ||||||
|  |  | ||||||
|  |   private observer: IntersectionObserver; | ||||||
|  |   private pdfDocument: any; | ||||||
|  |   private canvases: PooledCanvas[] = []; | ||||||
|  |   private resizeObserver?: ResizeObserver; | ||||||
|  |   private previewContainer: HTMLElement | null = null; | ||||||
|  |   private stackElement: HTMLElement | null = null; | ||||||
|  |   private loadedPdfUrl: string | null = null; | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     super(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public render(): TemplateResult { | ||||||
|  |     return html` | ||||||
|  |       <div | ||||||
|  |         class="preview-container ${this.loading ? 'loading' : ''} ${this.error ? 'error' : ''} ${this.clickable ? 'clickable' : ''}" | ||||||
|  |         @click=${this.handleClick} | ||||||
|  |         @mouseenter=${this.handleMouseEnter} | ||||||
|  |         @mouseleave=${this.handleMouseLeave} | ||||||
|  |         @mousemove=${this.handleMouseMove} | ||||||
|  |       > | ||||||
|  |         ${this.loading ? html` | ||||||
|  |           <div class="preview-loading"> | ||||||
|  |             <div class="preview-spinner"></div> | ||||||
|  |             <div class="preview-text">Loading preview...</div> | ||||||
|  |           </div> | ||||||
|  |         ` : ''} | ||||||
|  |  | ||||||
|  |         ${this.error ? html` | ||||||
|  |           <div class="preview-error"> | ||||||
|  |             <dees-icon icon="lucide:FileX"></dees-icon> | ||||||
|  |             <div class="preview-text">Failed to load PDF</div> | ||||||
|  |           </div> | ||||||
|  |         ` : ''} | ||||||
|  |  | ||||||
|  |         ${!this.loading && !this.error ? html` | ||||||
|  |           <div class="preview-stack ${!this.isA4Format ? 'non-a4' : ''}"> | ||||||
|  |             <canvas | ||||||
|  |               class="preview-canvas" | ||||||
|  |               data-page="${this.currentPreviewPage}" | ||||||
|  |             ></canvas> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           ${this.pageCount > 1 && this.isHovering ? html` | ||||||
|  |             <div class="preview-page-indicator"> | ||||||
|  |               Page ${this.currentPreviewPage} of ${this.pageCount} | ||||||
|  |             </div> | ||||||
|  |           ` : ''} | ||||||
|  |  | ||||||
|  |           ${this.pageCount > 0 && !this.isHovering ? html` | ||||||
|  |             <div class="preview-info"> | ||||||
|  |               <dees-icon icon="lucide:FileText"></dees-icon> | ||||||
|  |               <span class="preview-pages">${this.pageCount} page${this.pageCount > 1 ? 's' : ''}</span> | ||||||
|  |             </div> | ||||||
|  |           ` : ''} | ||||||
|  |  | ||||||
|  |           ${this.clickable ? html` | ||||||
|  |             <div class="preview-overlay"> | ||||||
|  |               <dees-icon icon="lucide:Eye"></dees-icon> | ||||||
|  |               <span>View PDF</span> | ||||||
|  |             </div> | ||||||
|  |           ` : ''} | ||||||
|  |         ` : ''} | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleMouseEnter() { | ||||||
|  |     this.isHovering = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleMouseLeave() { | ||||||
|  |     this.isHovering = false; | ||||||
|  |     // Reset to first page when not hovering | ||||||
|  |     if (this.currentPreviewPage !== 1) { | ||||||
|  |       this.currentPreviewPage = 1; | ||||||
|  |       void this.scheduleRenderPages(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleMouseMove(e: MouseEvent) { | ||||||
|  |     if (!this.isHovering || this.pageCount <= 1) return; | ||||||
|  |  | ||||||
|  |     const rect = this.getBoundingClientRect(); | ||||||
|  |     const x = e.clientX - rect.left; | ||||||
|  |     const width = rect.width; | ||||||
|  |  | ||||||
|  |     // Calculate which page to show based on horizontal position | ||||||
|  |     const percentage = Math.max(0, Math.min(1, x / width)); | ||||||
|  |     const newPage = Math.ceil(percentage * this.pageCount) || 1; | ||||||
|  |  | ||||||
|  |     if (newPage !== this.currentPreviewPage) { | ||||||
|  |       this.currentPreviewPage = newPage; | ||||||
|  |       void this.scheduleRenderPages(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async connectedCallback() { | ||||||
|  |     await super.connectedCallback(); | ||||||
|  |     this.setupIntersectionObserver(); | ||||||
|  |     await this.updateComplete; | ||||||
|  |     this.cacheElements(); | ||||||
|  |     this.setupResizeObserver(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async disconnectedCallback() { | ||||||
|  |     await super.disconnectedCallback(); | ||||||
|  |     this.cleanup(); | ||||||
|  |     if (this.observer) { | ||||||
|  |       this.observer.disconnect(); | ||||||
|  |     } | ||||||
|  |     this.resizeObserver?.disconnect(); | ||||||
|  |     this.resizeObserver = undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private setupIntersectionObserver() { | ||||||
|  |     const options = { | ||||||
|  |       root: null, | ||||||
|  |       rootMargin: '200px', | ||||||
|  |       threshold: 0.01, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.observer = new IntersectionObserver( | ||||||
|  |       throttle((entries) => { | ||||||
|  |         for (const entry of entries) { | ||||||
|  |           if (entry.isIntersecting && !this.rendered && this.pdfUrl) { | ||||||
|  |             this.loadAndRenderPreview(); | ||||||
|  |           } else if (!entry.isIntersecting && this.rendered) { | ||||||
|  |             // Optional: Clear canvases when out of view for memory optimization | ||||||
|  |             // this.clearCanvases(); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, 100), | ||||||
|  |       options | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     this.observer.observe(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async loadAndRenderPreview() { | ||||||
|  |     if (this.rendered || this.loading) return; | ||||||
|  |  | ||||||
|  |     this.loading = true; | ||||||
|  |     this.error = false; | ||||||
|  |     PerformanceMonitor.mark(`preview-load-${this.pdfUrl}`); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl); | ||||||
|  |       this.pageCount = this.pdfDocument.numPages; | ||||||
|  |       this.currentPreviewPage = 1; | ||||||
|  |       this.loadedPdfUrl = this.pdfUrl; | ||||||
|  |  | ||||||
|  |       // Force an update to ensure the canvas element is in the DOM | ||||||
|  |       this.loading = false; | ||||||
|  |       await this.updateComplete; | ||||||
|  |       this.cacheElements(); | ||||||
|  |  | ||||||
|  |       // Now render the first page | ||||||
|  |       await this.scheduleRenderPages(); | ||||||
|  |  | ||||||
|  |       this.rendered = true; | ||||||
|  |  | ||||||
|  |       const duration = PerformanceMonitor.measure(`preview-render-${this.pdfUrl}`, `preview-load-${this.pdfUrl}`); | ||||||
|  |       console.log(`PDF preview rendered in ${duration}ms`); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Failed to load PDF preview:', error); | ||||||
|  |       this.error = true; | ||||||
|  |       this.loading = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private scheduleRenderPages(): Promise<void> { | ||||||
|  |     if (!this.pdfDocument) { | ||||||
|  |       return Promise.resolve(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this.renderPagesTask) { | ||||||
|  |       this.renderPagesQueued = true; | ||||||
|  |       return this.renderPagesTask; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.renderPagesTask = (async () => { | ||||||
|  |       try { | ||||||
|  |         await this.performRenderPages(); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error('Failed to render PDF preview pages:', error); | ||||||
|  |       } | ||||||
|  |     })().finally(() => { | ||||||
|  |       this.renderPagesTask = null; | ||||||
|  |       if (this.renderPagesQueued) { | ||||||
|  |         this.renderPagesQueued = false; | ||||||
|  |         void this.scheduleRenderPages(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return this.renderPagesTask; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async performRenderPages() { | ||||||
|  |     if (!this.pdfDocument) return; | ||||||
|  |  | ||||||
|  |     // Wait a frame to ensure DOM is ready | ||||||
|  |     await new Promise(resolve => requestAnimationFrame(resolve)); | ||||||
|  |  | ||||||
|  |     const canvas = this.shadowRoot?.querySelector('.preview-canvas') as HTMLCanvasElement; | ||||||
|  |     if (!canvas) { | ||||||
|  |       console.warn('Preview canvas not found in DOM'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Release old canvases | ||||||
|  |     this.clearCanvases(); | ||||||
|  |  | ||||||
|  |     this.cacheElements(); | ||||||
|  |  | ||||||
|  |     // Get available size for the preview | ||||||
|  |     const { availableWidth, availableHeight } = this.getAvailableSize(); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Get the page to render | ||||||
|  |       const pageNum = this.currentPreviewPage; | ||||||
|  |       const page = await this.pdfDocument.getPage(pageNum); | ||||||
|  |  | ||||||
|  |       // Calculate scale to fit within available area while keeping aspect ratio | ||||||
|  |       // Use higher scale for sharper rendering | ||||||
|  |       const initialViewport = page.getViewport({ scale: 1 }); | ||||||
|  |  | ||||||
|  |       // Check if this is standard paper format (A4 or US Letter) | ||||||
|  |       const aspectRatio = initialViewport.height / initialViewport.width; | ||||||
|  |  | ||||||
|  |       // Common paper format ratios | ||||||
|  |       const a4PortraitRatio = 1.414; // 297mm / 210mm | ||||||
|  |       const a4LandscapeRatio = 0.707; // 210mm / 297mm | ||||||
|  |       const letterPortraitRatio = 1.294; // 11" / 8.5" | ||||||
|  |       const letterLandscapeRatio = 0.773; // 8.5" / 11" | ||||||
|  |  | ||||||
|  |       // Check for standard formats with 5% tolerance | ||||||
|  |       const tolerance = 0.05; | ||||||
|  |       const isA4Portrait = Math.abs(aspectRatio - a4PortraitRatio) < (a4PortraitRatio * tolerance); | ||||||
|  |       const isA4Landscape = Math.abs(aspectRatio - a4LandscapeRatio) < (a4LandscapeRatio * tolerance); | ||||||
|  |       const isLetterPortrait = Math.abs(aspectRatio - letterPortraitRatio) < (letterPortraitRatio * tolerance); | ||||||
|  |       const isLetterLandscape = Math.abs(aspectRatio - letterLandscapeRatio) < (letterLandscapeRatio * tolerance); | ||||||
|  |  | ||||||
|  |       // Consider it standard format if it matches A4 or US Letter | ||||||
|  |       this.isA4Format = isA4Portrait || isA4Landscape || isLetterPortrait || isLetterLandscape; | ||||||
|  |  | ||||||
|  |       // Debug logging | ||||||
|  |       console.log(`PDF aspect ratio: ${aspectRatio.toFixed(3)}, standard format: ${this.isA4Format}`) | ||||||
|  |  | ||||||
|  |       // Adjust available size for non-A4 documents (account for padding) | ||||||
|  |       const adjustedWidth = this.isA4Format ? availableWidth : availableWidth - 24; | ||||||
|  |       const adjustedHeight = this.isA4Format ? availableHeight : availableHeight - 24; | ||||||
|  |  | ||||||
|  |       const scaleX = adjustedWidth > 0 ? adjustedWidth / initialViewport.width : 0; | ||||||
|  |       const scaleY = adjustedHeight > 0 ? adjustedHeight / initialViewport.height : 0; | ||||||
|  |       // Increase scale by 2x for sharper rendering, but limit to 3.0 max | ||||||
|  |       const baseScale = Math.min(scaleX || 0.5, scaleY || scaleX || 0.5); | ||||||
|  |       const renderScale = Math.min(baseScale * 2, 3.0); | ||||||
|  |  | ||||||
|  |       if (!Number.isFinite(renderScale) || renderScale <= 0) { | ||||||
|  |         page.cleanup?.(); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const viewport = page.getViewport({ scale: renderScale }); | ||||||
|  |  | ||||||
|  |       // Acquire canvas from pool | ||||||
|  |       const pooledCanvas = CanvasPool.acquire(viewport.width, viewport.height); | ||||||
|  |       this.canvases.push(pooledCanvas); | ||||||
|  |  | ||||||
|  |       // Render to pooled canvas first | ||||||
|  |       const renderContext = { | ||||||
|  |         canvasContext: pooledCanvas.ctx, | ||||||
|  |         viewport: viewport, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       await page.render(renderContext).promise; | ||||||
|  |  | ||||||
|  |       // Transfer to display canvas | ||||||
|  |       // Set actual canvas resolution for sharpness | ||||||
|  |       canvas.width = viewport.width; | ||||||
|  |       canvas.height = viewport.height; | ||||||
|  |  | ||||||
|  |       // Scale down display size to fit the container while keeping high resolution | ||||||
|  |       // For A4, fill the container; for non-A4, respect padding | ||||||
|  |       const displayWidth = adjustedWidth; | ||||||
|  |       const displayHeight = (viewport.height / viewport.width) * adjustedWidth; | ||||||
|  |  | ||||||
|  |       // If it fits height-wise better, scale by height instead | ||||||
|  |       if (displayHeight > adjustedHeight) { | ||||||
|  |         const altDisplayHeight = adjustedHeight; | ||||||
|  |         const altDisplayWidth = (viewport.width / viewport.height) * adjustedHeight; | ||||||
|  |         canvas.style.width = `${altDisplayWidth}px`; | ||||||
|  |         canvas.style.height = `${altDisplayHeight}px`; | ||||||
|  |       } else { | ||||||
|  |         canvas.style.width = `${displayWidth}px`; | ||||||
|  |         canvas.style.height = `${displayHeight}px`; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const ctx = canvas.getContext('2d'); | ||||||
|  |       if (ctx) { | ||||||
|  |         // Enable image smoothing for better quality | ||||||
|  |         ctx.imageSmoothingEnabled = true; | ||||||
|  |         ctx.imageSmoothingQuality = 'high'; | ||||||
|  |         ctx.drawImage(pooledCanvas.canvas, 0, 0); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Release page to free memory | ||||||
|  |       page.cleanup(); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error(`Failed to render page ${this.currentPreviewPage}:`, error); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private clearCanvases() { | ||||||
|  |     // Release pooled canvases | ||||||
|  |     for (const pooledCanvas of this.canvases) { | ||||||
|  |       CanvasPool.release(pooledCanvas); | ||||||
|  |     } | ||||||
|  |     this.canvases = []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private cleanup() { | ||||||
|  |     this.clearCanvases(); | ||||||
|  |  | ||||||
|  |     if (this.pdfDocument) { | ||||||
|  |       PdfManager.releaseDocument(this.loadedPdfUrl ?? this.pdfUrl); | ||||||
|  |       this.pdfDocument = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.renderPagesQueued = false; | ||||||
|  |  | ||||||
|  |     this.pageCount = 0; | ||||||
|  |     this.currentPreviewPage = 1; | ||||||
|  |     this.isHovering = false; | ||||||
|  |     this.isA4Format = true; | ||||||
|  |     this.previewContainer = null; | ||||||
|  |     this.stackElement = null; | ||||||
|  |     this.loadedPdfUrl = null; | ||||||
|  |     this.rendered = false; | ||||||
|  |     this.loading = false; | ||||||
|  |     this.error = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private handleClick() { | ||||||
|  |     if (!this.clickable) return; | ||||||
|  |  | ||||||
|  |     // Dispatch custom event for parent to handle | ||||||
|  |     this.dispatchEvent(new CustomEvent('pdf-preview-click', { | ||||||
|  |       detail: { | ||||||
|  |         pdfUrl: this.pdfUrl, | ||||||
|  |         pageCount: this.pageCount, | ||||||
|  |       }, | ||||||
|  |       bubbles: true, | ||||||
|  |       composed: true, | ||||||
|  |     })); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updated(changedProperties: Map<PropertyKey, unknown>) { | ||||||
|  |     super.updated(changedProperties); | ||||||
|  |  | ||||||
|  |     if (changedProperties.has('pdfUrl') && this.pdfUrl) { | ||||||
|  |       const previousUrl = changedProperties.get('pdfUrl') as string | undefined; | ||||||
|  |       if (previousUrl) { | ||||||
|  |         PdfManager.releaseDocument(previousUrl); | ||||||
|  |       } | ||||||
|  |       this.cleanup(); | ||||||
|  |       this.rendered = false; | ||||||
|  |       this.currentPreviewPage = 1; | ||||||
|  |  | ||||||
|  |       // Check if in viewport and render if so | ||||||
|  |       if (this.observer) { | ||||||
|  |         const rect = this.getBoundingClientRect(); | ||||||
|  |         if (rect.top < window.innerHeight && rect.bottom > 0) { | ||||||
|  |           this.loadAndRenderPreview(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (changedProperties.has('currentPreviewPage') && this.rendered) { | ||||||
|  |       await this.scheduleRenderPages(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Provide context menu items for right-click functionality | ||||||
|  |    */ | ||||||
|  |   public getContextMenuItems() { | ||||||
|  |     const items: any[] = []; | ||||||
|  |  | ||||||
|  |     // If clickable, add option to view the PDF | ||||||
|  |     if (this.clickable) { | ||||||
|  |       items.push({ | ||||||
|  |         name: 'View PDF', | ||||||
|  |         iconName: 'lucide:Eye', | ||||||
|  |         action: async () => { | ||||||
|  |           this.handleClick(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       items.push({ divider: true }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     items.push( | ||||||
|  |       { | ||||||
|  |         name: 'Open PDF in New Tab', | ||||||
|  |         iconName: 'lucide:ExternalLink', | ||||||
|  |         action: async () => { | ||||||
|  |           window.open(this.pdfUrl, '_blank'); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { divider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'Copy PDF URL', | ||||||
|  |         iconName: 'lucide:Copy', | ||||||
|  |         action: async () => { | ||||||
|  |           await navigator.clipboard.writeText(this.pdfUrl); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'Download PDF', | ||||||
|  |         iconName: 'lucide:Download', | ||||||
|  |         action: async () => { | ||||||
|  |           const link = document.createElement('a'); | ||||||
|  |           link.href = this.pdfUrl; | ||||||
|  |           link.download = this.pdfUrl.split('/').pop() || 'document.pdf'; | ||||||
|  |           link.click(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     // Add page count info as a disabled item | ||||||
|  |     if (this.pageCount > 0) { | ||||||
|  |       items.push( | ||||||
|  |         { divider: true }, | ||||||
|  |         { | ||||||
|  |           name: `${this.pageCount} page${this.pageCount > 1 ? 's' : ''}`, | ||||||
|  |           iconName: 'lucide:FileText', | ||||||
|  |           disabled: true, | ||||||
|  |           action: async () => {} | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return items; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private cacheElements() { | ||||||
|  |     if (!this.previewContainer) { | ||||||
|  |       this.previewContainer = this.shadowRoot?.querySelector('.preview-container') as HTMLElement; | ||||||
|  |     } | ||||||
|  |     if (!this.stackElement) { | ||||||
|  |       this.stackElement = this.shadowRoot?.querySelector('.preview-stack') as HTMLElement; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private setupResizeObserver() { | ||||||
|  |     if (!this.previewContainer || this.resizeObserver) return; | ||||||
|  |  | ||||||
|  |     this.resizeObserver = new ResizeObserver(() => { | ||||||
|  |       if (this.rendered && this.pdfDocument && !this.loading) { | ||||||
|  |         void this.scheduleRenderPages(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this.resizeObserver.observe(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private getAvailableSize() { | ||||||
|  |     if (!this.stackElement) { | ||||||
|  |       // Try to get the stack element if it's not cached | ||||||
|  |       this.stackElement = this.shadowRoot?.querySelector('.preview-stack') as HTMLElement; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!this.stackElement) { | ||||||
|  |       // Fallback to default size if element not found | ||||||
|  |       return { | ||||||
|  |         availableWidth: 200,  // Full container width | ||||||
|  |         availableHeight: 260, // Full container height | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const rect = this.stackElement.getBoundingClientRect(); | ||||||
|  |     const availableWidth = Math.max(rect.width, 0) || 200; | ||||||
|  |     const availableHeight = Math.max(rect.height, 0) || 260; | ||||||
|  |  | ||||||
|  |     return { availableWidth, availableHeight }; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										189
									
								
								ts_web/elements/dees-pdf-preview/demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								ts_web/elements/dees-pdf-preview/demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | import { html } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const demo = () => { | ||||||
|  |   const samplePdfs = [ | ||||||
|  |     'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf', | ||||||
|  |     'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf', | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   const generateGridItems = (count: number) => { | ||||||
|  |     const items = []; | ||||||
|  |     for (let i = 0; i < count; i++) { | ||||||
|  |       const pdfUrl = samplePdfs[i % samplePdfs.length]; | ||||||
|  |       items.push(html` | ||||||
|  |         <dees-pdf-preview | ||||||
|  |           pdfUrl="${pdfUrl}" | ||||||
|  |           maxPages="3" | ||||||
|  |           stackOffset="6" | ||||||
|  |           clickable="true" | ||||||
|  |           grid-mode | ||||||
|  |           @pdf-preview-click=${(e: CustomEvent) => { | ||||||
|  |             console.log('PDF Preview clicked:', e.detail); | ||||||
|  |             alert(`PDF clicked: ${e.detail.pageCount} pages`); | ||||||
|  |           }} | ||||||
|  |         ></dees-pdf-preview> | ||||||
|  |       `); | ||||||
|  |     } | ||||||
|  |     return items; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return html` | ||||||
|  |     <style> | ||||||
|  |       .demo-container { | ||||||
|  |         padding: 40px; | ||||||
|  |         background: #f5f5f5; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .demo-section { | ||||||
|  |         margin-bottom: 60px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       h3 { | ||||||
|  |         margin-bottom: 20px; | ||||||
|  |         font-size: 18px; | ||||||
|  |         font-weight: 600; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .preview-grid { | ||||||
|  |         display: grid; | ||||||
|  |         grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | ||||||
|  |         gap: 24px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .preview-row { | ||||||
|  |         display: flex; | ||||||
|  |         gap: 24px; | ||||||
|  |         align-items: center; | ||||||
|  |         margin-bottom: 20px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .preview-label { | ||||||
|  |         font-size: 14px; | ||||||
|  |         font-weight: 500; | ||||||
|  |         min-width: 100px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .performance-stats { | ||||||
|  |         margin-top: 20px; | ||||||
|  |         padding: 16px; | ||||||
|  |         background: white; | ||||||
|  |         border-radius: 8px; | ||||||
|  |         font-size: 14px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .stats-grid { | ||||||
|  |         display: grid; | ||||||
|  |         grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | ||||||
|  |         gap: 12px; | ||||||
|  |         margin-top: 12px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .stat-item { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         gap: 4px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .stat-label { | ||||||
|  |         font-size: 12px; | ||||||
|  |         color: #666; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .stat-value { | ||||||
|  |         font-size: 16px; | ||||||
|  |         font-weight: 600; | ||||||
|  |       } | ||||||
|  |     </style> | ||||||
|  |  | ||||||
|  |     <div class="demo-container"> | ||||||
|  |       <div class="demo-section"> | ||||||
|  |         <h3>Single PDF Preview with Stacked Pages</h3> | ||||||
|  |         <dees-pdf-preview | ||||||
|  |           pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf" | ||||||
|  |           maxPages="3" | ||||||
|  |           stackOffset="8" | ||||||
|  |           clickable="true" | ||||||
|  |         ></dees-pdf-preview> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="demo-section"> | ||||||
|  |         <h3>Different Sizes</h3> | ||||||
|  |         <div class="preview-row"> | ||||||
|  |           <div class="preview-label">Small:</div> | ||||||
|  |           <dees-pdf-preview | ||||||
|  |             size="small" | ||||||
|  |             pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf" | ||||||
|  |             maxPages="2" | ||||||
|  |             stackOffset="6" | ||||||
|  |             clickable="true" | ||||||
|  |           ></dees-pdf-preview> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="preview-row"> | ||||||
|  |           <div class="preview-label">Default:</div> | ||||||
|  |           <dees-pdf-preview | ||||||
|  |             pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf" | ||||||
|  |             maxPages="3" | ||||||
|  |             stackOffset="8" | ||||||
|  |             clickable="true" | ||||||
|  |           ></dees-pdf-preview> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="preview-row"> | ||||||
|  |           <div class="preview-label">Large:</div> | ||||||
|  |           <dees-pdf-preview | ||||||
|  |             size="large" | ||||||
|  |             pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf" | ||||||
|  |             maxPages="4" | ||||||
|  |             stackOffset="10" | ||||||
|  |             clickable="true" | ||||||
|  |           ></dees-pdf-preview> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="demo-section"> | ||||||
|  |         <h3>Non-Clickable Preview</h3> | ||||||
|  |         <dees-pdf-preview | ||||||
|  |           pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf" | ||||||
|  |           maxPages="3" | ||||||
|  |           stackOffset="8" | ||||||
|  |           clickable="false" | ||||||
|  |         ></dees-pdf-preview> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="demo-section"> | ||||||
|  |         <h3>Performance Grid - 50 PDFs with Lazy Loading</h3> | ||||||
|  |         <p style="margin-bottom: 20px; font-size: 14px; color: #666;"> | ||||||
|  |           This grid demonstrates the performance optimizations with 50 PDF previews. | ||||||
|  |           Scroll to see lazy loading in action - previews render only when visible. | ||||||
|  |         </p> | ||||||
|  |  | ||||||
|  |         <div class="preview-grid"> | ||||||
|  |           ${generateGridItems(50)} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="performance-stats"> | ||||||
|  |           <h4>Performance Features</h4> | ||||||
|  |           <div class="stats-grid"> | ||||||
|  |             <div class="stat-item"> | ||||||
|  |               <span class="stat-label">Lazy Loading</span> | ||||||
|  |               <span class="stat-value">✓ Enabled</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="stat-item"> | ||||||
|  |               <span class="stat-label">Canvas Pooling</span> | ||||||
|  |               <span class="stat-value">✓ Active</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="stat-item"> | ||||||
|  |               <span class="stat-label">Memory Management</span> | ||||||
|  |               <span class="stat-value">✓ Optimized</span> | ||||||
|  |             </div> | ||||||
|  |             <div class="stat-item"> | ||||||
|  |               <span class="stat-label">Intersection Observer</span> | ||||||
|  |               <span class="stat-value">200px margin</span> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   `; | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								ts_web/elements/dees-pdf-preview/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ts_web/elements/dees-pdf-preview/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './component.js'; | ||||||
							
								
								
									
										223
									
								
								ts_web/elements/dees-pdf-preview/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								ts_web/elements/dees-pdf-preview/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const previewStyles = [ | ||||||
|  |   cssManager.defaultStyles, | ||||||
|  |   css` | ||||||
|  |     :host { | ||||||
|  |       display: inline-block; | ||||||
|  |       position: relative; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-container { | ||||||
|  |       position: relative; | ||||||
|  |       width: 200px; | ||||||
|  |       height: 260px; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(215 20% 14%)')}; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       overflow: hidden; | ||||||
|  |       transition: transform 0.2s ease, box-shadow 0.2s ease; | ||||||
|  |       box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-container.clickable { | ||||||
|  |       cursor: pointer; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-container.clickable:hover { | ||||||
|  |       transform: translateY(-2px); | ||||||
|  |       box-shadow: 0 8px 24px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.3)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-container.clickable:hover .preview-overlay { | ||||||
|  |       opacity: 1; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-stack { | ||||||
|  |       position: relative; | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       box-sizing: border-box; | ||||||
|  |       overflow: hidden; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-stack.non-a4 { | ||||||
|  |       padding: 12px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-canvas { | ||||||
|  |       position: relative; | ||||||
|  |       background: white; | ||||||
|  |       display: block; | ||||||
|  |       max-width: 100%; | ||||||
|  |       max-height: 100%; | ||||||
|  |       width: auto; | ||||||
|  |       height: auto; | ||||||
|  |       object-fit: contain; | ||||||
|  |       image-rendering: auto; | ||||||
|  |       -webkit-font-smoothing: antialiased; | ||||||
|  |       box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .non-a4 .preview-canvas { | ||||||
|  |       border: 1px solid ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 25% 24%)')}; | ||||||
|  |       border-radius: 4px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-info { | ||||||
|  |       position: absolute; | ||||||
|  |       bottom: 8px; | ||||||
|  |       left: 8px; | ||||||
|  |       right: 8px; | ||||||
|  |       padding: 6px 10px; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.92)', 'hsl(215 20% 12% / 0.92)')}; | ||||||
|  |       border-radius: 6px; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 6px; | ||||||
|  |       font-size: 12px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |       backdrop-filter: blur(12px); | ||||||
|  |       box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||||||
|  |       z-index: 10; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-info dees-icon { | ||||||
|  |       font-size: 13px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-pages { | ||||||
|  |       font-weight: 500; | ||||||
|  |       font-size: 11px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-overlay { | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       left: 0; | ||||||
|  |       right: 0; | ||||||
|  |       bottom: 0; | ||||||
|  |       background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.7)', 'rgba(0, 0, 0, 0.8)')}; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       gap: 8px; | ||||||
|  |       opacity: 0; | ||||||
|  |       transition: opacity 0.2s ease; | ||||||
|  |       z-index: 20; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-overlay dees-icon { | ||||||
|  |       font-size: 24px; | ||||||
|  |       color: white; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-overlay span { | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-weight: 500; | ||||||
|  |       color: white; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-loading, | ||||||
|  |     .preview-error { | ||||||
|  |       position: absolute; | ||||||
|  |       top: 0; | ||||||
|  |       left: 0; | ||||||
|  |       right: 0; | ||||||
|  |       bottom: 0; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       gap: 12px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-loading { | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(215 20% 14%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-error { | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 72% 98%)', 'hsl(0 62% 20%)')}; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-spinner { | ||||||
|  |       width: 24px; | ||||||
|  |       height: 24px; | ||||||
|  |       border-radius: 50%; | ||||||
|  |       border: 2px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 25% 28%)')}; | ||||||
|  |       border-top-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; | ||||||
|  |       animation: spin 0.8s linear infinite; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @keyframes spin { | ||||||
|  |       to { | ||||||
|  |         transform: rotate(360deg); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-text { | ||||||
|  |       font-size: 13px; | ||||||
|  |       font-weight: 500; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-error dees-icon { | ||||||
|  |       font-size: 32px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .preview-page-indicator { | ||||||
|  |       position: absolute; | ||||||
|  |       top: 8px; | ||||||
|  |       left: 8px; | ||||||
|  |       right: 8px; | ||||||
|  |       padding: 5px 8px; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.7)', 'hsl(0 0% 100% / 0.9)')}; | ||||||
|  |       color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')}; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       font-size: 11px; | ||||||
|  |       font-weight: 600; | ||||||
|  |       text-align: center; | ||||||
|  |       backdrop-filter: blur(12px); | ||||||
|  |       z-index: 15; | ||||||
|  |       pointer-events: none; | ||||||
|  |       animation: fadeIn 0.2s ease; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @keyframes fadeIn { | ||||||
|  |       from { | ||||||
|  |         opacity: 0; | ||||||
|  |         transform: translateY(-4px); | ||||||
|  |       } | ||||||
|  |       to { | ||||||
|  |         opacity: 1; | ||||||
|  |         transform: translateY(0); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /* Responsive sizes */ | ||||||
|  |     :host([size="small"]) .preview-container { | ||||||
|  |       width: 150px; | ||||||
|  |       height: 195px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     :host([size="large"]) .preview-container { | ||||||
|  |       width: 250px; | ||||||
|  |       height: 325px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /* Grid optimizations */ | ||||||
|  |     :host([grid-mode]) .preview-container { | ||||||
|  |       will-change: auto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     :host([grid-mode]) .preview-canvas { | ||||||
|  |       image-rendering: -webkit-optimize-contrast; | ||||||
|  |       image-rendering: crisp-edges; | ||||||
|  |     } | ||||||
|  |   `, | ||||||
|  | ]; | ||||||
							
								
								
									
										135
									
								
								ts_web/elements/dees-pdf-shared/CanvasPool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								ts_web/elements/dees-pdf-shared/CanvasPool.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | export interface PooledCanvas { | ||||||
|  |   canvas: HTMLCanvasElement; | ||||||
|  |   ctx: CanvasRenderingContext2D; | ||||||
|  |   inUse: boolean; | ||||||
|  |   lastUsed: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class CanvasPool { | ||||||
|  |   private static pool: PooledCanvas[] = []; | ||||||
|  |   private static maxPoolSize = 20; | ||||||
|  |   private static readonly MIN_CANVAS_SIZE = 256; | ||||||
|  |   private static readonly MAX_CANVAS_SIZE = 4096; | ||||||
|  |  | ||||||
|  |   public static acquire(width: number, height: number): PooledCanvas { | ||||||
|  |     // Try to find a suitable canvas from the pool | ||||||
|  |     const suitable = this.pool.find( | ||||||
|  |       (item) => !item.inUse && | ||||||
|  |       item.canvas.width >= width && | ||||||
|  |       item.canvas.height >= height && | ||||||
|  |       item.canvas.width <= width * 1.5 && | ||||||
|  |       item.canvas.height <= height * 1.5 | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (suitable) { | ||||||
|  |       suitable.inUse = true; | ||||||
|  |       suitable.lastUsed = Date.now(); | ||||||
|  |  | ||||||
|  |       // Clear and resize if needed | ||||||
|  |       suitable.canvas.width = width; | ||||||
|  |       suitable.canvas.height = height; | ||||||
|  |       suitable.ctx.clearRect(0, 0, width, height); | ||||||
|  |  | ||||||
|  |       return suitable; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Create new canvas if pool not full | ||||||
|  |     if (this.pool.length < this.maxPoolSize) { | ||||||
|  |       const canvas = document.createElement('canvas'); | ||||||
|  |       const ctx = canvas.getContext('2d', { | ||||||
|  |         alpha: true, | ||||||
|  |         desynchronized: true, | ||||||
|  |       }) as CanvasRenderingContext2D; | ||||||
|  |  | ||||||
|  |       canvas.width = Math.min(Math.max(width, this.MIN_CANVAS_SIZE), this.MAX_CANVAS_SIZE); | ||||||
|  |       canvas.height = Math.min(Math.max(height, this.MIN_CANVAS_SIZE), this.MAX_CANVAS_SIZE); | ||||||
|  |  | ||||||
|  |       const pooledCanvas: PooledCanvas = { | ||||||
|  |         canvas, | ||||||
|  |         ctx, | ||||||
|  |         inUse: true, | ||||||
|  |         lastUsed: Date.now(), | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       this.pool.push(pooledCanvas); | ||||||
|  |       return pooledCanvas; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Evict and reuse least recently used canvas | ||||||
|  |     const lru = this.pool | ||||||
|  |       .filter((item) => !item.inUse) | ||||||
|  |       .sort((a, b) => a.lastUsed - b.lastUsed)[0]; | ||||||
|  |  | ||||||
|  |     if (lru) { | ||||||
|  |       lru.canvas.width = width; | ||||||
|  |       lru.canvas.height = height; | ||||||
|  |       lru.ctx.clearRect(0, 0, width, height); | ||||||
|  |       lru.inUse = true; | ||||||
|  |       lru.lastUsed = Date.now(); | ||||||
|  |       return lru; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Fallback: create temporary canvas (shouldn't normally happen) | ||||||
|  |     const canvas = document.createElement('canvas'); | ||||||
|  |     const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; | ||||||
|  |     canvas.width = width; | ||||||
|  |     canvas.height = height; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       canvas, | ||||||
|  |       ctx, | ||||||
|  |       inUse: true, | ||||||
|  |       lastUsed: Date.now(), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static release(pooledCanvas: PooledCanvas) { | ||||||
|  |     if (this.pool.includes(pooledCanvas)) { | ||||||
|  |       pooledCanvas.inUse = false; | ||||||
|  |       // Clear canvas to free memory | ||||||
|  |       pooledCanvas.ctx.clearRect(0, 0, pooledCanvas.canvas.width, pooledCanvas.canvas.height); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static releaseAll() { | ||||||
|  |     for (const item of this.pool) { | ||||||
|  |       item.inUse = false; | ||||||
|  |       item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static destroy() { | ||||||
|  |     for (const item of this.pool) { | ||||||
|  |       item.canvas.width = 0; | ||||||
|  |       item.canvas.height = 0; | ||||||
|  |     } | ||||||
|  |     this.pool = []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static getStats() { | ||||||
|  |     return { | ||||||
|  |       poolSize: this.pool.length, | ||||||
|  |       maxPoolSize: this.maxPoolSize, | ||||||
|  |       inUse: this.pool.filter((item) => item.inUse).length, | ||||||
|  |       available: this.pool.filter((item) => !item.inUse).length, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static adjustPoolSize(newSize: number) { | ||||||
|  |     if (newSize < this.pool.length) { | ||||||
|  |       // Remove excess canvases | ||||||
|  |       const toRemove = this.pool.length - newSize; | ||||||
|  |       const removed = this.pool | ||||||
|  |         .filter((item) => !item.inUse) | ||||||
|  |         .slice(0, toRemove); | ||||||
|  |  | ||||||
|  |       for (const item of removed) { | ||||||
|  |         const index = this.pool.indexOf(item); | ||||||
|  |         if (index > -1) { | ||||||
|  |           this.pool.splice(index, 1); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     this.maxPoolSize = newSize; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								ts_web/elements/dees-pdf-shared/PdfManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								ts_web/elements/dees-pdf-shared/PdfManager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | import { domtools } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export class PdfManager { | ||||||
|  |   private static pdfjsLib: any; | ||||||
|  |   private static initialized = false; | ||||||
|  |  | ||||||
|  |   public static async initialize() { | ||||||
|  |     if (this.initialized) return; | ||||||
|  |  | ||||||
|  |     // @ts-ignore | ||||||
|  |     this.pdfjsLib = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm'); | ||||||
|  |     this.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.worker.mjs'; | ||||||
|  |  | ||||||
|  |     this.initialized = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static async loadDocument(url: string): Promise<any> { | ||||||
|  |     await this.initialize(); | ||||||
|  |  | ||||||
|  |     // IMPORTANT: Disabled caching to ensure component isolation | ||||||
|  |     // Each viewer instance gets its own document to prevent state sharing | ||||||
|  |     // This fixes issues where multiple viewers interfere with each other | ||||||
|  |     const loadingTask = this.pdfjsLib.getDocument(url); | ||||||
|  |     const document = await loadingTask.promise; | ||||||
|  |  | ||||||
|  |     return document; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static releaseDocument(_url: string) { | ||||||
|  |     // No-op since we're not caching documents anymore | ||||||
|  |     // Each viewer manages its own document lifecycle | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Cache methods removed to ensure component isolation | ||||||
|  |   // Each viewer now manages its own document lifecycle | ||||||
|  | } | ||||||
							
								
								
									
										98
									
								
								ts_web/elements/dees-pdf-shared/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								ts_web/elements/dees-pdf-shared/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | export function debounce<T extends (...args: any[]) => any>( | ||||||
|  |   func: T, | ||||||
|  |   wait: number | ||||||
|  | ): (...args: Parameters<T>) => void { | ||||||
|  |   let timeout: number | undefined; | ||||||
|  |  | ||||||
|  |   return function executedFunction(...args: Parameters<T>) { | ||||||
|  |     const later = () => { | ||||||
|  |       clearTimeout(timeout); | ||||||
|  |       func(...args); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     clearTimeout(timeout); | ||||||
|  |     timeout = window.setTimeout(later, wait); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function throttle<T extends (...args: any[]) => any>( | ||||||
|  |   func: T, | ||||||
|  |   limit: number | ||||||
|  | ): (...args: Parameters<T>) => void { | ||||||
|  |   let inThrottle: boolean; | ||||||
|  |  | ||||||
|  |   return function executedFunction(...args: Parameters<T>) { | ||||||
|  |     if (!inThrottle) { | ||||||
|  |       func.apply(this, args); | ||||||
|  |       inThrottle = true; | ||||||
|  |       setTimeout(() => inThrottle = false, limit); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function formatFileSize(bytes: number): string { | ||||||
|  |   if (bytes === 0) return '0 Bytes'; | ||||||
|  |  | ||||||
|  |   const k = 1024; | ||||||
|  |   const sizes = ['Bytes', 'KB', 'MB', 'GB']; | ||||||
|  |   const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||||
|  |  | ||||||
|  |   return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function isInViewport(element: Element, margin = 0): boolean { | ||||||
|  |   const rect = element.getBoundingClientRect(); | ||||||
|  |   return ( | ||||||
|  |     rect.top >= -margin && | ||||||
|  |     rect.left >= -margin && | ||||||
|  |     rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + margin && | ||||||
|  |     rect.right <= (window.innerWidth || document.documentElement.clientWidth) + margin | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class PerformanceMonitor { | ||||||
|  |   private static marks = new Map<string, number>(); | ||||||
|  |   private static measures: Array<{ name: string; duration: number }> = []; | ||||||
|  |  | ||||||
|  |   public static mark(name: string) { | ||||||
|  |     this.marks.set(name, performance.now()); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static measure(name: string, startMark: string) { | ||||||
|  |     const start = this.marks.get(startMark); | ||||||
|  |     if (start) { | ||||||
|  |       const duration = performance.now() - start; | ||||||
|  |       this.measures.push({ name, duration }); | ||||||
|  |       this.marks.delete(startMark); | ||||||
|  |       return duration; | ||||||
|  |     } | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static getReport() { | ||||||
|  |     const report = { | ||||||
|  |       measures: [...this.measures], | ||||||
|  |       averages: {} as Record<string, number>, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Calculate averages for repeated measures | ||||||
|  |     const grouped = new Map<string, number[]>(); | ||||||
|  |     for (const measure of this.measures) { | ||||||
|  |       if (!grouped.has(measure.name)) { | ||||||
|  |         grouped.set(measure.name, []); | ||||||
|  |       } | ||||||
|  |       grouped.get(measure.name)!.push(measure.duration); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const [name, durations] of grouped) { | ||||||
|  |       report.averages[name] = durations.reduce((a, b) => a + b, 0) / durations.length; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return report; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static clear() { | ||||||
|  |     this.marks.clear(); | ||||||
|  |     this.measures = []; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1023
									
								
								ts_web/elements/dees-pdf-viewer/component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1023
									
								
								ts_web/elements/dees-pdf-viewer/component.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										69
									
								
								ts_web/elements/dees-pdf-viewer/demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								ts_web/elements/dees-pdf-viewer/demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | import { html } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const demo = () => html` | ||||||
|  |   <style> | ||||||
|  |     .demo-container { | ||||||
|  |       padding: 40px; | ||||||
|  |       background: #f5f5f5; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .demo-section { | ||||||
|  |       margin-bottom: 40px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     h3 { | ||||||
|  |       margin-bottom: 20px; | ||||||
|  |       font-size: 18px; | ||||||
|  |       font-weight: 600; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     dees-pdf-viewer { | ||||||
|  |       border: 1px solid #ddd; | ||||||
|  |       border-radius: 8px; | ||||||
|  |       overflow: hidden; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .viewer-tall { | ||||||
|  |       height: 800px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .viewer-compact { | ||||||
|  |       height: 500px; | ||||||
|  |     } | ||||||
|  |   </style> | ||||||
|  |  | ||||||
|  |   <div class="demo-container"> | ||||||
|  |     <div class="demo-section"> | ||||||
|  |       <h3>Full Featured PDF Viewer with Toolbar</h3> | ||||||
|  |       <dees-pdf-viewer | ||||||
|  |         class="viewer-tall" | ||||||
|  |         pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf" | ||||||
|  |         showToolbar="true" | ||||||
|  |         showSidebar="false" | ||||||
|  |         initialZoom="page-fit" | ||||||
|  |       ></dees-pdf-viewer> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="demo-section"> | ||||||
|  |       <h3>PDF Viewer with Sidebar Navigation</h3> | ||||||
|  |       <dees-pdf-viewer | ||||||
|  |         class="viewer-tall" | ||||||
|  |         pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf" | ||||||
|  |         showToolbar="true" | ||||||
|  |         showSidebar="true" | ||||||
|  |         initialZoom="page-width" | ||||||
|  |       ></dees-pdf-viewer> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="demo-section"> | ||||||
|  |       <h3>Compact Viewer without Controls</h3> | ||||||
|  |       <dees-pdf-viewer | ||||||
|  |         class="viewer-compact" | ||||||
|  |         pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf" | ||||||
|  |         showToolbar="false" | ||||||
|  |         showSidebar="false" | ||||||
|  |         initialZoom="auto" | ||||||
|  |       ></dees-pdf-viewer> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | `; | ||||||
							
								
								
									
										1
									
								
								ts_web/elements/dees-pdf-viewer/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ts_web/elements/dees-pdf-viewer/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './component.js'; | ||||||
							
								
								
									
										291
									
								
								ts_web/elements/dees-pdf-viewer/styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								ts_web/elements/dees-pdf-viewer/styles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | |||||||
|  | import { css, cssManager } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const viewerStyles = [ | ||||||
|  |   cssManager.defaultStyles, | ||||||
|  |   css` | ||||||
|  |     :host { | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       height: 600px; | ||||||
|  |       position: relative; | ||||||
|  |       font-family: 'Geist Sans', sans-serif; | ||||||
|  |       contain: layout style; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .pdf-viewer { | ||||||
|  |       width: 100%; | ||||||
|  |       height: 100%; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(215 20% 10%)')}; | ||||||
|  |       position: relative; | ||||||
|  |       overflow: hidden; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar { | ||||||
|  |       height: 48px; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 15%)')}; | ||||||
|  |       border-bottom: 1px solid ${cssManager.bdTheme('hsl(214 31% 91%)', 'hsl(217 25% 22%)')}; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       padding: 0 16px; | ||||||
|  |       gap: 16px; | ||||||
|  |       flex-shrink: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar-group { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 4px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar-group--end { | ||||||
|  |       margin-left: auto; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar-button { | ||||||
|  |       width: 32px; | ||||||
|  |       height: 32px; | ||||||
|  |       border-radius: 6px; | ||||||
|  |       background: transparent; | ||||||
|  |       border: none; | ||||||
|  |       cursor: pointer; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       transition: background 0.15s ease; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar-button:hover:not(:disabled) { | ||||||
|  |       background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 25% 22%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar-button:disabled { | ||||||
|  |       opacity: 0.4; | ||||||
|  |       cursor: not-allowed; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .toolbar-button dees-icon { | ||||||
|  |       font-size: 16px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .page-info { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 8px; | ||||||
|  |       padding: 0 8px; | ||||||
|  |       font-size: 14px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .page-input { | ||||||
|  |       width: 48px; | ||||||
|  |       height: 28px; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       border: 1px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 25% 28%)')}; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 12%)')}; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(222 47% 11%)', 'hsl(210 20% 96%)')}; | ||||||
|  |       text-align: center; | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-family: inherit; | ||||||
|  |       outline: none; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .page-input:focus { | ||||||
|  |       border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .page-separator { | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 60%)', 'hsl(215 16% 50%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .zoom-level { | ||||||
|  |       font-size: 13px; | ||||||
|  |       font-weight: 500; | ||||||
|  |       min-width: 48px; | ||||||
|  |       text-align: center; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .viewer-container { | ||||||
|  |       flex: 1; | ||||||
|  |       display: flex; | ||||||
|  |       overflow: hidden; | ||||||
|  |       position: relative; | ||||||
|  |       min-height: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sidebar { | ||||||
|  |       width: 200px; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20% 15%)')}; | ||||||
|  |       border-right: 1px solid ${cssManager.bdTheme('hsl(214 31% 91%)', 'hsl(217 25% 22%)')}; | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       height: 100%; | ||||||
|  |       overflow: hidden; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sidebar-header { | ||||||
|  |       height: 40px; | ||||||
|  |       padding: 0 12px; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       border-bottom: 1px solid ${cssManager.bdTheme('hsl(214 31% 91%)', 'hsl(217 25% 22%)')}; | ||||||
|  |       font-size: 13px; | ||||||
|  |       font-weight: 600; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sidebar-close { | ||||||
|  |       width: 24px; | ||||||
|  |       height: 24px; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       background: transparent; | ||||||
|  |       border: none; | ||||||
|  |       cursor: pointer; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |       transition: background 0.15s ease; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sidebar-close:hover { | ||||||
|  |       background: ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 25% 22%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sidebar-close dees-icon { | ||||||
|  |       font-size: 14px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sidebar-content { | ||||||
|  |       flex: 1; | ||||||
|  |       overflow-y: auto; | ||||||
|  |       overflow-x: hidden; | ||||||
|  |       padding: 12px; | ||||||
|  |       display: block; | ||||||
|  |       overscroll-behavior: contain; | ||||||
|  |       min-height: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .thumbnail { | ||||||
|  |       position: relative; | ||||||
|  |       border-radius: 8px; | ||||||
|  |       overflow: hidden; | ||||||
|  |       cursor: pointer; | ||||||
|  |       border: 2px solid transparent; | ||||||
|  |       transition: border-color 0.15s ease; | ||||||
|  |       background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(215 20% 18%)')}; | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       margin-bottom: 12px; | ||||||
|  |       /* Default A4 aspect ratio (297mm / 210mm ≈ 1.414) */ | ||||||
|  |       min-height: calc(176px * 1.414); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .thumbnail:last-child { | ||||||
|  |       margin-bottom: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .thumbnail:hover { | ||||||
|  |       border-color: ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 25% 35%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .thumbnail.active { | ||||||
|  |       border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .thumbnail-canvas { | ||||||
|  |       display: block; | ||||||
|  |       width: 100%; | ||||||
|  |       height: auto; | ||||||
|  |       image-rendering: -webkit-optimize-contrast; | ||||||
|  |       image-rendering: crisp-edges; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .thumbnail-number { | ||||||
|  |       position: absolute; | ||||||
|  |       bottom: 4px; | ||||||
|  |       right: 4px; | ||||||
|  |       background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.7)', 'rgba(0, 0, 0, 0.8)')}; | ||||||
|  |       color: white; | ||||||
|  |       font-size: 11px; | ||||||
|  |       font-weight: 500; | ||||||
|  |       padding: 2px 6px; | ||||||
|  |       border-radius: 4px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .viewer-main { | ||||||
|  |       flex: 1; | ||||||
|  |       overflow-y: auto; | ||||||
|  |       overflow-x: hidden; | ||||||
|  |       padding: 20px; | ||||||
|  |       scroll-behavior: smooth; | ||||||
|  |       overscroll-behavior: contain; | ||||||
|  |       min-height: 0; | ||||||
|  |       position: relative; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .loading-container { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       height: 100%; | ||||||
|  |       gap: 16px; | ||||||
|  |       color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .loading-spinner { | ||||||
|  |       width: 32px; | ||||||
|  |       height: 32px; | ||||||
|  |       border-radius: 50%; | ||||||
|  |       border: 3px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 25% 28%)')}; | ||||||
|  |       border-top-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')}; | ||||||
|  |       animation: spin 0.8s linear infinite; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @keyframes spin { | ||||||
|  |       to { | ||||||
|  |         transform: rotate(360deg); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .loading-text { | ||||||
|  |       font-size: 14px; | ||||||
|  |       font-weight: 500; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .pages-container { | ||||||
|  |       display: flex; | ||||||
|  |       flex-direction: column; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 20px; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .page-wrapper { | ||||||
|  |       display: flex; | ||||||
|  |       justify-content: center; | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .canvas-container { | ||||||
|  |       background: white; | ||||||
|  |       box-shadow: 0 2px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')}; | ||||||
|  |       border-radius: 4px; | ||||||
|  |       overflow: hidden; | ||||||
|  |       display: inline-block; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .page-canvas { | ||||||
|  |       display: block; | ||||||
|  |       image-rendering: -webkit-optimize-contrast; | ||||||
|  |       image-rendering: crisp-edges; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .pdf-viewer.with-sidebar .viewer-main { | ||||||
|  |       margin-left: 0; | ||||||
|  |     } | ||||||
|  |   `, | ||||||
|  | ]; | ||||||
| @@ -1,6 +1,8 @@ | |||||||
| import { DeesElement, property, html, customElement, domtools, type TemplateResult, type CSSResult, } from '@design.estate/dees-element'; | import { DeesElement, property, html, customElement, domtools, type TemplateResult, type CSSResult, } from '@design.estate/dees-element'; | ||||||
| 
 | 
 | ||||||
| import { Deferred } from '@push.rocks/smartpromise'; | import { Deferred } from '@push.rocks/smartpromise'; | ||||||
|  | import { DeesContextmenu } from '../dees-contextmenu.js'; | ||||||
|  | import '../dees-icon.js'; | ||||||
| 
 | 
 | ||||||
| // import type pdfjsTypes from 'pdfjs-dist';
 | // import type pdfjsTypes from 'pdfjs-dist';
 | ||||||
| 
 | 
 | ||||||
| @@ -10,6 +12,11 @@ declare global { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @deprecated Use DeesPdfViewer or DeesPdfPreview instead | ||||||
|  |  * - DeesPdfViewer: Full-featured PDF viewing with controls, navigation, zoom | ||||||
|  |  * - DeesPdfPreview: Lightweight, performance-optimized preview for grids | ||||||
|  |  */ | ||||||
| @customElement('dees-pdf') | @customElement('dees-pdf') | ||||||
| export class DeesPdf extends DeesElement { | export class DeesPdf extends DeesElement { | ||||||
|   // DEMO
 |   // DEMO
 | ||||||
| @@ -21,6 +28,8 @@ export class DeesPdf extends DeesElement { | |||||||
|   public pdfUrl: string = |   public pdfUrl: string = | ||||||
|     'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf'; |     'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf'; | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   constructor() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
| 
 | 
 | ||||||
| @@ -44,9 +53,15 @@ export class DeesPdf extends DeesElement { | |||||||
|         #pdfcanvas { |         #pdfcanvas { | ||||||
|           box-shadow: 0px 0px 5px #ccc; |           box-shadow: 0px 0px 5px #ccc; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|  |           cursor: pointer; | ||||||
|         } |         } | ||||||
|       </style> |       </style> | ||||||
|       <canvas id="pdfcanvas" .height=${0} .width=${0}></canvas> |       <canvas | ||||||
|  |         id="pdfcanvas" | ||||||
|  |         .height=${0} | ||||||
|  |         .width=${0} | ||||||
|  | 
 | ||||||
|  |       ></canvas> | ||||||
|     `;
 |     `;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -64,6 +79,8 @@ export class DeesPdf extends DeesElement { | |||||||
|     } |     } | ||||||
|     await DeesPdf.pdfJsReady; |     await DeesPdf.pdfJsReady; | ||||||
|     this.displayContent(); |     this.displayContent(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async displayContent() { |   public async displayContent() { | ||||||
| @@ -107,4 +124,37 @@ export class DeesPdf extends DeesElement { | |||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Provide context menu items for the global context menu handler | ||||||
|  |    */ | ||||||
|  |   public getContextMenuItems() { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         name: 'Open PDF in New Tab', | ||||||
|  |         iconName: 'lucide:ExternalLink', | ||||||
|  |         action: async () => { | ||||||
|  |           window.open(this.pdfUrl, '_blank'); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { divider: true }, | ||||||
|  |       { | ||||||
|  |         name: 'Copy PDF URL', | ||||||
|  |         iconName: 'lucide:Copy', | ||||||
|  |         action: async () => { | ||||||
|  |           await navigator.clipboard.writeText(this.pdfUrl); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'Download PDF', | ||||||
|  |         iconName: 'lucide:Download', | ||||||
|  |         action: async () => { | ||||||
|  |           const link = document.createElement('a'); | ||||||
|  |           link.href = this.pdfUrl; | ||||||
|  |           link.download = this.pdfUrl.split('/').pop() || 'document.pdf'; | ||||||
|  |           link.click(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
| } | } | ||||||
							
								
								
									
										1
									
								
								ts_web/elements/dees-pdf/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ts_web/elements/dees-pdf/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './component.js'; | ||||||
							
								
								
									
										134
									
								
								ts_web/elements/dees-stepper/dees-stepper.demo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								ts_web/elements/dees-stepper/dees-stepper.demo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | |||||||
|  | import { html } from '@design.estate/dees-element'; | ||||||
|  |  | ||||||
|  | export const stepperDemo = () => html` | ||||||
|  |   <dees-stepper | ||||||
|  |     .steps=${[ | ||||||
|  |       { | ||||||
|  |         title: 'Account Setup', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-text key="email" label="Work Email" required></dees-input-text> | ||||||
|  |             <dees-input-text key="password" label="Create Password" type="password" required></dees-input-text> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Profile Details', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-text key="firstName" label="First Name" required></dees-input-text> | ||||||
|  |             <dees-input-text key="lastName" label="Last Name" required></dees-input-text> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Contact Information', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone> | ||||||
|  |             <dees-input-text key="company" label="Company"></dees-input-text> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Team Size', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-dropdown | ||||||
|  |               key="teamSize" | ||||||
|  |               label="How big is your team?" | ||||||
|  |               .options=${[ | ||||||
|  |                 { label: '1-5', value: '1-5' }, | ||||||
|  |                 { label: '6-20', value: '6-20' }, | ||||||
|  |                 { label: '21-50', value: '21-50' }, | ||||||
|  |                 { label: '51+', value: '51+' }, | ||||||
|  |               ]} | ||||||
|  |               required | ||||||
|  |             ></dees-input-dropdown> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Goals', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-multitoggle | ||||||
|  |               key="goal" | ||||||
|  |               label="Main objective" | ||||||
|  |               .options=${[ | ||||||
|  |                 { label: 'Onboarding', value: 'onboarding' }, | ||||||
|  |                 { label: 'Analytics', value: 'analytics' }, | ||||||
|  |                 { label: 'Automation', value: 'automation' }, | ||||||
|  |               ]} | ||||||
|  |               required | ||||||
|  |             ></dees-input-multitoggle> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Brand Preferences', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-text key="brandColor" label="Primary brand color"></dees-input-text> | ||||||
|  |             <dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Integrations', | ||||||
|  |         content: html` | ||||||
|  |           <dees-form> | ||||||
|  |             <dees-input-list | ||||||
|  |               key="integrations" | ||||||
|  |               label="Integrations in use" | ||||||
|  |               placeholder="Add integration" | ||||||
|  |             ></dees-input-list> | ||||||
|  |             <dees-form-submit>Continue</dees-form-submit> | ||||||
|  |           </dees-form> | ||||||
|  |         `, | ||||||
|  |         validationFunc: async (stepperArg, elementArg) => { | ||||||
|  |           const deesForm = elementArg.querySelector('dees-form'); | ||||||
|  |           deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         title: 'Review & Launch', | ||||||
|  |         content: html` | ||||||
|  |           <dees-panel> | ||||||
|  |             <p>Almost there! Review your selections and launch whenever you're ready.</p> | ||||||
|  |           </dees-panel> | ||||||
|  |         `, | ||||||
|  |       }, | ||||||
|  |     ] as const} | ||||||
|  |   ></dees-stepper> | ||||||
|  | `; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as plugins from './00plugins.js'; | import * as plugins from '../00plugins.js'; | ||||||
| import * as colors from './00colors.js'; | import * as colors from '../00colors.js'; | ||||||
| 
 | 
 | ||||||
| import { | import { | ||||||
|   DeesElement, |   DeesElement, | ||||||
| @@ -14,6 +14,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 { stepperDemo } from './dees-stepper.demo.js'; | ||||||
| 
 | 
 | ||||||
| export interface IStep { | export interface IStep { | ||||||
|   title: string; |   title: string; | ||||||
| @@ -31,139 +32,7 @@ declare global { | |||||||
| 
 | 
 | ||||||
| @customElement('dees-stepper') | @customElement('dees-stepper') | ||||||
| export class DeesStepper extends DeesElement { | export class DeesStepper extends DeesElement { | ||||||
|   public static demo = () => |   public static demo = stepperDemo; | ||||||
|     html` |  | ||||||
|       <dees-stepper |  | ||||||
|         .steps=${[ |  | ||||||
|           { |  | ||||||
|             title: 'Account Setup', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-text key="email" label="Work Email" required></dees-input-text> |  | ||||||
|                 <dees-input-text key="password" label="Create Password" type="password" required></dees-input-text> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Profile Details', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-text key="firstName" label="First Name" required></dees-input-text> |  | ||||||
|                 <dees-input-text key="lastName" label="Last Name" required></dees-input-text> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Contact Information', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-phone key="phone" label="Mobile Number" required></dees-input-phone> |  | ||||||
|                 <dees-input-text key="company" label="Company"></dees-input-text> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Team Size', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-dropdown |  | ||||||
|                   key="teamSize" |  | ||||||
|                   label="How big is your team?" |  | ||||||
|                   .options=${[ |  | ||||||
|                     { label: '1-5', value: '1-5' }, |  | ||||||
|                     { label: '6-20', value: '6-20' }, |  | ||||||
|                     { label: '21-50', value: '21-50' }, |  | ||||||
|                     { label: '51+', value: '51+' }, |  | ||||||
|                   ]} |  | ||||||
|                   required |  | ||||||
|                 ></dees-input-dropdown> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Goals', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-multitoggle |  | ||||||
|                   key="goal" |  | ||||||
|                   label="Main objective" |  | ||||||
|                   .options=${[ |  | ||||||
|                     { label: 'Onboarding', value: 'onboarding' }, |  | ||||||
|                     { label: 'Analytics', value: 'analytics' }, |  | ||||||
|                     { label: 'Automation', value: 'automation' }, |  | ||||||
|                   ]} |  | ||||||
|                   required |  | ||||||
|                 ></dees-input-multitoggle> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Brand Preferences', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-text key="brandColor" label="Primary brand color"></dees-input-text> |  | ||||||
|                 <dees-input-text key="tone" label="Preferred tone (e.g. friendly, formal)"></dees-input-text> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Integrations', |  | ||||||
|             content: html` |  | ||||||
|               <dees-form> |  | ||||||
|                 <dees-input-list |  | ||||||
|                   key="integrations" |  | ||||||
|                   label="Integrations in use" |  | ||||||
|                   placeholder="Add integration" |  | ||||||
|                 ></dees-input-list> |  | ||||||
|                 <dees-form-submit>Continue</dees-form-submit> |  | ||||||
|               </dees-form> |  | ||||||
|             `,
 |  | ||||||
|             validationFunc: async (stepperArg, elementArg) => { |  | ||||||
|               const deesForm = elementArg.querySelector('dees-form'); |  | ||||||
|               deesForm.addEventListener('formData', () => stepperArg.goNext(), { once: true }); |  | ||||||
|             }, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             title: 'Review & Launch', |  | ||||||
|             content: html` |  | ||||||
|               <dees-panel> |  | ||||||
|                 <p>Almost there! Review your selections and launch whenever you're ready.</p> |  | ||||||
|               </dees-panel> |  | ||||||
|             `,
 |  | ||||||
|           }, |  | ||||||
|         ] as IStep[]} |  | ||||||
|       ></dees-stepper> |  | ||||||
|     `;
 |  | ||||||
| 
 | 
 | ||||||
|   @property({ |   @property({ | ||||||
|     type: Array, |     type: Array, | ||||||
							
								
								
									
										1
									
								
								ts_web/elements/dees-stepper/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								ts_web/elements/dees-stepper/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './dees-stepper.js'; | ||||||
							
								
								
									
										2
									
								
								ts_web/elements/dees-table/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								ts_web/elements/dees-table/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | export * from './dees-table.js'; | ||||||
|  | export * from './types.js'; | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user