Compare commits

...

83 Commits

Author SHA1 Message Date
e697419843 v3.3.3
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 12:37:54 +00:00
a69f504c2f fix(tests): update test imports to new dees-input-wysiwyg paths 2025-12-19 12:37:54 +00:00
2d1d9d901b v3.3.2
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 12:36:12 +00:00
811ec492d8 fix(build): update build config, bump dependencies, and adjust test import paths after element reorganization 2025-12-19 12:36:12 +00:00
761b0b678b v3.3.1
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-11 23:35:43 +00:00
12a7156928 fix(dees-pdf-viewer): Scroll active PDF thumbnail into view after rendering and on page changes; update dependency versions 2025-12-11 23:35:43 +00:00
59a870c3bc v3.3.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-09 08:26:24 +00:00
13fa654c0f feat(dees-appui-base): Add unified App UI API to dees-appui-base with ViewRegistry, AppRouter and StateManager 2025-12-09 08:26:24 +00:00
3616bbb9a7 v3.2.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 20:23:03 +00:00
27c071f7dc feat(dees-simple-appdash,dees-simple-login,dees-terminal): Revamp UI: dashboard & login styling, standardize icons to Lucide, and add terminal background/config 2025-12-08 20:23:03 +00:00
ac1ef4e497 v3.1.2
Some checks failed
Default (tags) / security (push) Failing after 12s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 16:17:52 +00:00
9c61c0542b fix(DeesAppuiMainmenu, DeesAppuiSecondarymenu): Add position: relative to main and secondary app UI menus to fix positioning of overlays and tooltips 2025-12-08 16:17:52 +00:00
5c099c8057 v3.1.1
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 16:16:25 +00:00
82b4afa95a fix(dees-appui): Extract demos for main and secondary app menus, adjust collapsed styles and toggle placement, bump devDependency 2025-12-08 16:16:25 +00:00
888430d55a v3.1.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 15:40:12 +00:00
85424d07cd feat(dees-appui): Add collapsible/compact mode to AppUI sidebars (mainmenu & secondarymenu) with toggles, tooltips and improved z-index stacking 2025-12-08 15:40:12 +00:00
24d3afe85d v3.0.1
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 14:50:53 +00:00
9735af05c8 fix(dees-appui): Normalize header heights and box-sizing for App UI components 2025-12-08 14:50:53 +00:00
9471c419fa v3.0.0
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 14:35:06 +00:00
778f457ed5 BREAKING CHANGE(dees-appui-secondarymenu): Add SecondaryMenu component and replace Mainselector with new SecondaryMenu in AppUI 2025-12-08 14:35:06 +00:00
a91098527c v2.0.7
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-08 12:04:01 +00:00
8f8aedc6b0 fix(structure): group components into groups inside the repo 2025-12-08 12:04:01 +00:00
f67be189eb v2.0.6
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-06 20:19:58 +00:00
4b8b1fa446 fix(dees-input-richtext): Initialize editor and link input element references in firstUpdated to ensure they exist before editor initialization. 2025-12-06 20:19:58 +00:00
0f9bc67a8e v2.0.5
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-06 14:09:21 +00:00
b33d51cebf fix(build): Bump devDependencies: update @git.zone/tsbundle and @git.zone/tswatch to patched versions 2025-12-06 14:09:21 +00:00
021e0fda3d v2.0.4
Some checks failed
Default (tags) / security (push) Failing after 19s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-06 13:54:17 +00:00
2ed0d8e0f2 fix(imports): Normalize and fix relative import paths for web components to ensure correct module resolution 2025-12-06 13:54:17 +00:00
5e4514c913 chore: remove obsolete files and documentation from the project 2025-12-05 10:20:29 +00:00
d1bc562b5c Refactor import paths for consistency and clarity across multiple components
- Updated import paths in dees-panel, dees-pdf, dees-progressbar, dees-searchbar, dees-shopping-productcard, dees-simple-appdash, dees-speechbubble, dees-statsgrid, dees-table, dees-toast, dees-updater, and dees-windowlayer to use consistent directory structure.
- Created index.ts files for various components to streamline imports and improve modularity.
- Ensured all imports point to the correct subdirectory structure, enhancing maintainability and readability of the codebase.
2025-12-05 10:19:37 +00:00
7adad49cb1 feat(structure): adjust 2025-12-05 10:19:11 +00:00
d07fec834f v2.0.3
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-03 09:22:35 +00:00
6f54bd228c fix(dependencies): Bump dependencies and developer tooling versions 2025-12-03 09:22:35 +00:00
ca7aa12218 v2.0.2
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-30 23:57:14 +00:00
c2ee19308d fix(dees-stepper): Make step validation abortable and cancel active step listeners when navigating 2025-11-30 23:57:14 +00:00
5e27449e50 v2.0.1
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-30 23:46:39 +00:00
d69f777b25 fix(dees-stepper): Improve dees-stepper visual style and transitions 2025-11-30 23:46:39 +00:00
caa954a539 update 2025-11-30 23:39:04 +00:00
997520f3ba v2.0.0
Some checks failed
Default (tags) / security (push) Failing after 20s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-17 13:27:12 +00:00
92f69e2aa6 BREAKING CHANGE(decorators): Migrate to TC39 standard decorators (accessor) across components, update tsconfig and bump dependencies 2025-11-17 13:27:11 +00:00
70c29c778c 1.12.6
Some checks failed
Default (tags) / security (push) Failing after 26s
Default (tags) / test (push) Failing after 17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-23 23:46:40 +00:00
0fc302699e fix(dependencies): Bump FontAwesome to ^7.1.0 2025-10-23 23:46:40 +00:00
dcb7ca2df3 1.12.5
Some checks failed
Default (tags) / security (push) Failing after 28s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-23 20:26:55 +00:00
ccbb0415e4 fix(ci): Add local permissions settings for development 2025-09-23 20:26:55 +00:00
496f54cedd feat(dees-pdf-viewer): add toggle button for sidebar visibility and enhance thumbnail re-rendering logic 2025-09-23 19:43:51 +00:00
83b5ecebeb feat(dees-pdf-viewer): update styles to improve layout with full height and hidden overflow 2025-09-20 22:09:11 +00:00
53b5cbed07 feat(dees-pdf-viewer): optimize thumbnail rendering and styles for improved layout and responsiveness 2025-09-20 22:07:41 +00:00
352fe79791 feat(dees-pdf-viewer): improve scrolling behavior and styles for better user experience 2025-09-20 22:03:47 +00:00
a95d5a96a0 feat(dees-pdf-viewer): add functionality to scroll thumbnail into view when sidebar is visible 2025-09-20 22:00:40 +00:00
ece7bb9a94 feat(dees-pdf-viewer): enhance page rendering and scrolling behavior with new data structure and styles 2025-09-20 21:56:23 +00:00
d42859b7b2 1.12.4
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-20 21:52:27 +00:00
f5655ad20b fix(ci): Add local assistant settings to enable permitted dev tooling commands 2025-09-20 21:52:27 +00:00
d3463f009b feat(dees-pdf-preview): enhance A4 format detection and improve canvas rendering quality 2025-09-20 21:46:52 +00:00
bb883ce341 feat(dees-pdf-preview): enhance hover functionality and page indicator display
feat(dees-pdf-viewer): improve input handling and remove unused variables
2025-09-20 21:36:04 +00:00
d9703d3ce3 feat: Update PDF components to improve rendering performance and manage document lifecycle without caching 2025-09-20 21:28:43 +00:00
7b5ba74d8b feat: Add context menu functionality for PDF components with options to view, copy URL, and download 2025-09-20 11:54:37 +00:00
a61f57db13 feat: Add PDF viewer and preview components with styling and functionality
- Implemented DeesPdfViewer for full-featured PDF viewing with toolbar and sidebar navigation.
- Created DeesPdfPreview for lightweight PDF previews.
- Introduced PdfManager for managing PDF document loading and caching.
- Added CanvasPool for efficient canvas management.
- Developed utility functions for performance monitoring and file size formatting.
- Established styles for viewer and preview components to enhance UI/UX.
- Included demo examples for showcasing PDF viewer capabilities.
2025-09-20 11:42:22 +00:00
c33ad2e405 fix(dees-input-fileupload): reorder baseStyles import for consistent styling application 2025-09-19 18:23:45 +00:00
4190324cb4 1.12.3
Some checks failed
Default (tags) / security (push) Failing after 20s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-19 17:36:03 +00:00
1b108fcc8c fix(dees-input-fileupload): Show selected files inside dropzone and improve file upload UX 2025-09-19 17:36:03 +00:00
0b2675c7e5 fix(dees-input-fileupload): enhance dropzone styles and improve file list rendering 2025-09-19 17:35:58 +00:00
12b0aa0aad Refactor dees-input-fileupload component and styles
- Updated demo.ts to enhance layout and styling, including renaming classes and adjusting spacing.
- Removed unused template rendering logic from template.ts.
- Simplified index.ts by removing the export of renderFileupload.
- Revamped styles in styles.ts for improved design consistency and responsiveness.
- Enhanced file upload functionality with better descriptions and validation messages.
2025-09-19 17:31:26 +00:00
987ae70e7a feat: add DeesInputFileupload and DeesInputRichtext components
- Implemented DeesInputFileupload component with file upload functionality, including drag-and-drop support, file previews, and clear all option.
- Developed DeesInputRichtext component featuring a rich text editor with a formatting toolbar, link management, and word count display.
- Created demo for DeesInputRichtext showcasing various use cases including basic editing, placeholder text, different heights, and disabled state.
- Added styles for both components to ensure a consistent and user-friendly interface.
- Introduced types for toolbar buttons in the rich text editor for better type safety and maintainability.
2025-09-19 15:26:21 +00:00
3ba673282a fix: update dees-wcctools dependency to version 1.2.0; adjust workspace dependencies and refactor demo function 2025-09-19 14:16:48 +00:00
20a52d1b3e 1.12.2
Some checks failed
Default (tags) / security (push) Failing after 21s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-18 16:04:02 +00:00
dafcf3834c fix(dees-input-wysiwyg): Integrate output format preview into WYSIWYG demo; update plan and add local dev settings 2025-09-18 16:04:02 +00:00
639672358a 1.12.1
Some checks failed
Default (tags) / security (push) Failing after 21s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-18 14:42:16 +00:00
671fb7dc66 fix(ci): Add local settings to allow running pnpm scripts and enable dev chat permission 2025-09-18 14:42:16 +00:00
b92966ef28 feat: consolidate contributor documentation by merging codex.md and CLAUDE.md into readme.info.md 2025-09-18 14:39:19 +00:00
c1102634f3 feat(dees-stepper): implement stepper demo with multi-step form functionality 2025-09-18 14:30:11 +00:00
ee470775b2 feat: Add WYSIWYG editor components and utilities
- Implemented WysiwygModalManager for managing modals related to code blocks and block settings.
- Created WysiwygSelection for handling text selection across Shadow DOM boundaries.
- Introduced WysiwygShortcuts for managing keyboard shortcuts and slash menu items.
- Developed wysiwygStyles for consistent styling of the WYSIWYG editor.
- Defined types for blocks, slash menu items, and shortcut patterns in wysiwyg.types.ts.
2025-09-18 14:23:42 +00:00
ba0f1602a1 feat: refactor imports and add index files for modular structure 2025-09-18 14:18:43 +00:00
682955212e feat(dees-stepper): add DeesStepper component with multi-step form functionality and validation 2025-09-18 14:18:36 +00:00
0410f6c196 1.12.0
Some checks failed
Default (tags) / security (push) Failing after 21s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-18 14:10:56 +00:00
24aa7588c5 feat(dees-stepper): Revamp dees-stepper: modern styling, new steps and improved navigation/validation 2025-09-18 14:10:55 +00:00
b46fe8fe93 feat(dees-editor): integrate Monaco version management and update CDN references 2025-09-18 13:39:59 +00:00
b47c2053b5 feat(dees-editor): add DeesEditor component with Monaco editor integration and content management 2025-09-18 13:39:52 +00:00
16bf8001ae feat(dees-dashboardgrid): implement collision detection during widget swap to prevent overlaps 2025-09-18 12:37:52 +00:00
792e77f824 feat(dees-dashboardgrid): enhance widget placement validation and logging for drag-and-drop interactions 2025-09-18 10:39:11 +00:00
9b39196195 1.11.8
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-09-18 09:25:37 +00:00
ad59e3d334 fix(ci): Add local tool permissions config to allow running pnpm scripts and enable mcp__zen__chat 2025-09-18 09:25:37 +00:00
0de4283fae feat(dees-dashboardgrid): enhance drag-and-drop functionality with preview state and previous position tracking 2025-09-18 08:05:41 +00:00
6f9c92a866 feat: implement DeesDashboardgrid component with drag-and-drop functionality
- Added DeesDashboardgrid class for managing a grid of dashboard widgets.
- Implemented widget dragging and resizing capabilities.
- Introduced layout management with collision detection and margin resolution.
- Created styles for grid layout, widget appearance, and animations.
- Added support for customizable margins, cell height, and grid lines.
- Included methods for adding, removing, and updating widgets dynamically.
- Implemented context menu for widget actions and keyboard navigation support.
- Established a responsive design with breakpoint handling for different layouts.
2025-09-17 21:46:44 +00:00
338 changed files with 15774 additions and 11096 deletions

1
.serena/.gitignore vendored
View File

@@ -1 +0,0 @@
/cache

View File

@@ -1,6 +0,0 @@
Before finishing a task:
- Run `pnpm run build` to ensure TypeScript compile + bundling succeed.
- Verify `dist_ts_web/` and `dist_bundle/bundle.js` updated.
- Optionally run `pnpm run test` and inspect failures.
- Avoid changing public APIs unless required; keep changes scoped.
- Update readme or inline docs only if user-facing behavior changes.

View File

@@ -1,11 +0,0 @@
Project: @design.estate/dees-catalog
Purpose: A component library of dynamic Web Components (TypeScript) for building modern web apps.
Tech stack: TypeScript (ES2022, NodeNext), decorators, custom elements via @design.estate/dees-element (Lit-style), bundling with esbuild via @git.zone/tsbundle, TypeScript building via @git.zone/tsbuild (tsfolders), tests with @git.zone/tstest, various UI libs (tiptap, apexcharts, monaco-editor runtime via CDN), DOM helpers via @design.estate/dees-domtools.
Structure:
- ts_web/: source of web components and pages
- dist_ts_web/: transpiled TS output
- dist_bundle/: production bundle (bundle.js + map)
- test/: tests
- html/: static demo assets
Key configs: tsconfig.json sets ES2022, NodeNext module/resolution, decorators enabled, skipLibCheck enabled to avoid third-party d.ts issues.
Entrypoints: ts_web/index.ts for bundling; custom elements annotated with @customElement.

View File

@@ -1,5 +0,0 @@
Language: TypeScript, ES2022 target, NodeNext module + resolution.
Patterns: Web Components with @customElement decorators; class-based components extending DeesElement; styles via css/cssManager; template render via html tagged literal.
Typing: Prefer explicit types where practical; tolerate `any` for external browser-injected libs (e.g., monaco) to keep build healthy.
Config: skipLibCheck enabled to avoid third-party d.ts breakages; exclude built declaration outputs.
Formatting/Linting: Not explicitly configured; follow existing style (2-space indents, single quotes often, semicolons present).

View File

@@ -1,6 +0,0 @@
Build: pnpm run build
Watch: pnpm run watch
Test: pnpm run test
Docs: pnpm run buildDocs
Inspect bundle size: ls -lh dist_bundle/bundle.js
Open demo (if applicable): serve static `html/` with any web server

View File

@@ -1,67 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "dees-catalog"

174
CLAUDE.md
View File

@@ -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

View File

@@ -1,5 +1,225 @@
# Changelog
## 2025-12-19 - 3.3.3 - fix(tests)
update test imports to new dees-input-wysiwyg paths
- Updated imports in test/test.wysiwyg-registry.both.ts to point to ts_web/elements/00group-input/dees-input-wysiwyg/*
- Aligns test references with relocated WYSIWYG block handlers and block registration module; no behavior changes to implementation
## 2025-12-19 - 3.3.2 - fix(build)
update build config, bump dependencies, and adjust test import paths after element reorganization
- npmextra.json: renamed gitzone entry to @git.zone/cli, moved tsdoc key to @git.zone/tsdoc, added @ship.zone/szci entry and added release registries + accessLevel
- package.json: bumped @design.estate/dees-wcctools ^2.0.1 -> ^3.1.0, lucide ^0.560.0 -> ^0.562.0, @git.zone/tsbuild ^3.1.2 -> ^4.0.2, @types/node ^25.0.0 -> ^25.0.3
- tests: updated import paths to follow reorganized source layout (wysiwyg files moved under elements/00group-input/dees-input-wysiwyg and dees-contextmenu moved to elements/dees-contextmenu/dees-contextmenu.js); updated BlockRegistry and blockregistration import paths
- Purpose: align tests and build metadata with refactored element file locations and updated tool/dependency versions
## 2025-12-11 - 3.3.1 - fix(dees-pdf-viewer)
Scroll active PDF thumbnail into view after rendering and on page changes; update dependency versions
- Ensure the active thumbnail is scrolled into view after thumbnails are rendered (improves sidebar navigation for dees-pdf-viewer).
- Scroll the thumbnail into view when navigating pages if the sidebar is visible (prevents the active page from being off-screen).
- Retain re-setup of the intersection observer for lazy-loading pages after thumbnail re-render.
- Bumped dependencies in package.json: @design.estate/dees-wcctools -> ^2.0.1, lucide -> ^0.560.0, @git.zone/tswatch -> ^2.3.13, @types/node -> ^25.0.0.
## 2025-12-09 - 3.3.0 - feat(dees-appui-base)
Add unified App UI API to dees-appui-base with ViewRegistry, AppRouter and StateManager
- Introduce ViewRegistry for declarative view registration and rendering (supports tag names, element classes and template functions).
- Add AppRouter with hash/history/external/none modes, URL synchronization, navigate/back/forward and onRouteChange listener support.
- Add StateManager to persist UI state (localStorage, sessionStorage or in-memory) with save/load/update/clear APIs.
- Extend interfaces (interfaces/appconfig.ts) with IAppConfig, IViewDefinition, IRoutingConfig, IStatePersistenceConfig and IAppUIState.
- Expose new public DeesAppuiBase methods: configure, navigateToView, getCurrentView, getUIState, restoreUIState, saveState, loadState, getViewRegistry, getRouter.
- Maintain backward compatibility with existing property-based API and slot usage.
- Export new modules (view.registry, app.router, state.manager) from dees-appui-base index and update element exports.
## 2025-12-08 - 3.2.0 - feat(dees-simple-appdash,dees-simple-login,dees-terminal)
Revamp UI: dashboard & login styling, standardize icons to Lucide, and add terminal background/config
- Standardize icon usage to Lucide prefixes in dees-simple-appdash; add fallback handling for legacy icon names
- Revamped dees-simple-appdash sidebar: updated spacing, typography, header icon wrapper, scrollbar styling, section labels, hover/selected states, and visual indicators
- Change 'Logout' label to 'Sign out' in dees-simple-appdash and add explicit status classes for controlbar (connected, terminal)
- Improve terminal UX: smoother launch/close animations, updated shadow and sizing logic in dees-simple-appdash
- Add background property to dees-terminal, sync it to a CSS variable and apply it to xterm theme for configurable terminal background
- Redesign dees-simple-login: new header/subheader, card layout, spacing, and updated submit text to 'Sign in'
- Bump devDependency @git.zone/tswatch to ^2.3.5
## 2025-12-08 - 3.1.2 - fix(DeesAppuiMainmenu, DeesAppuiSecondarymenu)
Add position: relative to main and secondary app UI menus to fix positioning of overlays and tooltips
- ts_web/elements/00group-appui/dees-appui-mainmenu/dees-appui-mainmenu.ts: add `position: relative` to host styles
- ts_web/elements/00group-appui/dees-appui-secondarymenu/dees-appui-secondarymenu.ts: add `position: relative` to host styles
- Fixes incorrect positioning for absolutely positioned children (tooltips, overlays, badges) inside the main and secondary menus
## 2025-12-08 - 3.1.1 - fix(dees-appui)
Extract demos for main and secondary app menus, adjust collapsed styles and toggle placement, bump devDependency
- Extracted inline demo markup into separate demo files: ts_web/elements/00group-appui/dees-appui-mainmenu/dees-appui-mainmenu.demo.ts and ts_web/elements/00group-appui/dees-appui-secondarymenu/dees-appui-secondarymenu.demo.ts and wired them up via imported demoFunc to reduce component size.
- Moved collapse toggle button markup in both dees-appui-mainmenu and dees-appui-secondarymenu templates to after the main container to improve layout/stacking and focus behavior.
- Adjusted collapsed logo/heading styles: removed extra padding/gap and hide logo text using display:none for a cleaner collapsed state.
- Bumped devDependency @git.zone/tswatch from ^2.3.1 to ^2.3.2 in package.json.
## 2025-12-08 - 3.1.0 - feat(dees-appui)
Add collapsible/compact mode to AppUI sidebars (mainmenu & secondarymenu) with toggles, tooltips and improved z-index stacking
- Add collapsed property to dees-appui-mainmenu and dees-appui-secondarymenu (reflect: true) to enable compact horizontal mode.
- Add floating collapse toggle buttons and public toggleCollapse() methods on mainmenu and secondarymenu; these dispatch 'collapse-change' events (bubbles & composed).
- Expose and track collapse state in dees-appui-base via mainmenuCollapsed and secondarymenuCollapsed properties; bind states to child components and re-emit collapse-change events as mainmenu-collapse-change and secondarymenu-collapse-change.
- Implement collapsed styles and animations: reduced sidebar widths, hide/compact labels and headers, center icons, hide badges, and add smooth width/opacity transitions.
- Add tooltips that appear for tabs/items when sidebars are collapsed to preserve discoverability.
- Adjust layout grid in DeesAppuiBase (use auto columns) and add explicit z-index layering to ensure proper stacking order of mainmenu, secondarymenu, maincontent and activitylog.
## 2025-12-08 - 3.0.1 - fix(dees-appui)
Normalize header heights and box-sizing for App UI components
- Set topbar/header heights to 48px (was 40px) and adjusted dependent offsets (activity container top, topShadow position) in dees-appui-activitylog.
- Make logo and secondary menu headers fixed 48px tall and replace vertical padding with horizontal padding for consistent vertical alignment (dees-appui-mainmenu, dees-appui-secondarymenu).
- Ensure tabs wrapper uses explicit 48px height and tabsContainer fills height (height:100%) to keep tab items vertically centered (dees-appui-tabs).
- Add box-sizing: border-box to affected header/logo containers to prevent overflow and ensure correct sizing.
- Minor CSS alignment and overflow fixes to improve consistent layout and scrolling behavior across the app UI components.
## 2025-12-08 - 3.0.0 - BREAKING CHANGE(dees-appui-secondarymenu)
Add SecondaryMenu component and replace Mainselector with new SecondaryMenu in AppUI
- Add dees-appui-secondarymenu component: collapsible groups, badges, dynamic heading, context menu and legacy flat-options support
- Introduce interfaces ISecondaryMenuItem and ISecondaryMenuGroup under elements/interfaces
- Replace dees-appui-mainselector usage with dees-appui-secondarymenu in DeesAppuiBase (props/events updated: secondarymenuGroups, secondarymenuHeading, secondarymenuOptions, item-select / secondarymenu-item-select)
- Remove dees-appui-mainselector implementation and its index export; update group exports and imports to expose secondarymenu
- Update demos and pages to showcase the new SecondaryMenu and adjust import paths for grouped components
- Bump devDependency @git.zone/tswatch to ^2.3.1
## 2025-12-08 - 2.0.7 - fix(structure)
Add many new UI components, input controls, charts, editors, and demos
- Introduce App UI components: dees-appui-appbar, dees-appui-mainmenu, dees-appui-mainselector, dees-appui-maincontent, dees-appui-activitylog, dees-appui-profiledropdown, dees-appui-tabs, dees-appui-base, dees-appui-view (templates, styles and demos included).
- Add a comprehensive set of input components: dees-input-text, dees-input-checkbox, dees-input-dropdown, dees-input-fileupload, dees-input-datepicker, dees-input-phone, dees-input-iban, dees-input-quantityselector, dees-input-list, dees-input-typelist, dees-input-tags, dees-input-multitoggle, dees-input-radiogroup, dees-input-richtext and supporting demos/styles/templates.
- Add form primitives and integration: dees-form and dees-form-submit with validation, collection and demo pages showcasing usage.
- Add button family and utilities: dees-button (with updated variants, sizes, status handling and demo), dees-button-group and dees-button-exit.
- Add charting components: dees-chart-area (ApexCharts integration) and dees-chart-log (log viewer) plus rich demo scenarios and realtime features.
- Add data display components: dees-dataview-codebox (highlight.js integration) and dees-dataview-statusobject with copy/context behaviours and demos.
- Add editor tooling: dees-editor (Monaco loader/version management), dees-editor-markdown and dees-editor-markdownoutlet; also TipTap-based richtext input with toolbar and link handling.
- Add global utilities and infra: dees-toast (programmatic toast API and containers), z-index registry and theme/font helpers (fonts, color tokens), plus many styles and accessibility/keyboard improvements across components.
- Export and index updates: new group exports added to ts_web/elements index and many index.ts files to expose the new components and demos.
- Extensive demos and showcase pages added (input-showcase, component demos) to illustrate integration, keyboard navigation, theming and form flows.
## 2025-12-06 - 2.0.6 - fix(dees-input-richtext)
Initialize editor and link input element references in firstUpdated to ensure they exist before editor initialization.
- Assign editorElement from shadowRoot.querySelector('.editor-content') in firstUpdated.
- Assign linkInputElement from shadowRoot.querySelector('.link-input input') in firstUpdated.
- Call initializeEditor() after DOM references are set to avoid undefined-element runtime errors.
## 2025-12-06 - 2.0.5 - fix(build)
Bump devDependencies: update @git.zone/tsbundle and @git.zone/tswatch to patched versions
- Update @git.zone/tsbundle from ^2.6.2 to ^2.6.3
- Update @git.zone/tswatch from ^2.2.2 to ^2.2.3
## 2025-12-06 - 2.0.4 - fix(imports)
Normalize and fix relative import paths for web components to ensure correct module resolution
- Replaced numerous './<component>.js' imports with explicit '../<component>/<component>.js' paths across many elements and demos to fix module resolution.
- Updated imports for core shared components such as dees-icon, dees-panel, dees-contextmenu, dees-windowlayer, dees-windowcontrols and several app-ui components (appbar, maincontent, mainselector, activitylog, mobilenavigation, modal, pdf, profilepicture, statsgrid, etc.).
- No runtime behavior changes — this is a refactor to import paths to address build/bundling and resolution issues.
## 2025-12-03 - 2.0.3 - fix(dependencies)
Bump dependencies and developer tooling versions
- Upgrade lucide from ^0.553.0 to ^0.555.0
- Bump @git.zone/tsbuild from ^3.1.0 to ^3.1.2
- Bump @git.zone/tsbundle from ^2.5.2 to ^2.6.2
- Bump @git.zone/tstest from ^2.8.1 to ^3.1.3
- Bump @git.zone/tswatch from ^2.2.1 to ^2.2.2
- Upgrade @types/node from ^22.0.0 to ^24.10.1
- Patch release: increment package version to 2.0.3
## 2025-11-30 - 2.0.2 - fix(dees-stepper)
Make step validation abortable and cancel active step listeners when navigating
- Extend IStep.validationFunc signature to accept an optional AbortSignal so validation handlers can be cancelled.
- Store an AbortController on the selected step and pass its signal into validationFunc when invoked.
- Abort the step's AbortController when navigating to the previous or next step to cancel any active listeners or async operations.
## 2025-11-30 - 2.0.1 - fix(dees-stepper)
Improve dees-stepper visual style and transitions
- Smooth animation: extend .step transition duration and use a cubic-bezier curve for smoother motion.
- Add .step.entrance class with a shorter easing for entrance animations to keep entrance timing distinct.
- Visual tweaks: reduce border-radius from 18px to 12px and increase inner content padding to 32px.
- Color and border updates: adjust background and border colors for light/dark themes to more consistent values.
- Shadow simplification: replace theme-dependent heavy shadows with a single subtle shadow (0 8px 32px rgba(0,0,0,0.4)).
- Remove selected-state border/box-shadow overrides (selection visuals simplified).
- Remove background-clip: padding-box to simplify rendering.
## 2025-11-17 - 2.0.0 - BREAKING CHANGE(decorators)
Migrate to TC39 standard decorators (accessor) across components, update tsconfig and bump dependencies
- Replaced experimental decorator-backed class fields with the TC39-compatible "accessor" form across ~69 web component files (properties and state fields) to follow Lit 3.x recommendations.
- Updated tsconfig.json to remove experimentalDecorators and useDefineForClassFields, aligning compiler settings with the standard decorators migration.
- Fixed optional/nullable fields to explicit `Type | undefined = undefined` where necessary to preserve runtime behavior and typing.
- Adjusted/remove usages of some non-reactive decorators/@query patterns to be compatible with the new decorator model (notable changes in a few components).
- Bumped several dependencies and devDependencies (examples: @design.estate/dees-domtools, @design.estate/dees-element, @design.estate/dees-wcctools, @git.zone/tsbuild, @git.zone/tstest, apexcharts, lucide).
- Added migration notes and testing summary to readme.hints.md documenting the TC39 decorators migration and verification steps.
## 2025-10-23 - 1.12.6 - fix(dependencies)
Bump FontAwesome to ^7.1.0 and add local claude settings
- Updated @fortawesome packages (@fortawesome/fontawesome-svg-core, @fortawesome/free-brands-svg-icons, @fortawesome/free-regular-svg-icons, @fortawesome/free-solid-svg-icons) to ^7.1.0 in package.json
- Added .claude/settings.local.json to configure local Claude/tooling permissions for repository operations
## 2025-09-23 - 1.12.5 - fix(ci)
Add local permissions settings for development
- Adds a new local settings file: .claude/settings.local.json
- Provides explicit permission entries for development tasks (allow running pnpm scripts, reading files, searching/replacing patterns, activating project, and helper tooling)
- Intended for local dev environment to enable tool automation without changing repository code
## 2025-09-20 - 1.12.4 - fix(ci)
Add local assistant settings to enable permitted dev tooling commands
- Add a local assistant settings file to configure allowed development tooling commands.
- Allows running pnpm scripts, file read/search/replace operations and other local project helper actions.
- Local configuration only — does not change library code or public API.
## 2025-09-19 - 1.12.3 - fix(dees-input-fileupload)
Show selected files inside dropzone and improve file upload UX
- Render the selected file list inside the dropzone container so files are displayed inline with the drop area
- Add dropzone--has-files class and styles to visually indicate when files are present
- Avoid opening the file selector when clicking on the browse button or inside the file list (prevents accidental re-opening)
- Refine file list and file-row styles (sizes, paddings, border radius, hover/background behavior and thumbnail/icon sizes) for a more compact and consistent appearance
- Simplify empty-state handling by returning an empty template when no files are present (file list is only rendered when files exist)
## 2025-09-18 - 1.12.2 - fix(dees-input-wysiwyg)
Integrate output format preview into WYSIWYG demo; update plan and add local dev settings
- Wire output format preview into the WYSIWYG demo (ts_web/elements/dees-input-wysiwyg.demo.ts) by calling setupOutputFormatDemo(editors.meeting, editors.recipe) so HTML/Markdown preview controls are initialized.
- Update readme.plan.md: mark the Output Formats review tasks as completed and document that preview controls were added.
- Add a local settings file to allow running local tooling tasks (grants permission for pnpm run scripts and related local commands).
- No library API or runtime component behavior changed — this is a demo/documentation and local-settings update.
## 2025-09-18 - 1.12.1 - fix(ci)
Add local settings to allow running pnpm scripts and enable dev chat permission
- Add a repository-local settings file granting permission to run pnpm scripts (Bash(pnpm run:*)) for development tooling.
- Enable the mcp__zen__chat permission for local dev workflows.
## 2025-09-18 - 1.12.0 - feat(dees-stepper)
Revamp dees-stepper: modern styling, new steps and improved navigation/validation
- Visual refresh for dees-stepper: updated card shapes, shadows, refined borders and stronger selected-state visuals for a modern shadcn-inspired look
- Improved transitions and animations (transform, box-shadow, filter) for smoother step selection and show/hide behavior
- Expanded default/demo steps: replaced small sample with a richer multi-step flow (Account Setup, Profile Details, Contact Information, Team Size, Goals, Brand Preferences, Integrations, Review & Launch)
- Enhanced step interactions: safer goNext/goBack handling with boundary checks and reset of validation flags to avoid stale validation state
- Better toolbar/controls placement for stepper demo (spacing, counters, accessible back control) and improved keyboard/UX affordances
- Minor documentation and meta updates: readme.plan.md extended with dees-stepper plan items and added .claude/settings.local.json
## 2025-09-18 - 1.11.8 - fix(ci)
Add local tool permissions config to allow running pnpm scripts and enable mcp__zen__chat
- Add local settings file to grant permission to run pnpm scripts (Bash(pnpm run:*))
- Enable mcp__zen__chat permission in local tool settings
## 2025-09-16 - 1.11.7 - fix(readme)
Expand README with comprehensive component documentation, examples and developer guide; add local Claude settings
@@ -215,7 +435,7 @@ Add dees-searchbar component with live search and filter demo
## 2025-04-22 - 1.6.0 - feat(documentation/dees-heading)
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
- Update component export index to include dees-heading

View File

@@ -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.

View File

@@ -1,5 +1,5 @@
{
"gitzone": {
"@git.zone/cli": {
"projectType": "wcc",
"module": {
"githost": "code.foss.global",
@@ -35,13 +35,19 @@
"Modern Web",
"Frontend Development"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"npmci": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"tsdoc": {
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "1.11.7",
"version": "3.3.3",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",
@@ -10,18 +10,19 @@
"test": "tstest test/ --web --verbose --timeout 30 --logfile",
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild",
"watch": "tswatch element",
"buildDocs": "tsdoc"
"buildDocs": "tsdoc",
"postinstall": "node scripts/update-monaco-version.cjs"
},
"author": "Lossless GmbH",
"license": "MIT",
"dependencies": {
"@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.1.2",
"@design.estate/dees-wcctools": "^1.1.1",
"@fortawesome/fontawesome-svg-core": "^7.0.1",
"@fortawesome/free-brands-svg-icons": "^7.0.1",
"@fortawesome/free-regular-svg-icons": "^7.0.1",
"@fortawesome/free-solid-svg-icons": "^7.0.1",
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^3.1.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.1.0",
@@ -31,25 +32,26 @@
"@tiptap/extension-typography": "^2.23.0",
"@tiptap/extension-underline": "^2.23.0",
"@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.2.0",
"@tsclass/tsclass": "^9.3.0",
"@webcontainer/api": "1.2.0",
"apexcharts": "^5.3.5",
"apexcharts": "^5.3.6",
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",
"lucide": "^0.544.0",
"lit": "^3.3.1",
"lucide": "^0.562.0",
"monaco-editor": "0.52.2",
"pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tstest": "^2.3.8",
"@git.zone/tswatch": "^2.2.1",
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.0.0"
"@types/node": "^25.0.3"
},
"files": [
"ts/**/*",

7239
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
onlyBuiltDependencies:
- puppeteer

View File

@@ -604,4 +604,200 @@ import { zIndexLayers } from './00zindex.js';
z-index: ${zIndexLayers.overlay.modal};
```
This system ensures proper stacking order for all overlay components and prevents z-index conflicts.
This system ensures proper stacking order for all overlay components and prevents z-index conflicts.
## TC39 Standard Decorators Migration (2025-01-17)
Successfully migrated from experimental TypeScript decorators to standard TC39 decorators as recommended by Lit 3.x documentation.
### Migration Overview:
#### 1. Changes Made:
- **Added `accessor` keyword** to all `@property` and `@state` decorated fields across 69 component files
- **Updated tsconfig.json**: Removed `experimentalDecorators: true` and `useDefineForClassFields: false`
- **Fixed optional properties**: Changed `accessor prop?: Type` to `accessor prop: Type | undefined = undefined`
- **Removed incompatible decorators**: Removed `@query` and non-reactive `@state` decorators from regular fields
#### 2. Key Pattern Changes:
**Before (Experimental Decorators):**
```typescript
@property({ type: String })
public value: string = '';
@property({ type: Boolean })
public disabled?: boolean;
```
**After (Standard TC39 Decorators):**
```typescript
@property({ type: String })
accessor value: string = '';
@property({ type: Boolean })
accessor disabled: boolean | undefined = undefined;
```
#### 3. Important Rules:
- **@property and @state**: MUST use `accessor` keyword for reactive properties
- **@query decorators**: Should NOT use `accessor` (they work with regular fields)
- **Optional properties**: Cannot use `?` syntax with accessor, must use `| undefined = undefined`
- **Private fields**: Non-reactive private fields should not use decorators
#### 4. TypeScript Configuration:
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
```
Note: `experimentalDecorators` defaults to false, and `useDefineForClassFields` defaults to true with ES2022 target.
#### 5. Build Results:
- ✅ Build successful with standard decorators
- ✅ Tests passing (7/8 - same as before migration)
- ✅ No bundle size changes reported
- ✅ All components working correctly
#### 6. Files Modified:
- 69 component files with decorator updates
- 16 files with optional property fixes
- 3 files with @query decorator removals
- tsconfig.json configuration update
### Why This Migration:
According to Lit's documentation (https://lit.dev/docs/components/decorators/#decorator-versions):
- TC39 standard decorators are the future-proof approach
- Provides better TypeScript integration
- Aligns with JavaScript specification
- While bundle sizes are slightly larger, the standardization benefits outweigh this
### Testing:
- All unit tests passing
- Manual testing of key components verified
- No regressions detected
- Focus management and interactions working correctly
## Enhanced AppUI API (2025-12-08)
The `dees-appui-base` component has been enhanced with a unified configuration API for building real-world applications.
### New Modules:
1. **ViewRegistry** (`view.registry.ts`)
- Manages view definitions and their lifecycle
- Supports tag names, element classes, and template functions as view content
- Methods: register, get, renderView, findByRoute
2. **AppRouter** (`app.router.ts`)
- Built-in routing with hash or history mode
- External router support for framework integration
- Methods: navigate, back, forward, onRouteChange
3. **StateManager** (`state.manager.ts`)
- Persists UI state (collapsed menus, selections, current view)
- Supports localStorage, sessionStorage, or memory storage
- Methods: save, load, update, clear
### New Interfaces (in `interfaces/appconfig.ts`):
```typescript
interface IAppConfig {
branding?: { logoIcon?: string; logoText?: string };
appBar?: IAppBarConfig;
views: IViewDefinition[];
mainMenu?: IMainMenuConfig;
routing?: IRoutingConfig;
statePersistence?: IStatePersistenceConfig;
onViewChange?: (viewId: string, view: IViewDefinition) => void;
}
interface IViewDefinition {
id: string;
name: string;
iconName?: string;
content: string | (new () => HTMLElement) | (() => TemplateResult);
secondaryMenu?: ISecondaryMenuGroup[];
contentTabs?: ITab[];
route?: string;
}
interface IRoutingConfig {
mode: 'hash' | 'history' | 'external' | 'none';
basePath?: string;
defaultView?: string;
syncUrl?: boolean;
}
```
### New Public Methods on DeesAppuiBase:
```typescript
// Configure with unified config
configure(config: IAppConfig): void
// Navigation
navigateToView(viewId: string): boolean
getCurrentView(): IViewDefinition | undefined
// State management
getUIState(): IAppUIState
restoreUIState(state: IAppUIState): void
saveState(): void
loadState(): boolean
// Access internals
getViewRegistry(): ViewRegistry
getRouter(): AppRouter | null
```
### Usage Example (New Unified Config API):
```typescript
import type { IAppConfig } from '@design.estate/dees-catalog';
const config: IAppConfig = {
branding: { logoIcon: 'lucide:box', logoText: 'My App' },
views: [
{ id: 'dashboard', name: 'Dashboard', iconName: 'lucide:home', content: 'my-dashboard' },
{ id: 'settings', name: 'Settings', iconName: 'lucide:settings', content: 'my-settings' },
],
mainMenu: {
sections: [{ views: ['dashboard'] }],
bottomItems: ['settings'],
},
routing: { mode: 'hash', defaultView: 'dashboard' },
statePersistence: { enabled: true, storage: 'localStorage' },
};
html`<dees-appui-base .config=${config}></dees-appui-base>`;
```
### Backward Compatibility:
The existing property-based API still works:
```typescript
html`
<dees-appui-base
.mainmenuGroups=${groups}
.secondarymenuGroups=${secondaryGroups}
@mainmenu-tab-select=${handler}
>
<div slot="maincontent">...</div>
</dees-appui-base>
`;
```
### Key Features:
- **Declarative View Registry**: Map menu items to view components
- **Built-in Routing**: Hash or history mode with URL synchronization
- **External Router Support**: Integrate with Angular Router or other frameworks
- **State Persistence**: Save/restore collapsed menus, selections, and current view
- **View-specific Menus**: Each view can define its own secondary menu and tabs
- **Full Backward Compatibility**: Existing code continues to work

80
readme.info.md Normal file
View 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.

Binary file not shown.

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const projectRoot = path.resolve(__dirname, '..');
function resolveMonacoPackageJson() {
try {
const resolvedPath = require.resolve('monaco-editor/package.json', {
paths: [projectRoot],
});
return resolvedPath;
} catch (error) {
console.error('[dees-editor] Unable to resolve monaco-editor/package.json');
throw error;
}
}
function getMonacoVersion() {
const monacoPackagePath = resolveMonacoPackageJson();
const monacoPackage = require(monacoPackagePath);
if (!monacoPackage.version) {
throw new Error('[dees-editor] monaco-editor/package.json does not expose a version field');
}
return monacoPackage.version;
}
function writeVersionModule(version) {
const targetDir = path.join(projectRoot, 'ts_web', 'elements', 'dees-editor');
fs.mkdirSync(targetDir, { recursive: true });
const targetFile = path.join(targetDir, 'version.ts');
const fileContent = `// Auto-generated by scripts/update-monaco-version.cjs\nexport const MONACO_VERSION = '${version}';\n`;
fs.writeFileSync(targetFile, fileContent, 'utf8');
console.log(`[dees-editor] Wrote ${path.relative(projectRoot, targetFile)} with monaco-editor@${version}`);
}
try {
const version = getMonacoVersion();
writeVersionModule(version);
} catch (error) {
console.error('[dees-editor] Failed to update Monaco version module.');
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}

View File

@@ -1,6 +1,6 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { demoFunc } from '../ts_web/elements/dees-contextmenu.demo.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
import { demoFunc } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.demo.js';
tap.test('should render context menu demo', async () => {
// Create demo container

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should close all parent menus when clicking action in nested submenu', async () => {
let actionCalled = false;

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
import { DeesElement, customElement, html } from '@design.estate/dees-element';
// Create a test element with shadow DOM

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should show context menu with nested submenu', async () => {
// Create a test element with context menu items

View File

@@ -0,0 +1,28 @@
import { tap, expect } from '@push.rocks/tapbundle';
import {
resolveWidgetPlacement,
collectCollisions,
} from '../ts_web/elements/dees-dashboardgrid/layout.ts';
import type { DashboardWidget } from '../ts_web/elements/dees-dashboardgrid/types.ts';
tap.test('dashboardgrid does not overlap widgets after swap attempt', async () => {
const widgets: DashboardWidget[] = [
{ id: 'w0', x: 6, y: 5, w: 1, h: 3 },
{ id: 'w1', x: 6, y: 1, w: 1, h: 3 },
{ id: 'w2', x: 3, y: 0, w: 2, h: 2 },
{ id: 'w3', x: 9, y: 0, w: 1, h: 2 },
{ id: 'w4', x: 4, y: 3, w: 1, h: 2 },
];
const placement = resolveWidgetPlacement(widgets, 'w0', { x: 6, y: 3 }, 12);
expect(placement).toBeTruthy();
const layout = placement!.widgets;
for (const widget of layout) {
const collisions = collectCollisions(layout, widget, widget.x, widget.y, widget.w, widget.h);
expect(collisions).toBeEmptyArray();
}
});
export default tap.start();

View File

@@ -1,6 +1,6 @@
import { expect, tap, webhelpers } from '@push.rocks/tapbundle';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { WysiwygSelection } from '../ts_web/elements/wysiwyg/wysiwyg.selection.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
import { WysiwygSelection } from '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.selection.js';
tap.test('Shadow DOM containment should work correctly', async () => {
console.log('=== Testing Shadow DOM Containment ===');

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
tap.test('should create wysiwyg editor', async () => {
const editor = new DeesInputWysiwyg();

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,11 +1,11 @@
import { tap, expect, webhelpers } from '@push.rocks/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import { BlockRegistry } from '../ts_web/elements/wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/wysiwyg/wysiwyg.blockregistration.js';
import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
tap.test('Debug: should create empty wysiwyg block component', async () => {
try {

View File

@@ -1,11 +1,11 @@
import { tap, expect, webhelpers } from '@push.rocks/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
import { BlockRegistry } from '../ts_web/elements/wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/wysiwyg/wysiwyg.blockregistration.js';
import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
tap.test('BlockRegistry should have registered handlers', async () => {
// Test divider handler

View File

@@ -1,6 +1,6 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should change block type via context menu', async () => {
// Create WYSIWYG editor with a paragraph

View File

@@ -1,6 +1,6 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js';
tap.test('should show context menu on WYSIWYG blocks', async () => {
// Create WYSIWYG editor

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,5 +1,5 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
// Initialize the element
DeesInputWysiwyg;

View File

@@ -1,6 +1,6 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Keyboard: Arrow navigation between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(

View File

@@ -1,6 +1,6 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Phase 3: Quote block should render and work correctly', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(

View File

@@ -1,12 +1,12 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import { BlockRegistry } from '../ts_web/elements/wysiwyg/blocks/block.registry.js';
import { DividerBlockHandler } from '../ts_web/elements/wysiwyg/blocks/content/divider.block.js';
import { ParagraphBlockHandler } from '../ts_web/elements/wysiwyg/blocks/text/paragraph.block.js';
import { HeadingBlockHandler } from '../ts_web/elements/wysiwyg/blocks/text/heading.block.js';
import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
import { DividerBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/content/divider.block.js';
import { ParagraphBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/text/paragraph.block.js';
import { HeadingBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/text/heading.block.js';
// Import block registration to ensure handlers are registered
import '../ts_web/elements/wysiwyg/wysiwyg.blockregistration.js';
import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
tap.test('BlockRegistry should register and retrieve handlers', async () => {
// Test divider handler

View File

@@ -1,6 +1,6 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting should work consistently for all block types', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(

View File

@@ -1,6 +1,6 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting basic test', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(

View File

@@ -1,7 +1,7 @@
import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
tap.test('should split paragraph content on Enter key', async () => {
// Create the wysiwyg editor

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '1.11.7',
version: '3.3.3',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -1,4 +1,4 @@
import * as plugins from './00plugins.js';
import * as plugins from '../../00plugins.js';
import {
DeesElement,
type TemplateResult,
@@ -10,8 +10,8 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from './dees-contextmenu.js';
import './dees-icon.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import '../../dees-icon/dees-icon.js';
@customElement('dees-appui-activitylog')
export class DeesAppuiActivitylog extends DeesElement {
@@ -63,13 +63,14 @@ export class DeesAppuiActivitylog extends DeesElement {
.topbar {
position: absolute;
top: 0px;
height: 40px;
height: 48px;
width: 100%;
padding: 0px 16px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
display: flex;
align-items: center;
box-sizing: border-box;
}
.topbar .heading {
@@ -81,7 +82,7 @@ export class DeesAppuiActivitylog extends DeesElement {
.activityContainer {
position: absolute;
top: 40px;
top: 48px;
bottom: 48px;
width: 100%;
padding: 12px 0px;
@@ -315,7 +316,7 @@ export class DeesAppuiActivitylog extends DeesElement {
position: absolute;
width: 100%;
height: 24px;
top: 40px;
top: 48px;
background: ${cssManager.bdTheme(
'linear-gradient(0deg, transparent 0%, #fafafa 100%)',
'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)'

View File

@@ -0,0 +1 @@
export * from './dees-appui-activitylog.js';

View File

@@ -5,19 +5,19 @@ import {
property,
state,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import * as interfaces from './interfaces/index.js';
import * as plugins from './00plugins.js';
import { demoFunc } from './dees-appui-appbar.demo.js';
import * as interfaces from '../../interfaces/index.js';
import * as plugins from '../../00plugins.js';
import { demoFunc } from './demo.js';
import { appuiAppbarStyles } from './styles.js';
import { renderAppuiAppbar } from './template.js';
// Import required components
import './dees-icon.js';
import './dees-windowcontrols.js';
import './dees-appui-profiledropdown.js';
import '../../dees-icon/dees-icon.js';
import '../../dees-windowcontrols/dees-windowcontrols.js';
import '../dees-appui-profiledropdown/dees-appui-profiledropdown.js';
declare global {
interface HTMLElementTagNameMap {
@@ -31,301 +31,58 @@ export class DeesAppuiBar extends DeesElement {
// INSTANCE PROPERTIES
@property({ type: Array })
public menuItems: interfaces.IAppBarMenuItem[] = [];
accessor menuItems: interfaces.IAppBarMenuItem[] = [];
@property({ type: String })
public breadcrumbs: string = '';
accessor breadcrumbs: string = '';
@property({ type: String })
public breadcrumbSeparator: string = ' > ';
accessor breadcrumbSeparator: string = ' > ';
@property({ type: Boolean })
public showWindowControls: boolean = true;
accessor showWindowControls: boolean = true;
@property({ type: Object })
public user?: {
accessor user: {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
};
} | undefined = undefined;
@property({ type: Array })
public profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
accessor profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
@property({ type: Boolean })
public showSearch: boolean = false;
accessor showSearch: boolean = false;
// STATE
@state()
private activeMenu: string | null = null;
accessor activeMenu: string | null = null;
@state()
private openDropdowns: Set<string> = new Set();
accessor openDropdowns: Set<string> = new Set();
@state()
private focusedItem: string | null = null;
accessor focusedItem: string | null = null;
@state()
private focusedDropdownItem: number = -1;
accessor focusedDropdownItem: number = -1;
@state()
private isProfileDropdownOpen: boolean = false;
accessor isProfileDropdownOpen: boolean = false;
public static styles = [
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;
}
`,
];
public static styles = appuiAppbarStyles;
// INSTANCE
public render(): TemplateResult {
return html`
<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>
`;
return renderAppuiAppbar(this);
}
private renderMenuItems(): TemplateResult {
public renderMenuItems(): TemplateResult {
return html`
${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) {
return html``;
}
@@ -417,7 +174,7 @@ export class DeesAppuiBar extends DeesElement {
`;
}
private renderAccountSection(): TemplateResult {
public renderAccountSection(): TemplateResult {
return html`
${this.showSearch ? html`
<dees-icon

View File

@@ -1,7 +1,8 @@
import { html, css } from '@design.estate/dees-element';
import type { DeesAppuiBar } from './dees-appui-appbar.js';
import type { IAppBarMenuItem } from './interfaces/appbarmenuitem.js';
import type { DeesAppuiBar } from './component.js';
import type { IAppBarMenuItem } from '../../interfaces/appbarmenuitem.js';
import '@design.estate/dees-wcctools/demotools';
import './component.js';
export const demoFunc = () => {
// Sample menu items with various configurations

View File

@@ -0,0 +1 @@
export * from './component.js';

View 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;
}
`,
];

View 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>
`;
};

View File

@@ -0,0 +1,271 @@
import type { IRoutingConfig, IViewDefinition } from '../../interfaces/appconfig.js';
import type { ViewRegistry } from './view.registry.js';
export type TRouteChangeCallback = (viewId: string, params?: Record<string, string>) => void;
/**
* Router for managing view navigation and URL synchronization
*/
export class AppRouter {
private config: Required<Omit<IRoutingConfig, 'notFound'>> & Pick<IRoutingConfig, 'notFound'>;
private viewRegistry: ViewRegistry;
private listeners: Set<TRouteChangeCallback> = new Set();
private currentViewId: string | null = null;
private isInitialized: boolean = false;
constructor(config: IRoutingConfig, viewRegistry: ViewRegistry) {
this.config = {
mode: config.mode,
basePath: config.basePath || '',
defaultView: config.defaultView || '',
syncUrl: config.syncUrl ?? true,
notFound: config.notFound,
};
this.viewRegistry = viewRegistry;
}
/**
* Initialize the router
*/
public init(): void {
if (this.isInitialized) return;
if (this.config.mode === 'hash') {
window.addEventListener('hashchange', this.handleHashChange);
// Check initial hash
const initialView = this.getViewFromHash();
if (initialView) {
this.navigate(initialView, { source: 'initial' });
} else if (this.config.defaultView) {
this.navigate(this.config.defaultView, { source: 'initial' });
}
} else if (this.config.mode === 'history') {
window.addEventListener('popstate', this.handlePopState);
// Check initial path
const initialView = this.getViewFromPath();
if (initialView) {
this.navigate(initialView, { source: 'initial' });
} else if (this.config.defaultView) {
this.navigate(this.config.defaultView, { source: 'initial' });
}
} else if (this.config.mode === 'none' && this.config.defaultView) {
this.navigate(this.config.defaultView, { source: 'initial' });
}
// For 'external' mode, we don't set up listeners - the external router handles it
this.isInitialized = true;
}
/**
* Navigate to a view by ID
*/
public navigate(
viewId: string,
options: {
source?: 'navigation' | 'popstate' | 'initial' | 'programmatic';
replace?: boolean;
params?: Record<string, string>;
} = {}
): boolean {
const { source = 'programmatic', replace = false, params } = options;
const view = this.viewRegistry.get(viewId);
if (!view) {
console.warn(`Cannot navigate to unknown view: ${viewId}`);
if (this.config.notFound) {
if (typeof this.config.notFound === 'function') {
this.config.notFound();
} else {
return this.navigate(this.config.notFound, { source, replace: true });
}
}
return false;
}
const previousViewId = this.currentViewId;
this.currentViewId = viewId;
// Update URL if configured
if (this.config.syncUrl && this.config.mode !== 'none' && this.config.mode !== 'external') {
this.updateUrl(view, replace);
}
// Notify listeners
this.notifyListeners(viewId, params);
return true;
}
/**
* Navigate back in history
*/
public back(): void {
if (this.config.mode === 'hash' || this.config.mode === 'history') {
window.history.back();
}
}
/**
* Navigate forward in history
*/
public forward(): void {
if (this.config.mode === 'hash' || this.config.mode === 'history') {
window.history.forward();
}
}
/**
* Get current view ID
*/
public getCurrentViewId(): string | null {
return this.currentViewId;
}
/**
* Add a route change listener
*/
public onRouteChange(callback: TRouteChangeCallback): () => void {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
/**
* Handle external navigation (for external router mode)
*/
public handleExternalNavigation(viewId: string, params?: Record<string, string>): void {
if (this.config.mode !== 'external') {
console.warn('handleExternalNavigation should only be used in external mode');
}
const previousViewId = this.currentViewId;
this.currentViewId = viewId;
this.notifyListeners(viewId, params);
}
/**
* Sync state with URL (for external router integration)
*/
public syncWithUrl(): string | null {
if (this.config.mode === 'hash') {
return this.getViewFromHash();
} else if (this.config.mode === 'history') {
return this.getViewFromPath();
}
return null;
}
/**
* Get the current route from the URL
*/
public getCurrentRoute(): string {
if (this.config.mode === 'hash') {
return window.location.hash.slice(1) || '';
} else if (this.config.mode === 'history') {
let path = window.location.pathname;
if (this.config.basePath && path.startsWith(this.config.basePath)) {
path = path.slice(this.config.basePath.length);
}
return path.replace(/^\//, '');
}
return '';
}
/**
* Build a URL for a view
*/
public buildUrl(viewId: string): string {
const view = this.viewRegistry.get(viewId);
const route = view?.route || viewId;
if (this.config.mode === 'hash') {
return `#${route}`;
} else if (this.config.mode === 'history') {
return `${this.config.basePath}/${route}`;
}
return '';
}
/**
* Destroy the router
*/
public destroy(): void {
if (this.config.mode === 'hash') {
window.removeEventListener('hashchange', this.handleHashChange);
} else if (this.config.mode === 'history') {
window.removeEventListener('popstate', this.handlePopState);
}
this.listeners.clear();
this.isInitialized = false;
}
// Private methods
private handleHashChange = (): void => {
const viewId = this.getViewFromHash();
if (viewId && viewId !== this.currentViewId) {
this.navigate(viewId, { source: 'popstate' });
}
};
private handlePopState = (): void => {
const viewId = this.getViewFromPath();
if (viewId && viewId !== this.currentViewId) {
this.navigate(viewId, { source: 'popstate' });
}
};
private getViewFromHash(): string | null {
const hash = window.location.hash.slice(1); // Remove #
if (!hash) return null;
// Try to find view by route
const view = this.viewRegistry.findByRoute(hash);
return view?.id || null;
}
private getViewFromPath(): string | null {
let path = window.location.pathname;
// Remove base path if configured
if (this.config.basePath) {
if (path.startsWith(this.config.basePath)) {
path = path.slice(this.config.basePath.length);
}
}
// Remove leading slash
path = path.replace(/^\//, '');
if (!path) return null;
const view = this.viewRegistry.findByRoute(path);
return view?.id || null;
}
private updateUrl(view: IViewDefinition, replace: boolean): void {
const route = view.route || view.id;
if (this.config.mode === 'hash') {
const newHash = `#${route}`;
if (replace) {
window.history.replaceState(null, '', newHash);
} else {
window.history.pushState(null, '', newHash);
}
} else if (this.config.mode === 'history') {
const basePath = this.config.basePath || '';
const newPath = `${basePath}/${route}`;
if (replace) {
window.history.replaceState({ viewId: view.id }, '', newPath);
} else {
window.history.pushState({ viewId: view.id }, '', newPath);
}
}
}
private notifyListeners(viewId: string, params?: Record<string, string>): void {
for (const listener of this.listeners) {
listener(viewId, params);
}
}
}

View File

@@ -0,0 +1,238 @@
import { html, css } from '@design.estate/dees-element';
import type { DeesAppuiBase } from '../dees-appui-base/dees-appui-base.js';
import type { IAppBarMenuItem } from '../../interfaces/appbarmenuitem.js';
import type { ITab } from '../../interfaces/tab.js';
import type { ISelectionOption } from '../../interfaces/selectionoption.js';
import type { IMenuGroup } from '../../interfaces/menugroup.js';
import type { ISecondaryMenuGroup } from '../../interfaces/secondarymenu.js';
import * as plugins from '../../00plugins.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {
// Menu items for the appbar
const menuItems: IAppBarMenuItem[] = [
{
name: 'File',
action: async () => {},
submenu: [
{ name: 'New Project', shortcut: 'Cmd+N', iconName: 'filePlus', action: async () => console.log('New project') },
{ name: 'Open Project...', shortcut: 'Cmd+O', iconName: 'folderOpen', action: async () => console.log('Open project') },
{ name: 'Recent Projects', action: async () => {}, submenu: [
{ name: 'my-app', action: async () => console.log('Open my-app') },
{ name: 'component-lib', action: async () => console.log('Open component-lib') },
{ name: 'api-server', action: async () => console.log('Open api-server') },
]},
{ divider: true },
{ name: 'Save All', shortcut: 'Cmd+Shift+S', iconName: 'save', action: async () => console.log('Save all') },
{ divider: true },
{ name: 'Close Project', action: async () => console.log('Close project') },
]
},
{
name: 'Edit',
action: async () => {},
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
{ divider: true },
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
]
},
{
name: 'View',
action: async () => {},
submenu: [
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
{ divider: true },
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoomIn', action: async () => console.log('Zoom in') },
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoomOut', action: async () => console.log('Zoom out') },
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
]
},
{
name: 'Help',
action: async () => {},
submenu: [
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
{ name: 'Release Notes', iconName: 'fileText', action: async () => console.log('Release notes') },
{ divider: true },
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
}
];
// Main menu groups (left sidebar)
const mainMenuGroups: IMenuGroup[] = [
{
tabs: [
{ key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard selected') },
{ key: 'Inbox', iconName: 'lucide:inbox', action: () => console.log('Inbox selected') },
]
},
{
name: 'Workspace',
tabs: [
{ key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects selected') },
{ key: 'Tasks', iconName: 'lucide:checkSquare', action: () => console.log('Tasks selected') },
{ key: 'Documents', iconName: 'lucide:fileText', action: () => console.log('Documents selected') },
]
},
{
name: 'Analytics',
tabs: [
{ key: 'Reports', iconName: 'lucide:barChart3', action: () => console.log('Reports selected') },
{ key: 'Insights', iconName: 'lucide:lightbulb', action: () => console.log('Insights selected') },
]
}
];
// Main menu bottom tabs (pinned to bottom)
const mainMenuBottomTabs: ITab[] = [
{ key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings selected') },
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help selected') },
];
// Secondary menu groups (second sidebar with collapsible groups)
// These showcase the new shadcn-style design with badges and collapsible sections
const secondaryMenuGroups: ISecondaryMenuGroup[] = [
{
name: 'Quick Access',
iconName: 'lucide:zap',
items: [
{ key: 'Overview', iconName: 'layoutDashboard', action: () => console.log('Overview selected') },
{ key: 'Recent Activity', iconName: 'clock', action: () => console.log('Recent Activity selected'), badge: 5 },
{ key: 'Favorites', iconName: 'star', action: () => console.log('Favorites selected') },
]
},
{
name: 'Resources',
iconName: 'lucide:layers',
items: [
{ key: 'Components', iconName: 'package', action: () => console.log('Components selected'), badge: 24 },
{ key: 'Services', iconName: 'server', action: () => console.log('Services selected'), badge: 'new', badgeVariant: 'success' },
{ key: 'APIs', iconName: 'globe', action: () => console.log('APIs selected'), badge: 3, badgeVariant: 'warning' },
{ key: 'Webhooks', iconName: 'webhook', action: () => console.log('Webhooks selected') },
]
},
{
name: 'Data Management',
iconName: 'lucide:database',
items: [
{ key: 'Database', iconName: 'database', action: () => console.log('Database selected') },
{ key: 'Storage', iconName: 'hardDrive', action: () => console.log('Storage selected'), badge: '85%', badgeVariant: 'warning' },
{ key: 'Backups', iconName: 'archive', action: () => console.log('Backups selected'), badge: 'OK', badgeVariant: 'success' },
]
},
{
name: 'System',
iconName: 'lucide:settings',
collapsed: true,
items: [
{ key: 'Configuration', iconName: 'sliders', action: () => console.log('Configuration selected') },
{ key: 'Integrations', iconName: 'plug', action: () => console.log('Integrations selected'), badge: 2, badgeVariant: 'error' },
{ key: 'Permissions', iconName: 'shield', action: () => console.log('Permissions selected') },
{ key: 'Logs', iconName: 'fileText', action: () => console.log('Logs selected') },
]
}
];
// Main content tabs
const mainContentTabs: ITab[] = [
{ key: 'Details', iconName: 'lucide:file', action: () => console.log('Details tab') },
{ key: 'Logs', iconName: 'lucide:list', action: () => console.log('Logs tab') },
{ key: 'Metrics', iconName: 'lucide:lineChart', action: () => console.log('Metrics tab') },
];
// Profile menu items
const profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile settings') },
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account settings') },
{ divider: true },
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
];
return html`
<dees-demowrapper>
<style>
${css`
.demo-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
`}
</style>
<div class="demo-container">
<dees-appui-base
.appbarMenuItems=${menuItems}
.appbarBreadcrumbs=${'Dashboard'}
.appbarUser=${{
name: 'Jane Smith',
email: 'jane.smith@example.com',
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
}}
.appbarProfileMenuItems=${profileMenuItems}
.appbarShowWindowControls=${true}
.appbarShowSearch=${true}
.mainmenuLogoIcon=${'lucide:box'}
.mainmenuLogoText=${'Acme App'}
.mainmenuGroups=${mainMenuGroups}
.mainmenuBottomTabs=${mainMenuBottomTabs}
.secondarymenuHeading=${'Dashboard'}
.secondarymenuGroups=${secondaryMenuGroups}
.maincontentTabs=${mainContentTabs}
@appbar-menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail)}
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
@appbar-search-click=${() => console.log('Search clicked')}
@appbar-user-menu-open=${() => console.log('User menu opened')}
@appbar-profile-menu-select=${(e: CustomEvent) => console.log('Profile menu selected:', e.detail)}
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
@secondarymenu-item-select=${(e: CustomEvent) => console.log('Item selected:', e.detail)}
>
<div slot="maincontent" style="padding: 40px; color: #a3a3a3; font-family: 'Geist Sans', 'Inter', -apple-system, sans-serif;">
<h1 style="color: #fafafa; font-weight: 600; font-size: 24px; margin-bottom: 8px;">Welcome to Acme App</h1>
<p style="color: #737373; margin-bottom: 32px;">This demo showcases the AppUI component system with the new SecondaryMenu.</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 32px;">
<div style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 20px;">
<h3 style="color: #fafafa; font-size: 14px; font-weight: 600; margin-bottom: 8px;">SecondaryMenu Features</h3>
<ul style="margin: 0; padding-left: 20px; font-size: 13px; line-height: 1.8;">
<li>Collapsible groups with smooth animations</li>
<li>Badge support (counts, status, variants)</li>
<li>Dynamic heading from MainMenu selection</li>
<li>shadcn-inspired modern design</li>
</ul>
</div>
<div style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 20px;">
<h3 style="color: #fafafa; font-size: 14px; font-weight: 600; margin-bottom: 8px;">Badge Variants</h3>
<div style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px;">
<span style="background: #27272a; color: #a1a1aa; padding: 2px 8px; border-radius: 9px;">default</span>
<span style="background: #14532d; color: #4ade80; padding: 2px 8px; border-radius: 9px;">success</span>
<span style="background: #451a03; color: #fbbf24; padding: 2px 8px; border-radius: 9px;">warning</span>
<span style="background: #450a0a; color: #f87171; padding: 2px 8px; border-radius: 9px;">error</span>
</div>
</div>
</div>
<p style="font-size: 13px; color: #525252;">
Try clicking items in the MainMenu (left) - the SecondaryMenu heading updates automatically.
Click group headers in the SecondaryMenu to collapse/expand sections.
</p>
</div>
</dees-appui-base>
</div>
</dees-demowrapper>
`;
};

View File

@@ -0,0 +1,582 @@
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import * as interfaces from '../../interfaces/index.js';
import * as plugins from '../../00plugins.js';
import type { DeesAppuiBar } from '../dees-appui-appbar/index.js';
import type { DeesAppuiMainmenu } from '../dees-appui-mainmenu/dees-appui-mainmenu.js';
import type { DeesAppuiSecondarymenu } from '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
import type { DeesAppuiMaincontent } from '../dees-appui-maincontent/dees-appui-maincontent.js';
import type { DeesAppuiActivitylog } from '../dees-appui-activitylog/dees-appui-activitylog.js';
import { demoFunc } from './dees-appui-base.demo.js';
// New module imports
import { ViewRegistry } from './view.registry.js';
import { AppRouter } from './app.router.js';
import { StateManager } from './state.manager.js';
// Import child components
import '../dees-appui-appbar/index.js';
import '../dees-appui-mainmenu/dees-appui-mainmenu.js';
import '../dees-appui-secondarymenu/dees-appui-secondarymenu.js';
import '../dees-appui-maincontent/dees-appui-maincontent.js';
import '../dees-appui-activitylog/dees-appui-activitylog.js';
@customElement('dees-appui-base')
export class DeesAppuiBase extends DeesElement {
public static demo = demoFunc;
// Properties for appbar
@property({ type: Array })
accessor appbarMenuItems: interfaces.IAppBarMenuItem[] = [];
@property({ type: String })
accessor appbarBreadcrumbs: string = '';
@property({ type: String })
accessor appbarBreadcrumbSeparator: string = ' > ';
@property({ type: Boolean })
accessor appbarShowWindowControls: boolean = true;
@property({ type: Object })
accessor appbarUser: {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
} | undefined = undefined;
@property({ type: Array })
accessor appbarProfileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
@property({ type: Boolean })
accessor appbarShowSearch: boolean = false;
// Properties for mainmenu
@property({ type: String })
accessor mainmenuLogoIcon: string = '';
@property({ type: String })
accessor mainmenuLogoText: string = '';
@property({ type: Array })
accessor mainmenuGroups: interfaces.IMenuGroup[] = [];
@property({ type: Array })
accessor mainmenuBottomTabs: interfaces.ITab[] = [];
@property({ type: Array })
accessor mainmenuTabs: interfaces.ITab[] = [];
@property({ type: Object })
accessor mainmenuSelectedTab: interfaces.ITab | undefined = undefined;
// Properties for secondarymenu
@property({ type: String })
accessor secondarymenuHeading: string = 'Menu';
@property({ type: Array })
accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = [];
@property({ type: Object })
accessor secondarymenuSelectedItem: interfaces.ISecondaryMenuItem | undefined = undefined;
/** Legacy support for flat options (backward compatibility) */
@property({ type: Array })
accessor secondarymenuOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
// Collapse states
@property({ type: Boolean })
accessor mainmenuCollapsed: boolean = false;
@property({ type: Boolean })
accessor secondarymenuCollapsed: boolean = false;
// Properties for maincontent
@property({ type: Array })
accessor maincontentTabs: interfaces.ITab[] = [];
// References to child components
@state()
accessor appbar: DeesAppuiBar | undefined = undefined;
@state()
accessor mainmenu: DeesAppuiMainmenu | undefined = undefined;
@state()
accessor secondarymenu: DeesAppuiSecondarymenu | undefined = undefined;
@state()
accessor maincontent: DeesAppuiMaincontent | undefined = undefined;
@state()
accessor activitylog: DeesAppuiActivitylog | undefined = undefined;
// NEW: Unified config property
@property({ type: Object })
accessor config: interfaces.IAppConfig | undefined = undefined;
// NEW: Current view state
@state()
accessor currentView: interfaces.IViewDefinition | undefined = undefined;
// NEW: Internal services (not reactive, managed internally)
private viewRegistry: ViewRegistry = new ViewRegistry();
private router: AppRouter | null = null;
private stateManager: StateManager | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
position: absolute;
height: 100%;
width: 100%;
background: ${cssManager.bdTheme('#f0f0f0', '#1a1a1a')};
}
.maingrid {
position: absolute;
top: 40px;
height: calc(100% - 40px);
width: 100%;
display: grid;
grid-template-columns: auto auto 1fr 240px;
grid-template-rows: 1fr;
}
/* Z-index layering for proper stacking (position: relative required for z-index to work) */
.maingrid > dees-appui-mainmenu {
position: relative;
z-index: 3;
}
.maingrid > dees-appui-secondarymenu {
position: relative;
z-index: 2;
}
.maingrid > dees-appui-maincontent {
position: relative;
z-index: 1;
}
.maingrid > dees-appui-activitylog {
position: relative;
z-index: 1;
}
/* View container for dynamically loaded views */
.view-container {
display: contents;
}
.view-container:empty {
display: none;
}
`,
];
// INSTANCE
public render(): TemplateResult {
return html`
<style></style>
<dees-appui-appbar
.menuItems=${this.appbarMenuItems}
.breadcrumbs=${this.appbarBreadcrumbs}
.breadcrumbSeparator=${this.appbarBreadcrumbSeparator}
.showWindowControls=${this.appbarShowWindowControls}
.user=${this.appbarUser}
.profileMenuItems=${this.appbarProfileMenuItems}
.showSearch=${this.appbarShowSearch}
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
@search-click=${() => this.handleAppbarSearchClick()}
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
></dees-appui-appbar>
<div class="maingrid">
<dees-appui-mainmenu
.logoIcon=${this.mainmenuLogoIcon}
.logoText=${this.mainmenuLogoText}
.menuGroups=${this.mainmenuGroups}
.bottomTabs=${this.mainmenuBottomTabs}
.tabs=${this.mainmenuTabs}
.selectedTab=${this.mainmenuSelectedTab}
.collapsed=${this.mainmenuCollapsed}
@tab-select=${(e: CustomEvent) => this.handleMainmenuTabSelect(e)}
@collapse-change=${(e: CustomEvent) => this.handleMainmenuCollapseChange(e)}
></dees-appui-mainmenu>
<dees-appui-secondarymenu
.heading=${this.secondarymenuHeading}
.groups=${this.secondarymenuGroups}
.selectionOptions=${this.secondarymenuOptions}
.selectedItem=${this.secondarymenuSelectedItem}
.collapsed=${this.secondarymenuCollapsed}
@item-select=${(e: CustomEvent) => this.handleSecondarymenuItemSelect(e)}
@collapse-change=${(e: CustomEvent) => this.handleSecondarymenuCollapseChange(e)}
></dees-appui-secondarymenu>
<dees-appui-maincontent
.tabs=${this.maincontentTabs}
>
<div class="view-container"></div>
<slot name="maincontent"></slot>
</dees-appui-maincontent>
<dees-appui-activitylog></dees-appui-activitylog>
</div>
`;
}
async firstUpdated() {
// Get references to child components
this.appbar = this.shadowRoot.querySelector('dees-appui-appbar');
this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu');
this.secondarymenu = this.shadowRoot.querySelector('dees-appui-secondarymenu');
this.maincontent = this.shadowRoot.querySelector('dees-appui-maincontent');
this.activitylog = this.shadowRoot.querySelector('dees-appui-activitylog');
// Initialize from config if provided
if (this.config) {
this.applyConfig(this.config);
// Restore state if enabled
if (this.config.statePersistence?.enabled) {
this.loadState();
}
// Initialize router after state restore
this.router?.init();
}
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.router?.destroy();
}
// Event handlers for appbar
private handleAppbarMenuSelect(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarBreadcrumbNavigate(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-breadcrumb-navigate', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleAppbarSearchClick() {
this.dispatchEvent(new CustomEvent('appbar-search-click', {
bubbles: true,
composed: true
}));
}
private handleAppbarUserMenuOpen() {
this.dispatchEvent(new CustomEvent('appbar-user-menu-open', {
bubbles: true,
composed: true
}));
}
private handleAppbarProfileMenuSelect(e: CustomEvent) {
this.dispatchEvent(new CustomEvent('appbar-profile-menu-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// Event handlers for mainmenu
private handleMainmenuTabSelect(e: CustomEvent) {
this.mainmenuSelectedTab = e.detail.tab;
// Update secondary menu heading based on main menu selection
this.secondarymenuHeading = e.detail.tab.key;
this.dispatchEvent(new CustomEvent('mainmenu-tab-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// Event handlers for secondarymenu
private handleSecondarymenuItemSelect(e: CustomEvent) {
this.secondarymenuSelectedItem = e.detail.item;
this.dispatchEvent(new CustomEvent('secondarymenu-item-select', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// Event handlers for collapse state changes
private handleMainmenuCollapseChange(e: CustomEvent) {
this.mainmenuCollapsed = e.detail.collapsed;
this.dispatchEvent(new CustomEvent('mainmenu-collapse-change', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
private handleSecondarymenuCollapseChange(e: CustomEvent) {
this.secondarymenuCollapsed = e.detail.collapsed;
this.dispatchEvent(new CustomEvent('secondarymenu-collapse-change', {
detail: e.detail,
bubbles: true,
composed: true
}));
}
// ==========================================
// NEW: Public methods for unified config API
// ==========================================
/**
* Configure the app shell with a unified config object
*/
public configure(config: interfaces.IAppConfig): void {
this.config = config;
this.applyConfig(config);
}
/**
* Navigate to a view by ID
*/
public navigateToView(viewId: string): boolean {
if (this.router) {
return this.router.navigate(viewId);
}
// Fallback for non-routed mode
const view = this.viewRegistry.get(viewId);
if (view) {
this.loadView(view);
return true;
}
return false;
}
/**
* Get the current view
*/
public getCurrentView(): interfaces.IViewDefinition | undefined {
return this.currentView;
}
/**
* Get UI state for serialization
*/
public getUIState(): interfaces.IAppUIState {
return {
currentViewId: this.currentView?.id,
mainMenuCollapsed: this.mainmenuCollapsed,
secondaryMenuCollapsed: this.secondarymenuCollapsed,
secondaryMenuSelectedKey: this.secondarymenuSelectedItem?.key,
collapsedGroups: [], // TODO: Get from secondarymenu if needed
timestamp: Date.now(),
};
}
/**
* Restore UI state from a state object
*/
public restoreUIState(state: interfaces.IAppUIState): void {
if (state.mainMenuCollapsed !== undefined) {
this.mainmenuCollapsed = state.mainMenuCollapsed;
}
if (state.secondaryMenuCollapsed !== undefined) {
this.secondarymenuCollapsed = state.secondaryMenuCollapsed;
}
if (state.currentViewId) {
this.navigateToView(state.currentViewId);
}
}
/**
* Save current UI state
*/
public saveState(): void {
this.stateManager?.save(this.getUIState());
}
/**
* Load and restore saved UI state
*/
public loadState(): boolean {
const state = this.stateManager?.load();
if (state) {
this.restoreUIState(state);
return true;
}
return false;
}
/**
* Get access to the view registry
*/
public getViewRegistry(): ViewRegistry {
return this.viewRegistry;
}
/**
* Get access to the router
*/
public getRouter(): AppRouter | null {
return this.router;
}
// ==========================================
// NEW: Private helper methods
// ==========================================
private applyConfig(config: interfaces.IAppConfig): void {
// Register views
if (config.views) {
this.viewRegistry.clear();
this.viewRegistry.registerAll(config.views);
}
// Apply branding
if (config.branding) {
this.mainmenuLogoIcon = config.branding.logoIcon || '';
this.mainmenuLogoText = config.branding.logoText || '';
}
// Apply app bar config
if (config.appBar) {
this.appbarMenuItems = config.appBar.menuItems || [];
this.appbarBreadcrumbs = config.appBar.breadcrumbs || '';
this.appbarBreadcrumbSeparator = config.appBar.breadcrumbSeparator || ' > ';
this.appbarShowWindowControls = config.appBar.showWindowControls ?? true;
this.appbarShowSearch = config.appBar.showSearch ?? false;
this.appbarUser = config.appBar.user;
this.appbarProfileMenuItems = config.appBar.profileMenuItems || [];
}
// Build main menu from view references
if (config.mainMenu) {
this.mainmenuGroups = this.buildMainMenuGroups(config);
this.mainmenuBottomTabs = this.buildBottomTabs(config);
}
// Initialize state manager
if (config.statePersistence) {
this.stateManager = new StateManager(config.statePersistence);
}
// Initialize router
if (config.routing && config.routing.mode !== 'none') {
this.router = new AppRouter(config.routing, this.viewRegistry);
this.router.onRouteChange((viewId) => {
const view = this.viewRegistry.get(viewId);
if (view) {
this.loadView(view);
}
});
}
// Bind event callbacks
if (config.onViewChange) {
this.addEventListener('view-change', ((e: CustomEvent) => {
config.onViewChange!(e.detail.viewId, e.detail.view);
}) as EventListener);
}
if (config.onSearch) {
this.addEventListener('appbar-search-click', () => {
config.onSearch!();
});
}
}
private buildMainMenuGroups(config: interfaces.IAppConfig): interfaces.IMenuGroup[] {
if (!config.mainMenu?.sections) return [];
return config.mainMenu.sections.map((section) => ({
name: section.name,
tabs: section.views
.map((viewId) => {
const view = this.viewRegistry.get(viewId);
if (!view) {
console.warn(`View "${viewId}" not found in registry`);
return null;
}
return {
key: view.name,
iconName: view.iconName,
action: () => this.navigateToView(viewId),
};
})
.filter(Boolean) as interfaces.ITab[],
}));
}
private buildBottomTabs(config: interfaces.IAppConfig): interfaces.ITab[] {
if (!config.mainMenu?.bottomItems) return [];
return config.mainMenu.bottomItems
.map((viewId) => {
const view = this.viewRegistry.get(viewId);
if (!view) {
console.warn(`View "${viewId}" not found in registry`);
return null;
}
return {
key: view.name,
iconName: view.iconName,
action: () => this.navigateToView(viewId),
};
})
.filter(Boolean) as interfaces.ITab[];
}
private loadView(view: interfaces.IViewDefinition): void {
const previousView = this.currentView;
this.currentView = view;
// Update secondary menu
if (view.secondaryMenu) {
this.secondarymenuGroups = view.secondaryMenu;
this.secondarymenuHeading = view.name;
}
// Update content tabs
if (view.contentTabs) {
this.maincontentTabs = view.contentTabs;
}
// Render view content into the view container
const viewContainer = this.maincontent?.shadowRoot?.querySelector('.view-container')
|| this.shadowRoot?.querySelector('.view-container');
if (viewContainer) {
this.viewRegistry.renderView(view.id, viewContainer as HTMLElement);
}
// Save state if configured
this.stateManager?.update({ currentViewId: view.id });
// Dispatch event
this.dispatchEvent(
new CustomEvent('view-change', {
detail: { viewId: view.id, view, previousView },
bubbles: true,
composed: true,
})
);
}
}

View File

@@ -0,0 +1,4 @@
export * from './dees-appui-base.js';
export * from './view.registry.js';
export * from './app.router.js';
export * from './state.manager.js';

View File

@@ -0,0 +1,185 @@
import type { IStatePersistenceConfig, IAppUIState } from '../../interfaces/appconfig.js';
/**
* Manager for persisting and restoring UI state
*/
export class StateManager {
private config: Required<IStatePersistenceConfig>;
private memoryStorage: Map<string, string> = new Map();
constructor(config: IStatePersistenceConfig = { enabled: false }) {
this.config = {
enabled: config.enabled,
storageKey: config.storageKey || 'dees-appui-state',
storage: config.storage || 'localStorage',
persist: {
mainMenuCollapsed: true,
secondaryMenuCollapsed: true,
selectedView: true,
secondaryMenuSelection: true,
collapsedGroups: true,
...config.persist,
},
};
}
/**
* Check if state persistence is enabled
*/
public isEnabled(): boolean {
return this.config.enabled;
}
/**
* Save current UI state
*/
public save(state: Partial<IAppUIState>): void {
if (!this.config.enabled) return;
const existingState = this.load() || {};
const newState: IAppUIState = {
...existingState,
timestamp: Date.now(),
};
// Only save what's configured
if (this.config.persist.selectedView && state.currentViewId !== undefined) {
newState.currentViewId = state.currentViewId;
}
if (this.config.persist.mainMenuCollapsed && state.mainMenuCollapsed !== undefined) {
newState.mainMenuCollapsed = state.mainMenuCollapsed;
}
if (this.config.persist.secondaryMenuCollapsed && state.secondaryMenuCollapsed !== undefined) {
newState.secondaryMenuCollapsed = state.secondaryMenuCollapsed;
}
if (this.config.persist.secondaryMenuSelection && state.secondaryMenuSelectedKey !== undefined) {
newState.secondaryMenuSelectedKey = state.secondaryMenuSelectedKey;
}
if (this.config.persist.collapsedGroups && state.collapsedGroups !== undefined) {
newState.collapsedGroups = state.collapsedGroups;
}
this.setItem(this.config.storageKey, JSON.stringify(newState));
}
/**
* Load persisted UI state
*/
public load(): IAppUIState | null {
if (!this.config.enabled) return null;
try {
const data = this.getItem(this.config.storageKey);
if (!data) return null;
return JSON.parse(data) as IAppUIState;
} catch (e) {
console.warn('Failed to load UI state:', e);
return null;
}
}
/**
* Clear persisted state
*/
public clear(): void {
this.removeItem(this.config.storageKey);
}
/**
* Check if state exists
*/
public hasState(): boolean {
return this.getItem(this.config.storageKey) !== null;
}
/**
* Get state age in milliseconds
*/
public getStateAge(): number | null {
const state = this.load();
if (!state?.timestamp) return null;
return Date.now() - state.timestamp;
}
/**
* Update specific state properties
*/
public update(updates: Partial<IAppUIState>): void {
const currentState = this.load() || {};
this.save({ ...currentState, ...updates });
}
/**
* Get the storage key being used
*/
public getStorageKey(): string {
return this.config.storageKey;
}
// Storage abstraction methods
private getItem(key: string): string | null {
switch (this.config.storage) {
case 'localStorage':
try {
return localStorage.getItem(key);
} catch {
return null;
}
case 'sessionStorage':
try {
return sessionStorage.getItem(key);
} catch {
return null;
}
case 'memory':
return this.memoryStorage.get(key) || null;
default:
return null;
}
}
private setItem(key: string, value: string): void {
switch (this.config.storage) {
case 'localStorage':
try {
localStorage.setItem(key, value);
} catch (e) {
console.warn('Failed to save to localStorage:', e);
}
break;
case 'sessionStorage':
try {
sessionStorage.setItem(key, value);
} catch (e) {
console.warn('Failed to save to sessionStorage:', e);
}
break;
case 'memory':
this.memoryStorage.set(key, value);
break;
}
}
private removeItem(key: string): void {
switch (this.config.storage) {
case 'localStorage':
try {
localStorage.removeItem(key);
} catch {
// Ignore
}
break;
case 'sessionStorage':
try {
sessionStorage.removeItem(key);
} catch {
// Ignore
}
break;
case 'memory':
this.memoryStorage.delete(key);
break;
}
}
}

View File

@@ -0,0 +1,167 @@
import { html, render, type TemplateResult } from '@design.estate/dees-element';
import type { IViewDefinition } from '../../interfaces/appconfig.js';
/**
* Registry for managing views and their lifecycle
*/
export class ViewRegistry {
private views: Map<string, IViewDefinition> = new Map();
private instances: Map<string, HTMLElement> = new Map();
private currentViewId: string | null = null;
/**
* Register a single view
*/
public register(view: IViewDefinition): void {
if (this.views.has(view.id)) {
console.warn(`View with id "${view.id}" already registered. Overwriting.`);
}
this.views.set(view.id, view);
}
/**
* Register multiple views
*/
public registerAll(views: IViewDefinition[]): void {
views.forEach((view) => this.register(view));
}
/**
* Get a view definition by ID
*/
public get(viewId: string): IViewDefinition | undefined {
return this.views.get(viewId);
}
/**
* Get all registered view IDs
*/
public getViewIds(): string[] {
return Array.from(this.views.keys());
}
/**
* Get all views
*/
public getAll(): IViewDefinition[] {
return Array.from(this.views.values());
}
/**
* Get route for a view
*/
public getRoute(viewId: string): string {
const view = this.views.get(viewId);
return view?.route || view?.id || '';
}
/**
* Find view by route
*/
public findByRoute(route: string): IViewDefinition | undefined {
for (const view of this.views.values()) {
const viewRoute = view.route || view.id;
if (viewRoute === route) {
return view;
}
}
return undefined;
}
/**
* Render a view's content into a container
*/
public renderView(viewId: string, container: HTMLElement): HTMLElement | null {
const view = this.views.get(viewId);
if (!view) {
console.error(`View "${viewId}" not found in registry`);
return null;
}
// Clear container
container.innerHTML = '';
let element: HTMLElement;
if (typeof view.content === 'string') {
// Tag name string
element = document.createElement(view.content);
} else if (typeof view.content === 'function') {
// Check if it's a class constructor or template function
if (view.content.prototype instanceof HTMLElement) {
// Element class constructor
element = new (view.content as new () => HTMLElement)();
} else {
// Template function - wrap in a container and use Lit's render
const wrapper = document.createElement('div');
wrapper.className = 'view-content-wrapper';
wrapper.style.cssText = 'display: contents;';
const template = (view.content as () => TemplateResult)();
render(template, wrapper);
element = wrapper;
}
} else {
console.error(`Invalid content type for view "${viewId}"`);
return null;
}
container.appendChild(element);
this.instances.set(viewId, element);
this.currentViewId = viewId;
return element;
}
/**
* Get currently active view ID
*/
public getCurrentViewId(): string | null {
return this.currentViewId;
}
/**
* Get cached instance of a view
*/
public getInstance(viewId: string): HTMLElement | undefined {
return this.instances.get(viewId);
}
/**
* Clear all instances
*/
public clearInstances(): void {
this.instances.clear();
this.currentViewId = null;
}
/**
* Unregister a view
*/
public unregister(viewId: string): boolean {
this.instances.delete(viewId);
return this.views.delete(viewId);
}
/**
* Clear the registry
*/
public clear(): void {
this.views.clear();
this.instances.clear();
this.currentViewId = null;
}
/**
* Check if a view is registered
*/
public has(viewId: string): boolean {
return this.views.has(viewId);
}
/**
* Get the number of registered views
*/
public get size(): number {
return this.views.size;
}
}

View File

@@ -1,4 +1,4 @@
import * as interfaces from './interfaces/index.js';
import * as interfaces from '../../interfaces/index.js';
import {
DeesElement,
@@ -11,8 +11,8 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import './dees-appui-tabs.js';
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
import '../dees-appui-tabs/dees-appui-tabs.js';
import type { DeesAppuiTabs } from '../dees-appui-tabs/dees-appui-tabs.js';
@customElement('dees-appui-maincontent')
export class DeesAppuiMaincontent extends DeesElement {
@@ -35,12 +35,12 @@ export class DeesAppuiMaincontent extends DeesElement {
@property({
type: Array,
})
public tabs: interfaces.ITab[] = [
accessor tabs: interfaces.ITab[] = [
{ key: '⚠️ Please set tabs', action: () => console.warn('No tabs configured for maincontent') },
];
@property({ type: Object })
public selectedTab: interfaces.ITab | null = null;
accessor selectedTab: interfaces.ITab | null = null;
public static styles = [
cssManager.defaultStyles,

View File

@@ -0,0 +1 @@
export * from './dees-appui-maincontent.js';

View File

@@ -0,0 +1,50 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<style>
.demo-mainmenu-container {
display: flex;
height: 100%;
background: #1a1a1a;
border-radius: 8px;
}
.demo-mainmenu-container .spacer {
flex: 1;
background: #0f0f0f;
}
</style>
<div class="demo-mainmenu-container">
<dees-appui-mainmenu
.logoIcon=${'lucide:box'}
.logoText=${'Acme App'}
.menuGroups=${[
{
tabs: [
{ key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard') },
{ key: 'Inbox', iconName: 'lucide:inbox', action: () => console.log('Inbox') },
]
},
{
name: 'Workspace',
tabs: [
{ key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects') },
{ key: 'Tasks', iconName: 'lucide:checkSquare', action: () => console.log('Tasks') },
{ key: 'Documents', iconName: 'lucide:fileText', action: () => console.log('Documents') },
]
},
{
name: 'Analytics',
tabs: [
{ key: 'Reports', iconName: 'lucide:barChart3', action: () => console.log('Reports') },
{ key: 'Insights', iconName: 'lucide:lightbulb', action: () => console.log('Insights') },
]
}
]}
.bottomTabs=${[
{ key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings') },
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help') },
]}
></dees-appui-mainmenu>
<div class="spacer"></div>
</div>
`;

View File

@@ -0,0 +1,460 @@
import * as plugins from '../../00plugins.js';
import * as interfaces from '../../interfaces/index.js';
import { zIndexLayers } from '../../00zindex.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import { demoFunc } from './dees-appui-mainmenu.demo.js';
/**
* the most left menu
* usually used as organization selector
*/
@customElement('dees-appui-mainmenu')
export class DeesAppuiMainmenu extends DeesElement {
public static demo = demoFunc;
// INSTANCE
// Logo properties
@property({ type: String })
accessor logoIcon: string = '';
@property({ type: String })
accessor logoText: string = '';
// Menu groups (new way)
@property({ type: Array })
accessor menuGroups: interfaces.IMenuGroup[] = [];
// Bottom tabs (pinned to bottom)
@property({ type: Array })
accessor bottomTabs: interfaces.ITab[] = [];
// Legacy tabs property (for backward compatibility)
@property({ type: Array })
accessor tabs: interfaces.ITab[] = [];
@property()
accessor selectedTab: interfaces.ITab;
@property({ type: Boolean, reflect: true })
accessor collapsed: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
--menu-width-expanded: 200px;
--menu-width-collapsed: 56px;
--tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')};
--tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')};
position: relative;
display: block;
height: 100%;
}
.mainContainer {
color: ${cssManager.bdTheme('#666', '#ccc')};
z-index: ${zIndexLayers.fixed.appBar};
display: flex;
flex-direction: column;
position: relative;
width: var(--menu-width-expanded);
height: 100%;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
user-select: none;
border-right: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
font-family: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
transition: width 0.25s ease;
}
:host([collapsed]) .mainContainer {
width: var(--menu-width-collapsed);
}
/* Floating collapse toggle button */
.collapse-toggle {
position: absolute;
right: -12px;
top: 24px;
transform: translateY(-50%);
width: 24px;
height: 24px;
border-radius: 50%;
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#3f3f46')};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#737373', '#a1a1aa')};
opacity: 0;
transition: opacity 0.2s ease, background 0.15s ease;
padding: 0;
}
.collapse-toggle:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
:host(:hover) .collapse-toggle {
opacity: 1;
}
.collapse-toggle dees-icon {
font-size: 14px;
}
/* Logo Section */
.logoSection {
display: flex;
align-items: center;
gap: 10px;
height: 48px;
padding: 0 14px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
flex-shrink: 0;
box-sizing: border-box;
}
.logoSection .logoIcon {
font-size: 22px;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
flex-shrink: 0;
}
.logoSection .logoText {
flex: 1;
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease, width 0.25s ease;
}
:host([collapsed]) .logoSection {
justify-content: center;
padding: 0;
gap: 0;
}
:host([collapsed]) .logoSection .logoText {
display: none;
}
/* Middle Section (scrollable) */
.menuSection {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
}
.menuSection::-webkit-scrollbar {
width: 6px;
}
.menuSection::-webkit-scrollbar-track {
background: transparent;
}
.menuSection::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.15)', 'rgba(255, 255, 255, 0.15)')};
border-radius: 3px;
}
.menuSection::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.25)', 'rgba(255, 255, 255, 0.25)')};
}
/* Menu Group */
.menuGroup {
padding: 0 8px;
margin-bottom: 8px;
}
.menuGroup:last-child {
margin-bottom: 0;
}
.groupHeader {
padding: 8px 12px 6px;
font-size: 11px;
font-weight: 600;
color: ${cssManager.bdTheme('#737373', '#737373')};
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.2s ease, max-height 0.25s ease;
max-height: 30px;
}
:host([collapsed]) .groupHeader {
opacity: 0;
max-height: 0;
padding: 0;
margin: 0;
}
.groupTabs {
display: flex;
flex-direction: column;
gap: 2px;
}
:host([collapsed]) .menuGroup {
padding: 0 4px;
}
/* Tab Item */
.tab {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('#525252', '#a3a3a3')};
}
.tab:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
color: ${cssManager.bdTheme('#262626', '#e5e5e5')};
}
.tab:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
}
.tab.selectedTab {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
.tab.selectedTab::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
border-radius: 0 2px 2px 0;
}
.tab dees-icon {
font-size: 18px;
opacity: 0.85;
flex-shrink: 0;
}
.tab.selectedTab dees-icon {
opacity: 1;
}
.tab .tabLabel {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease, width 0.25s ease;
}
/* Collapsed tab styles */
:host([collapsed]) .tab {
justify-content: center;
padding: 10px;
gap: 0;
}
:host([collapsed]) .tab .tabLabel {
opacity: 0;
width: 0;
position: absolute;
}
:host([collapsed]) .tab.selectedTab::before {
left: -4px;
}
/* Tooltip for collapsed state */
.tab-tooltip {
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 12px;
padding: 6px 12px;
background: var(--tooltip-bg);
color: var(--tooltip-fg);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.tab-tooltip::before {
content: '';
position: absolute;
left: -4px;
top: 50%;
transform: translateY(-50%);
border: 4px solid transparent;
border-right-color: var(--tooltip-bg);
}
:host([collapsed]) .tab:hover .tab-tooltip {
opacity: 1;
transition-delay: 1s;
}
/* Bottom Section */
.bottomSection {
flex-shrink: 0;
padding: 8px;
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
display: flex;
flex-direction: column;
gap: 2px;
}
:host([collapsed]) .bottomSection {
padding: 8px 4px;
}
`,
];
public render(): TemplateResult {
// Get all tabs for selection (from groups or legacy tabs)
const allTabs = this.getAllTabs();
return html`
<div class="mainContainer" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [{
name: 'app settings',
action: async () => {},
iconName: 'gear',
}])
}}>
${this.logoIcon || this.logoText ? html`
<div class="logoSection">
${this.logoIcon ? html`<dees-icon class="logoIcon" .icon="${this.logoIcon}"></dees-icon>` : ''}
${this.logoText ? html`<span class="logoText">${this.logoText}</span>` : ''}
</div>
` : ''}
<div class="menuSection">
${this.menuGroups.length > 0 ? this.renderMenuGroups() : this.renderLegacyTabs()}
</div>
${this.bottomTabs.length > 0 ? html`
<div class="bottomSection">
${this.bottomTabs.map((tabArg) => this.renderTab(tabArg))}
</div>
` : ''}
</div>
<button class="collapse-toggle" @click="${() => this.toggleCollapse()}">
<dees-icon .icon="${this.collapsed ? 'lucide:chevronRight' : 'lucide:chevronLeft'}"></dees-icon>
</button>
`;
}
private renderMenuGroups(): TemplateResult {
return html`
${this.menuGroups.map((group) => html`
<div class="menuGroup">
${group.name ? html`<div class="groupHeader">${group.name}</div>` : ''}
<div class="groupTabs">
${group.tabs.map((tabArg) => this.renderTab(tabArg))}
</div>
</div>
`)}
`;
}
private renderLegacyTabs(): TemplateResult {
return html`
<div class="menuGroup">
<div class="groupTabs">
${this.tabs.map((tabArg) => this.renderTab(tabArg))}
</div>
</div>
`;
}
private renderTab(tabArg: interfaces.ITab): TemplateResult {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
@click="${() => {
this.updateTab(tabArg);
}}"
>
<dees-icon .icon="${tabArg.iconName || ''}"></dees-icon>
<span class="tabLabel">${tabArg.key}</span>
<span class="tab-tooltip">${tabArg.key}</span>
</div>
`;
}
private getAllTabs(): interfaces.ITab[] {
if (this.menuGroups.length > 0) {
const groupTabs = this.menuGroups.flatMap(group => group.tabs);
return [...groupTabs, ...this.bottomTabs];
}
return [...this.tabs, ...this.bottomTabs];
}
updateTab(tabArg: interfaces.ITab) {
this.selectedTab = tabArg;
this.selectedTab.action();
// Emit tab-select event
this.dispatchEvent(new CustomEvent('tab-select', {
detail: { tab: tabArg },
bubbles: true,
composed: true
}));
}
firstUpdated() {
const allTabs = this.getAllTabs();
if (allTabs.length > 0) {
this.updateTab(allTabs[0]);
}
}
public toggleCollapse(): void {
this.collapsed = !this.collapsed;
this.dispatchEvent(new CustomEvent('collapse-change', {
detail: { collapsed: this.collapsed },
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1 @@
export * from './dees-appui-mainmenu.js';

View File

@@ -1,5 +1,5 @@
import * as plugins from './00plugins.js';
import { zIndexLayers } from './00zindex.js';
import * as plugins from '../../00plugins.js';
import { zIndexLayers } from '../../00zindex.js';
import {
DeesElement,
@@ -36,21 +36,21 @@ export class DeesAppuiProfileDropdown extends DeesElement {
`;
@property({ type: Object })
public user?: {
accessor user: {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
};
} | undefined = undefined;
@property({ type: Array })
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
accessor menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [];
@property({ type: Boolean, reflect: true })
public isOpen: boolean = false;
accessor isOpen: boolean = false;
@property({ type: String })
public position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'top-right';
accessor position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'top-right';
public static styles = [
cssManager.defaultStyles,

View File

@@ -0,0 +1 @@
export * from './dees-appui-profiledropdown.js';

View File

@@ -0,0 +1,52 @@
import { html } from '@design.estate/dees-element';
import type * as interfaces from '../../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-secondarymenu-container {
display: flex;
height: 100%;
background: #1a1a1a;
border-radius: 8px;
}
.demo-secondarymenu-container .spacer {
flex: 1;
background: #0f0f0f;
}
</style>
<div class="demo-secondarymenu-container">
<dees-appui-secondarymenu
.heading=${'Projects'}
.groups=${[
{
name: 'Active',
iconName: 'lucide:folder',
items: [
{ key: 'Frontend App', iconName: 'code', action: () => console.log('Frontend'), badge: 3, badgeVariant: 'warning' },
{ key: 'API Server', iconName: 'server', action: () => console.log('API'), badge: 'new', badgeVariant: 'success' },
{ key: 'Database', iconName: 'database', action: () => console.log('Database') },
]
},
{
name: 'Archived',
iconName: 'lucide:archive',
collapsed: true,
items: [
{ key: 'Legacy System', iconName: 'box', action: () => console.log('Legacy') },
{ key: 'Old API', iconName: 'server', action: () => console.log('Old API') },
]
},
{
name: 'Settings',
iconName: 'lucide:settings',
items: [
{ key: 'Configuration', iconName: 'sliders', action: () => console.log('Config') },
{ key: 'Integrations', iconName: 'plug', action: () => console.log('Integrations'), badge: 5, badgeVariant: 'error' },
]
}
] as interfaces.ISecondaryMenuGroup[]}
@item-select=${(e: CustomEvent) => console.log('Selected:', e.detail)}
></dees-appui-secondarymenu>
<div class="spacer"></div>
</div>
`;

View File

@@ -0,0 +1,601 @@
import * as plugins from '../../00plugins.js';
import * as interfaces from '../../interfaces/index.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import '../../dees-icon/dees-icon.js';
import {
DeesElement,
type TemplateResult,
property,
state,
customElement,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import { demoFunc } from './dees-appui-secondarymenu.demo.js';
/**
* Secondary navigation menu for sub-navigation within MainMenu views
* Supports collapsible groups, badges, and dynamic headings
*/
@customElement('dees-appui-secondarymenu')
export class DeesAppuiSecondarymenu extends DeesElement {
public static demo = demoFunc;
// INSTANCE
/** Dynamic heading - typically shows the selected MainMenu item */
@property({ type: String })
accessor heading: string = 'Menu';
/** Grouped items with collapse support */
@property({ type: Array })
accessor groups: interfaces.ISecondaryMenuGroup[] = [];
/** Legacy flat list support for backward compatibility */
@property({ type: Array })
accessor selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
/** Currently selected item */
@property({ type: Object })
accessor selectedItem: interfaces.ISecondaryMenuItem | null = null;
/** Internal state for collapsed groups */
@state()
accessor collapsedGroups: Set<string> = new Set();
/** Horizontal collapse state */
@property({ type: Boolean, reflect: true })
accessor collapsed: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
--sidebar-width-expanded: 240px;
--sidebar-width-collapsed: 56px;
--sidebar-bg: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
--sidebar-fg: ${cssManager.bdTheme('#525252', '#a3a3a3')};
--sidebar-fg-muted: ${cssManager.bdTheme('#737373', '#737373')};
--sidebar-fg-active: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
--sidebar-border: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
--sidebar-hover: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
--sidebar-active: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.08)')};
--sidebar-accent: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
--tooltip-bg: ${cssManager.bdTheme('#18181b', '#fafafa')};
--tooltip-fg: ${cssManager.bdTheme('#fafafa', '#18181b')};
/* Badge colors */
--badge-default-bg: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
--badge-default-fg: ${cssManager.bdTheme('#3f3f46', '#a1a1aa')};
--badge-success-bg: ${cssManager.bdTheme('#dcfce7', '#14532d')};
--badge-success-fg: ${cssManager.bdTheme('#166534', '#4ade80')};
--badge-warning-bg: ${cssManager.bdTheme('#fef3c7', '#451a03')};
--badge-warning-fg: ${cssManager.bdTheme('#92400e', '#fbbf24')};
--badge-error-bg: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
--badge-error-fg: ${cssManager.bdTheme('#991b1b', '#f87171')};
position: relative;
display: block;
height: 100%;
width: var(--sidebar-width-expanded);
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
font-family: 'Geist Sans', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
user-select: none;
transition: width 0.25s ease;
}
:host([collapsed]) {
width: var(--sidebar-width-collapsed);
}
.maincontainer {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
position: relative;
}
/* Floating collapse toggle button */
.collapse-toggle {
position: absolute;
right: -12px;
top: 24px;
transform: translateY(-50%);
width: 24px;
height: 24px;
border-radius: 50%;
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#3f3f46')};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#737373', '#a1a1aa')};
opacity: 0;
transition: opacity 0.2s ease, background 0.15s ease;
padding: 0;
}
.collapse-toggle:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#3f3f46')};
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
:host(:hover) .collapse-toggle {
opacity: 1;
}
.collapse-toggle dees-icon {
font-size: 14px;
}
/* Header Section */
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 48px;
padding: 0 16px;
border-bottom: 1px solid var(--sidebar-border);
flex-shrink: 0;
box-sizing: border-box;
}
.header .heading {
flex: 1;
font-size: 14px;
font-weight: 600;
color: var(--sidebar-fg-active);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease, width 0.25s ease;
}
:host([collapsed]) .header {
justify-content: center;
padding: 0 8px;
}
:host([collapsed]) .header .heading {
opacity: 0;
width: 0;
overflow: hidden;
}
/* Scrollable Menu Section */
.menuSection {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 8px 0;
}
.menuSection::-webkit-scrollbar {
width: 6px;
}
.menuSection::-webkit-scrollbar-track {
background: transparent;
}
.menuSection::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.15)', 'rgba(255, 255, 255, 0.15)')};
border-radius: 3px;
}
.menuSection::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.25)', 'rgba(255, 255, 255, 0.25)')};
}
/* Menu Group */
.menuGroup {
padding: 0 8px;
margin-bottom: 4px;
}
:host([collapsed]) .menuGroup {
padding: 0 4px;
}
.groupHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 8px;
cursor: pointer;
border-radius: 6px;
transition: background 0.15s ease, opacity 0.2s ease, max-height 0.25s ease;
max-height: 40px;
}
.groupHeader:hover {
background: var(--sidebar-hover);
}
.groupHeader .groupTitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 600;
color: var(--sidebar-fg-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
overflow: hidden;
}
.groupHeader .groupTitle dees-icon {
font-size: 14px;
opacity: 0.7;
}
.groupHeader .chevron {
font-size: 12px;
transition: transform 0.2s ease;
color: var(--sidebar-fg-muted);
}
.groupHeader.collapsed .chevron {
transform: rotate(-90deg);
}
/* Hide group headers when horizontally collapsed */
:host([collapsed]) .groupHeader {
opacity: 0;
max-height: 0;
padding: 0;
margin: 0;
pointer-events: none;
}
/* Group Items Container */
.groupItems {
overflow: hidden;
transition: max-height 0.25s ease, opacity 0.2s ease;
max-height: 500px;
opacity: 1;
}
.groupItems.collapsed {
max-height: 0;
opacity: 0;
}
/* Always show items when horizontally collapsed (regardless of group collapse state) */
:host([collapsed]) .groupItems {
max-height: none;
opacity: 1;
}
/* Menu Item */
.menuItem {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
margin: 2px 0;
font-size: 13px;
font-weight: 450;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
color: var(--sidebar-fg);
}
.menuItem:hover {
background: var(--sidebar-hover);
color: var(--sidebar-fg-active);
}
.menuItem:active {
background: var(--sidebar-active);
}
.menuItem.selected {
background: var(--sidebar-active);
color: var(--sidebar-fg-active);
font-weight: 500;
}
.menuItem.selected::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: var(--sidebar-accent);
border-radius: 0 2px 2px 0;
}
.menuItem dees-icon {
font-size: 16px;
opacity: 0.7;
flex-shrink: 0;
}
.menuItem.selected dees-icon {
opacity: 1;
}
.menuItem .itemLabel {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 0.2s ease, width 0.25s ease;
}
/* Collapsed menu item styles */
:host([collapsed]) .menuItem {
justify-content: center;
padding: 8px;
gap: 0;
}
:host([collapsed]) .menuItem .itemLabel {
opacity: 0;
width: 0;
position: absolute;
}
:host([collapsed]) .menuItem.selected::before {
left: -4px;
}
/* Tooltip for collapsed state */
.item-tooltip {
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 12px;
padding: 6px 12px;
background: var(--tooltip-bg);
color: var(--tooltip-fg);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.item-tooltip::before {
content: '';
position: absolute;
left: -4px;
top: 50%;
transform: translateY(-50%);
border: 4px solid transparent;
border-right-color: var(--tooltip-bg);
}
:host([collapsed]) .menuItem:hover .item-tooltip {
opacity: 1;
transition-delay: 1s;
}
/* Badge Styles */
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 6px;
font-size: 10px;
font-weight: 600;
border-radius: 9px;
flex-shrink: 0;
}
.badge.default {
background: var(--badge-default-bg);
color: var(--badge-default-fg);
}
.badge.success {
background: var(--badge-success-bg);
color: var(--badge-success-fg);
}
.badge.warning {
background: var(--badge-warning-bg);
color: var(--badge-warning-fg);
}
.badge.error {
background: var(--badge-error-bg);
color: var(--badge-error-fg);
}
:host([collapsed]) .badge {
display: none;
}
/* Divider */
.divider {
height: 1px;
background: var(--sidebar-border);
margin: 8px 12px;
}
/* Legacy options container */
.legacyOptions {
padding: 0 8px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="maincontainer">
<div class="header">
<span class="heading">${this.heading}</span>
</div>
<div class="menuSection">
${this.groups.length > 0
? this.renderGroups()
: this.renderLegacyOptions()}
</div>
</div>
<button class="collapse-toggle" @click="${() => this.toggleCollapse()}">
<dees-icon .icon="${this.collapsed ? 'lucide:chevronRight' : 'lucide:chevronLeft'}"></dees-icon>
</button>
`;
}
private renderGroups(): TemplateResult {
return html`
${this.groups.map((group) => html`
<div class="menuGroup">
<div
class="groupHeader ${this.collapsedGroups.has(group.name) ? 'collapsed' : ''}"
@click="${() => this.toggleGroup(group.name)}"
>
<span class="groupTitle">
${group.iconName ? html`<dees-icon .icon="${group.iconName.startsWith('lucide:') ? group.iconName : `lucide:${group.iconName}`}"></dees-icon>` : ''}
${group.name}
</span>
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
</div>
<div class="groupItems ${this.collapsedGroups.has(group.name) ? 'collapsed' : ''}">
${group.items.map((item) => this.renderMenuItem(item, group))}
</div>
</div>
`)}
`;
}
private renderMenuItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): TemplateResult {
const isSelected = this.selectedItem?.key === item.key;
return html`
<div
class="menuItem ${isSelected ? 'selected' : ''}"
@click="${() => this.selectItem(item, group)}"
@contextmenu="${(e: MouseEvent) => this.handleContextMenu(e, item)}"
>
${item.iconName ? html`<dees-icon .icon="${item.iconName.startsWith('lucide:') ? item.iconName : `lucide:${item.iconName}`}"></dees-icon>` : ''}
<span class="itemLabel">${item.key}</span>
${item.badge !== undefined ? html`
<span class="badge ${item.badgeVariant || 'default'}">${item.badge}</span>
` : ''}
<span class="item-tooltip">${item.key}</span>
</div>
`;
}
private renderLegacyOptions(): TemplateResult {
return html`
<div class="legacyOptions">
${this.selectionOptions.map((option) => {
if ('divider' in option && option.divider) {
return html`<div class="divider"></div>`;
}
const item = option as interfaces.ISelectionOption;
return this.renderMenuItem({
key: item.key,
iconName: item.iconName,
action: item.action,
});
})}
</div>
`;
}
private toggleGroup(groupName: string): void {
const newCollapsed = new Set(this.collapsedGroups);
if (newCollapsed.has(groupName)) {
newCollapsed.delete(groupName);
} else {
newCollapsed.add(groupName);
}
this.collapsedGroups = newCollapsed;
}
public toggleCollapse(): void {
this.collapsed = !this.collapsed;
this.dispatchEvent(new CustomEvent('collapse-change', {
detail: { collapsed: this.collapsed },
bubbles: true,
composed: true
}));
}
private selectItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): void {
this.selectedItem = item;
item.action();
this.dispatchEvent(new CustomEvent('item-select', {
detail: { item, group },
bubbles: true,
composed: true
}));
}
private handleContextMenu(event: MouseEvent, item: interfaces.ISecondaryMenuItem): void {
DeesContextmenu.openContextMenuWithOptions(event, [
{
name: 'View details',
action: async () => {},
iconName: 'lucide:eye',
},
{
name: 'Edit',
action: async () => {},
iconName: 'lucide:pencil',
},
]);
}
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
// Initialize collapsed state from group defaults
if (this.groups.length > 0) {
const initialCollapsed = new Set<string>();
this.groups.forEach(group => {
if (group.collapsed) {
initialCollapsed.add(group.name);
}
});
this.collapsedGroups = initialCollapsed;
// Auto-select first item if none selected
if (!this.selectedItem && this.groups[0]?.items.length > 0) {
this.selectItem(this.groups[0].items[0], this.groups[0]);
}
} else if (this.selectionOptions.length > 0) {
// Legacy mode: select first non-divider option
const firstOption = this.selectionOptions.find(opt => !('divider' in opt)) as interfaces.ISelectionOption;
if (firstOption && !this.selectedItem) {
this.selectItem({
key: firstOption.key,
iconName: firstOption.iconName,
action: firstOption.action,
});
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
'dees-appui-secondarymenu': DeesAppuiSecondarymenu;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-appui-secondarymenu.js';

View File

@@ -1,4 +1,4 @@
import * as interfaces from './interfaces/index.js';
import * as interfaces from '../../interfaces/index.js';
import {
DeesElement,
@@ -107,16 +107,16 @@ export class DeesAppuiTabs extends DeesElement {
@property({
type: Array,
})
public tabs: interfaces.ITab[] = [];
accessor tabs: interfaces.ITab[] = [];
@property({ type: Object })
public selectedTab: interfaces.ITab | null = null;
accessor selectedTab: interfaces.ITab | null = null;
@property({ type: Boolean })
public showTabIndicator: boolean = true;
accessor showTabIndicator: boolean = true;
@property({ type: String })
public tabStyle: 'horizontal' | 'vertical' = 'horizontal';
accessor tabStyle: 'horizontal' | 'vertical' = 'horizontal';
public static styles = [
cssManager.defaultStyles,
@@ -132,7 +132,9 @@ export class DeesAppuiTabs extends DeesElement {
}
.tabs-wrapper.horizontal-wrapper {
height: 48px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
box-sizing: border-box;
}
.tabsContainer {
@@ -146,7 +148,7 @@ export class DeesAppuiTabs extends DeesElement {
font-size: 14px;
overflow-x: auto;
scrollbar-width: none;
height: 48px;
height: 100%;
padding: 0 16px;
gap: 4px;
}

View File

@@ -0,0 +1 @@
export * from './dees-appui-tabs.js';

View File

@@ -1,4 +1,4 @@
import * as interfaces from './interfaces/index.js';
import * as interfaces from '../../interfaces/index.js';
import {
DeesElement,
@@ -11,8 +11,8 @@ import {
state,
} from '@design.estate/dees-element';
import './dees-appui-tabs.js';
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
import '../dees-appui-tabs/dees-appui-tabs.js';
import type { DeesAppuiTabs } from '../dees-appui-tabs/dees-appui-tabs.js';
export interface IAppViewTab extends interfaces.ITab {
content?: TemplateResult | (() => TemplateResult);
@@ -60,13 +60,13 @@ export class DeesAppuiView extends DeesElement {
// INSTANCE
@property({ type: Object })
public viewConfig: IAppView;
accessor viewConfig: IAppView;
@state()
private selectedTab: IAppViewTab | null = null;
accessor selectedTab: IAppViewTab | null = null;
@state()
private tabs: DeesAppuiTabs;
accessor tabs: DeesAppuiTabs;
public static styles = [
cssManager.defaultStyles,

View File

@@ -0,0 +1 @@
export * from './dees-appui-view.js';

View File

@@ -0,0 +1,10 @@
// App UI Components
export * from './dees-appui-activitylog/index.js';
export * from './dees-appui-appbar/index.js';
export * from './dees-appui-base/index.js';
export * from './dees-appui-maincontent/index.js';
export * from './dees-appui-mainmenu/index.js';
export * from './dees-appui-secondarymenu/index.js';
export * from './dees-appui-profiledropdown/index.js';
export * from './dees-appui-tabs/index.js';
export * from './dees-appui-view/index.js';

View File

@@ -21,7 +21,7 @@ export class DeesButtonExit extends DeesElement {
@property({
type: Number
})
public size: number = 24;
accessor size: number = 24;
public styles = [
cssManager.defaultStyles,

View File

@@ -0,0 +1 @@
export * from './dees-button-exit.js';

View File

@@ -22,10 +22,10 @@ export class DeesButtonGroup extends DeesElement {
public static demo = demoFunc;
@property()
public label: string = '';
accessor label: string = '';
@property()
public direction: 'horizontal' | 'vertical' = 'horizontal';
accessor direction: 'horizontal' | 'vertical' = 'horizontal';
constructor() {
super();

View File

@@ -0,0 +1 @@
export * from './dees-button-group.js';

View File

@@ -1,11 +1,11 @@
import { html, css, cssManager, domtools } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import './dees-form.js';
import './dees-form-submit.js';
import './dees-input-text.js';
import './dees-icon.js';
import type { DeesButton } from './dees-button.js';
import '../../dees-panel/dees-panel.js';
import '../../00group-form/dees-form/dees-form.js';
import '../../00group-form/dees-form-submit/dees-form-submit.js';
import '../../00group-input/dees-input-text/dees-input-text.js';
import '../../dees-icon/dees-icon.js';
import type { DeesButton } from '../dees-button/dees-button.js';
export const demoFunc = () => html`
<style>

View File

@@ -29,42 +29,42 @@ export class DeesButton extends DeesElement {
return true;
}
})
public text: string;
accessor text: string;
@property()
public eventDetailData: string;
accessor eventDetailData: string;
@property({
type: Boolean,
reflect: true,
})
public disabled = false;
accessor disabled = false;
@property({
type: Boolean
})
public isHidden = false;
accessor isHidden = false;
@property({
type: String
})
public type: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default';
@property({
type: String
})
public size: 'default' | 'sm' | 'lg' | 'icon' = 'default';
accessor type: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default';
@property({
type: String
})
public status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
accessor size: 'default' | 'sm' | 'lg' | 'icon' = 'default';
@property({
type: String
})
accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
@property({
type: Boolean,
reflect: true
})
public insideForm: boolean = false;
accessor insideForm: boolean = false;
constructor() {
super();

View File

@@ -0,0 +1 @@
export * from './dees-button.js';

View File

@@ -0,0 +1,4 @@
// Button Components
export * from './dees-button/index.js';
export * from './dees-button-exit/index.js';
export * from './dees-button-group/index.js';

View File

@@ -1,16 +1,15 @@
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-chart-area.demo.js';
import { demoFunc } from './demo.js';
import { chartAreaStyles } from './styles.js';
import { renderChartArea } from './template.js';
import ApexCharts from 'apexcharts';
@@ -26,36 +25,36 @@ export class DeesChartArea extends DeesElement {
// instance
@state()
public chart: ApexCharts;
accessor chart: ApexCharts;
@property()
public label: string = 'Untitled Chart';
accessor label: string = 'Untitled Chart';
@property({ type: Array })
public series: ApexAxisChartSeries = [];
accessor series: ApexAxisChartSeries = [];
// Override getter to return internal chart data
get chartSeries(): ApexAxisChartSeries {
return this.internalChartData.length > 0 ? this.internalChartData : this.series;
}
@property({ attribute: false })
public yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
accessor yAxisFormatter: (value: number) => string = (val) => `${val} Mbps`;
@property({ type: Number })
public rollingWindow: number = 0; // 0 means no rolling window
accessor rollingWindow: number = 0; // 0 means no rolling window
@property({ type: Boolean })
public realtimeMode: boolean = false;
accessor realtimeMode: boolean = false;
@property({ type: String })
public yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic';
accessor yAxisScaling: 'fixed' | 'dynamic' | 'percentage' = 'dynamic';
@property({ type: Number })
public yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage'
accessor yAxisMax: number = 100; // Used when yAxisScaling is 'fixed' or 'percentage'
@property({ type: Number })
public autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable)
accessor autoScrollInterval: number = 1000; // Auto-scroll interval in milliseconds (0 to disable)
private resizeObserver: ResizeObserver;
private resizeTimeout: number;
@@ -141,73 +140,14 @@ export class DeesChartArea extends DeesElement {
}
}
public static styles = [
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 static styles = chartAreaStyles;
public render(): TemplateResult {
return html`
<div class="mainbox">
<div class="chartTitle">${this.label}</div>
<div class="chartContainer"></div>
</div>
`;
return renderChartArea(this);
}
public async firstUpdated() {
await this.domtoolsPromise;

View File

@@ -1,6 +1,7 @@
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 './component.js';
export const demoFunc = () => {
// Initial dataset values

View File

@@ -0,0 +1 @@
export * from './component.js';

View 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;
}
`,
];

View 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>
`;
};

View File

@@ -1,5 +1,5 @@
import { html } from '@design.estate/dees-element';
import type { DeesChartLog } from './dees-chart-log.js';
import type { DeesChartLog } from '../dees-chart-log/dees-chart-log.js';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => {

View File

@@ -30,16 +30,16 @@ export class DeesChartLog extends DeesElement {
public static demo = demoFunc;
@property()
public label: string = 'Server Logs';
accessor label: string = 'Server Logs';
@property({ type: Array })
public logEntries: ILogEntry[] = [];
accessor logEntries: ILogEntry[] = [];
@property({ type: Boolean })
public autoScroll: boolean = true;
accessor autoScroll: boolean = true;
@property({ type: Number })
public maxEntries: number = 1000;
accessor maxEntries: number = 1000;
private logContainer: HTMLDivElement;

View File

@@ -0,0 +1 @@
export * from './dees-chart-log.js';

View File

@@ -0,0 +1,3 @@
// Chart Components
export * from './dees-chart-area/index.js';
export * from './dees-chart-log/index.js';

View File

@@ -8,14 +8,14 @@ import {
state,
cssManager,
} from '@design.estate/dees-element';
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
import { cssGeistFontFamily, cssMonoFontFamily } from '../../00fonts.js';
import hlight from 'highlight.js';
import * as smartstring from '@push.rocks/smartstring';
import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from './dees-contextmenu.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
declare global {
interface HTMLElementTagNameMap {
@@ -28,13 +28,13 @@ export class DeesDataviewCodebox extends DeesElement {
public static demo = demoFunc;
@property()
public progLang: string = 'typescript';
accessor progLang: string = 'typescript';
@property({
type: String,
reflect: true,
})
public codeToDisplay: string = '';
accessor codeToDisplay: string = '';
constructor() {
super();
@@ -228,7 +228,6 @@ export class DeesDataviewCodebox extends DeesElement {
`;
}
@state()
private codeToDisplayStore = '';
public async updated(_changedProperties) {

View File

@@ -0,0 +1 @@
export * from './dees-dataview-codebox.js';

View File

@@ -1,5 +1,5 @@
import * as colors from './00colors.js';
import * as plugins from './00plugins.js';
import * as colors from '../../00colors.js';
import * as plugins from '../../00plugins.js';
import { demoFunc } from './dees-dataview-statusobject.demo.js';
import {
@@ -15,7 +15,7 @@ import {
} from '@design.estate/dees-element';
import * as tsclass from '@tsclass/tsclass';
import { DeesContextmenu } from './dees-contextmenu.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
declare global {
interface HTMLElementTagNameMap {
@@ -27,7 +27,7 @@ declare global {
export class DeesDataviewStatusobject extends DeesElement {
public static demo = demoFunc;
@property({ type: Object }) statusObject: tsclass.code.IStatusObject;
@property({ type: Object }) accessor statusObject: tsclass.code.IStatusObject;
public static styles = [
cssManager.defaultStyles,

View File

@@ -0,0 +1 @@
export * from './dees-dataview-statusobject.js';

View File

@@ -0,0 +1,3 @@
// Data View Components
export * from './dees-dataview-codebox/index.js';
export * from './dees-dataview-statusobject/index.js';

View File

@@ -0,0 +1 @@
export * from './dees-editor-markdown.js';

View File

@@ -0,0 +1 @@
export * from './dees-editor-markdownoutlet.js';

View File

@@ -8,6 +8,7 @@ import {
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { MONACO_VERSION } from './version.js';
import type * as monaco from 'monaco-editor';
@@ -32,17 +33,17 @@ export class DeesEditor extends DeesElement {
@property({
type: String
})
public content = "function hello() {\n\talert('Hello world!');\n}";
accessor content = "function hello() {\n\talert('Hello world!');\n}";
@property({
type: Object
})
public contentSubject = new domtools.plugins.smartrx.rxjs.Subject<string>();
accessor contentSubject = new domtools.plugins.smartrx.rxjs.Subject<string>();
@property({
type: Boolean
})
public wordWrap: monaco.editor.IStandaloneEditorConstructionOptions['wordWrap'] = 'off';
accessor wordWrap: monaco.editor.IStandaloneEditorConstructionOptions['wordWrap'] = 'off';
constructor() {
super();
@@ -80,10 +81,11 @@ export class DeesEditor extends DeesElement {
): Promise<void> {
super.firstUpdated(_changedProperties);
const container = this.shadowRoot.getElementById('container');
const monacoCdnBase = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}`;
if (!DeesEditor.monacoDeferred) {
DeesEditor.monacoDeferred = domtools.plugins.smartpromise.defer();
const scriptUrl = `https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/loader.js`;
const scriptUrl = `${monacoCdnBase}/min/vs/loader.js`;
const script = document.createElement('script');
script.src = scriptUrl;
script.onload = () => {
@@ -94,7 +96,7 @@ export class DeesEditor extends DeesElement {
await DeesEditor.monacoDeferred.promise;
(window as any).require.config({
paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor/min/vs' },
paths: { vs: `${monacoCdnBase}/min/vs` },
});
(window as any).require(['vs/editor/editor.main'], async () => {
const editor = ((window as any).monaco.editor as typeof monaco.editor).create(container, {
@@ -109,7 +111,7 @@ export class DeesEditor extends DeesElement {
this.editorDeferred.resolve(editor);
});
const css = await (
await fetch('https://cdn.jsdelivr.net/npm/monaco-editor/min/vs/editor/editor.main.css')
await fetch(`${monacoCdnBase}/min/vs/editor/editor.main.css`)
).text();
const styleElement = document.createElement('style');
styleElement.textContent = css;

Some files were not shown because too many files have changed in this diff Show More