Compare commits

..

12 Commits

Author SHA1 Message Date
76748a81c9 v3.5.1
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 02:09:09 +00:00
1e432ca92e fix(dees-appui-view): remove DeesAppuiView component, its demo, documentation snippet, and related exports 2025-12-29 02:09:09 +00:00
965d328559 v3.5.0
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-29 01:20:24 +00:00
e38d3cd42a feat(theme,interfaces): Introduce a global theming system and unify menu/tab interfaces; migrate components to use themeDefaultStyles and update APIs accordingly 2025-12-29 01:20:24 +00:00
9175799ec6 v3.4.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-19 13:54:37 +00:00
eeb863b197 feat(dees-appui-base): overhaul AppUI core: replace simple view rendering with a full-featured ViewRegistry (caching, hide/show lifecycle, async lazy-loading), introduce view lifecycle hooks and activation context, add activity log API/component, remove built-in router and state manager, and update configuration interfaces and demos 2025-12-19 13:54:37 +00:00
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
110 changed files with 4090 additions and 2126 deletions

View File

@@ -1,5 +1,56 @@
# Changelog
## 2025-12-29 - 3.5.1 - fix(dees-appui-view)
remove DeesAppuiView component, its demo, documentation snippet, and related exports
- Deleted component implementation: ts_web/elements/00group-appui/dees-appui-view/dees-appui-view.ts
- Deleted demo file: ts_web/elements/00group-appui/dees-appui-view/dees-appui-view.demo.ts
- Removed index re-export: ts_web/elements/00group-appui/dees-appui-view/index.ts (deleted) and removed export from ts_web/elements/00group-appui/index.ts
- Removed documentation section for DeesAppuiView from readme.md
- Breaking change: any consumers using the <dees-appui-view> component or its public API (selectTab, getMenuItems, getTabs) must be migrated to alternate components/approach
## 2025-12-29 - 3.5.0 - feat(theme,interfaces)
Introduce a global theming system and unify menu/tab interfaces; migrate components to use themeDefaultStyles and update APIs accordingly
- Add a new theme module and component (00theme.ts + dees-theme) that provides CSS tokens and themeDefaultStyles to import into components
- Migrate many components to include themeDefaultStyles in their static styles and add TODOs to replace hardcoded values with CSS variables
- Rename ITab -> IMenuItem and replace group.tabs with group.items across interfaces and components (IMenuGroup shape changed) — this is a breaking API change
- Remove legacy interfaces (ISecondaryMenuGroup, ISelectionOption) and update method and property types in DeesAppui* components and app config to use IMenuItem/IMenuGroup
- Move @design.estate/dees-wcctools from dependencies to devDependencies and bump its version to ^3.3.0
- Add numerous demo files and expand README with usage, examples and theme documentation
## 2025-12-19 - 3.4.0 - feat(dees-appui-base)
overhaul AppUI core: replace simple view rendering with a full-featured ViewRegistry (caching, hide/show lifecycle, async lazy-loading), introduce view lifecycle hooks and activation context, add activity log API/component, remove built-in router and state manager, and update configuration interfaces and demos
- Removed files: app.router.ts and state.manager.ts — routing and state-persistence internals were removed (breaking).
- ViewRegistry rewritten: supports cached instances, activate/deactivate lifecycle, canDeactivate checks, async content loading, parameterized routes, and legacy renderView kept as deprecated compatibility.
- New interfaces added/changed: IViewActivationContext, IViewLifecycle, IActivityEntry, IActivityLogAPI, IViewLifecycleEvent; IViewDefinition.content now accepts async loaders and a cache flag; IMainMenuConfig and ITab expanded (logo, groups, badges).
- Activity log: dees-appui-activitylog now implements IActivityLogAPI and exposes reactive entries; demo and readme updated with usage and examples.
- App config changed: routing and statePersistence config entries removed/adjusted; defaultView moved into IAppConfig; view change and lifecycle event shapes changed (breaking).
- Demos and documentation: dees-appui-base demo and readme added/updated to showcase new lifecycle hooks, secondary menu behavior, activity log and new APIs.
## 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

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": "3.3.0",
"version": "3.5.1",
"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,6 @@
"dependencies": {
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^1.2.1",
"@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 +37,21 @@
"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",
"@design.estate/dees-wcctools": "^3.3.0",
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tstest": "^3.1.3",
"@git.zone/tswatch": "^2.3.5",
"@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/**/*",

224
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.3.0
version: 3.3.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,8 +91,8 @@ 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
@@ -100,8 +100,8 @@ importers:
specifier: ^3.1.3
version: 3.1.3(@push.rocks/smartserve@1.4.0)(socks@2.8.7)(typescript@5.9.3)
'@git.zone/tswatch':
specifier: ^2.3.5
version: 2.3.5(@tiptap/pm@2.27.1)
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:
@@ -322,6 +322,9 @@ packages:
'@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'}
@@ -332,8 +335,8 @@ packages:
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
'@design.estate/dees-catalog@3.1.2':
resolution: {integrity: sha512-T4ZhoZzl6NAWGhrz00MDFla5syeMz53euZ+QcBftRMmHuqPHDU/JlBi+K2SmILtnRfWH/lGYWBKugYop6q0chg==}
'@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==}
@@ -344,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.3.0':
resolution: {integrity: sha512-ZOxG5LkbLLsqDQWO+JCOjFkL77l9FuLDa7LBuZRkTSX0jRoYG6ICI1UoI9i6twxm4JKSzQ4iHjL/F5mHbQiKTg==}
'@emnapi/core@1.7.1':
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
@@ -535,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':
@@ -555,8 +561,8 @@ packages:
resolution: {integrity: sha512-t+/cKV21JHK8X7NGAmihs5M/eMm+V+jn4R5rzfwGG97WJFAcP5qE1Os9VYtyZw3tx/NZXA2yA4abo/ELluTuRA==}
hasBin: true
'@git.zone/tswatch@2.3.5':
resolution: {integrity: sha512-wYekG7Q/wg5uptXHPHhVi7dHq19QnLoevQpcpAzF6hMiNbOozN3+4zxOktyJBl6EHUYcFvHXA4fZ4bkJpo5TcA==}
'@git.zone/tswatch@2.3.13':
resolution: {integrity: sha512-43995OlWl8UzCA+cX3ehYba/ksm6CqHbMljHKjosrDRpx8EU+LY4bWTc8JT/Ldgwsw3iW9vur2bBqpgMmdeJJw==}
hasBin: true
'@hapi/bourne@3.0.0':
@@ -809,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==}
@@ -867,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==}
@@ -885,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==}
@@ -1023,8 +1041,8 @@ packages:
'@push.rocks/smartversion@3.0.5':
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
'@push.rocks/smartwatch@6.1.1':
resolution: {integrity: sha512-wmhLKu9bdpvRcjOfitJOi4jsNKD7S2hVlVq6fAv3IhB2ZbRlSB+Hai4DwOlrdUZaWrg+dFIZU+/ifTOozOPiMg==}
'@push.rocks/smartwatch@6.3.0':
resolution: {integrity: sha512-TeZ1PGBoBMpC4/CK8StIj5InEiFfKp7xWJSm3aYMjB/uaoeRP0vXqv1ORIC/TKYGJuEDuAXUsit8tZVjn0qT1Q==}
engines: {node: '>=20.0.0'}
'@push.rocks/smartxml@2.0.0':
@@ -1798,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==}
@@ -3309,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'}
@@ -4712,8 +4733,8 @@ snapshots:
'@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.20251205.0
'@design.estate/dees-catalog': 3.1.2(@tiptap/pm@2.27.1)
'@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
@@ -4738,7 +4759,7 @@ snapshots:
'@push.rocks/smartsitemap': 2.0.4
'@push.rocks/smartstream': 3.2.5
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smartwatch': 6.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
@@ -5281,6 +5302,8 @@ snapshots:
'@cloudflare/workers-types@4.20251205.0': {}
'@cloudflare/workers-types@4.20251211.0': {}
'@colors/colors@1.6.0': {}
'@configvault.io/interfaces@1.0.17':
@@ -5293,11 +5316,11 @@ snapshots:
enabled: 2.0.0
kuler: 2.0.0
'@design.estate/dees-catalog@3.1.2(@tiptap/pm@2.27.1)':
'@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.2.1
'@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
@@ -5374,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.3.0':
dependencies:
'@design.estate/dees-domtools': 2.3.6
'@design.estate/dees-element': 2.1.3
@@ -5502,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
@@ -5543,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'
@@ -5618,7 +5656,7 @@ snapshots:
- utf-8-validate
- vue
'@git.zone/tswatch@2.3.5(@tiptap/pm@2.27.1)':
'@git.zone/tswatch@2.3.13(@tiptap/pm@2.27.1)':
dependencies:
'@api.global/typedserver': 7.11.1(@tiptap/pm@2.27.1)
'@git.zone/tsbundle': 2.6.3
@@ -5631,7 +5669,7 @@ snapshots:
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartshell': 3.3.0
'@push.rocks/smartwatch': 6.1.1
'@push.rocks/smartwatch': 6.3.0
'@push.rocks/taskbuffer': 3.5.0
transitivePeerDependencies:
- '@nuxt/kit'
@@ -5677,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
@@ -5987,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
@@ -6032,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
@@ -6050,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
@@ -6224,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
@@ -6291,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
@@ -6678,12 +6761,13 @@ snapshots:
'@types/semver': 7.7.1
semver: 7.7.3
'@push.rocks/smartwatch@6.1.1':
'@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':
@@ -7479,18 +7563,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': {}
@@ -7507,17 +7591,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': {}
@@ -7528,11 +7612,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': {}
@@ -7544,7 +7628,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:
@@ -7552,7 +7636,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
@@ -7565,17 +7649,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:
@@ -7609,7 +7693,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 24.10.1
'@types/node': 25.0.3
'@types/keygrip@1.0.6': {}
@@ -7626,7 +7710,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': {}
@@ -7649,9 +7733,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
@@ -7669,18 +7753,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:
@@ -7699,11 +7783,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': {}
@@ -7729,11 +7813,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': {}
@@ -7743,7 +7827,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': {}
@@ -8379,7 +8463,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
@@ -9115,7 +9199,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
@@ -9324,6 +9408,8 @@ snapshots:
lucide@0.555.0: {}
lucide@0.562.0: {}
make-dir@3.1.0:
dependencies:
semver: 6.3.1

679
readme.md

File diff suppressed because it is too large Load Diff

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: '3.3.0',
version: '3.5.1',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -0,0 +1,45 @@
import { html, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import type { DeesAppuiActivitylog } from './dees-appui-activitylog.js';
export const demoFunc = () => {
// Create the activity log element
const activityLog = document.createElement('dees-appui-activitylog') as DeesAppuiActivitylog;
// Add demo entries after the element is connected
setTimeout(() => {
activityLog.addMany([
{ type: 'login', user: 'John Doe', message: 'logged in from Chrome on macOS' },
{ type: 'create', user: 'John Doe', message: 'created a new project "Frontend App"' },
{ type: 'update', user: 'Jane Smith', message: 'updated API documentation' },
{ type: 'view', user: 'John Doe', message: 'viewed dashboard analytics' },
{ type: 'delete', user: 'Admin', message: 'removed deprecated endpoint' },
{ type: 'custom', user: 'System', message: 'scheduled backup completed', iconName: 'lucide:database' },
{ type: 'logout', user: 'Alice Brown', message: 'logged out' },
{ type: 'create', user: 'Jane Smith', message: 'created invoice #1234' },
]);
// Subscribe to updates
activityLog.entries$.subscribe((entries) => {
console.log('Activity log updated:', entries.length, 'entries');
});
}, 100);
return html`
<dees-demowrapper>
<style>
.demo-container {
display: flex;
justify-content: center;
align-items: center;
height: 600px;
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
padding: 32px;
}
</style>
<div class="demo-container">
${activityLog}
</div>
</dees-demowrapper>
`;
};

View File

@@ -1,4 +1,3 @@
import * as plugins from '../../00plugins.js';
import {
DeesElement,
type TemplateResult,
@@ -7,35 +6,40 @@ import {
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import '../../dees-icon/dees-icon.js';
import type { IActivityEntry, IActivityLogAPI } from '../../interfaces/appconfig.js';
import { demoFunc } from './dees-appui-activitylog.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-activitylog')
export class DeesAppuiActivitylog extends DeesElement {
export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI {
// STATIC
public static demo = () => html`
<style>
.demo-container {
display: flex;
justify-content: center;
align-items: center;
height: 600px;
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
padding: 32px;
}
</style>
<div class="demo-container">
<dees-appui-activitylog></dees-appui-activitylog>
</div>
`;
public static demo = demoFunc;
// INSTANCE
// INSTANCE PROPERTIES
@state()
accessor entries: IActivityEntry[] = [];
@state()
accessor searchQuery: string = '';
@state()
accessor filterCriteria: { user?: string; type?: IActivityEntry['type'] } = {};
// RxJS Subject for reactive updates
public entries$ = new domtools.plugins.smartrx.rxjs.Subject<IActivityEntry[]>();
// STYLES
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
position: relative;
@@ -90,24 +94,32 @@ export class DeesAppuiActivitylog extends DeesElement {
scrollbar-width: thin;
scrollbar-color: ${cssManager.bdTheme('#e5e7eb', '#27272a')} transparent;
}
.activityContainer::-webkit-scrollbar {
width: 6px;
}
.activityContainer::-webkit-scrollbar-track {
background: transparent;
}
.activityContainer::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 3px;
}
.activityContainer::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
}
.empty-state {
font-size: 13px;
text-align: center;
padding: 32px 16px;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
font-family: 'Geist Sans', sans-serif;
}
.streamingIndicator {
font-size: 11px;
text-align: center;
@@ -122,7 +134,7 @@ export class DeesAppuiActivitylog extends DeesElement {
justify-content: center;
gap: 8px;
}
.streamingIndicator::before {
content: '';
width: 6px;
@@ -131,15 +143,24 @@ export class DeesAppuiActivitylog extends DeesElement {
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
.streamingIndicator.bottom {
padding-top: 8px;
padding-bottom: 16px;
.date-separator {
padding: 12px 16px 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
background: ${cssManager.bdTheme('#f9fafb', '#09090b')};
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
position: sticky;
top: 0;
z-index: 1;
}
.activityentry {
@@ -154,7 +175,7 @@ export class DeesAppuiActivitylog extends DeesElement {
line-height: 1.4;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
@@ -182,7 +203,7 @@ export class DeesAppuiActivitylog extends DeesElement {
flex-shrink: 0;
min-width: 45px;
}
.activity-icon {
width: 28px;
height: 28px;
@@ -194,55 +215,51 @@ export class DeesAppuiActivitylog extends DeesElement {
flex-shrink: 0;
font-size: 14px;
}
.activity-icon.login {
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.1)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.activity-icon.logout {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.activity-icon.view {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
color: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
}
.activity-icon.create {
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.1)')};
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
.activity-icon.update {
background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.1)', 'rgba(251, 146, 60, 0.1)')};
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
.activity-icon.delete {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.activity-icon.custom {
background: ${cssManager.bdTheme('rgba(100, 116, 139, 0.1)', 'rgba(100, 116, 139, 0.1)')};
color: ${cssManager.bdTheme('#475569', '#94a3b8')};
}
.activity-text {
flex: 1;
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
}
.activity-user {
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.date-separator {
padding: 12px 16px 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
background: ${cssManager.bdTheme('#f9fafb', '#09090b')};
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
position: sticky;
top: 0;
z-index: 1;
}
.searchbox {
position: absolute;
@@ -253,13 +270,13 @@ export class DeesAppuiActivitylog extends DeesElement {
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
padding: 8px;
}
.search-wrapper {
position: relative;
width: 100%;
height: 32px;
}
.search-icon {
position: absolute;
left: 10px;
@@ -270,7 +287,7 @@ export class DeesAppuiActivitylog extends DeesElement {
pointer-events: none;
transition: color 0.15s ease;
}
.searchbox input {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
@@ -293,7 +310,7 @@ export class DeesAppuiActivitylog extends DeesElement {
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
}
.searchbox input:focus ~ .search-icon,
.search-wrapper:has(input:focus) .search-icon {
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
@@ -311,7 +328,7 @@ export class DeesAppuiActivitylog extends DeesElement {
pointer-events: none;
opacity: 0.8;
}
.topShadow {
position: absolute;
width: 100%;
@@ -327,7 +344,11 @@ export class DeesAppuiActivitylog extends DeesElement {
`,
];
// RENDER
public render(): TemplateResult {
const filteredEntries = this.getFilteredEntries();
const groupedEntries = this.groupEntriesByDate(filteredEntries);
return html`
${domtools.elementBasic.styles}
<style></style>
@@ -336,173 +357,28 @@ export class DeesAppuiActivitylog extends DeesElement {
<div class="heading">Activity Log</div>
</div>
<div class="activityContainer">
<div class="streamingIndicator">Live Updates</div>
<div class="date-separator">Today</div>
<div class="activityentry" @contextmenu=${async eventArg => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Copy activity',
action: async () => {},
},
{
name: 'View details',
action: async () => {},
},
{
name: 'Filter by user',
action: async () => {},
},
]);
}}>
<span class="timestamp">22:20</span>
<div class="activity-icon logout">
<dees-icon .icon=${'lucide:logOut'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged out
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:19</span>
<div class="activity-icon update">
<dees-icon .icon=${'lucide:checkCircle'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> approved a payment
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:18</span>
<div class="activity-icon view">
<dees-icon .icon=${'lucide:archive'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> archived an invoice
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:17</span>
<div class="activity-icon login">
<dees-icon .icon=${'lucide:logIn'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged in
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:16</span>
<div class="activity-icon logout">
<dees-icon .icon=${'lucide:logOut'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged out
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:15</span>
<div class="activity-icon update">
<dees-icon .icon=${'lucide:key'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> changed password
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:14</span>
<div class="activity-icon create">
<dees-icon .icon=${'lucide:userPlus'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> added a new user
</div>
</div>
<div class="activityentry">
<span class="timestamp">22:13</span>
<div class="activity-icon view">
<dees-icon .icon=${'lucide:messageCircle'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> contacted support
</div>
</div>
<div class="date-separator">Yesterday</div>
<div class="activityentry">
<span class="timestamp">18:45</span>
<div class="activity-icon update">
<dees-icon .icon=${'lucide:trash2'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> deleted an invoice
</div>
</div>
<div class="activityentry">
<span class="timestamp">17:30</span>
<div class="activity-icon login">
<dees-icon .icon=${'lucide:logIn'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged in
</div>
</div>
<div class="activityentry">
<span class="timestamp">16:15</span>
<div class="activity-icon logout">
<dees-icon .icon=${'lucide:logOut'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> logged out
</div>
</div>
<div class="activityentry">
<span class="timestamp">14:20</span>
<div class="activity-icon view">
<dees-icon .icon=${'lucide:barChart'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> viewed reports
</div>
</div>
<div class="activityentry">
<span class="timestamp">13:45</span>
<div class="activity-icon create">
<dees-icon .icon=${'lucide:send'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> sent an invoice
</div>
</div>
<div class="activityentry">
<span class="timestamp">13:30</span>
<div class="activity-icon create">
<dees-icon .icon=${'lucide:filePlus'}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">Max Mustermann</span> created a new invoice
</div>
</div>
<div class="streamingIndicator bottom">Loading History</div>
${filteredEntries.length > 0
? html`<div class="streamingIndicator">Live Updates</div>`
: ''}
${filteredEntries.length === 0
? html`<div class="empty-state">No activity entries</div>`
: groupedEntries.map(
(group) => html`
<div class="date-separator">${group.label}</div>
${group.entries.map((entry) => this.renderActivityEntry(entry))}
`
)}
</div>
<div class="searchbox">
<div class="search-wrapper">
<dees-icon class="search-icon" .icon=${'lucide:search'}></dees-icon>
<input type="text" placeholder="Search activities, users..." />
<input
type="text"
placeholder="Search activities, users..."
.value=${this.searchQuery}
@input=${this.handleSearchInput}
/>
</div>
</div>
<div class="topShadow"></div>
@@ -510,4 +386,205 @@ export class DeesAppuiActivitylog extends DeesElement {
</div>
`;
}
private renderActivityEntry(entry: IActivityEntry): TemplateResult {
const timestamp = entry.timestamp || new Date();
const timeStr = this.formatTime(timestamp);
const iconName = entry.iconName || this.getIconForType(entry.type);
return html`
<div
class="activityentry"
@contextmenu=${(e: MouseEvent) => this.handleContextMenu(e, entry)}
>
<span class="timestamp">${timeStr}</span>
<div class="activity-icon ${entry.type}">
<dees-icon .icon=${iconName}></dees-icon>
</div>
<div class="activity-text">
<span class="activity-user">${entry.user}</span> ${entry.message}
</div>
</div>
`;
}
// API METHODS
public add(entry: IActivityEntry): void {
const newEntry: IActivityEntry = {
...entry,
id: entry.id || this.generateId(),
timestamp: entry.timestamp || new Date(),
};
this.entries = [newEntry, ...this.entries];
this.entries$.next(this.entries);
}
public addMany(entries: IActivityEntry[]): void {
const newEntries = entries.map((entry) => ({
...entry,
id: entry.id || this.generateId(),
timestamp: entry.timestamp || new Date(),
}));
this.entries = [...newEntries.reverse(), ...this.entries];
this.entries$.next(this.entries);
}
public clear(): void {
this.entries = [];
this.entries$.next(this.entries);
}
public getEntries(): IActivityEntry[] {
return [...this.entries];
}
public filter(criteria: { user?: string; type?: IActivityEntry['type'] }): IActivityEntry[] {
return this.entries.filter((entry) => {
if (criteria.user && entry.user !== criteria.user) return false;
if (criteria.type && entry.type !== criteria.type) return false;
return true;
});
}
public search(query: string): IActivityEntry[] {
const lowerQuery = query.toLowerCase();
return this.entries.filter(
(entry) =>
entry.message.toLowerCase().includes(lowerQuery) ||
entry.user.toLowerCase().includes(lowerQuery)
);
}
// PRIVATE HELPERS
private generateId(): string {
return `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private getFilteredEntries(): IActivityEntry[] {
let result = this.entries;
if (this.searchQuery) {
const lowerQuery = this.searchQuery.toLowerCase();
result = result.filter(
(entry) =>
entry.message.toLowerCase().includes(lowerQuery) ||
entry.user.toLowerCase().includes(lowerQuery)
);
}
if (this.filterCriteria.user || this.filterCriteria.type) {
result = result.filter((entry) => {
if (this.filterCriteria.user && entry.user !== this.filterCriteria.user) return false;
if (this.filterCriteria.type && entry.type !== this.filterCriteria.type) return false;
return true;
});
}
return result;
}
private groupEntriesByDate(
entries: IActivityEntry[]
): Array<{ label: string; entries: IActivityEntry[] }> {
const groups: Map<string, IActivityEntry[]> = new Map();
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
for (const entry of entries) {
const date = entry.timestamp || new Date();
let label: string;
if (this.isSameDay(date, today)) {
label = 'Today';
} else if (this.isSameDay(date, yesterday)) {
label = 'Yesterday';
} else {
label = date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined,
});
}
if (!groups.has(label)) {
groups.set(label, []);
}
groups.get(label)!.push(entry);
}
return Array.from(groups.entries()).map(([label, entries]) => ({
label,
entries,
}));
}
private isSameDay(date1: Date, date2: Date): boolean {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
}
private formatTime(date: Date): string {
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
}
private getIconForType(type: IActivityEntry['type']): string {
const icons: Record<IActivityEntry['type'], string> = {
login: 'lucide:logIn',
logout: 'lucide:logOut',
view: 'lucide:eye',
create: 'lucide:plus',
update: 'lucide:edit',
delete: 'lucide:trash2',
custom: 'lucide:activity',
};
return icons[type] || icons.custom;
}
private handleSearchInput(e: InputEvent): void {
const target = e.target as HTMLInputElement;
this.searchQuery = target.value;
}
private handleContextMenu(e: MouseEvent, entry: IActivityEntry): void {
e.preventDefault();
DeesContextmenu.openContextMenuWithOptions(e, [
{
name: 'Copy activity',
iconName: 'lucide:copy',
action: async () => {
await navigator.clipboard.writeText(`${entry.user} ${entry.message}`);
},
},
{
name: 'Filter by user',
iconName: 'lucide:user',
action: async () => {
this.filterCriteria = { user: entry.user };
},
},
{
name: 'Filter by type',
iconName: 'lucide:filter',
action: async () => {
this.filterCriteria = { type: entry.type };
},
},
{
name: 'Clear filters',
iconName: 'lucide:x',
action: async () => {
this.filterCriteria = {};
this.searchQuery = '';
},
},
]);
}
}

View File

@@ -1,271 +0,0 @@
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

@@ -1,238 +1,608 @@
import { html, css } from '@design.estate/dees-element';
import type { DeesAppuiBase } from '../dees-appui-base/dees-appui-base.js';
import type { IAppBarMenuItem } from '../../interfaces/appbarmenuitem.js';
import type { ITab } from '../../interfaces/tab.js';
import type { ISelectionOption } from '../../interfaces/selectionoption.js';
import type { IMenuGroup } from '../../interfaces/menugroup.js';
import type { ISecondaryMenuGroup } from '../../interfaces/secondarymenu.js';
import * as plugins from '../../00plugins.js';
import { html, css, DeesElement, customElement, state } from '@design.estate/dees-element';
import type { DeesAppuiBase } from './dees-appui-base.js';
import type { IAppConfig, IViewActivationContext } from '../../interfaces/appconfig.js';
import '@design.estate/dees-wcctools/demotools';
// Demo view component with lifecycle hooks
@customElement('demo-dashboard-view')
class DemoDashboardView extends DeesElement {
@state()
accessor activated: boolean = false;
onActivate(context: IViewActivationContext) {
this.activated = true;
console.log('Dashboard activated with context:', context);
// Set view-specific secondary menu
context.appui.setSecondaryMenu({
heading: 'Dashboard',
groups: [
{
name: 'Quick Access',
iconName: 'lucide:zap',
items: [
{ key: 'overview', iconName: 'layoutDashboard', action: () => console.log('Overview') },
{ key: 'recent', iconName: 'clock', badge: 5, action: () => console.log('Recent') },
]
},
{
name: 'Analytics',
iconName: 'lucide:barChart3',
items: [
{ key: 'metrics', iconName: 'activity', action: () => console.log('Metrics') },
{ key: 'reports', iconName: 'fileText', badge: 'new', badgeVariant: 'success', action: () => console.log('Reports') },
]
}
]
});
// Set content tabs for dashboard
context.appui.setContentTabs([
{ key: 'Overview', iconName: 'lucide:layoutDashboard', action: () => console.log('Overview tab') },
{ key: 'Analytics', iconName: 'lucide:barChart', action: () => console.log('Analytics tab') },
{ key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports tab') },
]);
}
onDeactivate() {
this.activated = false;
console.log('Dashboard deactivated');
}
render() {
return html`
<style>
:host {
display: block;
padding: 40px;
color: #a3a3a3;
font-family: 'Geist Sans', 'Inter', -apple-system, sans-serif;
}
h1 { color: #fafafa; font-weight: 600; font-size: 24px; margin-bottom: 8px; }
p { color: #737373; margin-bottom: 32px; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.card {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 20px;
}
.card h3 { color: #fafafa; font-size: 14px; font-weight: 600; margin-bottom: 8px; }
.metric { font-size: 32px; font-weight: 700; color: #fafafa; }
.status { display: inline-block; padding: 2px 8px; border-radius: 9px; font-size: 12px; }
.status.active { background: #14532d; color: #4ade80; }
</style>
<h1>Dashboard</h1>
<p>Welcome back! Here's an overview of your system.</p>
<div class="grid">
<div class="card">
<h3>Active Users</h3>
<div class="metric">1,234</div>
<span class="status active">Online</span>
</div>
<div class="card">
<h3>API Calls</h3>
<div class="metric">45.2K</div>
<p style="color: #4ade80; font-size: 12px; margin: 0;">+12% from last hour</p>
</div>
<div class="card">
<h3>System Health</h3>
<div class="metric">99.9%</div>
<p style="color: #737373; font-size: 12px; margin: 0;">All systems operational</p>
</div>
</div>
`;
}
}
// Settings view with route params and canDeactivate guard
@customElement('demo-settings-view')
class DemoSettingsView extends DeesElement {
@state()
accessor section: string = 'general';
@state()
accessor hasChanges: boolean = false;
private appui: DeesAppuiBase;
onActivate(context: IViewActivationContext) {
this.appui = context.appui as any;
console.log('Settings activated with params:', context.params);
if (context.params?.section) {
this.section = context.params.section;
}
// Set settings-specific secondary menu
context.appui.setSecondaryMenu({
heading: 'Settings',
groups: [
{
name: 'Account',
iconName: 'lucide:user',
items: [
{ key: 'general', iconName: 'settings', action: () => this.showSection('general') },
{ key: 'profile', iconName: 'user', action: () => this.showSection('profile') },
{ key: 'security', iconName: 'shield', action: () => this.showSection('security') },
]
},
{
name: 'Preferences',
iconName: 'lucide:sliders',
items: [
{ key: 'notifications', iconName: 'bell', badge: 3, action: () => this.showSection('notifications') },
{ key: 'appearance', iconName: 'palette', action: () => this.showSection('appearance') },
]
}
]
});
context.appui.setSecondaryMenuSelection(this.section);
// Clear content tabs for settings
context.appui.setContentTabs([]);
}
onDeactivate() {
console.log('Settings deactivated');
this.hasChanges = false;
}
canDeactivate(): boolean | string {
if (this.hasChanges) {
return 'You have unsaved changes. Leave anyway?';
}
return true;
}
showSection(section: string) {
this.section = section;
this.appui?.setSecondaryMenuSelection(section);
}
simulateChange() {
this.hasChanges = true;
}
render() {
return html`
<style>
:host {
display: block;
padding: 40px;
color: #a3a3a3;
font-family: 'Geist Sans', 'Inter', -apple-system, sans-serif;
}
h1 { color: #fafafa; font-weight: 600; font-size: 24px; margin-bottom: 8px; }
p { color: #737373; margin-bottom: 24px; }
.section-name {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
padding: 24px;
font-size: 18px;
color: #fafafa;
margin-bottom: 16px;
}
.actions {
display: flex;
gap: 12px;
}
button {
background: #3b82f6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #2563eb;
}
.warning {
color: #fbbf24;
font-size: 13px;
margin-top: 16px;
}
</style>
<h1>Settings</h1>
<p>Manage your account and application preferences.</p>
<div class="section-name">
Current section: <strong>${this.section}</strong>
</div>
<div class="actions">
<button @click=${() => this.simulateChange()}>Make Changes</button>
</div>
${this.hasChanges ? html`<p class="warning">You have unsaved changes. Navigation will prompt for confirmation.</p>` : ''}
`;
}
}
// Projects view
@customElement('demo-projects-view')
class DemoProjectsView extends DeesElement {
onActivate(context: IViewActivationContext) {
context.appui.setSecondaryMenu({
heading: 'Projects',
groups: [
{
name: 'My Projects',
items: [
{ key: 'active', iconName: 'folder', badge: 3, action: () => console.log('Active') },
{ key: 'archived', iconName: 'archive', action: () => console.log('Archived') },
{ key: 'shared', iconName: 'users', badge: 2, badgeVariant: 'warning', action: () => console.log('Shared') },
]
}
]
});
context.appui.setContentTabs([
{ key: 'Grid', iconName: 'lucide:grid', action: () => console.log('Grid view') },
{ key: 'List', iconName: 'lucide:list', action: () => console.log('List view') },
{ key: 'Board', iconName: 'lucide:kanban', action: () => console.log('Board view') },
]);
}
render() {
return html`
<style>
:host {
display: block;
padding: 40px;
color: #a3a3a3;
font-family: 'Geist Sans', 'Inter', -apple-system, sans-serif;
}
h1 { color: #fafafa; font-weight: 600; font-size: 24px; margin-bottom: 24px; }
.projects {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.project {
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 20px;
cursor: pointer;
transition: border-color 0.2s;
}
.project:hover {
border-color: rgba(255,255,255,0.2);
}
.project h3 { color: #fafafa; margin: 0 0 8px 0; font-size: 16px; }
.project p { color: #737373; margin: 0; font-size: 13px; }
.badge {
display: inline-block;
background: #14532d;
color: #4ade80;
padding: 2px 8px;
border-radius: 9px;
font-size: 11px;
margin-left: 8px;
}
</style>
<h1>Projects</h1>
<div class="projects">
<div class="project">
<h3>Frontend App <span class="badge">Active</span></h3>
<p>React-based dashboard application</p>
</div>
<div class="project">
<h3>API Server <span class="badge">Active</span></h3>
<p>Node.js REST API backend</p>
</div>
<div class="project">
<h3>Mobile App <span class="badge">Active</span></h3>
<p>React Native iOS/Android app</p>
</div>
<div class="project">
<h3>Documentation</h3>
<p>Technical documentation site</p>
</div>
</div>
`;
}
}
// Tasks view showing inline template content
@customElement('demo-tasks-view')
class DemoTasksView extends DeesElement {
onActivate(context: IViewActivationContext) {
context.appui.setSecondaryMenu({
heading: 'Tasks',
groups: [
{
name: 'Filters',
items: [
{ key: 'all', iconName: 'list', badge: 12, action: () => console.log('All') },
{ key: 'today', iconName: 'calendar', badge: 3, action: () => console.log('Today') },
{ key: 'upcoming', iconName: 'clock', action: () => console.log('Upcoming') },
{ key: 'completed', iconName: 'checkCircle', action: () => console.log('Completed') },
]
}
]
});
context.appui.setContentTabs([
{ key: 'List', iconName: 'lucide:list', action: () => console.log('List') },
{ key: 'Calendar', iconName: 'lucide:calendar', action: () => console.log('Calendar') },
]);
}
render() {
return html`
<style>
:host {
display: block;
padding: 40px;
color: #a3a3a3;
font-family: 'Geist Sans', 'Inter', -apple-system, sans-serif;
}
h1 { color: #fafafa; font-weight: 600; font-size: 24px; margin-bottom: 24px; }
.task-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.task {
display: flex;
align-items: center;
gap: 12px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 12px 16px;
}
.checkbox {
width: 18px;
height: 18px;
border: 2px solid #525252;
border-radius: 4px;
cursor: pointer;
}
.task-text { color: #fafafa; flex: 1; }
.due-date { color: #737373; font-size: 12px; }
.priority {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
}
.priority.high { background: #450a0a; color: #f87171; }
.priority.medium { background: #451a03; color: #fbbf24; }
</style>
<h1>Tasks</h1>
<div class="task-list">
<div class="task">
<div class="checkbox"></div>
<span class="task-text">Review pull request #42</span>
<span class="due-date">Today</span>
<span class="priority high">High</span>
</div>
<div class="task">
<div class="checkbox"></div>
<span class="task-text">Update documentation</span>
<span class="due-date">Tomorrow</span>
<span class="priority medium">Medium</span>
</div>
<div class="task">
<div class="checkbox"></div>
<span class="task-text">Write unit tests</span>
<span class="due-date">Dec 20</span>
</div>
</div>
`;
}
}
export const demoFunc = () => {
// Menu items for the appbar
const menuItems: IAppBarMenuItem[] = [
{
name: 'File',
action: async () => {},
submenu: [
{ name: 'New Project', shortcut: 'Cmd+N', iconName: 'filePlus', action: async () => console.log('New project') },
{ name: 'Open Project...', shortcut: 'Cmd+O', iconName: 'folderOpen', action: async () => console.log('Open project') },
{ name: 'Recent Projects', action: async () => {}, submenu: [
{ name: 'my-app', action: async () => console.log('Open my-app') },
{ name: 'component-lib', action: async () => console.log('Open component-lib') },
{ name: 'api-server', action: async () => console.log('Open api-server') },
]},
// App configuration using the new unified API
const appConfig: IAppConfig = {
branding: {
logoIcon: 'lucide:box',
logoText: 'Acme App'
},
appBar: {
menuItems: [
{
name: 'File',
action: async () => {},
submenu: [
{ name: 'New Project', shortcut: 'Cmd+N', iconName: 'filePlus', action: async () => console.log('New') },
{ name: 'Open...', shortcut: 'Cmd+O', iconName: 'folderOpen', action: async () => console.log('Open') },
{ name: 'Recent Projects', action: async () => {}, submenu: [
{ name: 'my-app', action: async () => console.log('Open my-app') },
{ name: 'component-lib', action: async () => console.log('Open component-lib') },
]},
{ divider: true },
{ name: 'Save All', shortcut: 'Cmd+S', iconName: 'save', action: async () => console.log('Save') },
]
},
{
name: 'Edit',
action: async () => {},
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
{ divider: true },
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
]
},
{
name: 'View',
action: async () => {},
submenu: [
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
{ name: 'Toggle Activity Log', shortcut: 'Cmd+Shift+A', action: async () => console.log('Toggle activity') },
]
},
{
name: 'Help',
action: async () => {},
submenu: [
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Docs') },
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+/', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
}
],
breadcrumbs: 'Dashboard',
showWindowControls: true,
showSearch: true,
user: {
name: 'Jane Smith',
email: 'jane.smith@example.com',
status: 'online'
},
profileMenuItems: [
{ name: 'Profile', iconName: 'user', action: async () => console.log('Profile') },
{ name: 'Account Settings', iconName: 'settings', action: async () => console.log('Settings') },
{ divider: true },
{ name: 'Save All', shortcut: 'Cmd+Shift+S', iconName: 'save', action: async () => console.log('Save all') },
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
{ divider: true },
{ name: 'Close Project', action: async () => console.log('Close project') },
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
]
},
{
name: 'Edit',
action: async () => {},
submenu: [
{ name: 'Undo', shortcut: 'Cmd+Z', iconName: 'undo', action: async () => console.log('Undo') },
{ name: 'Redo', shortcut: 'Cmd+Shift+Z', iconName: 'redo', action: async () => console.log('Redo') },
{ divider: true },
{ name: 'Cut', shortcut: 'Cmd+X', iconName: 'scissors', action: async () => console.log('Cut') },
{ name: 'Copy', shortcut: 'Cmd+C', iconName: 'copy', action: async () => console.log('Copy') },
{ name: 'Paste', shortcut: 'Cmd+V', iconName: 'clipboard', action: async () => console.log('Paste') },
]
views: [
{
id: 'dashboard',
name: 'Dashboard',
iconName: 'lucide:home',
content: 'demo-dashboard-view',
route: 'dashboard'
},
{
id: 'projects',
name: 'Projects',
iconName: 'lucide:folder',
content: 'demo-projects-view',
route: 'projects',
badge: 3
},
{
id: 'tasks',
name: 'Tasks',
iconName: 'lucide:checkSquare',
content: 'demo-tasks-view',
route: 'tasks',
badge: 12
},
{
id: 'settings',
name: 'Settings',
iconName: 'lucide:settings',
content: 'demo-settings-view',
route: 'settings/:section?'
},
],
mainMenu: {
sections: [
{ name: 'Main', views: ['dashboard'] },
{ name: 'Workspace', views: ['projects', 'tasks'] },
],
bottomItems: ['settings']
},
{
name: 'View',
action: async () => {},
submenu: [
{ name: 'Toggle Sidebar', shortcut: 'Cmd+B', action: async () => console.log('Toggle sidebar') },
{ name: 'Toggle Terminal', shortcut: 'Cmd+J', iconName: 'terminal', action: async () => console.log('Toggle terminal') },
{ divider: true },
{ name: 'Zoom In', shortcut: 'Cmd++', iconName: 'zoomIn', action: async () => console.log('Zoom in') },
{ name: 'Zoom Out', shortcut: 'Cmd+-', iconName: 'zoomOut', action: async () => console.log('Zoom out') },
{ name: 'Reset Zoom', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
]
defaultView: 'dashboard',
onViewChange: (viewId, view) => {
console.log(`View changed to: ${viewId} (${view.name})`);
},
{
name: 'Help',
action: async () => {},
submenu: [
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
{ name: 'Release Notes', iconName: 'fileText', action: async () => console.log('Release notes') },
{ divider: true },
{ name: 'Report Issue', iconName: 'bug', action: async () => console.log('Report issue') },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
onSearch: (query) => {
console.log('Search query:', query);
}
];
};
// Main menu groups (left sidebar)
const mainMenuGroups: IMenuGroup[] = [
{
tabs: [
{ key: 'Dashboard', iconName: 'lucide:home', action: () => console.log('Dashboard selected') },
{ key: 'Inbox', iconName: 'lucide:inbox', action: () => console.log('Inbox selected') },
]
},
{
name: 'Workspace',
tabs: [
{ key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects selected') },
{ key: 'Tasks', iconName: 'lucide:checkSquare', action: () => console.log('Tasks selected') },
{ key: 'Documents', iconName: 'lucide:fileText', action: () => console.log('Documents selected') },
]
},
{
name: 'Analytics',
tabs: [
{ key: 'Reports', iconName: 'lucide:barChart3', action: () => console.log('Reports selected') },
{ key: 'Insights', iconName: 'lucide:lightbulb', action: () => console.log('Insights selected') },
]
}
];
// Use a container element to properly initialize the demo
const containerElement = document.createElement('div');
containerElement.className = 'demo-container';
containerElement.style.cssText = 'position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden;';
// Main menu bottom tabs (pinned to bottom)
const mainMenuBottomTabs: ITab[] = [
{ key: 'Settings', iconName: 'lucide:settings', action: () => console.log('Settings selected') },
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help selected') },
];
const appuiElement = document.createElement('dees-appui-base') as DeesAppuiBase;
containerElement.appendChild(appuiElement);
// Secondary menu groups (second sidebar with collapsible groups)
// These showcase the new shadcn-style design with badges and collapsible sections
const secondaryMenuGroups: ISecondaryMenuGroup[] = [
{
name: 'Quick Access',
iconName: 'lucide:zap',
items: [
{ key: 'Overview', iconName: 'layoutDashboard', action: () => console.log('Overview selected') },
{ key: 'Recent Activity', iconName: 'clock', action: () => console.log('Recent Activity selected'), badge: 5 },
{ key: 'Favorites', iconName: 'star', action: () => console.log('Favorites selected') },
]
},
{
name: 'Resources',
iconName: 'lucide:layers',
items: [
{ key: 'Components', iconName: 'package', action: () => console.log('Components selected'), badge: 24 },
{ key: 'Services', iconName: 'server', action: () => console.log('Services selected'), badge: 'new', badgeVariant: 'success' },
{ key: 'APIs', iconName: 'globe', action: () => console.log('APIs selected'), badge: 3, badgeVariant: 'warning' },
{ key: 'Webhooks', iconName: 'webhook', action: () => console.log('Webhooks selected') },
]
},
{
name: 'Data Management',
iconName: 'lucide:database',
items: [
{ key: 'Database', iconName: 'database', action: () => console.log('Database selected') },
{ key: 'Storage', iconName: 'hardDrive', action: () => console.log('Storage selected'), badge: '85%', badgeVariant: 'warning' },
{ key: 'Backups', iconName: 'archive', action: () => console.log('Backups selected'), badge: 'OK', badgeVariant: 'success' },
]
},
{
name: 'System',
iconName: 'lucide:settings',
collapsed: true,
items: [
{ key: 'Configuration', iconName: 'sliders', action: () => console.log('Configuration selected') },
{ key: 'Integrations', iconName: 'plug', action: () => console.log('Integrations selected'), badge: 2, badgeVariant: 'error' },
{ key: 'Permissions', iconName: 'shield', action: () => console.log('Permissions selected') },
{ key: 'Logs', iconName: 'fileText', action: () => console.log('Logs selected') },
]
}
];
// Initialize after element is connected
setTimeout(async () => {
await appuiElement.updateComplete;
// Main content tabs
const mainContentTabs: ITab[] = [
{ key: 'Details', iconName: 'lucide:file', action: () => console.log('Details tab') },
{ key: 'Logs', iconName: 'lucide:list', action: () => console.log('Logs tab') },
{ key: 'Metrics', iconName: 'lucide:lineChart', action: () => console.log('Metrics tab') },
];
// Configure using the unified API
appuiElement.configure(appConfig);
// Profile menu items
const profileMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string } | { divider: true })[] = [
{ name: 'Profile Settings', iconName: 'user', action: async () => console.log('Profile settings') },
{ name: 'Account', iconName: 'settings', action: async () => console.log('Account settings') },
{ divider: true },
{ name: 'Help & Support', iconName: 'helpCircle', action: async () => console.log('Help') },
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', shortcut: 'Cmd+K', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'Sign Out', iconName: 'logOut', action: async () => console.log('Sign out') }
];
// Add demo activity entries
setTimeout(() => {
appuiElement.activityLog.addMany([
{
type: 'login',
user: 'Jane Smith',
message: 'logged in from Chrome on macOS'
},
{
type: 'create',
user: 'Jane Smith',
message: 'created project "Frontend App"'
},
{
type: 'update',
user: 'John Doe',
message: 'updated API documentation'
},
{
type: 'view',
user: 'Jane Smith',
message: 'viewed dashboard analytics'
},
{
type: 'delete',
user: 'Admin',
message: 'removed deprecated endpoint'
},
{
type: 'custom',
user: 'System',
message: 'scheduled backup completed',
iconName: 'lucide:database'
}
]);
}, 500);
// Subscribe to view changes
appuiElement.viewChanged$.subscribe((event) => {
console.log('View changed event:', event);
// Update breadcrumbs based on view
appuiElement.setBreadcrumbs(event.view.name);
});
// Subscribe to lifecycle events
appuiElement.viewLifecycle$.subscribe((event) => {
console.log('Lifecycle event:', event.type, event.viewId);
});
// Demo: Dynamically update a badge after 5 seconds
setTimeout(() => {
appuiElement.setMainMenuBadge('tasks', 15);
appuiElement.activityLog.add({
type: 'update',
user: 'System',
message: 'new tasks added'
});
}, 5000);
}, 0);
return html`
<dees-demowrapper>
<style>
${css`
.demo-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
`}
</style>
<div class="demo-container">
<dees-appui-base
.appbarMenuItems=${menuItems}
.appbarBreadcrumbs=${'Dashboard'}
.appbarUser=${{
name: 'Jane Smith',
email: 'jane.smith@example.com',
status: 'online' as 'online' | 'offline' | 'busy' | 'away'
}}
.appbarProfileMenuItems=${profileMenuItems}
.appbarShowWindowControls=${true}
.appbarShowSearch=${true}
.mainmenuLogoIcon=${'lucide:box'}
.mainmenuLogoText=${'Acme App'}
.mainmenuGroups=${mainMenuGroups}
.mainmenuBottomTabs=${mainMenuBottomTabs}
.secondarymenuHeading=${'Dashboard'}
.secondarymenuGroups=${secondaryMenuGroups}
.maincontentTabs=${mainContentTabs}
@appbar-menu-select=${(e: CustomEvent) => console.log('Menu selected:', e.detail)}
@appbar-breadcrumb-navigate=${(e: CustomEvent) => console.log('Breadcrumb:', e.detail)}
@appbar-search-click=${() => console.log('Search clicked')}
@appbar-user-menu-open=${() => console.log('User menu opened')}
@appbar-profile-menu-select=${(e: CustomEvent) => console.log('Profile menu selected:', e.detail)}
@mainmenu-tab-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
@secondarymenu-item-select=${(e: CustomEvent) => console.log('Item selected:', e.detail)}
>
<div slot="maincontent" style="padding: 40px; color: #a3a3a3; font-family: 'Geist Sans', 'Inter', -apple-system, sans-serif;">
<h1 style="color: #fafafa; font-weight: 600; font-size: 24px; margin-bottom: 8px;">Welcome to Acme App</h1>
<p style="color: #737373; margin-bottom: 32px;">This demo showcases the AppUI component system with the new SecondaryMenu.</p>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 32px;">
<div style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 20px;">
<h3 style="color: #fafafa; font-size: 14px; font-weight: 600; margin-bottom: 8px;">SecondaryMenu Features</h3>
<ul style="margin: 0; padding-left: 20px; font-size: 13px; line-height: 1.8;">
<li>Collapsible groups with smooth animations</li>
<li>Badge support (counts, status, variants)</li>
<li>Dynamic heading from MainMenu selection</li>
<li>shadcn-inspired modern design</li>
</ul>
</div>
<div style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 20px;">
<h3 style="color: #fafafa; font-size: 14px; font-weight: 600; margin-bottom: 8px;">Badge Variants</h3>
<div style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px;">
<span style="background: #27272a; color: #a1a1aa; padding: 2px 8px; border-radius: 9px;">default</span>
<span style="background: #14532d; color: #4ade80; padding: 2px 8px; border-radius: 9px;">success</span>
<span style="background: #451a03; color: #fbbf24; padding: 2px 8px; border-radius: 9px;">warning</span>
<span style="background: #450a0a; color: #f87171; padding: 2px 8px; border-radius: 9px;">error</span>
</div>
</div>
</div>
<p style="font-size: 13px; color: #525252;">
Try clicking items in the MainMenu (left) - the SecondaryMenu heading updates automatically.
Click group headers in the SecondaryMenu to collapse/expand sections.
</p>
</div>
</dees-appui-base>
</div>
${containerElement}
</dees-demowrapper>
`;
};
};

View File

@@ -1,4 +1,2 @@
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,560 @@
# DeesAppuiBase
A comprehensive application shell component providing a complete UI framework with navigation, menus, activity logging, and view management.
## Quick Start
```typescript
import { html, DeesElement, customElement } from '@design.estate/dees-element';
import { DeesAppuiBase } from '@design.estate/dees-catalog';
@customElement('my-app')
class MyApp extends DeesElement {
private appui: DeesAppuiBase;
async firstUpdated() {
this.appui = this.shadowRoot.querySelector('dees-appui-base');
// Configure with views and menu
this.appui.configure({
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: [{ name: 'Main', views: ['dashboard', 'settings'] }]
},
defaultView: 'dashboard'
});
}
render() {
return html`<dees-appui-base></dees-appui-base>`;
}
}
```
## Configuration API
### `configure(config: IAppConfig)`
Configure the entire application shell with a single configuration object.
```typescript
interface IAppConfig {
branding?: IBrandingConfig;
appBar?: IAppBarConfig;
views: IViewDefinition[];
mainMenu?: IMainMenuConfig;
defaultView?: string;
activityLog?: IActivityLogConfig;
onViewChange?: (viewId: string, view: IViewDefinition) => void;
onSearch?: (query: string) => void;
}
```
### View Definition
```typescript
interface IViewDefinition {
id: string; // Unique identifier
name: string; // Display name
iconName?: string; // Icon (e.g., 'lucide:home')
content: // View content
| string // Tag name ('my-component')
| (new () => HTMLElement) // Class constructor
| (() => TemplateResult) // Template function
| (() => Promise<...>); // Async for lazy loading
secondaryMenu?: ISecondaryMenuGroup[];
contentTabs?: ITab[];
route?: string; // URL route (default: id)
badge?: string | number;
cache?: boolean; // Cache view instance (default: true)
}
```
---
## Programmatic APIs
### App Bar API
Control the top application bar.
```typescript
// Set menu items (File, Edit, View, etc.)
appui.setAppBarMenus([
{
name: 'File',
submenu: [
{ name: 'New', shortcut: 'Cmd+N', action: () => {} },
{ name: 'Save', shortcut: 'Cmd+S', action: () => {} },
]
}
]);
// Update single menu
appui.updateAppBarMenu('File', { submenu: [...newItems] });
// Breadcrumbs
appui.setBreadcrumbs('Dashboard > Settings > Profile');
appui.setBreadcrumbs(['Dashboard', 'Settings', 'Profile']);
// User profile
appui.setUser({
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatars/john.png',
status: 'online' // 'online' | 'offline' | 'busy' | 'away'
});
appui.setProfileMenuItems([
{ name: 'Profile', iconName: 'lucide:user', action: () => {} },
{ divider: true },
{ name: 'Sign Out', iconName: 'lucide:log-out', action: () => {} }
]);
// Search
appui.setSearchVisible(true);
appui.onSearch((query) => console.log('Search:', query));
// Window controls (for Electron/Tauri apps)
appui.setWindowControlsVisible(false);
```
### Main Menu API (Left Sidebar)
Control the main navigation menu.
```typescript
// Set entire menu
appui.setMainMenu({
logoIcon: 'lucide:box',
logoText: 'My App',
groups: [
{
name: 'Main',
tabs: [
{ key: 'dashboard', iconName: 'lucide:home', action: () => {} },
{ key: 'inbox', iconName: 'lucide:inbox', badge: 5, action: () => {} },
]
}
],
bottomTabs: [
{ key: 'settings', iconName: 'lucide:settings', action: () => {} }
]
});
// Update specific group
appui.updateMainMenuGroup('Main', { tabs: [...newTabs] });
// Add/remove items
appui.addMainMenuItem('Main', { key: 'tasks', iconName: 'lucide:check', action: () => {} });
appui.removeMainMenuItem('Main', 'tasks');
// Selection
appui.setMainMenuSelection('dashboard');
appui.setMainMenuCollapsed(true);
// Badges
appui.setMainMenuBadge('inbox', 12);
appui.clearMainMenuBadge('inbox');
```
### Secondary Menu API
Views can control the secondary (contextual) menu.
```typescript
// Set menu
appui.setSecondaryMenu({
heading: 'Settings',
groups: [
{
name: 'Account',
items: [
{ key: 'profile', iconName: 'lucide:user', action: () => {} },
{ key: 'security', iconName: 'lucide:shield', action: () => {} },
]
}
]
});
// Update group
appui.updateSecondaryMenuGroup('Account', { items: newItems });
// Add item
appui.addSecondaryMenuItem('Account', {
key: 'notifications',
iconName: 'lucide:bell',
action: () => {}
});
// Selection
appui.setSecondaryMenuSelection('profile');
// Clear
appui.clearSecondaryMenu();
```
### Content Tabs API
Control tabs in the main content area.
```typescript
// Set tabs
appui.setContentTabs([
{ key: 'code', iconName: 'lucide:code', action: () => {} },
{ key: 'preview', iconName: 'lucide:eye', action: () => {} }
]);
// Add/remove
appui.addContentTab({ key: 'debug', iconName: 'lucide:bug', action: () => {} });
appui.removeContentTab('debug');
// Select
appui.selectContentTab('preview');
// Get current
const current = appui.getSelectedContentTab();
```
### Activity Log API
Add activity entries to the right-side activity log.
```typescript
// Add single entry
appui.activityLog.add({
type: 'create', // 'login' | 'logout' | 'view' | 'create' | 'update' | 'delete' | 'custom'
user: 'John Doe',
message: 'created a new invoice',
iconName: 'lucide:file-plus', // Optional custom icon
data: { invoiceId: '123' } // Optional metadata
});
// Add multiple
appui.activityLog.addMany([...entries]);
// Clear
appui.activityLog.clear();
// Query
const entries = appui.activityLog.getEntries();
const filtered = appui.activityLog.filter({ user: 'John', type: 'create' });
const searched = appui.activityLog.search('invoice');
```
### Navigation API
Navigate between views programmatically.
```typescript
// Navigate to view
await appui.navigateToView('settings');
await appui.navigateToView('settings', { section: 'profile' });
// Get current view
const current = appui.getCurrentView();
// Subscribe to view changes
appui.viewChanged$.subscribe((event) => {
console.log(`Navigated to: ${event.viewId}`);
});
// Subscribe to lifecycle events
appui.viewLifecycle$.subscribe((event) => {
if (event.type === 'activated') {
console.log(`View ${event.viewId} activated`);
}
});
```
---
## View Lifecycle Hooks
Views can implement lifecycle hooks to respond to activation/deactivation.
```typescript
import { DeesElement, customElement } from '@design.estate/dees-element';
import type { IViewActivationContext, IViewLifecycle } from '@design.estate/dees-catalog';
@customElement('my-settings-view')
class MySettingsView extends DeesElement implements IViewLifecycle {
/**
* Called when view is activated (displayed)
* Receives typed context with appui reference
*/
async onActivate(context: IViewActivationContext) {
const { appui, viewId, params } = context;
// Set view-specific secondary menu
appui.setSecondaryMenu({
heading: 'Settings',
groups: [{ name: 'Options', items: [...] }]
});
// Set view-specific tabs
appui.setContentTabs([...]);
// Load data based on route params
if (params?.section) {
await this.loadSection(params.section);
}
}
/**
* Called when view is deactivated (hidden)
*/
onDeactivate() {
this.cleanup();
}
/**
* Called before navigation away
* Return false or a message string to block navigation
*/
canDeactivate(): boolean | string {
if (this.hasUnsavedChanges) {
return 'You have unsaved changes. Leave anyway?';
}
return true;
}
}
```
### IViewActivationContext
```typescript
interface IViewActivationContext {
appui: DeesAppuiBase; // Reference to the app shell
viewId: string; // The view ID being activated
params?: Record<string, string>; // Route parameters
}
```
---
## Routing
Routes are automatically registered from view definitions using `domtools.router`.
```typescript
const views = [
{ id: 'dashboard', route: 'dashboard', ... },
{ id: 'settings', route: 'settings/:section?', ... }, // Parameterized
{ id: 'user', route: 'users/:id', ... },
];
// URL: #dashboard → navigates to dashboard view
// URL: #settings/profile → navigates to settings with params.section = 'profile'
// URL: #users/123 → navigates to user with params.id = '123'
```
### Hash-based Routing
The router uses hash-based routing by default (`#viewId`). URLs are automatically synchronized when navigating via `navigateToView()`.
---
## View Caching
Views are cached by default. When navigating away and back, the same DOM element is reused (hidden/shown) rather than destroyed and recreated.
```typescript
// Disable caching for a specific view
{
id: 'reports',
name: 'Reports',
content: 'my-reports-view',
cache: false // Always recreate this view
}
```
---
## Lazy Loading
Use async content functions for lazy loading views.
```typescript
{
id: 'analytics',
name: 'Analytics',
content: async () => {
const module = await import('./views/analytics.js');
return module.AnalyticsView;
}
}
```
---
## RxJS Observables
The component exposes RxJS Subjects for reactive programming.
```typescript
// View lifecycle events
appui.viewLifecycle$.subscribe((event) => {
// event.type: 'loading' | 'activated' | 'deactivated' | 'loaded' | 'loadError'
// event.viewId: string
// event.element?: HTMLElement
// event.params?: Record<string, string>
// event.error?: unknown
});
// View change events
appui.viewChanged$.subscribe((event) => {
// event.viewId: string
// event.view: IViewDefinition
// event.previousView?: IViewDefinition
// event.params?: Record<string, string>
});
```
---
## Complete Example
```typescript
import { html, DeesElement, customElement } from '@design.estate/dees-element';
import { DeesAppuiBase, IViewActivationContext } from '@design.estate/dees-catalog';
@customElement('my-app')
class MyApp extends DeesElement {
private appui: DeesAppuiBase;
async firstUpdated() {
this.appui = this.shadowRoot.querySelector('dees-appui-base');
this.appui.configure({
branding: {
logoIcon: 'lucide:briefcase',
logoText: 'CRM Pro'
},
appBar: {
menuItems: [
{ name: 'File', submenu: [...] },
{ name: 'Edit', submenu: [...] }
],
showSearch: true,
user: { name: 'Jane Smith', status: 'online' }
},
views: [
{
id: 'dashboard',
name: 'Dashboard',
iconName: 'lucide:home',
content: 'crm-dashboard',
route: 'dashboard'
},
{
id: 'contacts',
name: 'Contacts',
iconName: 'lucide:users',
content: 'crm-contacts',
route: 'contacts',
badge: 42
},
{
id: 'settings',
name: 'Settings',
iconName: 'lucide:settings',
content: 'crm-settings',
route: 'settings/:section?'
}
],
mainMenu: {
sections: [
{ name: 'Main', views: ['dashboard', 'contacts'] }
],
bottomItems: ['settings']
},
defaultView: 'dashboard',
onViewChange: (viewId, view) => {
console.log(`Navigated to: ${view.name}`);
},
onSearch: (query) => {
console.log(`Search: ${query}`);
}
});
// Load activity from backend
const activities = await fetch('/api/activities').then(r => r.json());
this.appui.activityLog.addMany(activities);
}
render() {
return html`<dees-appui-base></dees-appui-base>`;
}
}
// View with lifecycle hooks
@customElement('crm-settings')
class CrmSettings extends DeesElement {
private appui: DeesAppuiBase;
onActivate(context: IViewActivationContext) {
this.appui = context.appui;
// Set secondary menu for settings
this.appui.setSecondaryMenu({
heading: 'Settings',
groups: [
{
name: 'Account',
items: [
{ key: 'profile', iconName: 'lucide:user', action: () => this.showSection('profile') },
{ key: 'security', iconName: 'lucide:shield', action: () => this.showSection('security') }
]
},
{
name: 'Preferences',
items: [
{ key: 'notifications', iconName: 'lucide:bell', action: () => this.showSection('notifications') }
]
}
]
});
// Navigate to section from URL params
if (context.params?.section) {
this.showSection(context.params.section);
}
}
showSection(section: string) {
this.appui.setSecondaryMenuSelection(section);
// ... load section content
}
}
```
---
## TypeScript Types
All interfaces are exported from `@design.estate/dees-catalog`:
- `IAppConfig` - Main configuration
- `IViewDefinition` - View definition
- `IViewActivationContext` - Context passed to `onActivate`
- `IViewLifecycle` - Lifecycle hooks interface
- `IViewLifecycleEvent` - Lifecycle event for rxjs Subject
- `IViewChangeEvent` - View change event
- `IAppUser` - User configuration
- `IActivityEntry` - Activity log entry
- `IActivityLogAPI` - Activity log methods
- `IAppBarMenuItem` - App bar menu item
- `IMainMenuConfig` - Main menu configuration
- `ISecondaryMenuGroup` - Secondary menu group
- `ITab` - Tab definition

View File

@@ -1,185 +0,0 @@
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

@@ -1,13 +1,31 @@
import { html, render, type TemplateResult } from '@design.estate/dees-element';
import type { IViewDefinition } from '../../interfaces/appconfig.js';
import type {
IViewDefinition,
IViewActivationContext,
IViewLifecycle,
TDeesAppuiBase
} from '../../interfaces/appconfig.js';
/**
* Registry for managing views and their lifecycle
*
* Key features:
* - View caching with hide/show pattern (not destroy/create)
* - Async content loading support (lazy loading)
* - View lifecycle hooks (onActivate, onDeactivate, canDeactivate)
*/
export class ViewRegistry {
private views: Map<string, IViewDefinition> = new Map();
private instances: Map<string, HTMLElement> = new Map();
private currentViewId: string | null = null;
private appui: TDeesAppuiBase | null = null;
/**
* Set the appui reference for view activation context
*/
public setAppuiRef(appui: TDeesAppuiBase): void {
this.appui = appui;
}
/**
* Register a single view
@@ -56,20 +74,222 @@ export class ViewRegistry {
}
/**
* Find view by route
* Find view by route (supports parameterized routes like 'settings/:section')
*/
public findByRoute(route: string): IViewDefinition | undefined {
public findByRoute(route: string): { view: IViewDefinition; params: Record<string, string> } | undefined {
for (const view of this.views.values()) {
const viewRoute = view.route || view.id;
if (viewRoute === route) {
return view;
const params = this.matchRoute(viewRoute, route);
if (params !== null) {
return { view, params };
}
}
return undefined;
}
/**
* Render a view's content into a container
* Match a route pattern against an actual route
* Returns params if matched, null otherwise
*/
private matchRoute(pattern: string, route: string): Record<string, string> | null {
const patternParts = pattern.split('/');
const routeParts = route.split('/');
// Check for optional trailing param (ends with ?)
const hasOptionalParam = patternParts.length > 0 &&
patternParts[patternParts.length - 1].endsWith('?');
if (hasOptionalParam) {
// Allow route to be shorter by 1
if (routeParts.length < patternParts.length - 1 || routeParts.length > patternParts.length) {
return null;
}
} else if (patternParts.length !== routeParts.length) {
return null;
}
const params: Record<string, string> = {};
for (let i = 0; i < patternParts.length; i++) {
let part = patternParts[i];
const isOptional = part.endsWith('?');
if (isOptional) {
part = part.slice(0, -1);
}
if (part.startsWith(':')) {
// This is a parameter
const paramName = part.slice(1);
if (routeParts[i] !== undefined) {
params[paramName] = routeParts[i];
} else if (!isOptional) {
return null;
}
} else if (routeParts[i] !== part) {
return null;
}
}
return params;
}
/**
* Check if navigation away from current view is allowed
*/
public async canLeaveCurrentView(): Promise<boolean | string> {
if (!this.currentViewId) return true;
const instance = this.instances.get(this.currentViewId);
if (!instance) return true;
const lifecycle = instance as unknown as IViewLifecycle;
if (typeof lifecycle.canDeactivate === 'function') {
return await lifecycle.canDeactivate();
}
return true;
}
/**
* Activate a view - handles caching, lifecycle, and rendering
*/
public async activateView(
viewId: string,
container: HTMLElement,
params?: Record<string, string>
): Promise<HTMLElement | null> {
const view = this.views.get(viewId);
if (!view) {
console.error(`View "${viewId}" not found in registry`);
return null;
}
// Check if caching is enabled for this view (default: true)
const shouldCache = view.cache !== false;
// Deactivate current view
if (this.currentViewId && this.currentViewId !== viewId) {
await this.deactivateView(this.currentViewId);
}
// Check for cached instance
let element = shouldCache ? this.instances.get(viewId) : undefined;
if (element) {
// Reuse cached instance - just show it
element.style.display = '';
} else {
// Create new instance
element = await this.createViewElement(view);
if (!element) {
console.error(`Failed to create element for view "${viewId}"`);
return null;
}
// Add to container
container.appendChild(element);
// Cache if enabled
if (shouldCache) {
this.instances.set(viewId, element);
}
}
this.currentViewId = viewId;
// Call onActivate lifecycle hook
await this.callOnActivate(element, viewId, params);
return element;
}
/**
* Deactivate a view (hide and call lifecycle hook)
*/
private async deactivateView(viewId: string): Promise<void> {
const instance = this.instances.get(viewId);
if (!instance) return;
// Call onDeactivate lifecycle hook
const lifecycle = instance as unknown as IViewLifecycle;
if (typeof lifecycle.onDeactivate === 'function') {
await lifecycle.onDeactivate();
}
// Hide the element
instance.style.display = 'none';
}
/**
* Create a view element from its definition (supports async content)
*/
private async createViewElement(view: IViewDefinition): Promise<HTMLElement | null> {
let content = view.content;
// Handle async content (lazy loading)
if (typeof content === 'function' &&
!(content.prototype instanceof HTMLElement) &&
content.constructor.name === 'AsyncFunction') {
try {
content = await (content as () => Promise<string | (new () => HTMLElement) | (() => TemplateResult)>)();
} catch (error) {
console.error(`Failed to load async content for view "${view.id}":`, error);
return null;
}
}
let element: HTMLElement;
if (typeof content === 'string') {
// Tag name string
element = document.createElement(content);
} else if (typeof content === 'function') {
// Check if it's a class constructor or template function
if (content.prototype instanceof HTMLElement) {
// Element class constructor
element = new (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 = (content as () => TemplateResult)();
render(template, wrapper);
element = wrapper;
}
} else {
console.error(`Invalid content type for view "${view.id}"`);
return null;
}
// Add view ID as data attribute for debugging
element.dataset.viewId = view.id;
return element;
}
/**
* Call onActivate lifecycle hook on a view element
*/
private async callOnActivate(
element: HTMLElement,
viewId: string,
params?: Record<string, string>
): Promise<void> {
const lifecycle = element as unknown as IViewLifecycle;
if (typeof lifecycle.onActivate === 'function') {
const context: IViewActivationContext = {
appui: this.appui!,
viewId,
params,
};
await lifecycle.onActivate(context);
}
}
/**
* Legacy method - renders view without caching
* @deprecated Use activateView instead
*/
public renderView(viewId: string, container: HTMLElement): HTMLElement | null {
const view = this.views.get(viewId);
@@ -78,25 +298,22 @@ export class ViewRegistry {
return null;
}
// Clear container
// For legacy compatibility, clear container
container.innerHTML = '';
let element: HTMLElement;
const content = view.content;
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)();
if (typeof content === 'string') {
element = document.createElement(content);
} else if (typeof content === 'function') {
if ((content as any).prototype instanceof HTMLElement) {
element = new (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)();
const template = (content as () => TemplateResult)();
render(template, wrapper);
element = wrapper;
}
@@ -126,10 +343,29 @@ export class ViewRegistry {
return this.instances.get(viewId);
}
/**
* Clear a specific cached instance
*/
public clearInstance(viewId: string): void {
const instance = this.instances.get(viewId);
if (instance && instance.parentNode) {
instance.parentNode.removeChild(instance);
}
this.instances.delete(viewId);
if (this.currentViewId === viewId) {
this.currentViewId = null;
}
}
/**
* Clear all instances
*/
public clearInstances(): void {
for (const [viewId, instance] of this.instances) {
if (instance.parentNode) {
instance.parentNode.removeChild(instance);
}
}
this.instances.clear();
this.currentViewId = null;
}
@@ -138,7 +374,7 @@ export class ViewRegistry {
* Unregister a view
*/
public unregister(viewId: string): boolean {
this.instances.delete(viewId);
this.clearInstance(viewId);
return this.views.delete(viewId);
}
@@ -147,8 +383,7 @@ export class ViewRegistry {
*/
public clear(): void {
this.views.clear();
this.instances.clear();
this.currentViewId = null;
this.clearInstances();
}
/**

View File

@@ -13,6 +13,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import '../dees-appui-tabs/dees-appui-tabs.js';
import type { DeesAppuiTabs } from '../dees-appui-tabs/dees-appui-tabs.js';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-maincontent')
export class DeesAppuiMaincontent extends DeesElement {
@@ -35,16 +36,18 @@ export class DeesAppuiMaincontent extends DeesElement {
@property({
type: Array,
})
accessor tabs: interfaces.ITab[] = [
accessor tabs: interfaces.IMenuItem[] = [
{ key: '⚠️ Please set tabs', action: () => console.warn('No tabs configured for maincontent') },
];
@property({ type: Object })
accessor selectedTab: interfaces.ITab | null = null;
accessor selectedTab: interfaces.IMenuItem | null = null;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('#333', '#fff')};
display: block;

View File

@@ -13,6 +13,7 @@ import {
} from '@design.estate/dees-element';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import { demoFunc } from './dees-appui-mainmenu.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
/**
* the most left menu
@@ -37,21 +38,23 @@ export class DeesAppuiMainmenu extends DeesElement {
// Bottom tabs (pinned to bottom)
@property({ type: Array })
accessor bottomTabs: interfaces.ITab[] = [];
accessor bottomTabs: interfaces.IMenuItem[] = [];
// Legacy tabs property (for backward compatibility)
@property({ type: Array })
accessor tabs: interfaces.ITab[] = [];
accessor tabs: interfaces.IMenuItem[] = [];
@property()
accessor selectedTab: interfaces.ITab;
accessor selectedTab: interfaces.IMenuItem;
@property({ type: Boolean, reflect: true })
accessor collapsed: boolean = false;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
--menu-width-expanded: 200px;
--menu-width-collapsed: 56px;
@@ -390,7 +393,7 @@ export class DeesAppuiMainmenu extends DeesElement {
<div class="menuGroup">
${group.name ? html`<div class="groupHeader">${group.name}</div>` : ''}
<div class="groupTabs">
${group.tabs.map((tabArg) => this.renderTab(tabArg))}
${group.items.map((tabArg) => this.renderTab(tabArg))}
</div>
</div>
`)}
@@ -407,7 +410,7 @@ export class DeesAppuiMainmenu extends DeesElement {
`;
}
private renderTab(tabArg: interfaces.ITab): TemplateResult {
private renderTab(tabArg: interfaces.IMenuItem): TemplateResult {
return html`
<div
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
@@ -422,15 +425,15 @@ export class DeesAppuiMainmenu extends DeesElement {
`;
}
private getAllTabs(): interfaces.ITab[] {
private getAllTabs(): interfaces.IMenuItem[] {
if (this.menuGroups.length > 0) {
const groupTabs = this.menuGroups.flatMap(group => group.tabs);
const groupTabs = this.menuGroups.flatMap(group => group.items);
return [...groupTabs, ...this.bottomTabs];
}
return [...this.tabs, ...this.bottomTabs];
}
updateTab(tabArg: interfaces.ITab) {
updateTab(tabArg: interfaces.IMenuItem) {
this.selectedTab = tabArg;
this.selectedTab.action();

View File

@@ -11,6 +11,7 @@ import {
cssManager,
state,
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-profiledropdown')
export class DeesAppuiProfileDropdown extends DeesElement {
@@ -53,8 +54,10 @@ export class DeesAppuiProfileDropdown extends DeesElement {
accessor position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' = 'top-right';
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
position: absolute;

View File

@@ -44,7 +44,7 @@ export const demoFunc = () => html`
{ key: 'Integrations', iconName: 'plug', action: () => console.log('Integrations'), badge: 5, badgeVariant: 'error' },
]
}
] as interfaces.ISecondaryMenuGroup[]}
] as interfaces.IMenuGroup[]}
@item-select=${(e: CustomEvent) => console.log('Selected:', e.detail)}
></dees-appui-secondarymenu>
<div class="spacer"></div>

View File

@@ -15,6 +15,7 @@ import {
cssManager,
} from '@design.estate/dees-element';
import { demoFunc } from './dees-appui-secondarymenu.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
/**
* Secondary navigation menu for sub-navigation within MainMenu views
@@ -32,15 +33,15 @@ export class DeesAppuiSecondarymenu extends DeesElement {
/** Grouped items with collapse support */
@property({ type: Array })
accessor groups: interfaces.ISecondaryMenuGroup[] = [];
accessor groups: interfaces.IMenuGroup[] = [];
/** Legacy flat list support for backward compatibility */
@property({ type: Array })
accessor selectionOptions: (interfaces.ISelectionOption | { divider: true })[] = [];
accessor selectionOptions: (interfaces.IMenuItem | { divider: true })[] = [];
/** Currently selected item */
@property({ type: Object })
accessor selectedItem: interfaces.ISecondaryMenuItem | null = null;
accessor selectedItem: interfaces.IMenuItem | null = null;
/** Internal state for collapsed groups */
@state()
@@ -51,8 +52,10 @@ export class DeesAppuiSecondarymenu extends DeesElement {
accessor collapsed: boolean = false;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
--sidebar-width-expanded: 240px;
--sidebar-width-collapsed: 56px;
@@ -482,7 +485,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
`;
}
private renderMenuItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): TemplateResult {
private renderMenuItem(item: interfaces.IMenuItem, group?: interfaces.IMenuGroup): TemplateResult {
const isSelected = this.selectedItem?.key === item.key;
return html`
<div
@@ -507,7 +510,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
if ('divider' in option && option.divider) {
return html`<div class="divider"></div>`;
}
const item = option as interfaces.ISelectionOption;
const item = option as interfaces.IMenuItem;
return this.renderMenuItem({
key: item.key,
iconName: item.iconName,
@@ -537,7 +540,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
}));
}
private selectItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): void {
private selectItem(item: interfaces.IMenuItem, group?: interfaces.IMenuGroup): void {
this.selectedItem = item;
item.action();
@@ -548,7 +551,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
}));
}
private handleContextMenu(event: MouseEvent, item: interfaces.ISecondaryMenuItem): void {
private handleContextMenu(event: MouseEvent, item: interfaces.IMenuItem): void {
DeesContextmenu.openContextMenuWithOptions(event, [
{
name: 'View details',
@@ -582,7 +585,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
}
} 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;
const firstOption = this.selectionOptions.find(opt => !('divider' in opt)) as interfaces.IMenuItem;
if (firstOption && !this.selectedItem) {
this.selectItem({
key: firstOption.key,

View File

@@ -0,0 +1,91 @@
import { html, cssManager } from '@design.estate/dees-element';
import * as interfaces from '../../interfaces/index.js';
export const demoFunc = () => {
const horizontalTabs: interfaces.IMenuItem[] = [
{ key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') },
{ key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') },
{ key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') },
{ key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') },
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') },
];
const verticalTabs: interfaces.IMenuItem[] = [
{ key: 'Profile', iconName: 'lucide:user', action: () => console.log('Profile clicked') },
{ key: 'Security', iconName: 'lucide:shield', action: () => console.log('Security clicked') },
{ key: 'Notifications', iconName: 'lucide:bell', action: () => console.log('Notifications clicked') },
{ key: 'Integrations', iconName: 'lucide:link', action: () => console.log('Integrations clicked') },
{ key: 'Advanced', iconName: 'lucide:code', action: () => console.log('Advanced clicked') },
];
const noIndicatorTabs: interfaces.IMenuItem[] = [
{ key: 'All', action: () => console.log('All clicked') },
{ key: 'Active', action: () => console.log('Active clicked') },
{ key: 'Completed', action: () => console.log('Completed clicked') },
{ key: 'Archived', action: () => console.log('Archived clicked') },
];
const demoContent = (text: string) => html`
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
${text}
</div>
`;
return html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 32px;
padding: 48px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.two-column {
display: grid;
grid-template-columns: 200px 1fr;
gap: 24px;
align-items: start;
}
</style>
<div class="demo-container">
<div class="section">
<div class="section-title">Horizontal Tabs with Animated Indicator</div>
<dees-appui-tabs .tabs=${horizontalTabs}>
${demoContent('Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.')}
</dees-appui-tabs>
</div>
<div class="section">
<div class="section-title">Vertical Tabs Layout</div>
<div class="two-column">
<dees-appui-tabs .tabStyle=${'vertical'} .tabs=${verticalTabs}></dees-appui-tabs>
${demoContent('Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.')}
</div>
</div>
<div class="section">
<div class="section-title">Without Indicator</div>
<dees-appui-tabs .showTabIndicator=${false} .tabs=${noIndicatorTabs}>
${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')}
</dees-appui-tabs>
</div>
</div>
`;
};

View File

@@ -11,106 +11,21 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-appui-tabs.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-tabs')
export class DeesAppuiTabs extends DeesElement {
public static demo = () => {
const horizontalTabs: interfaces.ITab[] = [
{ key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') },
{ key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') },
{ key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') },
{ key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') },
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') },
];
const verticalTabs: interfaces.ITab[] = [
{ key: 'Profile', iconName: 'lucide:user', action: () => console.log('Profile clicked') },
{ key: 'Security', iconName: 'lucide:shield', action: () => console.log('Security clicked') },
{ key: 'Notifications', iconName: 'lucide:bell', action: () => console.log('Notifications clicked') },
{ key: 'Integrations', iconName: 'lucide:link', action: () => console.log('Integrations clicked') },
{ key: 'Advanced', iconName: 'lucide:code', action: () => console.log('Advanced clicked') },
];
const noIndicatorTabs: interfaces.ITab[] = [
{ key: 'All', action: () => console.log('All clicked') },
{ key: 'Active', action: () => console.log('Active clicked') },
{ key: 'Completed', action: () => console.log('Completed clicked') },
{ key: 'Archived', action: () => console.log('Archived clicked') },
];
const demoContent = (text: string) => html`
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
${text}
</div>
`;
return html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 32px;
padding: 48px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.two-column {
display: grid;
grid-template-columns: 200px 1fr;
gap: 24px;
align-items: start;
}
</style>
<div class="demo-container">
<div class="section">
<div class="section-title">Horizontal Tabs with Animated Indicator</div>
<dees-appui-tabs .tabs=${horizontalTabs}>
${demoContent('Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.')}
</dees-appui-tabs>
</div>
<div class="section">
<div class="section-title">Vertical Tabs Layout</div>
<div class="two-column">
<dees-appui-tabs .tabStyle=${'vertical'} .tabs=${verticalTabs}></dees-appui-tabs>
${demoContent('Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.')}
</div>
</div>
<div class="section">
<div class="section-title">Without Indicator</div>
<dees-appui-tabs .showTabIndicator=${false} .tabs=${noIndicatorTabs}>
${demoContent('Tabs can also be used without the animated indicator by setting showTabIndicator to false.')}
</dees-appui-tabs>
</div>
</div>
`;
};
public static demo = demoFunc;
// INSTANCE
@property({
type: Array,
})
accessor tabs: interfaces.ITab[] = [];
accessor tabs: interfaces.IMenuItem[] = [];
@property({ type: Object })
accessor selectedTab: interfaces.ITab | null = null;
accessor selectedTab: interfaces.IMenuItem | null = null;
@property({ type: Boolean })
accessor showTabIndicator: boolean = true;
@@ -119,8 +34,10 @@ export class DeesAppuiTabs extends DeesElement {
accessor tabStyle: 'horizontal' | 'vertical' = 'horizontal';
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
position: relative;
@@ -312,7 +229,7 @@ export class DeesAppuiTabs extends DeesElement {
`;
}
private renderTab(tab: interfaces.ITab, isHorizontal: boolean): TemplateResult {
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
const isSelected = tab === this.selectedTab;
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
@@ -336,11 +253,11 @@ export class DeesAppuiTabs extends DeesElement {
`;
}
private renderTabIcon(tab: interfaces.ITab): TemplateResult | '' {
private renderTabIcon(tab: interfaces.IMenuItem): TemplateResult | '' {
return tab.iconName ? html`<dees-icon .icon=${tab.iconName}></dees-icon>` : '';
}
private selectTab(tabArg: interfaces.ITab) {
private selectTab(tabArg: interfaces.IMenuItem) {
this.selectedTab = tabArg;
tabArg.action();

View File

@@ -1,192 +0,0 @@
import * as interfaces from '../../interfaces/index.js';
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import '../dees-appui-tabs/dees-appui-tabs.js';
import type { DeesAppuiTabs } from '../dees-appui-tabs/dees-appui-tabs.js';
export interface IAppViewTab extends interfaces.ITab {
content?: TemplateResult | (() => TemplateResult);
}
export interface IAppView {
id: string;
name: string;
description?: string;
iconName?: string;
tabs: IAppViewTab[];
menuItems?: interfaces.ISelectionOption[];
}
@customElement('dees-appui-view')
export class DeesAppuiView extends DeesElement {
public static demo = () => html`
<dees-appui-view
.viewConfig=${{
id: 'demo-view',
name: 'Demo View',
description: 'A demonstration view',
iconName: 'lucide:home',
tabs: [
{
key: 'overview',
iconName: 'lucide:lineChart',
action: () => console.log('Overview tab'),
content: html`<div style="padding: 20px;">Overview Content</div>`
},
{
key: 'details',
iconName: 'lucide:fileText',
action: () => console.log('Details tab'),
content: html`<div style="padding: 20px;">Details Content</div>`
}
],
menuItems: [
{ key: 'General', action: () => console.log('General') },
{ key: 'Advanced', action: () => console.log('Advanced') },
]
}}
></dees-appui-view>
`;
// INSTANCE
@property({ type: Object })
accessor viewConfig: IAppView;
@state()
accessor selectedTab: IAppViewTab | null = null;
@state()
accessor tabs: DeesAppuiTabs;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
background: #161616;
}
.view-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.view-header {
background: #000000;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.view-content {
flex: 1;
position: relative;
overflow: hidden;
}
.tab-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
opacity: 0;
transition: opacity 0.2s;
}
.tab-content.active {
opacity: 1;
}
dees-appui-tabs {
height: 60px;
}
`,
];
public render(): TemplateResult {
if (!this.viewConfig) {
return html`<div>No view configuration provided</div>`;
}
return html`
<div class="view-container">
<div class="view-header">
<dees-appui-tabs
.tabs=${this.viewConfig.tabs}
.selectedTab=${this.selectedTab}
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
></dees-appui-tabs>
</div>
<div class="view-content">
${this.viewConfig.tabs.map((tab) => {
const isActive = tab === this.selectedTab;
const content = typeof tab.content === 'function' ? tab.content() : tab.content;
return html`
<div class="tab-content ${isActive ? 'active' : ''}">
${content || html`<slot name="${tab.key}"></slot>`}
</div>
`;
})}
</div>
</div>
`;
}
async firstUpdated() {
this.tabs = this.shadowRoot.querySelector('dees-appui-tabs');
if (this.viewConfig?.tabs?.length > 0) {
this.selectedTab = this.viewConfig.tabs[0];
}
}
private handleTabSelect(e: CustomEvent) {
this.selectedTab = e.detail.tab;
// Re-emit the event with view context
this.dispatchEvent(new CustomEvent('view-tab-select', {
detail: {
view: this.viewConfig,
tab: e.detail.tab
},
bubbles: true,
composed: true
}));
}
// Public methods for external control
public selectTab(tabKey: string) {
const tab = this.viewConfig.tabs.find(t => t.key === tabKey);
if (tab) {
this.selectedTab = tab;
if (this.tabs) {
this.tabs.selectedTab = tab;
}
}
}
public getMenuItems(): interfaces.ISelectionOption[] {
return this.viewConfig?.menuItems || [];
}
public getTabs(): IAppViewTab[] {
return this.viewConfig?.tabs || [];
}
}

View File

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

View File

@@ -7,4 +7,3 @@ export * from './dees-appui-mainmenu/index.js';
export * from './dees-appui-secondarymenu/index.js';
export * from './dees-appui-profiledropdown/index.js';
export * from './dees-appui-tabs/index.js';
export * from './dees-appui-view/index.js';

View File

@@ -10,6 +10,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-button-group.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -33,8 +34,10 @@ export class DeesButtonGroup extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: inline-block;
}

View File

@@ -12,6 +12,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-button.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -79,8 +80,10 @@ export class DeesButton extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: inline-block;
box-sizing: border-box;

View File

@@ -10,6 +10,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-chart-log.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
@@ -50,8 +51,10 @@ export class DeesChartLog extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};

View File

@@ -16,6 +16,7 @@ import {
import * as tsclass from '@tsclass/tsclass';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -30,8 +31,10 @@ export class DeesDataviewStatusobject extends DeesElement {
@property({ type: Object }) accessor statusObject: tsclass.code.IStatusObject;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}

View File

@@ -8,6 +8,7 @@ import {
cssManager,
domtools
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
const deferred = domtools.plugins.smartpromise.defer();
@@ -22,8 +23,10 @@ export class DeesEditorMarkdown extends DeesElement {
public static demo = () => html`<dees-editormarkdown></dees-editormarkdown>`;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
.gridcontainer {
position: absolute;
height: 100%;

View File

@@ -9,6 +9,7 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { MONACO_VERSION } from './version.js';
import { themeDefaultStyles } from '../../00theme.js';
import type * as monaco from 'monaco-editor';
@@ -51,8 +52,10 @@ export class DeesEditor extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
}

View File

@@ -8,6 +8,7 @@ import {
property,
} from '@design.estate/dees-element';
import type { DeesForm } from '../dees-form/dees-form.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -39,7 +40,9 @@ export class DeesFormSubmit extends DeesElement {
super();
}
public static styles = [cssManager.defaultStyles, css``];
public static styles = [themeDefaultStyles, cssManager.defaultStyles, css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
`];
public render() {
return html`

View File

@@ -9,6 +9,7 @@ import {
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { demoFunc } from './dees-input-checkbox.demo.js';
import { cssGeistFontFamily } from '../../00fonts.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -38,9 +39,11 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
}
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
* {
box-sizing: border-box;
}

View File

@@ -11,6 +11,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { cssGeistFontFamily } from '../../00fonts.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -60,9 +61,11 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
accessor searchValue: string = '';
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
* {
box-sizing: border-box;
}

View File

@@ -11,6 +11,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import * as ibantools from 'ibantools';
import { demoFunc } from './dees-input-iban.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-input-iban')
export class DeesInputIban extends DeesInputBase<DeesInputIban> {
@@ -30,9 +31,11 @@ export class DeesInputIban extends DeesInputBase<DeesInputIban> {
accessor value = '';
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
/* IBAN input specific styles can go here */
`,
];

View File

@@ -11,6 +11,7 @@ import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import '../../dees-icon/dees-icon.js';
import '../../00group-button/dees-button/dees-button.js';
import { demoFunc } from './dees-input-list.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -64,9 +65,11 @@ export class DeesInputList extends DeesInputBase<DeesInputList> {
accessor dragOverIndex: number = -1;
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;

View File

@@ -11,6 +11,7 @@ import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import * as colors from '../../00colors.js'
import { demoFunc } from './dees-input-multitoggle.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -67,9 +68,11 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
}
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
user-select: none;

View File

@@ -10,6 +10,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { demoFunc } from './dees-input-phone.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -33,9 +34,11 @@ export class DeesInputPhone extends DeesInputBase<DeesInputPhone> {
accessor placeholder: string = '+1 (555) 123-4567';
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
/* Phone input specific styles can go here */
`,
];

View File

@@ -2,6 +2,7 @@ import { customElement, property, html, type TemplateResult, css, cssManager } f
import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { demoFunc } from './dees-input-quantityselector.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -23,14 +24,16 @@ export class DeesInputQuantitySelector extends DeesInputBase<DeesInputQuantitySe
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
width: auto;
user-select: none;
}
.quantity-container {
transition: all 0.15s ease;
font-size: 14px;

View File

@@ -8,6 +8,7 @@ import {
} from '@design.estate/dees-element';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { demoFunc } from './dees-input-radiogroup.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -59,9 +60,11 @@ export class DeesInputRadiogroup extends DeesInputBase<string | object> {
}
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
* {
box-sizing: border-box;
}

View File

@@ -10,6 +10,7 @@ import {
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import '../../dees-icon/dees-icon.js';
import { demoFunc } from './dees-input-tags.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -48,9 +49,11 @@ export class DeesInputTags extends DeesInputBase<DeesInputTags> {
accessor validationText: string = '';
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;

View File

@@ -11,6 +11,7 @@ import {
cssManager,
css,
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -56,9 +57,11 @@ export class DeesInputText extends DeesInputBase {
accessor validationFunction: (value: string) => boolean;
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
* {
box-sizing: border-box;
}

View File

@@ -11,6 +11,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { demoFunc } from './dees-input-typelist.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-input-typelist')
export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
@@ -27,9 +28,11 @@ export class DeesInputTypelist extends DeesInputBase<DeesInputTypelist> {
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('#333', '#fff')};
}

View File

@@ -10,6 +10,7 @@ import {
import { zIndexRegistry } from '../../00zindex.js';
import { WysiwygFormatting } from './wysiwyg.formatting.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -41,8 +42,10 @@ export class DeesFormattingMenu extends DeesElement {
private callback: ((command: string) => void | Promise<void>) | null = null;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
position: fixed;
pointer-events: none;

View File

@@ -28,6 +28,7 @@ import {
DeesSlashMenu,
DeesFormattingMenu
} from './index.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -86,6 +87,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
private history: WysiwygHistory;
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
wysiwygStyles

View File

@@ -12,6 +12,7 @@ import '../../dees-icon/dees-icon.js';
import { type ISlashMenuItem } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -49,8 +50,10 @@ export class DeesSlashMenu extends DeesElement {
private callback: ((type: string) => void) | null = null;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
position: fixed;
pointer-events: none;

View File

@@ -15,6 +15,7 @@ import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js';
import './wysiwyg.blockregistration.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
import '../../dees-contextmenu/dees-contextmenu.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -82,8 +83,10 @@ export class DeesWysiwygBlock extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
}

View File

@@ -12,6 +12,7 @@ import '../../dees-icon/dees-icon.js';
import '../../dees-label/dees-label.js';
import { ProfilePictureModal } from './profilepicture.modal.js';
import { demoFunc } from './dees-input-profilepicture.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -67,9 +68,11 @@ export class DeesInputProfilePicture extends DeesInputBase<DeesInputProfilePictu
private modalInstance: ProfilePictureModal | null = null;
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
position: relative;

View File

@@ -17,6 +17,7 @@ import '../../dees-windowlayer/dees-windowlayer.js';
import { DeesWindowLayer } from '../../dees-windowlayer/dees-windowlayer.js';
import { ImageCropper } from './profilepicture.cropper.js';
import type { ProfileShape } from './dees-input-profilepicture.js';
import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-profilepicture-modal')
export class ProfilePictureModal extends DeesElement {
@@ -46,8 +47,10 @@ export class ProfilePictureModal extends DeesElement {
private zIndex: number = 0;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
font-family: ${cssGeistFontFamily};
color: ${cssManager.bdTheme('#333', '#fff')};

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

@@ -15,6 +15,7 @@ import {
} from '@design.estate/dees-element';
import '../../dees-icon/dees-icon.js';
import type { DeesTerminal } from '../../dees-terminal/dees-terminal.js';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -48,8 +49,10 @@ export class DeesSimpleAppDash extends DeesElement {
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
user-select: none;

View File

@@ -9,6 +9,7 @@ import {
cssManager,
css,
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -26,8 +27,10 @@ export class DeesSimpleLogin extends DeesElement {
accessor name: string = 'Application';
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
user-select: none;

218
ts_web/elements/00theme.ts Normal file
View File

@@ -0,0 +1,218 @@
import { css, type CSSResult } from '@design.estate/dees-element';
// ============================================
// Theme Token Type Definitions
// ============================================
export interface IThemeColors {
bgPrimary: string;
bgSecondary: string;
bgTertiary: string;
textPrimary: string;
textSecondary: string;
textMuted: string;
borderDefault: string;
borderSubtle: string;
borderStrong: string;
accentPrimary: string;
accentSuccess: string;
accentWarning: string;
accentError: string;
}
export interface IThemeSpacing {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
'2xl': string;
'3xl': string;
}
export interface IThemeRadius {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
full: string;
}
export interface IThemeShadows {
xs: string;
sm: string;
md: string;
lg: string;
}
export interface IThemeTransitions {
fast: string;
default: string;
slow: string;
slower: string;
}
export interface IThemeControlHeights {
sm: string;
md: string;
lg: string;
xl: string;
}
export interface ITheme {
colors: {
light: IThemeColors;
dark: IThemeColors;
};
spacing: IThemeSpacing;
radius: IThemeRadius;
shadows: IThemeShadows;
transitions: IThemeTransitions;
controlHeights: IThemeControlHeights;
}
// ============================================
// Default Theme Values (TypeScript Object)
// ============================================
export const themeDefaults: ITheme = {
colors: {
light: {
bgPrimary: '#ffffff',
bgSecondary: '#fafafa',
bgTertiary: '#f4f4f5',
textPrimary: '#09090b',
textSecondary: '#374151',
textMuted: '#71717a',
borderDefault: '#e5e7eb',
borderSubtle: '#f4f4f5',
borderStrong: '#d1d5db',
accentPrimary: '#3b82f6',
accentSuccess: '#22c55e',
accentWarning: '#f59e0b',
accentError: '#ef4444',
},
dark: {
bgPrimary: '#09090b',
bgSecondary: '#0a0a0a',
bgTertiary: '#18181b',
textPrimary: '#fafafa',
textSecondary: '#d4d4d8',
textMuted: '#a1a1aa',
borderDefault: '#27272a',
borderSubtle: '#1a1a1a',
borderStrong: '#3f3f46',
accentPrimary: '#3b82f6',
accentSuccess: '#22c55e',
accentWarning: '#f59e0b',
accentError: '#ef4444',
},
},
spacing: {
xs: '4px',
sm: '8px',
md: '12px',
lg: '16px',
xl: '24px',
'2xl': '32px',
'3xl': '48px',
},
radius: {
xs: '2px',
sm: '4px',
md: '6px',
lg: '8px',
xl: '12px',
full: '999px',
},
shadows: {
xs: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
sm: '0 1px 3px rgba(0, 0, 0, 0.1)',
md: '0 2px 8px rgba(0, 0, 0, 0.15)',
lg: '0 4px 12px rgba(0, 0, 0, 0.15)',
},
transitions: {
fast: '0.1s',
default: '0.15s',
slow: '0.2s',
slower: '0.3s',
},
controlHeights: {
sm: '32px',
md: '36px',
lg: '40px',
xl: '48px',
},
};
// ============================================
// CSS Block for Component Import
// ============================================
/**
* Default theme styles to be imported into every component's static styles array.
* Provides CSS custom properties for spacing, radius, shadows, transitions, and control heights.
*
* Usage:
* ```typescript
* import { themeDefaultStyles } from '../00theme.js';
*
* @customElement('my-component')
* export class MyComponent extends DeesElement {
* public static styles = [
* themeDefaultStyles,
* cssManager.defaultStyles,
* css`...`
* ];
* }
* ```
*/
export const themeDefaultStyles: CSSResult = css`
:host {
/* ========================================
* Spacing Scale
* ======================================== */
--dees-spacing-xs: 4px;
--dees-spacing-sm: 8px;
--dees-spacing-md: 12px;
--dees-spacing-lg: 16px;
--dees-spacing-xl: 24px;
--dees-spacing-2xl: 32px;
--dees-spacing-3xl: 48px;
/* ========================================
* Border Radius Scale
* ======================================== */
--dees-radius-xs: 2px;
--dees-radius-sm: 4px;
--dees-radius-md: 6px;
--dees-radius-lg: 8px;
--dees-radius-xl: 12px;
--dees-radius-full: 999px;
/* ========================================
* Shadow Elevation Scale
* ======================================== */
--dees-shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--dees-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--dees-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.15);
--dees-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15);
/* ========================================
* Transition Duration Scale
* ======================================== */
--dees-transition-fast: 0.1s;
--dees-transition-default: 0.15s;
--dees-transition-slow: 0.2s;
--dees-transition-slower: 0.3s;
/* ========================================
* Control Height Scale
* ======================================== */
--dees-control-height-sm: 32px;
--dees-control-height-md: 36px;
--dees-control-height-lg: 40px;
--dees-control-height-xl: 48px;
}
`;

View File

@@ -11,6 +11,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-badge.demo.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -37,8 +38,10 @@ export class DeesBadge extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: inline-block;
}

View File

@@ -12,6 +12,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-chips.demo.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -51,8 +52,10 @@ export class DeesChips extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
box-sizing: border-box;

View File

@@ -16,6 +16,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from '../dees-windowlayer/dees-windowlayer.js';
import { zIndexLayers } from '../00zindex.js';
import '../dees-icon/dees-icon.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -143,8 +144,10 @@ export class DeesContextmenu extends DeesElement {
* STATIC STYLES
*/
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
transition: opacity 0.2s, transform 0.2s;

View File

@@ -11,6 +11,7 @@ import {
import { demoFunc } from './dees-heading.demo.js';
import { cssCalSansFontFamily } from '../00fonts.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -32,8 +33,10 @@ export class DeesHeading extends DeesElement {
// STATIC STYLES
public static styles: CSSResult[] = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
/* Heading styles */
h1, h2, h3, h4, h5, h6 {
margin: 16px 0 8px;

View File

@@ -11,6 +11,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-hint.demo.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -30,7 +31,9 @@ export class DeesHint extends DeesElement {
domtools.elementBasic.setup();
}
public static styles = [cssManager.defaultStyles, css``];
public static styles = [themeDefaultStyles, cssManager.defaultStyles, css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
`];
public render(): TemplateResult {
return html` <div class="mainbox"></div> `;

View File

@@ -9,6 +9,7 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { themeDefaultStyles } from '../00theme.js';
import { icon, type IconDefinition } from '@fortawesome/fontawesome-svg-core';
import {
@@ -325,8 +326,10 @@ export class DeesIcon extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: inline-flex;
align-items: center;
@@ -334,7 +337,7 @@ export class DeesIcon extends DeesElement {
line-height: 1;
vertical-align: middle;
}
/* Improve rendering performance */
#iconContainer svg {
display: block;

View File

@@ -13,6 +13,7 @@ import {
} from '@design.estate/dees-element';
import { demoFunc } from './dees-label.demo.js';
import { themeDefaultStyles } from '../00theme.js';
@customElement('dees-label')
export class DeesLabel extends DeesElement {
@@ -39,8 +40,10 @@ export class DeesLabel extends DeesElement {
accessor required: boolean = false;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}

View File

@@ -14,6 +14,7 @@ import {
} from '@design.estate/dees-element';
import { DeesWindowLayer } from '../dees-windowlayer/dees-windowlayer.js';
import '../dees-icon/dees-icon.js';
import { themeDefaultStyles } from '../00theme.js';
@customElement('dees-mobilenavigation')
export class DeesMobilenavigation extends DeesElement {
@@ -111,8 +112,10 @@ export class DeesMobilenavigation extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
font-family: ${cssGeistFontFamily};
}

View File

@@ -21,6 +21,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { DeesWindowLayer } from '../dees-windowlayer/dees-windowlayer.js';
import '../dees-icon/dees-icon.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -115,8 +116,10 @@ export class DeesModal extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
font-family: ${cssGeistFontFamily};
color: ${cssManager.bdTheme('#333', '#fff')};

View File

@@ -1,5 +1,6 @@
import { customElement, html, DeesElement, property, css, cssManager, type TemplateResult } from '@design.estate/dees-element';
import { demoFunc } from './dees-pagination.demo.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -23,8 +24,10 @@ export class DeesPagination extends DeesElement {
accessor total = 1;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: inline-flex;
align-items: center;

View File

@@ -9,6 +9,7 @@ import {
} from '@design.estate/dees-element';
import { demoFunc } from './dees-panel.demo.js';
import { cssGeistFontFamily } from '../00fonts.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -36,8 +37,10 @@ export class DeesPanel extends DeesElement {
accessor runAfterRender: ((elementArg: HTMLElement) => void | Promise<void>) | undefined = undefined;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
font-family: ${cssGeistFontFamily};

View File

@@ -16,6 +16,7 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { themeDefaultStyles } from '../00theme.js';
@customElement('dees-progressbar')
export class DeesProgressbar extends DeesElement {
@@ -29,8 +30,10 @@ export class DeesProgressbar extends DeesElement {
accessor percentage = 0;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
color: ${cssManager.bdTheme(colors.bright.text, colors.dark.text)};
}

View File

@@ -13,6 +13,7 @@ import {
import * as colors from '../00colors.js';
import { demoFunc } from './dees-searchbar.demo.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -27,8 +28,10 @@ export class DeesSearchbar extends DeesElement {
// STATIC
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
padding: 40px;
font-family: Dees Sans;

View File

@@ -8,6 +8,7 @@ import {
DeesElement,
} from '@design.estate/dees-element';
import { demoFunc } from './dees-shopping-productcard.demo.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -51,8 +52,10 @@ export class DeesShoppingProductcard extends DeesElement {
accessor selected: boolean = false;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
}

View File

@@ -17,6 +17,7 @@ import {
unsafeHTML,
} from '@design.estate/dees-element';
import { DeesWindowLayer } from '../dees-windowlayer/dees-windowlayer.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -78,8 +79,10 @@ export class DeesSpeechbubble extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
box-sizing: border-box;
color: ${cssManager.bdTheme('#333', '#fff')};

View File

@@ -11,6 +11,7 @@ import {
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -46,8 +47,10 @@ export class DeesSpinner extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
}

View File

@@ -16,6 +16,7 @@ import type { TemplateResult } from '@design.estate/dees-element';
import '../dees-icon/dees-icon.js';
import '../dees-contextmenu/dees-contextmenu.js';
import '../00group-button/dees-button/dees-button.js';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -75,8 +76,10 @@ export class DeesStatsGrid extends DeesElement {
accessor contextMenuActions: plugins.tsclass.website.IMenuItem[] = [];
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
width: 100%;

View File

@@ -15,6 +15,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { stepperDemo } from './dees-stepper.demo.js';
import { themeDefaultStyles } from '../00theme.js';
export interface IStep {
title: string;
@@ -50,8 +51,10 @@ export class DeesStepper extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
position: absolute;
width: 100%;

View File

@@ -14,6 +14,7 @@ import {
getViewData as getViewDataFn,
} from './data.js';
import { compileLucenePredicate } from './lucene.js';
import { themeDefaultStyles } from '../00theme.js';
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';

View File

@@ -1,9 +1,12 @@
import { cssManager, css, type CSSResult } from '@design.estate/dees-element';
import { cssGeistFontFamily } from '../00fonts.js';
import { themeDefaultStyles } from '../00theme.js';
export const tableStyles: CSSResult[] = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
display: block;
width: 100%;

View File

@@ -13,6 +13,7 @@ import * as webcontainer from '@webcontainer/api';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { themeDefaultStyles } from '../00theme.js';
declare global {
interface HTMLElementTagNameMap {
@@ -57,8 +58,10 @@ export class DeesTerminal extends DeesElement {
}
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host {
padding: 20px;
background: var(--dees-terminal-background, #000000);

View File

@@ -0,0 +1,275 @@
import { html, css, cssManager } from '@design.estate/dees-element';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 32px;
padding: 48px;
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
min-height: 100vh;
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.section-description {
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 24px;
}
.token-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.token-item {
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 6px;
padding: 16px;
}
.token-name {
font-family: 'Intel One Mono', monospace;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-bottom: 8px;
}
.token-value {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.spacing-demo {
display: flex;
align-items: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.spacing-box {
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-radius: 4px;
}
.radius-demo {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.radius-box {
width: 60px;
height: 60px;
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 11px;
font-weight: 500;
}
.shadow-demo {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.shadow-box {
width: 100px;
height: 100px;
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 500;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.height-demo {
display: flex;
gap: 16px;
align-items: flex-end;
flex-wrap: wrap;
}
.height-box {
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
width: 120px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: 500;
}
</style>
<div class="demo-container">
<dees-theme>
<div class="section">
<div class="section-title">Spacing Scale</div>
<div class="section-description">
CSS variables: --dees-spacing-xs through --dees-spacing-3xl
</div>
<div class="spacing-demo">
<div>
<div class="spacing-box" style="width: var(--dees-spacing-xs); height: var(--dees-spacing-xs);"></div>
<div class="token-name">xs (4px)</div>
</div>
<div>
<div class="spacing-box" style="width: var(--dees-spacing-sm); height: var(--dees-spacing-sm);"></div>
<div class="token-name">sm (8px)</div>
</div>
<div>
<div class="spacing-box" style="width: var(--dees-spacing-md); height: var(--dees-spacing-md);"></div>
<div class="token-name">md (12px)</div>
</div>
<div>
<div class="spacing-box" style="width: var(--dees-spacing-lg); height: var(--dees-spacing-lg);"></div>
<div class="token-name">lg (16px)</div>
</div>
<div>
<div class="spacing-box" style="width: var(--dees-spacing-xl); height: var(--dees-spacing-xl);"></div>
<div class="token-name">xl (24px)</div>
</div>
<div>
<div class="spacing-box" style="width: var(--dees-spacing-2xl); height: var(--dees-spacing-2xl);"></div>
<div class="token-name">2xl (32px)</div>
</div>
<div>
<div class="spacing-box" style="width: var(--dees-spacing-3xl); height: var(--dees-spacing-3xl);"></div>
<div class="token-name">3xl (48px)</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Border Radius Scale</div>
<div class="section-description">
CSS variables: --dees-radius-xs through --dees-radius-full
</div>
<div class="radius-demo">
<div>
<div class="radius-box" style="border-radius: var(--dees-radius-xs);">xs</div>
<div class="token-name">2px</div>
</div>
<div>
<div class="radius-box" style="border-radius: var(--dees-radius-sm);">sm</div>
<div class="token-name">4px</div>
</div>
<div>
<div class="radius-box" style="border-radius: var(--dees-radius-md);">md</div>
<div class="token-name">6px</div>
</div>
<div>
<div class="radius-box" style="border-radius: var(--dees-radius-lg);">lg</div>
<div class="token-name">8px</div>
</div>
<div>
<div class="radius-box" style="border-radius: var(--dees-radius-xl);">xl</div>
<div class="token-name">12px</div>
</div>
<div>
<div class="radius-box" style="border-radius: var(--dees-radius-full);">full</div>
<div class="token-name">999px</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Shadow Elevation Scale</div>
<div class="section-description">
CSS variables: --dees-shadow-xs through --dees-shadow-lg
</div>
<div class="shadow-demo">
<div>
<div class="shadow-box" style="box-shadow: var(--dees-shadow-xs);">xs</div>
<div class="token-name">minimal</div>
</div>
<div>
<div class="shadow-box" style="box-shadow: var(--dees-shadow-sm);">sm</div>
<div class="token-name">subtle</div>
</div>
<div>
<div class="shadow-box" style="box-shadow: var(--dees-shadow-md);">md</div>
<div class="token-name">medium</div>
</div>
<div>
<div class="shadow-box" style="box-shadow: var(--dees-shadow-lg);">lg</div>
<div class="token-name">prominent</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Control Height Scale</div>
<div class="section-description">
CSS variables: --dees-control-height-sm through --dees-control-height-xl
</div>
<div class="height-demo">
<div>
<div class="height-box" style="height: var(--dees-control-height-sm);">sm</div>
<div class="token-name">32px</div>
</div>
<div>
<div class="height-box" style="height: var(--dees-control-height-md);">md</div>
<div class="token-name">36px</div>
</div>
<div>
<div class="height-box" style="height: var(--dees-control-height-lg);">lg</div>
<div class="token-name">40px</div>
</div>
<div>
<div class="height-box" style="height: var(--dees-control-height-xl);">xl</div>
<div class="token-name">48px</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Transition Durations</div>
<div class="section-description">
CSS variables: --dees-transition-fast through --dees-transition-slower
</div>
<div class="token-grid">
<div class="token-item">
<div class="token-name">--dees-transition-fast</div>
<div class="token-value">0.1s</div>
</div>
<div class="token-item">
<div class="token-name">--dees-transition-default</div>
<div class="token-value">0.15s</div>
</div>
<div class="token-item">
<div class="token-name">--dees-transition-slow</div>
<div class="token-value">0.2s</div>
</div>
<div class="token-item">
<div class="token-name">--dees-transition-slower</div>
<div class="token-value">0.3s</div>
</div>
</div>
</div>
</dees-theme>
</div>
`;

View File

@@ -0,0 +1,224 @@
import {
DeesElement,
type TemplateResult,
property,
customElement,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import {
type ITheme,
type IThemeColors,
type IThemeSpacing,
type IThemeRadius,
type IThemeShadows,
type IThemeTransitions,
type IThemeControlHeights,
themeDefaults,
themeDefaultStyles,
} from '../00theme.js';
import { demoFunc } from './dees-theme.demo.js';
/**
* A theme provider component that wraps children and provides CSS custom properties.
* Can be used at the app root or around specific sections to customize theming.
*
* Usage:
* ```html
* <dees-theme>
* <my-app></my-app>
* </dees-theme>
* ```
*
* With custom overrides:
* ```html
* <dees-theme .customSpacing=${{ lg: '20px' }}>
* <my-section></my-section>
* </dees-theme>
* ```
*/
@customElement('dees-theme')
export class DeesTheme extends DeesElement {
public static demo = demoFunc;
// ============================================
// Properties for theme overrides
// ============================================
@property({ type: Object })
accessor customSpacing: Partial<IThemeSpacing> | null = null;
@property({ type: Object })
accessor customRadius: Partial<IThemeRadius> | null = null;
@property({ type: Object })
accessor customShadows: Partial<IThemeShadows> | null = null;
@property({ type: Object })
accessor customTransitions: Partial<IThemeTransitions> | null = null;
@property({ type: Object })
accessor customControlHeights: Partial<IThemeControlHeights> | null = null;
// ============================================
// Styles
// ============================================
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: contents;
}
`,
];
// ============================================
// Render
// ============================================
public render(): TemplateResult {
return html`
<style>
${this.generateCustomStyles()}
</style>
<slot></slot>
`;
}
// ============================================
// Private Methods
// ============================================
private generateCustomStyles(): string {
const styles: string[] = [':host {'];
// Custom spacing
if (this.customSpacing) {
for (const [key, value] of Object.entries(this.customSpacing)) {
if (value) {
styles.push(` --dees-spacing-${key}: ${value};`);
}
}
}
// Custom radius
if (this.customRadius) {
for (const [key, value] of Object.entries(this.customRadius)) {
if (value) {
styles.push(` --dees-radius-${key}: ${value};`);
}
}
}
// Custom shadows
if (this.customShadows) {
for (const [key, value] of Object.entries(this.customShadows)) {
if (value) {
styles.push(` --dees-shadow-${key}: ${value};`);
}
}
}
// Custom transitions
if (this.customTransitions) {
for (const [key, value] of Object.entries(this.customTransitions)) {
if (value) {
const cssKey = key === 'default' ? 'default' : key;
styles.push(` --dees-transition-${cssKey}: ${value};`);
}
}
}
// Custom control heights
if (this.customControlHeights) {
for (const [key, value] of Object.entries(this.customControlHeights)) {
if (value) {
styles.push(` --dees-control-height-${key}: ${value};`);
}
}
}
styles.push('}');
return styles.join('\n');
}
// ============================================
// Public API Methods
// ============================================
/**
* Set a spacing value dynamically
*/
public setSpacing(key: keyof IThemeSpacing, value: string): void {
this.customSpacing = { ...this.customSpacing, [key]: value };
}
/**
* Set a radius value dynamically
*/
public setRadius(key: keyof IThemeRadius, value: string): void {
this.customRadius = { ...this.customRadius, [key]: value };
}
/**
* Set a shadow value dynamically
*/
public setShadow(key: keyof IThemeShadows, value: string): void {
this.customShadows = { ...this.customShadows, [key]: value };
}
/**
* Set a transition value dynamically
*/
public setTransition(key: keyof IThemeTransitions, value: string): void {
this.customTransitions = { ...this.customTransitions, [key]: value };
}
/**
* Set a control height value dynamically
*/
public setControlHeight(key: keyof IThemeControlHeights, value: string): void {
this.customControlHeights = { ...this.customControlHeights, [key]: value };
}
/**
* Get the current theme configuration (defaults + overrides)
*/
public getTheme(): ITheme {
return {
colors: themeDefaults.colors,
spacing: { ...themeDefaults.spacing, ...this.customSpacing },
radius: { ...themeDefaults.radius, ...this.customRadius },
shadows: { ...themeDefaults.shadows, ...this.customShadows },
transitions: { ...themeDefaults.transitions, ...this.customTransitions },
controlHeights: { ...themeDefaults.controlHeights, ...this.customControlHeights },
};
}
/**
* Reset all custom overrides to defaults
*/
public resetToDefaults(): void {
this.customSpacing = null;
this.customRadius = null;
this.customShadows = null;
this.customTransitions = null;
this.customControlHeights = null;
}
/**
* Apply a complete theme object
*/
public applyTheme(theme: Partial<ITheme>): void {
if (theme.spacing) this.customSpacing = theme.spacing;
if (theme.radius) this.customRadius = theme.radius;
if (theme.shadows) this.customShadows = theme.shadows;
if (theme.transitions) this.customTransitions = theme.transitions;
if (theme.controlHeights) this.customControlHeights = theme.controlHeights;
}
}

View File

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

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