Compare commits

..

20 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
54 changed files with 2975 additions and 568 deletions

View File

@@ -1,5 +1,93 @@
# 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

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": "2.0.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",
@@ -18,7 +18,7 @@
"dependencies": {
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^1.2.1",
"@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",
@@ -38,20 +38,20 @@
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",
"lit": "^3.3.1",
"lucide": "^0.555.0",
"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": "^3.1.2",
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.2.3",
"@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^24.10.1"
"@types/node": "^25.0.3"
},
"files": [
"ts/**/*",

361
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ importers:
specifier: ^2.1.3
version: 2.1.3
'@design.estate/dees-wcctools':
specifier: ^1.2.1
version: 1.2.1
specifier: ^3.1.0
version: 3.1.0
'@fortawesome/fontawesome-svg-core':
specifier: ^7.1.0
version: 7.1.0
@@ -75,8 +75,8 @@ importers:
specifier: ^3.3.1
version: 3.3.1
lucide:
specifier: ^0.555.0
version: 0.555.0
specifier: ^0.562.0
version: 0.562.0
monaco-editor:
specifier: 0.52.2
version: 0.52.2
@@ -91,17 +91,17 @@ importers:
version: 0.8.0(xterm@5.3.0)
devDependencies:
'@git.zone/tsbuild':
specifier: ^3.1.2
version: 3.1.2
specifier: ^4.0.2
version: 4.0.2
'@git.zone/tsbundle':
specifier: ^2.6.3
version: 2.6.3
'@git.zone/tstest':
specifier: ^3.1.3
version: 3.1.3(socks@2.8.7)(typescript@5.9.3)
version: 3.1.3(@push.rocks/smartserve@1.4.0)(socks@2.8.7)(typescript@5.9.3)
'@git.zone/tswatch':
specifier: ^2.2.3
version: 2.2.3
specifier: ^2.3.13
version: 2.3.13(@tiptap/pm@2.27.1)
'@push.rocks/projectinfo':
specifier: ^5.0.2
version: 5.0.2
@@ -109,8 +109,8 @@ importers:
specifier: ^6.0.3
version: 6.0.3(socks@2.8.7)
'@types/node':
specifier: ^24.10.1
version: 24.10.1
specifier: ^25.0.3
version: 25.0.3
packages:
@@ -126,6 +126,9 @@ packages:
'@api.global/typedserver@3.0.80':
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
'@api.global/typedserver@7.11.1':
resolution: {integrity: sha512-1vQUJ2/DszDFHVkCmqqBy/qNiIP/jltFN3KxHtoNLxjbdBZYfw1Zd1Odjs6YSPalAD0p8wQ/alJblJEAewNQVg==}
'@api.global/typedsocket@3.1.1':
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
peerDependencies:
@@ -134,6 +137,11 @@ packages:
'@push.rocks/smartserve':
optional: true
'@api.global/typedsocket@4.1.0':
resolution: {integrity: sha512-ttmoU5BNHmLAkAF/o+Ta8F5O4F7CUmkFo6LK7NKHQvuYJvodPMYWdhJ6yCINTF4pfCgljkMDUqoVKobm6ea4mQ==}
peerDependencies:
'@push.rocks/smartserve': '>=1.1.0'
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
engines: {node: '>=16.0.0'}
@@ -308,9 +316,15 @@ packages:
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@cloudflare/workers-types@4.20251205.0':
resolution: {integrity: sha512-7pup7fYkuQW5XD8RUS/vkxF9SXlrGyCXuZ4ro3uVQvca/GTeSa+8bZ8T4wbq1Aea5lmLIGSlKbhl2msME7bRBA==}
'@cloudflare/workers-types@4.20251211.0':
resolution: {integrity: sha512-e87o6KbelCz7dnI5ngrGT2ca15vJZ+COb2eqJ52iDHmOaujyC6aYZ71e2vor8X6V9T6tcDElC5sAqPR93j09EA==}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
@@ -321,6 +335,9 @@ packages:
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
'@design.estate/dees-catalog@3.3.0':
resolution: {integrity: sha512-iPT8QtMrheH8ZO6xmyZfAC+51smTsA9Og63BC2mKFixoGiAG7UScN1ywM/uHEmnKCd+KBY+dn4ZaHyR4UrrX2A==}
'@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -330,8 +347,11 @@ packages:
'@design.estate/dees-element@2.1.3':
resolution: {integrity: sha512-TjXWxVcdSPaT1IOk31ckfxvAZnJLuTxhFGsNCKoh63/UE2FVf6slp8//UFvN+ADigiA9ZsY0azkY99XbJCwDDA==}
'@design.estate/dees-wcctools@1.2.1':
resolution: {integrity: sha512-ESFas1MPPwDUcXRssyHRsc63XPTBJSTBA+5RhYXDZx8mbV6HxEKiJR8Oz1Mv7DBdW+ZSuUTD/fA6Aa/fCxGYTQ==}
'@design.estate/dees-wcctools@1.3.0':
resolution: {integrity: sha512-+yd8c1gTIKNRQYCvG0xu6Am8dHsRm7ymluX2gnoBQN4aFOpZgIBi/v9CvGyPhTD1p/VRouIBz1wsUCejnwrFCA==}
'@design.estate/dees-wcctools@3.1.0':
resolution: {integrity: sha512-EZsgvZZ736PORt8FkHvzKzsmsHDkYOngcWfL1eBEutT3HBQq4PW5X6PVWSHXFzbhI6rt3O07GJHEIa+xU2mSkQ==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@@ -521,16 +541,16 @@ packages:
resolution: {integrity: sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==}
engines: {node: '>=6'}
'@git.zone/tsbuild@3.1.2':
resolution: {integrity: sha512-K0u840Qo0WEhvcpAtktvdBX6KEXjelU32o820WzcK7dMA7dd2YV+mPOEYfbmWLcdtFJkrjkigQq5fpLhTN4oKQ==}
'@git.zone/tsbuild@4.0.2':
resolution: {integrity: sha512-LcRlFnDbcUe53Pdoob585iXq9TAT90TyEaYl/wml/etFoPeBX+oQLm6GryejUPXrUP7i1opyTonadkQN1OyXOA==}
hasBin: true
'@git.zone/tsbundle@2.6.3':
resolution: {integrity: sha512-YD1qMYA/4eOuF57V0ccR+xo6ww1+QOYFA2K5gBPFBDNh9VdfvWxxDhOUybja8lT9PVMoli8PHG5WA5tKJkdXIQ==}
hasBin: true
'@git.zone/tspublish@1.10.3':
resolution: {integrity: sha512-o2/jvNsdLC8SRdH1kQ7JjNOQNu9el0FpJ/QOW3mgiC5C9reuTp18iU4kijsVVLgvw4KZv6Z289SoKPh3HPsS0g==}
'@git.zone/tspublish@1.11.0':
resolution: {integrity: sha512-dkgaDBTzZJ53lAV72r7OW/W7l/KqpkncFuPojr11JO35OKAbjjDhZbAwPv4oGX9NplyXrhC5VJRPNX/orqNTHA==}
hasBin: true
'@git.zone/tsrun@2.0.0':
@@ -541,8 +561,8 @@ packages:
resolution: {integrity: sha512-t+/cKV21JHK8X7NGAmihs5M/eMm+V+jn4R5rzfwGG97WJFAcP5qE1Os9VYtyZw3tx/NZXA2yA4abo/ELluTuRA==}
hasBin: true
'@git.zone/tswatch@2.2.3':
resolution: {integrity: sha512-BKyVYXWOjVL+eUcnwjfTKFhUsEBHpgpSv5rbuZYOhmVp9BcglWaEswuAstGEISkCioizUcHiZkpJVPXO99sLHQ==}
'@git.zone/tswatch@2.3.13':
resolution: {integrity: sha512-43995OlWl8UzCA+cX3ehYba/ksm6CqHbMljHKjosrDRpx8EU+LY4bWTc8JT/Ldgwsw3iW9vur2bBqpgMmdeJJw==}
hasBin: true
'@hapi/bourne@3.0.0':
@@ -795,6 +815,9 @@ packages:
'@push.rocks/mongodump@1.1.0':
resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==}
'@push.rocks/npmextra@5.3.3':
resolution: {integrity: sha512-snLpSHwaQ5OXlZzF1KX/FY71W5LwajjBzor82Vue0smjEPnSeUPY5/JcVdMwtdprdJe13pc/EQQuIiL/zw4/yg==}
'@push.rocks/projectinfo@5.0.2':
resolution: {integrity: sha512-zzieCal6jwR++o+fDl8gMpWkNV2cGEsbT96vCNZu/H9kr0iqRmapOiA4DFadkhOnhlDqvRr6TPaXESu2YUbI8Q==}
@@ -853,6 +876,9 @@ packages:
'@push.rocks/smartexit@1.0.23':
resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==}
'@push.rocks/smartexit@1.1.0':
resolution: {integrity: sha512-GD8VLIbxQuwvhPXwK4eH162XAYSj+M3wGKWGNO3i1iY4bj8P3BARcgsWx6/ntN3aCo5ygWtrevrfD5iecYY2Ng==}
'@push.rocks/smartexpect@2.5.0':
resolution: {integrity: sha512-yoyuCoQ3tTiAriuvF+/09fNbVfFnacudL2SwHSzPhX/ugaE7VTSWXQ9A34eKOWvil0MPyDcOY36fVZDxvrPd8A==}
@@ -871,9 +897,15 @@ packages:
'@push.rocks/smartfile@13.1.0':
resolution: {integrity: sha512-bSjH9vHl6l1nbe/gcSi4PcutFcTHUCVkMuQGGTVtn1cOgCuOXIHV04uhOXrZoKvlcSxxoiq8THolFt65lqn7cg==}
'@push.rocks/smartfile@13.1.2':
resolution: {integrity: sha512-DaEhwmnGEpX4coeeToaw4cZe3pNBhH7CY1iGr+d3pIXihozREvzzAR9/0i2r7bUXXL5+Lgy8YYIk5ZS+fwxMKA==}
'@push.rocks/smartfs@1.2.0':
resolution: {integrity: sha512-1R47jJZwX869z7DYgKeAZKTU1SbGnM7W/ZmgsI7AkQQhiascNqY3/gF4V5kIprmuf1WhpRbCbZyum8s7J1LDdg==}
'@push.rocks/smartfs@1.3.1':
resolution: {integrity: sha512-ZSduVS8tM+/erbyCTvRRvc9gLWwbpqN5xdIIkMr+gub7fowSeJb7tR2rnGwySa63DyimU0q2KTp79VV9YqGLeg==}
'@push.rocks/smartguard@3.1.0':
resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==}
@@ -970,6 +1002,9 @@ packages:
'@push.rocks/smarts3@3.0.3':
resolution: {integrity: sha512-Y9nXMwurthJ9Z7yi0RwjhPFUC58aY8Mhia8kFo6Xj1tBM4LE8Oxg/ydejF7otHqQGr3QyqV5C4YrDEG17rUuzg==}
'@push.rocks/smartserve@1.4.0':
resolution: {integrity: sha512-cEoXZQSBX3pOv9AyhxRPkrMAWzs2XQhTBmW95BFtTSNzZdji0XgqUu92p7iuF+NVuTFX1QZ8+dbCClLCoRRW7g==}
'@push.rocks/smartshell@3.3.0':
resolution: {integrity: sha512-m0w618H6YBs+vXGz1CgS4nPi5CUAnqRtckcS9/koGwfcIx1IpjqmiP47BoCTbdgcv0IPUxQVBG1IXTHPuZ8Z5g==}
@@ -1006,8 +1041,8 @@ packages:
'@push.rocks/smartversion@3.0.5':
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
'@push.rocks/smartwatch@5.0.0':
resolution: {integrity: sha512-uuWUlTo0l5LWOWoOuTMG7zzxpUNKBcyqoB+zyQ24NHTtSYNcaUJtaQzTO2gxMXr5sqiZDkohlThS0KvsBc3g7w==}
'@push.rocks/smartwatch@6.3.0':
resolution: {integrity: sha512-TeZ1PGBoBMpC4/CK8StIj5InEiFfKp7xWJSm3aYMjB/uaoeRP0vXqv1ORIC/TKYGJuEDuAXUsit8tZVjn0qT1Q==}
engines: {node: '>=20.0.0'}
'@push.rocks/smartxml@2.0.0':
@@ -1781,8 +1816,8 @@ packages:
'@types/node-forge@1.3.14':
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
'@types/node@25.0.3':
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
'@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
@@ -3292,6 +3327,9 @@ packages:
lucide@0.555.0:
resolution: {integrity: sha512-R7BkO2/XRpMADcMIRn1UOZOvirxr2Z6s/R82k0EUK71ZXXrlRbvkVwTAIf+9DRApeyH+zNMIGfiUdmrOhoAygQ==}
lucide@0.562.0:
resolution: {integrity: sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==}
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
@@ -4642,11 +4680,11 @@ snapshots:
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstream': 1.0.10
'@api.global/typedserver@3.0.80':
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@1.4.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.1.1
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@1.4.0)
'@cloudflare/workers-types': 4.20251205.0
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2
@@ -4690,7 +4728,53 @@ snapshots:
- utf-8-validate
- vue
'@api.global/typedsocket@3.1.1':
'@api.global/typedserver@7.11.1(@tiptap/pm@2.27.1)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.4.0)
'@cloudflare/workers-types': 4.20251211.0
'@design.estate/dees-catalog': 3.3.0(@tiptap/pm@2.27.1)
'@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 6.0.0
'@push.rocks/smartfeed': 1.4.0
'@push.rocks/smartfile': 13.1.0
'@push.rocks/smartfs': 1.2.0
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2
'@push.rocks/smartmanifest': 2.0.2
'@push.rocks/smartmatch': 2.0.0
'@push.rocks/smartmime': 2.0.4
'@push.rocks/smartntml': 2.0.8
'@push.rocks/smartopen': 2.0.0
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartserve': 1.4.0
'@push.rocks/smartsitemap': 2.0.4
'@push.rocks/smartstream': 3.2.5
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smartwatch': 6.3.0
'@push.rocks/taskbuffer': 3.5.0
'@push.rocks/webrequest': 4.0.1
'@push.rocks/webstore': 2.0.20
'@tsclass/tsclass': 9.3.0
lit: 3.3.1
transitivePeerDependencies:
- '@nuxt/kit'
- '@tiptap/pm'
- bufferutil
- react
- supports-color
- utf-8-validate
- vue
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@1.4.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
@@ -4700,6 +4784,8 @@ snapshots:
'@push.rocks/smartsocket': 2.1.0
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
optionalDependencies:
'@push.rocks/smartserve': 1.4.0
transitivePeerDependencies:
- '@nuxt/kit'
- bufferutil
@@ -4708,6 +4794,19 @@ snapshots:
- utf-8-validate
- vue
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@1.4.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartserve': 1.4.0
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
'@aws-crypto/crc32@5.2.0':
dependencies:
'@aws-crypto/util': 5.2.0
@@ -5199,8 +5298,12 @@ snapshots:
'@borewit/text-codec@0.1.1': {}
'@cfworker/json-schema@4.1.1': {}
'@cloudflare/workers-types@4.20251205.0': {}
'@cloudflare/workers-types@4.20251211.0': {}
'@colors/colors@1.6.0': {}
'@configvault.io/interfaces@1.0.17':
@@ -5213,6 +5316,42 @@ snapshots:
enabled: 2.0.0
kuler: 2.0.0
'@design.estate/dees-catalog@3.3.0(@tiptap/pm@2.27.1)':
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
'@design.estate/dees-wcctools': 1.3.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.3
'@push.rocks/smartstring': 4.1.0
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-link': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-text-align': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-typography': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-underline': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/starter-kit': 2.27.1
'@tsclass/tsclass': 9.3.0
'@webcontainer/api': 1.2.0
apexcharts: 5.3.6
highlight.js: 11.11.1
ibantools: 4.5.1
lit: 3.3.1
lucide: 0.555.0
monaco-editor: 0.52.2
pdfjs-dist: 4.10.38
xterm: 5.3.0
xterm-addon-fit: 0.8.0(xterm@5.3.0)
transitivePeerDependencies:
- '@nuxt/kit'
- '@tiptap/pm'
- react
- supports-color
- vue
'@design.estate/dees-comms@1.0.30':
dependencies:
'@api.global/typedrequest': 3.2.5
@@ -5258,7 +5397,19 @@ snapshots:
- supports-color
- vue
'@design.estate/dees-wcctools@1.2.1':
'@design.estate/dees-wcctools@1.3.0':
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
'@push.rocks/smartdelay': 3.0.5
lit: 3.3.1
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@design.estate/dees-wcctools@3.1.0':
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
@@ -5386,13 +5537,14 @@ snapshots:
dependencies:
'@fortawesome/fontawesome-common-types': 7.1.0
'@git.zone/tsbuild@3.1.2':
'@git.zone/tsbuild@4.0.2':
dependencies:
'@git.zone/tspublish': 1.10.3
'@git.zone/tspublish': 1.11.0
'@push.rocks/early': 4.0.4
'@push.rocks/smartcli': 4.0.19
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3
@@ -5427,16 +5579,18 @@ snapshots:
- '@swc/helpers'
- supports-color
'@git.zone/tspublish@1.10.3':
'@git.zone/tspublish@1.11.0':
dependencies:
'@push.rocks/consolecolor': 2.0.3
'@push.rocks/npmextra': 5.3.3
'@push.rocks/smartcli': 4.0.19
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartfile': 13.1.2
'@push.rocks/smartfs': 1.3.1
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartnpm': 2.0.6
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrequest': 4.4.2
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartshell': 3.3.0
transitivePeerDependencies:
- '@nuxt/kit'
@@ -5453,9 +5607,9 @@ snapshots:
'@push.rocks/smartshell': 3.3.0
tsx: 4.21.0
'@git.zone/tstest@3.1.3(socks@2.8.7)(typescript@5.9.3)':
'@git.zone/tstest@3.1.3(@push.rocks/smartserve@1.4.0)(socks@2.8.7)(typescript@5.9.3)':
dependencies:
'@api.global/typedserver': 3.0.80
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.4.0)
'@git.zone/tsbundle': 2.6.3
'@git.zone/tsrun': 2.0.0
'@push.rocks/consolecolor': 2.0.3
@@ -5502,9 +5656,9 @@ snapshots:
- utf-8-validate
- vue
'@git.zone/tswatch@2.2.3':
'@git.zone/tswatch@2.3.13(@tiptap/pm@2.27.1)':
dependencies:
'@api.global/typedserver': 3.0.80
'@api.global/typedserver': 7.11.1(@tiptap/pm@2.27.1)
'@git.zone/tsbundle': 2.6.3
'@git.zone/tsrun': 2.0.0
'@push.rocks/early': 4.0.4
@@ -5515,13 +5669,16 @@ snapshots:
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartshell': 3.3.0
'@push.rocks/smartwatch': 5.0.0
'@push.rocks/smartwatch': 6.3.0
'@push.rocks/taskbuffer': 3.5.0
transitivePeerDependencies:
- '@nuxt/kit'
- '@swc/helpers'
- '@tiptap/pm'
- bufferutil
- react
- supports-color
- utf-8-validate
- vue
'@hapi/bourne@3.0.0': {}
@@ -5558,7 +5715,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/yargs': 17.0.35
chalk: 4.1.2
@@ -5868,7 +6025,7 @@ snapshots:
'@push.rocks/smartbucket': 3.3.10
'@push.rocks/smartcache': 1.0.18
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartexit': 1.0.23
'@push.rocks/smartexit': 1.1.0
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpath': 6.0.0
@@ -5913,6 +6070,23 @@ snapshots:
- snappy
- socks
'@push.rocks/npmextra@5.3.3':
dependencies:
'@push.rocks/qenv': 6.1.3
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/taskbuffer': 3.5.0
'@tsclass/tsclass': 9.3.0
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@push.rocks/projectinfo@5.0.2':
dependencies:
'@push.rocks/smartfile': 10.0.41
@@ -5931,7 +6105,7 @@ snapshots:
'@push.rocks/smartarchive@4.2.4':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 13.1.0
'@push.rocks/smartfile': 13.1.2
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 4.4.2
@@ -6105,6 +6279,13 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
tree-kill: 1.2.2
'@push.rocks/smartexit@1.1.0':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3
tree-kill: 1.2.2
'@push.rocks/smartexpect@2.5.0':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -6172,10 +6353,31 @@ snapshots:
glob: 11.1.0
js-yaml: 4.1.1
'@push.rocks/smartfile@13.1.2':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile-interfaces': 1.0.7
'@push.rocks/smartfs': 1.3.1
'@push.rocks/smarthash': 3.2.6
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartmime': 2.0.4
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 4.4.2
'@push.rocks/smartstream': 3.2.5
'@types/js-yaml': 4.0.9
glob: 11.1.0
js-yaml: 4.1.1
'@push.rocks/smartfs@1.2.0':
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartfs@1.3.1':
dependencies:
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartguard@3.1.0':
dependencies:
'@push.rocks/smartpromise': 4.2.3
@@ -6441,6 +6643,19 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@push.rocks/smartserve@1.4.0':
dependencies:
'@api.global/typedrequest': 3.2.5
'@cfworker/json-schema': 4.1.1
'@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartpath': 6.0.0
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@push.rocks/smartshell@3.3.0':
dependencies:
'@push.rocks/smartdelay': 3.0.5
@@ -6462,7 +6677,7 @@ snapshots:
'@push.rocks/smartsocket@2.1.0':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedserver': 3.0.80
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.4.0)
'@push.rocks/isohash': 2.0.1
'@push.rocks/isounique': 1.0.5
'@push.rocks/lik': 6.2.2
@@ -6546,12 +6761,13 @@ snapshots:
'@types/semver': 7.7.1
semver: 7.7.3
'@push.rocks/smartwatch@5.0.0':
'@push.rocks/smartwatch@6.3.0':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 6.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
chokidar: 5.0.0
picomatch: 4.0.3
'@push.rocks/smartxml@2.0.0':
@@ -6589,17 +6805,20 @@ snapshots:
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@mongodb-js/zstd'
- '@nuxt/kit'
- aws-crt
- bare-abort-controller
- bufferutil
- gcp-metadata
- kerberos
- mongodb-client-encryption
- react
- react-native-b4a
- snappy
- socks
- supports-color
- utf-8-validate
- vue
'@push.rocks/taskbuffer@3.5.0':
dependencies:
@@ -7347,18 +7566,18 @@ snapshots:
'@types/accepts@1.3.7':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/babel__code-frame@7.0.6': {}
'@types/bn.js@5.2.0':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/buffer-json@2.0.3': {}
@@ -7375,17 +7594,17 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
source-map: 0.6.1
'@types/co-body@6.1.3':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/qs': 6.14.0
'@types/connect@3.4.38':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/content-disposition@0.5.9': {}
@@ -7396,11 +7615,11 @@ snapshots:
'@types/connect': 3.4.38
'@types/express': 5.0.6
'@types/keygrip': 1.0.6
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/cors@2.8.19':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/debounce@1.2.4': {}
@@ -7412,7 +7631,7 @@ snapshots:
'@types/dns-packet@5.6.5':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/elliptic@6.4.18':
dependencies:
@@ -7420,7 +7639,7 @@ snapshots:
'@types/express-serve-static-core@5.1.0':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 1.2.1
@@ -7433,17 +7652,17 @@ snapshots:
'@types/from2@2.3.6':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/hast@3.0.4':
dependencies:
@@ -7477,7 +7696,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/keygrip@1.0.6': {}
@@ -7494,7 +7713,7 @@ snapshots:
'@types/http-errors': 2.0.5
'@types/keygrip': 1.0.6
'@types/koa-compose': 3.2.9
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/linkify-it@5.0.0': {}
@@ -7517,9 +7736,9 @@ snapshots:
'@types/node-forge@1.3.14':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/node@24.10.1':
'@types/node@25.0.3':
dependencies:
undici-types: 7.16.0
@@ -7537,18 +7756,18 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/semver@7.7.1': {}
'@types/send@1.2.1':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/serve-static@2.2.0':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/sinon-chai@3.2.12':
dependencies:
@@ -7567,11 +7786,11 @@ snapshots:
'@types/tar-stream@3.1.4':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/through2@2.0.41':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/triple-beam@1.3.5': {}
@@ -7597,11 +7816,11 @@ snapshots:
'@types/ws@7.4.7':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/yargs-parser@21.0.3': {}
@@ -7611,7 +7830,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
optional: true
'@ungap/structured-clone@1.3.0': {}
@@ -8247,7 +8466,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.19
'@types/node': 24.10.1
'@types/node': 25.0.3
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@@ -8983,7 +9202,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 24.10.1
'@types/node': 25.0.3
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@@ -9192,6 +9411,8 @@ snapshots:
lucide@0.555.0: {}
lucide@0.562.0: {}
make-dir@3.1.0:
dependencies:
semver: 6.3.1

View File

@@ -681,3 +681,123 @@ According to Lit's documentation (https://lit.dev/docs/components/decorators/#de
- 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

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

@@ -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: '2.0.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

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

@@ -4,6 +4,7 @@ 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';
@@ -95,14 +96,48 @@ export const demoFunc = () => {
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help selected') },
];
// Selector options (second sidebar)
const selectorOptions: (ISelectionOption | { divider: true })[] = [
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview selected') },
{ key: 'Components', iconName: 'package', action: () => console.log('Components selected') },
{ key: 'Services', iconName: 'server', action: () => console.log('Services selected') },
{ divider: true },
// 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: 'Settings', iconName: 'settings', action: () => console.log('Settings 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
@@ -155,7 +190,8 @@ export const demoFunc = () => {
.mainmenuLogoText=${'Acme App'}
.mainmenuGroups=${mainMenuGroups}
.mainmenuBottomTabs=${mainMenuBottomTabs}
.mainselectorOptions=${selectorOptions}
.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)}
@@ -163,20 +199,38 @@ export const demoFunc = () => {
@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)}
@mainselector-option-select=${(e: CustomEvent) => console.log('Option selected:', e.detail)}
@secondarymenu-item-select=${(e: CustomEvent) => console.log('Item selected:', e.detail)}
>
<div slot="maincontent" style="padding: 40px; color: #ccc;">
<h1>Application Content</h1>
<p>This is the main content area where your application's primary interface would be displayed.</p>
<p>The layout includes:</p>
<ul>
<li>App bar with menus, breadcrumbs, and user account</li>
<li>Main menu (left sidebar) for primary navigation</li>
<li>Selector menu (second sidebar) for sub-navigation</li>
<li>Main content area (this section)</li>
<li>Activity log (right sidebar)</li>
<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

@@ -12,15 +12,20 @@ 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 { DeesAppuiMainselector } from '../dees-appui-mainselector/dees-appui-mainselector.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-mainselector/dees-appui-mainselector.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';
@@ -75,12 +80,26 @@ export class DeesAppuiBase extends DeesElement {
@property({ type: Object })
accessor mainmenuSelectedTab: interfaces.ITab | undefined = undefined;
// Properties for mainselector
// Properties for secondarymenu
@property({ type: String })
accessor secondarymenuHeading: string = 'Menu';
@property({ type: Array })
accessor mainselectorOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = [];
@property({ type: Object })
accessor mainselectorSelectedOption: interfaces.ISelectionOption | undefined = undefined;
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 })
@@ -94,7 +113,7 @@ export class DeesAppuiBase extends DeesElement {
accessor mainmenu: DeesAppuiMainmenu | undefined = undefined;
@state()
accessor mainselector: DeesAppuiMainselector | undefined = undefined;
accessor secondarymenu: DeesAppuiSecondarymenu | undefined = undefined;
@state()
accessor maincontent: DeesAppuiMaincontent | undefined = undefined;
@@ -102,6 +121,19 @@ export class DeesAppuiBase extends DeesElement {
@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`
@@ -117,9 +149,39 @@ export class DeesAppuiBase extends DeesElement {
height: calc(100% - 40px);
width: 100%;
display: grid;
grid-template-columns: 200px 240px 1fr 240px;
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;
}
`,
];
@@ -149,16 +211,23 @@ export class DeesAppuiBase extends DeesElement {
.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-mainselector
.selectionOptions=${this.mainselectorOptions}
.selectedOption=${this.mainselectorSelectedOption}
@option-select=${(e: CustomEvent) => this.handleMainselectorOptionSelect(e)}
></dees-appui-mainselector>
<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>
@@ -170,9 +239,27 @@ export class DeesAppuiBase extends DeesElement {
// Get references to child components
this.appbar = this.shadowRoot.querySelector('dees-appui-appbar');
this.mainmenu = this.shadowRoot.querySelector('dees-appui-mainmenu');
this.mainselector = this.shadowRoot.querySelector('dees-appui-mainselector');
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
@@ -217,6 +304,8 @@ export class DeesAppuiBase extends DeesElement {
// 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,
@@ -224,13 +313,270 @@ export class DeesAppuiBase extends DeesElement {
}));
}
// Event handlers for mainselector
private handleMainselectorOptionSelect(e: CustomEvent) {
this.mainselectorSelectedOption = e.detail.option;
this.dispatchEvent(new CustomEvent('mainselector-option-select', {
// 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

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

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

@@ -12,6 +12,7 @@ import {
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
@@ -19,49 +20,7 @@ import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
*/
@customElement('dees-appui-mainmenu')
export class DeesAppuiMainmenu extends DeesElement {
public static demo = () => html`
<style>
.demo-mainmenu-container {
height: 500px;
background: #1a1a1a;
border-radius: 8px;
overflow: hidden;
}
</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>
`;
public static demo = demoFunc;
// INSTANCE
@@ -87,27 +46,75 @@ export class DeesAppuiMainmenu extends DeesElement {
@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 {
--menuWidth: 200px;
color: ${cssManager.bdTheme('#666', '#ccc')};
z-index: ${zIndexLayers.fixed.appBar};
display: flex;
flex-direction: column;
position: relative;
width: var(--menuWidth);
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 */
@@ -115,23 +122,38 @@ export class DeesAppuiMainmenu extends DeesElement {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 14px;
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 dees-icon {
font-size: 22px;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
.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) */
@@ -176,6 +198,17 @@ export class DeesAppuiMainmenu extends DeesElement {
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 {
@@ -184,6 +217,10 @@ export class DeesAppuiMainmenu extends DeesElement {
gap: 2px;
}
:host([collapsed]) .menuGroup {
padding: 0 4px;
}
/* Tab Item */
.tab {
position: relative;
@@ -240,6 +277,60 @@ export class DeesAppuiMainmenu extends DeesElement {
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 */
@@ -251,6 +342,10 @@ export class DeesAppuiMainmenu extends DeesElement {
flex-direction: column;
gap: 2px;
}
:host([collapsed]) .bottomSection {
padding: 8px 4px;
}
`,
];
@@ -268,7 +363,7 @@ export class DeesAppuiMainmenu extends DeesElement {
}}>
${this.logoIcon || this.logoText ? html`
<div class="logoSection">
${this.logoIcon ? html`<dees-icon .icon="${this.logoIcon}"></dees-icon>` : ''}
${this.logoIcon ? html`<dees-icon class="logoIcon" .icon="${this.logoIcon}"></dees-icon>` : ''}
${this.logoText ? html`<span class="logoText">${this.logoText}</span>` : ''}
</div>
` : ''}
@@ -283,6 +378,9 @@ export class DeesAppuiMainmenu extends DeesElement {
</div>
` : ''}
</div>
<button class="collapse-toggle" @click="${() => this.toggleCollapse()}">
<dees-icon .icon="${this.collapsed ? 'lucide:chevronRight' : 'lucide:chevronLeft'}"></dees-icon>
</button>
`;
}
@@ -319,6 +417,7 @@ export class DeesAppuiMainmenu extends DeesElement {
>
<dees-icon .icon="${tabArg.iconName || ''}"></dees-icon>
<span class="tabLabel">${tabArg.key}</span>
<span class="tab-tooltip">${tabArg.key}</span>
</div>
`;
}
@@ -349,4 +448,13 @@ export class DeesAppuiMainmenu extends DeesElement {
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

@@ -1,211 +0,0 @@
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,
customElement,
html,
css,
cssManager,
} from '@design.estate/dees-element';
/**
* the property selector menu
* mainly used to select assets within in an organization
*/
@customElement('dees-appui-mainselector')
export class DeesAppuiMainselector extends DeesElement {
public static demo = () => html`
<dees-appui-mainselector
.selectionOptions=${[
{ key: 'Overview', iconName: 'home', action: () => console.log('Overview') },
{ key: 'Components', iconName: 'package', action: () => console.log('Components') },
{ key: 'Services', iconName: 'server', action: () => console.log('Services') },
{ key: 'Database', iconName: 'database', action: () => console.log('Database') },
{ key: 'Settings', iconName: 'settings', action: () => console.log('Settings') },
]}
></dees-appui-mainselector>
`;
// INSTANCE
@property({ type: Array })
accessor selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = [
{ key: '⚠️ Please set selection options', action: () => console.warn('No selection options configured for mainselector') },
];
@property()
accessor selectedOption: interfaces.ISelectionOption = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
color: ${cssManager.bdTheme('#333', '#fff')};
position: relative;
display: block;
width: 100%;
max-width: 300px;
height: 100%;
background: ${cssManager.bdTheme('#fafafa', '#000000')};
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.maincontainer {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
}
.topbar {
position: absolute;
height: 40px;
width: 100%;
display: flex;
align-items: center;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
}
.topbar .heading {
padding-left: 12px;
font-family: 'Geist Sans', sans-serif;
font-weight: 600;
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
text-transform: uppercase;
letter-spacing: 0.5px;
}
.selectionOptions {
position: absolute;
top: 40px;
left: 0px;
right: 0px;
bottom: 0px;
overflow-y: auto;
font-family: 'Geist Sans', sans-serif;
font-size: 12px;
padding: 4px 0;
}
.selectionOptions .selectionOption {
cursor: default;
padding: 8px 12px;
margin: 0;
transition: background 0.1s;
display: flex;
align-items: center;
gap: 8px;
color: ${cssManager.bdTheme('#333', '#ccc')};
user-select: none;
}
.selectionOptions .selectionOption:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.selectionOptions .selectionOption:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
.selectionOptions .selectionOption.selectedOption {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
color: ${cssManager.bdTheme('#000', '#fff')};
font-weight: 500;
}
.selectionOptions .selectionOption.selectedOption::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: ${cssManager.bdTheme('#26a69a', '#26a69a')};
}
.selectionOption {
position: relative;
}
.selection-divider {
height: 1px;
background: ${cssManager.bdTheme('#e0e0e0', '#202020')};
margin: 4px 0;
}
`,
];
public render(): TemplateResult {
return html`
<style></style>
<div class="maincontainer">
<div class="topbar">
<div class="heading">Selector</div>
</div>
<div class="selectionOptions">
${this.selectionOptions.map((selectionOptionArg) => {
if ('divider' in selectionOptionArg && selectionOptionArg.divider) {
return html`<div class="selection-divider"></div>`;
}
const option = selectionOptionArg as interfaces.ISelectionOption;
return html`
<div
class="selectionOption ${this.selectedOption === option
? 'selectedOption'
: null}"
@click="${() => {
this.selectOption(option);
}}"
@contextmenu="${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'property settings',
action: async () => {},
iconName: 'gear',
},
]);
}}"
>
${option.iconName ? html`
<dees-icon .icon="${`lucide:${option.iconName}`}" style="font-size: 14px; opacity: 0.7;"></dees-icon>
` : ''}
<span style="flex: 1;">${option.key}</span>
</div>
`;
})}
</div>
</div>
`;
}
private selectOption(optionArg: interfaces.ISelectionOption) {
this.selectedOption = optionArg;
this.selectedOption.action();
// Emit option-select event
this.dispatchEvent(new CustomEvent('option-select', {
detail: { option: optionArg },
bubbles: true,
composed: true
}));
}
async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
if (this.selectionOptions && this.selectionOptions.length > 0) {
await this.updateComplete;
// Find first non-divider option
const firstOption = this.selectionOptions.find(option => !('divider' in option)) as interfaces.ISelectionOption;
if (firstOption) {
this.selectOption(firstOption);
}
}
}
}

View File

@@ -1 +0,0 @@
export * from './dees-appui-mainselector.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

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

@@ -4,7 +4,7 @@ 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-mainselector/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

@@ -299,6 +299,8 @@ export class DeesPdfViewer extends DeesElement {
await this.renderThumbnails();
// Re-setup intersection observer for lazy loading of pages
this.setupIntersectionObserver();
// Scroll to active thumbnail after rendering
this.scrollThumbnailIntoView(this.currentPage);
}
}
@@ -578,6 +580,11 @@ export class DeesPdfViewer extends DeesElement {
// Update current page
this.currentPage = pageNum;
// Scroll thumbnail into view if sidebar is visible
if (this.showSidebar) {
this.scrollThumbnailIntoView(pageNum);
}
// Ensure the page is rendered
await this.renderPageIfNeeded(pageNum);
}

View File

@@ -266,17 +266,17 @@ export const demoFunc = () => html`
.viewTabs=${[
{
name: 'Dashboard',
iconName: 'home',
iconName: 'lucide:home',
element: DemoViewDashboard,
},
{
name: 'Analytics',
iconName: 'lineChart',
iconName: 'lucide:lineChart',
element: DemoViewAnalytics,
},
{
name: 'Settings',
iconName: 'settings',
iconName: 'lucide:settings',
element: DemoViewSettings,
}
] as IView[]}

View File

@@ -51,7 +51,7 @@ export class DeesSimpleAppDash extends DeesElement {
cssManager.defaultStyles,
css`
:host {
color: ${cssManager.bdTheme('#333', '#ccc')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
user-select: none;
display: block;
overflow: hidden;
@@ -75,67 +75,122 @@ export class DeesSimpleAppDash extends DeesElement {
left: 0px;
height: calc(100% - 24px);
width: 240px;
background: ${cssManager.bdTheme('#fafafa', '#000')};
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
font-size: 12px;
background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(0 0% 7%)')};
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')};
font-size: 13px;
font-family: 'Geist Sans', sans-serif;
z-index: 2;
display: grid;
grid-template-rows: auto min-content;
grid-template-rows: auto 1fr min-content;
overflow: hidden;
}
.sidebar-header {
padding: 16px 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
padding: 20px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')};
display: flex;
align-items: center;
gap: 8px;
gap: 12px;
}
.header-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
background: ${cssManager.bdTheme(
'linear-gradient(135deg, hsl(215 20% 95%) 0%, hsl(215 20% 90%) 100%)',
'linear-gradient(135deg, hsl(215 20% 18%) 0%, hsl(215 20% 14%) 100%)'
)};
box-shadow: ${cssManager.bdTheme(
'0 1px 2px rgb(0 0 0 / 0.05), inset 0 1px 0 rgb(255 255 255 / 0.5)',
'0 1px 2px rgb(0 0 0 / 0.2), inset 0 1px 0 rgb(255 255 255 / 0.05)'
)};
}
.header-icon-wrapper dees-icon {
font-size: 18px;
color: ${cssManager.bdTheme('hsl(215 20% 40%)', 'hsl(215 20% 70%)')};
}
.appName {
font-size: 14px;
font-size: 15px;
font-weight: 600;
color: ${cssManager.bdTheme('#000', '#fff')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: -0.02em;
}
.viewTabs-container {
flex: 1;
overflow-y: auto;
padding: 4px 0;
padding: 12px 8px;
scrollbar-width: thin;
scrollbar-color: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')} transparent;
}
.viewTabs-container::-webkit-scrollbar {
width: 6px;
}
.viewTabs-container::-webkit-scrollbar-track {
background: transparent;
}
.viewTabs-container::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
border-radius: 3px;
}
.viewTabs-container::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(0 0% 30%)')};
}
.section-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
padding: 8px 12px 8px;
margin-bottom: 4px;
}
.viewTabs {
display: flex;
flex-direction: column;
gap: 2px;
}
.viewTab {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
gap: 10px;
padding: 10px 12px;
cursor: default;
transition: background 0.1s;
color: ${cssManager.bdTheme('#333', '#ccc')};
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
user-select: none;
position: relative;
border-radius: 8px;
}
.viewTab:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.04)', 'hsl(0 0% 100% / 0.05)')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}
.viewTab:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.06)', 'hsl(0 0% 100% / 0.07)')};
transform: scale(0.99);
}
.viewTab.selected {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
color: ${cssManager.bdTheme('#000', '#fff')};
background: ${cssManager.bdTheme('hsl(215 25% 95%)', 'hsl(215 20% 15%)')};
color: ${cssManager.bdTheme('hsl(215 25% 30%)', 'hsl(215 25% 85%)')};
font-weight: 500;
}
@@ -143,41 +198,65 @@ export class DeesSimpleAppDash extends DeesElement {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
top: 8px;
bottom: 8px;
width: 3px;
background: ${cssManager.bdTheme('#26a69a', '#26a69a')};
border-radius: 0 2px 2px 0;
background: ${cssManager.bdTheme('hsl(215 70% 50%)', 'hsl(215 70% 60%)')};
}
.viewTab dees-icon {
font-size: 14px;
opacity: 0.7;
font-size: 16px;
opacity: 0.55;
transition: all 0.15s ease;
}
.viewTab:hover dees-icon {
opacity: 0.75;
}
.viewTab.selected dees-icon {
opacity: 0.9;
color: ${cssManager.bdTheme('hsl(215 70% 45%)', 'hsl(215 70% 65%)')};
}
.viewTab span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.appActions {
padding: 12px;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
padding: 12px 8px;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 91%)', 'hsl(0 0% 13%)')};
}
.action {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 4px;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: default;
transition: background 0.1s;
color: ${cssManager.bdTheme('#333', '#ccc')};
transition: all 0.15s ease;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
}
.action:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
background: ${cssManager.bdTheme('hsl(0 84% 60% / 0.08)', 'hsl(0 84% 60% / 0.12)')};
color: ${cssManager.bdTheme('hsl(0 84% 45%)', 'hsl(0 84% 65%)')};
}
.action dees-icon {
font-size: 14px;
opacity: 0.7;
font-size: 16px;
opacity: 0.6;
transition: all 0.15s ease;
}
.action:hover dees-icon {
opacity: 0.9;
color: ${cssManager.bdTheme('hsl(0 84% 45%)', 'hsl(0 84% 65%)')};
}
.appcontent {
@@ -189,7 +268,7 @@ export class DeesSimpleAppDash extends DeesElement {
bottom: 24px;
width: calc(100% - 240px);
overflow: auto;
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 5%)')};
overscroll-behavior: contain;
}
@@ -199,36 +278,51 @@ export class DeesSimpleAppDash extends DeesElement {
bottom: 0px;
left: 0px;
width: 100%;
border-top: 1px solid ${cssManager.bdTheme('#00000020', '#ffffff20')};
height: 24px;
background: ${cssManager.bdTheme('#2196f3', '#1565c0')};
background: ${cssManager.bdTheme('hsl(220 13% 18%)', 'hsl(220 13% 12%)')};
z-index: 2;
display: flex;
justify-content: flex-end;
align-items: center;
flex-direction: row;
font-size: 12px;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.01em;
}
.control {
display: flex;
align-items: center;
gap: 4px;
margin-right: 16px;
gap: 6px;
padding: 0 12px;
height: 100%;
white-space: nowrap;
cursor: default;
opacity: 0.8;
transition: opacity 0.2s;
color: hsl(0 0% 70%);
transition: all 0.15s ease;
border-left: 1px solid hsl(0 0% 100% / 0.08);
}
.control:first-child {
border-left: none;
}
.control:hover {
opacity: 1;
background: hsl(0 0% 100% / 0.06);
color: hsl(0 0% 95%);
}
.control dees-icon {
font-size: 14px;
font-size: 13px;
}
.control.status-connected dees-icon {
color: hsl(142 70% 50%);
}
.control.status-terminal dees-icon {
color: hsl(45 90% 55%);
}
`,
];
@@ -236,12 +330,14 @@ export class DeesSimpleAppDash extends DeesElement {
return html`
<div class="maincontainer">
<div class="appbar">
<div>
<div class="sidebar-header">
<dees-icon .icon="${'lucide:grid3x3'}" style="font-size: 18px;"></dees-icon>
<div class="header-icon-wrapper">
<dees-icon .icon="${'lucide:layoutGrid'}"></dees-icon>
</div>
<div class="appName">${this.name}</div>
</div>
<div class="viewTabs-container">
<div class="section-label">Navigation</div>
<div class="viewTabs">
${this.viewTabs.map(
(view) => html`
@@ -250,21 +346,22 @@ export class DeesSimpleAppDash extends DeesElement {
@click=${() => this.loadView(view)}
>
${view.iconName ? html`
<dees-icon .icon="${`${view.iconName}`}"></dees-icon>
` : ''}
<span style="flex: 1;">${view.name}</span>
<dees-icon .icon="${view.iconName.includes(':') ? view.iconName : `lucide:${view.iconName}`}"></dees-icon>
` : html`
<dees-icon .icon="${'lucide:file'}"></dees-icon>
`}
<span>${view.name}</span>
</div>
`
)}
</div>
</div>
</div>
<div class="appActions">
<div class="action" @click=${() => {
this.dispatchEvent(new CustomEvent('logout', { bubbles: true, composed: true }));
}}>
<dees-icon .icon="${'lucide:logOut'}"></dees-icon>
<span>Logout</span>
<span>Sign out</span>
</div>
</div>
</div>
@@ -272,11 +369,11 @@ export class DeesSimpleAppDash extends DeesElement {
<!-- Content goes here -->
</div>
<div class="controlbar">
<div class="control">
<div class="control status-connected">
<dees-icon .icon="${'lucide:wifi'}"></dees-icon>
<span>Connected</span>
</div>
<div class="control" @click=${this.launchTerminal}>
<div class="control status-terminal" @click=${this.launchTerminal}>
<dees-icon .icon="${'lucide:terminal'}"></dees-icon>
<span>Terminal</span>
</div>
@@ -315,10 +412,10 @@ export class DeesSimpleAppDash extends DeesElement {
terminal.style.right = '0px';
terminal.style.bottom = '24px';
terminal.style.opacity = '0';
terminal.style.transform = 'translateY(20px)';
terminal.style.transition = 'all 0.2s';
terminal.style.background = '#000';
terminal.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
terminal.style.transform = 'translateY(8px) scale(0.99)';
terminal.style.transition = 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)';
terminal.background = 'hsl(220 13% 8%)';
terminal.style.boxShadow = '0 25px 50px -12px rgb(0 0 0 / 0.5), 0 0 0 1px rgb(255 255 255 / 0.05)';
terminal.style.maxWidth = `calc(${maincontainer.clientWidth}px -240px)`;
terminal.style.maxHeight = `calc(${maincontainer.clientHeight}px - 24px)`;
@@ -327,7 +424,7 @@ export class DeesSimpleAppDash extends DeesElement {
await domtools.convenience.smartdelay.delayFor(0);
terminal.style.opacity = '1';
terminal.style.transform = 'translateY(0px)';
terminal.style.transform = 'translateY(0) scale(1)';
return terminal;
}
@@ -335,8 +432,8 @@ export class DeesSimpleAppDash extends DeesElement {
const domtools = await this.domtoolsPromise;
if (this.currentTerminal) {
this.currentTerminal.style.opacity = '0';
this.currentTerminal.style.transform = 'translateY(20px)';
await domtools.convenience.smartdelay.delayFor(200);
this.currentTerminal.style.transform = 'translateY(8px) scale(0.99)';
await domtools.convenience.smartdelay.delayFor(250);
this.currentTerminal.remove();
this.currentTerminal = null;
}

View File

@@ -29,7 +29,7 @@ export class DeesSimpleLogin extends DeesElement {
cssManager.defaultStyles,
css`
:host {
color: ${cssManager.bdTheme('#333', '#ccc')};
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
user-select: none;
display: block;
width: 100%;
@@ -46,7 +46,7 @@ export class DeesSimpleLogin extends DeesElement {
height: 100%;
top: 0;
left: 0;
background: ${cssManager.bdTheme('#f5f5f5', '#000')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
}
.slotContainer {
@@ -56,43 +56,56 @@ export class DeesSimpleLogin extends DeesElement {
top: 0;
left: 0;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
transition: opacity 0.2s ease, transform 0.2s ease;
pointer-events: none;
}
.login {
min-width: 320px;
background: ${cssManager.bdTheme('#ffffff', '#111')};
box-shadow: ${cssManager.bdTheme(
'0 4px 12px rgba(0, 0, 0, 0.15)',
'0 4px 12px rgba(0, 0, 0, 0.3)'
)};
border-radius: 8px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#202020')};
padding: 24px;
transition: opacity 0.3s, transform 0.3s;
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
gap: 24px;
}
.login-header {
display: flex;
flex-direction: column;
gap: 8px;
text-align: center;
}
.header {
text-align: center;
font-size: 16px;
font-size: 24px;
font-weight: 600;
margin-bottom: 20px;
color: ${cssManager.bdTheme('#000', '#fff')};
letter-spacing: -0.025em;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
}
.login dees-form {
.subheader {
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
}
.login-card {
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 8px;
padding: 24px;
}
.login-card dees-form {
display: flex;
flex-direction: column;
gap: 12px;
gap: 16px;
}
.login dees-input-text {
.login-card dees-input-text {
width: 100%;
}
.login dees-form-submit {
margin-top: 4px;
.login-card dees-form-submit {
margin-top: 8px;
width: 100%;
}
`,
@@ -102,14 +115,19 @@ export class DeesSimpleLogin extends DeesElement {
return html`
<div class="loginContainer">
<div class="login">
<div class="header">Login to ${this.name}</div>
<div class="login-header">
<div class="header">Sign in</div>
<div class="subheader">Enter your credentials to access ${this.name}</div>
</div>
<div class="login-card">
<dees-form>
<dees-input-text key="username" label="Username" required></dees-input-text>
<dees-input-text key="password" label="Password" isPasswordBool required></dees-input-text>
<dees-form-submit>Login</dees-form-submit>
<dees-form-submit>Sign in</dees-form-submit>
</dees-form>
</div>
</div>
</div>
<div class="slotContainer">
<slot></slot>
</div>

View File

@@ -38,6 +38,9 @@ export class DeesTerminal extends DeesElement {
@property()
accessor environment: {[key: string]: string} = {};
@property()
accessor background: string = '#000000';
// exposing webcontainer
private webcontainerDeferred = new domtools.plugins.smartpromise.Deferred<webcontainer.WebContainer>();
public webcontainerPromise = this.webcontainerDeferred.promise;
@@ -58,7 +61,7 @@ export class DeesTerminal extends DeesElement {
css`
:host {
padding: 20px;
background: #000;
background: var(--dees-terminal-background, #000000);
position: absolute;
height: 100%;
width: 100%;
@@ -153,7 +156,7 @@ export class DeesTerminal extends DeesElement {
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
background: var(--dees-terminal-background, #000000);
color: #fff;
display: none;
position: absolute;
@@ -167,7 +170,7 @@ export class DeesTerminal extends DeesElement {
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
background-color: var(--dees-terminal-background, #000000);
overflow-y: scroll;
cursor: default;
position: absolute;
@@ -261,11 +264,18 @@ export class DeesTerminal extends DeesElement {
): Promise<void> {
const domtools = await this.domtoolsPromise;
super.firstUpdated(_changedProperties);
// Sync CSS variable with background property
this.style.setProperty('--dees-terminal-background', this.background);
const container = this.shadowRoot.getElementById('container');
const term = new Terminal({
convertEol: true,
cursorBlink: true,
theme: {
background: this.background,
},
});
this.fitAddon = new FitAddon();
term.loadAddon(this.fitAddon);

View File

@@ -0,0 +1,189 @@
import type { TemplateResult } from '@design.estate/dees-element';
import type { IAppBarMenuItem } from './appbarmenuitem.js';
import type { ITab } from './tab.js';
import type { ISecondaryMenuGroup } from './secondarymenu.js';
/**
* User configuration for the app bar
*/
export interface IAppUser {
name: string;
email?: string;
avatar?: string;
status?: 'online' | 'offline' | 'busy' | 'away';
}
/**
* View definition for the view registry
*/
export interface IViewDefinition {
/** Unique identifier for routing */
id: string;
/** Display name */
name: string;
/** Optional icon */
iconName?: string;
/** The view content - can be a tag name, element class, or template function */
content: string | (new () => HTMLElement) | (() => TemplateResult);
/** Secondary menu items specific to this view */
secondaryMenu?: ISecondaryMenuGroup[];
/** Content tabs specific to this view */
contentTabs?: ITab[];
/** Optional route path (defaults to id) */
route?: string;
/** Badge to show on menu item */
badge?: string | number;
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
}
/**
* Main menu section with view references
*/
export interface IMainMenuSection {
/** Section name (optional for ungrouped) */
name?: string;
/** Views in this section (by ID reference) */
views: string[];
}
/**
* Main menu configuration
*/
export interface IMainMenuConfig {
/** Menu sections with view references */
sections?: IMainMenuSection[];
/** Bottom pinned items (view IDs) */
bottomItems?: string[];
}
/**
* App bar configuration
*/
export interface IAppBarConfig {
menuItems?: IAppBarMenuItem[];
breadcrumbs?: string;
breadcrumbSeparator?: string;
showWindowControls?: boolean;
showSearch?: boolean;
user?: IAppUser;
profileMenuItems?: IAppBarMenuItem[];
}
/**
* Branding configuration
*/
export interface IBrandingConfig {
logoIcon?: string;
logoText?: string;
}
/**
* Routing configuration
*/
export interface IRoutingConfig {
/** Routing mode */
mode: 'hash' | 'history' | 'external' | 'none';
/** Base path for history mode */
basePath?: string;
/** Default view ID to show on startup */
defaultView?: string;
/** Sync URL on view change */
syncUrl?: boolean;
/** Handle 404s - show view ID or callback */
notFound?: string | (() => void);
}
/**
* State persistence configuration
*/
export interface IStatePersistenceConfig {
/** Enable state persistence */
enabled: boolean;
/** Storage key prefix */
storageKey?: string;
/** Storage type */
storage?: 'localStorage' | 'sessionStorage' | 'memory';
/** What to persist */
persist?: {
mainMenuCollapsed?: boolean;
secondaryMenuCollapsed?: boolean;
selectedView?: boolean;
secondaryMenuSelection?: boolean;
collapsedGroups?: boolean;
};
}
/**
* Activity log configuration
*/
export interface IActivityLogConfig {
enabled?: boolean;
width?: number;
}
/**
* Main unified configuration interface for dees-appui-base
*/
export interface IAppConfig {
/** Application branding */
branding?: IBrandingConfig;
/** App bar configuration */
appBar?: IAppBarConfig;
/** View definitions (the registry) */
views: IViewDefinition[];
/** Main menu structure */
mainMenu?: IMainMenuConfig;
/** Routing configuration */
routing?: IRoutingConfig;
/** State persistence configuration */
statePersistence?: IStatePersistenceConfig;
/** Activity log configuration */
activityLog?: IActivityLogConfig;
/** Event callbacks (optional shorthand) */
onViewChange?: (viewId: string, view: IViewDefinition) => void;
onSearch?: () => void;
}
/**
* Serialized UI state for persistence
*/
export interface IAppUIState {
/** Current view ID */
currentViewId?: string;
/** Main menu collapsed state */
mainMenuCollapsed?: boolean;
/** Secondary menu collapsed state */
secondaryMenuCollapsed?: boolean;
/** Selected secondary menu item key */
secondaryMenuSelectedKey?: string;
/** Collapsed group names in secondary menu */
collapsedGroups?: string[];
/** Timestamp of last save */
timestamp?: number;
}
/**
* Route change event detail
*/
export interface IRouteChangeEvent {
viewId: string;
previousViewId: string | null;
params?: Record<string, string>;
source: 'navigation' | 'popstate' | 'initial' | 'programmatic';
}
/**
* View change event detail
*/
export interface IViewChangeEvent {
viewId: string;
view: IViewDefinition;
previousView?: IViewDefinition;
}

View File

@@ -2,3 +2,5 @@ export * from './tab.js';
export * from './selectionoption.js';
export * from './appbarmenuitem.js';
export * from './menugroup.js';
export * from './secondarymenu.js';
export * from './appconfig.js';

View File

@@ -0,0 +1,20 @@
/**
* Interface for individual menu items in the secondary menu
*/
export interface ISecondaryMenuItem {
key: string;
iconName?: string;
action: () => void;
badge?: string | number;
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
}
/**
* Interface for collapsible groups in the secondary menu
*/
export interface ISecondaryMenuGroup {
name: string;
items: ISecondaryMenuItem[];
collapsed?: boolean;
iconName?: string;
}

View File

@@ -2,15 +2,15 @@ import { html, css, cssManager } from '@design.estate/dees-element';
import { DeesModal } from '../elements/dees-modal/dees-modal.js';
import { DeesToast } from '../elements/dees-toast/dees-toast.js';
import { DeesContextmenu } from '../elements/dees-contextmenu/dees-contextmenu.js';
import '../elements/dees-button/dees-button.js';
import '../elements/dees-input-dropdown/dees-input-dropdown.js';
import '../elements/dees-form/dees-form.js';
import '../elements/00group-button/dees-button/dees-button.js';
import '../elements/00group-input/dees-input-dropdown/dees-input-dropdown.js';
import '../elements/00group-form/dees-form/dees-form.js';
import '../elements/dees-panel/dees-panel.js';
import '../elements/dees-input-text/dees-input-text.js';
import '../elements/dees-input-radiogroup/dees-input-radiogroup.js';
import '../elements/dees-input-tags/dees-input-tags.js';
import '../elements/dees-input-wysiwyg.js';
import '../elements/dees-appui-profiledropdown/dees-appui-profiledropdown.js';
import '../elements/00group-input/dees-input-text/dees-input-text.js';
import '../elements/00group-input/dees-input-radiogroup/dees-input-radiogroup.js';
import '../elements/00group-input/dees-input-tags/dees-input-tags.js';
import '../elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import '../elements/00group-appui/dees-appui-profiledropdown/dees-appui-profiledropdown.js';
export const zIndexShowcase = () => html`
<style>