Compare commits

...

71 Commits

Author SHA1 Message Date
bc4b87b83a v3.48.4
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-14 17:10:20 +00:00
eb055e7214 fix(storage-browser): rename S3-specific storage browser interfaces to generic storage types 2026-03-14 17:10:20 +00:00
c55eb948fe v3.48.3
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-14 16:29:46 +00:00
35779209ea fix(dataview): rename dees-s3-browser exports and custom elements to dees-storage-browser 2026-03-14 16:29:46 +00:00
8c6738ea15 v3.48.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 20:11:52 +00:00
e7da1d8b44 fix(repo): no changes to commit 2026-03-12 20:11:52 +00:00
358d82e7fa v3.48.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 20:04:12 +00:00
6452e05e1d fix(repo): no changes to commit 2026-03-12 20:04:12 +00:00
07b536ea9a v3.48.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 15:18:52 +00:00
3fcb0cbf89 feat(dataview): add an S3 browser component with column and list views, file preview, editing, and object management 2026-03-12 15:18:52 +00:00
3285cbf0e7 v3.47.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-11 21:49:34 +00:00
a2d750b2f6 fix(deps): bump @design.estate/dees-domtools and @design.estate/dees-element dependencies 2026-03-11 21:49:34 +00:00
d4276710e6 v3.47.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-11 18:04:55 +00:00
66d64bf476 fix(dees-statsgrid): add tablet breakpoint to render stats grid as three columns 2026-03-11 18:04:55 +00:00
2504251707 v3.47.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-11 08:49:14 +00:00
fed130f291 feat(dees-statsgrid): add container-responsive behavior and responsive CSS to dees-statsgrid; bump @design.estate/dees-element dependency to ^2.2.1 2026-03-11 08:49:14 +00:00
4f05b5907b v3.46.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-10 19:49:46 +00:00
e517320dcd fix(dees-appui): add min-height: 0 to mainmenu and secondarymenu to prevent unintended container height and fix layout stacking 2026-03-10 19:49:46 +00:00
ade5a25b3a v3.46.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-10 16:04:28 +00:00
a396dfea12 feat(dees-tile): unify tile metadata into a consistent bottom info bar and add PDF file-size display 2026-03-10 16:04:28 +00:00
d0105e1b80 v3.45.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-10 14:42:02 +00:00
1eeebb35e6 fix(dees-appui): substitute route params into URL hash when navigating 2026-03-10 14:42:02 +00:00
14e8b8c533 v3.45.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-10 13:56:22 +00:00
eaf327ea75 feat(dees-form): register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates 2026-03-10 13:56:22 +00:00
09741e0b37 v3.44.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-10 12:39:21 +00:00
5cadd1fc7f feat(appui-tabs): add support for left/right tab action buttons and content tab action APIs 2026-03-10 12:39:21 +00:00
1795235c6d v3.43.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-09 17:37:50 +00:00
ba7d387acb fix(media): remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies 2026-03-09 17:37:50 +00:00
26ca16a284 v3.43.3
Some checks failed
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 18:25:54 +00:00
3ab3eb5e5e fix(dees-table): use lucide icon identifier for Search action in dees-table 2026-02-24 18:25:54 +00:00
da5dbc70e2 v3.43.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-21 21:12:18 +00:00
19b7981542 fix(dees-chart-log): avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs 2026-02-21 21:12:18 +00:00
bad105074e v3.43.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-21 18:06:42 +00:00
f124091784 fix(dees-chart-log): replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs 2026-02-21 18:06:42 +00:00
68790a26ed v3.43.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-17 17:34:53 +00:00
2abf7e356d feat(dees-form): add layout styles to dees-form and standardize demo input grouping 2026-02-17 17:34:53 +00:00
ecd35683e6 v3.42.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-16 17:16:49 +00:00
93cb632448 fix(dees-chart-area): add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data 2026-02-16 17:16:49 +00:00
047e42c6a3 v3.42.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-16 16:03:23 +00:00
59efa8cff0 fix(dees-table): Guard against undefined action.type in dees-table by using optional chaining and update several dependencies 2026-02-16 16:03:23 +00:00
09f0aa97dd v3.42.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-02 14:32:20 +00:00
7c62f45d77 feat(dees-form-submit): forward button properties to internal dees-button, use property bindings, add demo and styles 2026-02-02 14:32:20 +00:00
b123768474 v3.41.6
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-02 09:27:53 +00:00
f292e7a7f4 fix(dees-simple-appdash): respect selectedView when loading initial view, falling back to the first tab 2026-02-02 09:27:53 +00:00
d82e5603a7 v3.41.5
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-01 23:51:53 +00:00
7e2386bcdf fix(dees-service-lib-loader): prevent horizontal scrollbar by offsetting xterm WidthCache measurement container 2026-02-01 23:51:53 +00:00
eba2a03355 v3.41.4
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-29 13:21:30 +00:00
06c01f0690 fix(): no changes 2026-01-29 13:21:30 +00:00
91e03eb9c4 v3.41.3
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-29 07:46:19 +00:00
b7f3f47c61 fix(dees-pdf-viewer): use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling 2026-01-29 07:46:19 +00:00
0a83f0e136 update 2026-01-28 17:10:37 +00:00
2b048cf34f update 2026-01-28 17:02:52 +00:00
7e50b8cb3f update 2026-01-28 16:46:07 +00:00
b97601a876 v3.41.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-28 16:17:05 +00:00
5ddeb8fe7c fix(dees-pdf-viewer): account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays 2026-01-28 16:17:05 +00:00
25f46162c5 v3.41.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 14:02:25 +00:00
b84b0e7ce6 fix(dataview-codebox): fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies 2026-01-27 14:02:25 +00:00
69840de3a6 v3.41.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 12:36:46 +00:00
85badfbd21 feat(docs): document new media & tile components and expand Workspace/IDE component docs; update component count to 90+ 2026-01-27 12:36:46 +00:00
264e460365 v3.40.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 11:36:24 +00:00
bfda6b75e7 feat(dees-tile): unify tile badge styling and markup; replace component-specific badge classes with shared tile-badge classes and update related imports/tests 2026-01-27 11:36:24 +00:00
825a74b810 v3.39.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 11:10:39 +00:00
f6bf0f8a45 fix(dees-tile-note): use horizontal pointer position to scroll note body by computing percentage from clientX and element width instead of clientY and height 2026-01-27 11:10:39 +00:00
66661e05a9 v3.39.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 10:57:42 +00:00
162688cdb5 feat(components): add large set of new UI components and demos, reorganize groups, and bump a few dependencies 2026-01-27 10:57:42 +00:00
8158b791c7 v3.38.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-26 01:28:31 +00:00
ed8167385f feat(appui): add app shell and bottom bar APIs, new input components, and update README component listings and docs 2026-01-26 01:28:31 +00:00
b472057e9d v3.37.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-25 13:51:54 +00:00
1bbf853043 fix(editor): fix monaco/editor layout, update dev deps, simplify watch script and remove Playwright snapshots 2026-01-25 13:51:54 +00:00
8ff52fc562 v3.37.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-13 20:45:50 +00:00
5dd0367df0 feat(dees-button,dees-statsgrid): add unified icon property and icon-position support to dees-button; add partition and disk tile types to dees-statsgrid 2026-01-13 20:45:50 +00:00
290 changed files with 14352 additions and 5496 deletions

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# custom # custom
.playwright-mcp/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,5 +1,267 @@
# Changelog # Changelog
## 2026-03-14 - 3.48.4 - fix(storage-browser)
rename S3-specific storage browser interfaces to generic storage types
- Replaces IS3DataProvider, IS3Object, and IS3ChangeEvent with generic storage interface names across storage browser components
- Updates demo provider naming and user-facing demo text from S3 browser to Storage browser
- Aligns interface and utility comments with storage-agnostic terminology
## 2026-03-14 - 3.48.3 - fix(dataview)
rename dees-s3-browser exports and custom elements to dees-storage-browser
- Replaces the dees-s3-browser module path with dees-storage-browser in dataview exports
- Renames the main custom element from dees-s3-browser to dees-storage-browser
- Renames related columns, keys, preview, demo, interfaces, and utility entry points under the new storage-browser module
## 2026-03-12 - 3.48.2 - fix(repo)
no changes to commit
## 2026-03-12 - 3.48.1 - fix(repo)
no changes to commit
## 2026-03-12 - 3.48.0 - feat(dataview)
add an S3 browser component with column and list views, file preview, editing, and object management
- introduces a new dees-s3-browser module with shared interfaces, utilities, demo, and exports
- supports browsing S3-style prefixes in both column and list layouts with breadcrumb navigation
- adds file preview with text editing, download, and delete actions
- includes create, rename, move, delete, upload, and drag-and-drop handling for files and folders
- adds optional live change stream integration with refresh indicators
## 2026-03-11 - 3.47.2 - fix(deps)
bump @design.estate/dees-domtools and @design.estate/dees-element dependencies
- update @design.estate/dees-domtools from ^2.3.9 to ^2.5.1
- update @design.estate/dees-element from ^2.2.1 to ^2.2.2
## 2026-03-11 - 3.47.1 - fix(dees-statsgrid)
add tablet breakpoint to render stats grid as three columns
- Added cssManager.cssForTablet rule to set .stats-grid grid-template-columns: repeat(3, 1fr).
- Improves responsive layout on tablet devices for dees-statsgrid tiles.
- Change made in ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
## 2026-03-11 - 3.47.0 - feat(dees-statsgrid)
add container-responsive behavior and responsive CSS to dees-statsgrid; bump @design.estate/dees-element dependency to ^2.2.1
- Added @containerResponsive decorator and import to dees-statsgrid
- Added cssManager.cssForPhablet and cssManager.cssForPhone responsive style blocks to adjust layout, spacing and font sizes on smaller viewports
- Bumped dependency @design.estate/dees-element from ^2.1.6 to ^2.2.1
## 2026-03-10 - 3.46.1 - fix(dees-appui)
add min-height: 0 to mainmenu and secondarymenu to prevent unintended container height and fix layout stacking
- Modified ts_web/elements/00group-appui/dees-appui/dees-appui.ts: added min-height: 0 to .maingrid > dees-appui-mainmenu and .maingrid > dees-appui-secondarymenu
- Fixes layout issues where children or flexbox-derived min-height could cause menu containers to expand and interfere with z-index stacking
## 2026-03-10 - 3.46.0 - feat(dees-tile)
unify tile metadata into a consistent bottom info bar and add PDF file-size display
- Introduce renderBottomBar() hook in DeesTileBase and remove per-component bottom badges/labels in favor of a unified info bar.
- Implement renderBottomBar in audio, video, image, folder, note and pdf tiles to show label, counts, dimensions, duration, language/line info and page counts.
- PDF tile: add fileSize state, attempt to read download info and display formatted file size in the info bar; show currentPreviewPage/pageCount when hovering.
- Styling changes: replace legacy badges/labels with .tile-info-bar (.info-label, .info-detail, .info-spacer); adjust padding, font sizing, z-index, and remove hover translate for clickable tiles.
- PDF demo and styles: use cssManager theming for demo colors and adjust preview padding.
- Bump devDependencies: @git.zone/tswatch -> ^3.3.0 and @types/node -> ^25.4.0
## 2026-03-10 - 3.45.1 - fix(dees-appui)
substitute route params into URL hash when navigating
- Replaces :param placeholders in view.route with provided params before updating the URL hash.
- Ensures window.history.pushState is called with the resolved route so URLs do not contain literal parameter tokens.
## 2026-03-10 - 3.45.0 - feat(dees-form)
register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates
- Added imports and registration of DeesInputTags, DeesInputList, DeesInputWysiwyg, and DeesInputRichtext in dees-form
- Extended TFormInputElement union type to include the new input components
- DeesInputRichtext now calls changeSubject.next(this.value) in the editor onUpdate handler to propagate changes
## 2026-03-10 - 3.44.0 - feat(appui-tabs)
add support for left/right tab action buttons and content tab action APIs
- Introduce ITabAction interface and add actionsLeft/actionsRight properties to dees-appui-tabs, dees-appui-maincontent, and dees-appui.
- Render action buttons with new styles and renderActions() helper, including disabled state and click handlers; wire actions into tab components.
- Add public clear() on dees-appui-tabs and improve tab selection logic to reset selection when tabs become empty or when the selected tab is removed.
- Expose setContentTabActionsLeft and setContentTabActionsRight on the DeesAppui programmatic API and update interfaces/appconfig accordingly.
- Update demos to showcase action buttons, add clear-all behavior, and adjust layout/styling for action areas.
## 2026-03-09 - 3.43.4 - fix(media)
remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies
- Removed deprecated PDF components and related demos/styles: ts_web/elements/00group-media/dees-pdf/* and ts_web/elements/00group-media/dees-pdf-preview/*
- Removed exports for dees-pdf and dees-pdf-preview from ts_web/elements/00group-media/index.ts (public API removal)
- Dependency upgrades: @design.estate/dees-domtools → ^2.3.9, apexcharts → ^5.10.3, lucide → ^0.577.0, @fortawesome/* → ^7.2.0
- DevDependency upgrades: @git.zone/tsbuild → ^4.3.0, @git.zone/tsbundle → ^2.9.1, @git.zone/tstest → ^3.3.2, @git.zone/tswatch → ^3.2.5, @types/node → ^25.3.5
- Updated ts_web/services/versions.ts to align CDN/version constants (apexcharts, tiptap → 2.27.2, fontawesome)
## 2026-02-24 - 3.43.3 - fix(dees-table)
use lucide icon identifier for Search action in dees-table
- Replaced iconName 'magnifyingGlass' with 'lucide:Search' in ts_web/elements/00group-dataview/dees-table/dees-table.ts
- Updates the icon identifier for the header 'Search' action; no functional behavior changed
## 2026-02-21 - 3.43.2 - fix(dees-chart-log)
avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs
- Prevent property-bound logEntries from duplicating entries already present in logBuffer by deduplicating on timestamp|message
- Call updateMetrics() when replaying or appending log entries so metrics stay accurate
- Skip processing entirely when the incoming logEntries array is unchanged
- Optimize append-only updates by writing only the new tail entries instead of full re-render
- Enforce maxEntries when appending to the logBuffer to maintain buffer size
- Respect filterMode and searchQuery when deciding whether to write appended entries to the terminal
## 2026-02-21 - 3.43.1 - fix(dees-chart-log)
replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs
- Replay entries stored in logBuffer after terminal initialization to avoid losing entries that arrived early
- Add updated() lifecycle hook to copy logEntries into logBuffer and call reRenderFilteredLogs when terminalReady
- Call super.updated(changedProperties) to preserve base class behavior
## 2026-02-17 - 3.43.0 - feat(dees-form)
add layout styles to dees-form and standardize demo input grouping
- Add static CSS to dees-form: default column layout with gap and support for [horizontal-layout] (row wrapping, alignment and gap).
- Remove inline <style> from dees-form render to centralize styling.
- Simplify dees-input-base styles by removing host margins and making spacing container-driven.
- Update multiple demo files to wrap related inputs in a new .input-group container and include .input-group CSS for consistent vertical spacing.
## 2026-02-16 - 3.42.2 - fix(dees-chart-area)
add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data
- Introduces ApexAxisChartSeries type alias with support for number arrays, [x,y] tuples, and object {x,y,...} series entries
- Type-only change — no runtime or API behavior modified
- File changed: ts_web/elements/00group-chart/dees-chart-area/component.ts
## 2026-02-16 - 3.42.1 - fix(dees-table)
Guard against undefined action.type in dees-table by using optional chaining and update several dependencies
- Use optional chaining (action.type?.includes(...)) in ts_web/elements/.../dees-table.ts to prevent runtime errors when action.type is undefined
- Bump dependency apexcharts from ^5.3.6 to ^5.5.0
- Bump dependency lucide from ^0.563.0 to ^0.564.0
- Bump devDependency @git.zone/tswatch from ^3.0.1 to ^3.1.0
- Bump devDependency @types/node from ^25.0.10 to ^25.2.3
## 2026-02-02 - 3.42.0 - feat(dees-form-submit)
forward button properties to internal dees-button, use property bindings, add demo and styles
- Added forwarded properties: type, size, icon, iconPosition (with defaults) and preserved text/status/disabled
- Changed template to use property bindings (.prop) for dees-button instead of string attributes
- Switched internal event handler to listen for dees-button's @clicked event (was @click)
- Added component styles (:host display and dees-button width:100%) and improved layout
- Expanded demo with multiple usage examples (basic, icons, types, sizes, states, and a form context)
## 2026-02-02 - 3.41.6 - fix(dees-simple-appdash)
respect selectedView when loading initial view, falling back to the first tab
- firstUpdated now loads this.selectedView if set, otherwise loads the first view tab
- Prevents always loading the first tab and preserves a previously selected view on initial render
## 2026-02-01 - 3.41.5 - fix(dees-service-lib-loader)
prevent horizontal scrollbar by offsetting xterm WidthCache measurement container
- Injects additional CSS into DeesServiceLibLoader to move xterm.js WidthCache measurement div off-screen horizontally (selector: body > div[style*="top: -50000px"][style*="width: 50000px"])
- Fixes root cause where xterm creates a large-width measurement container (width: 50000px) on document.body that expands scrollWidth and causes a horizontal scrollbar
- Change applied in ts_web/services/DeesServiceLibLoader.ts by concatenating the fix CSS into the injected stylesheet
## 2026-01-29 - 3.41.4 - fix()
no changes
- No files changed in this commit; no code or documentation modified
- No release required
## 2026-01-29 - 3.41.3 - fix(dees-pdf-viewer)
use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling
- Download and Print now use pdfDocument.getData() to create Blob URLs so in-memory PDFs (pdf.js) can be saved/printed.
- Print flow now opens an HTML wrapper with an iframe to allow onafterprint handling, auto-close, popup-fallback and timed cleanup of Blob URLs.
- Added try/catch logging, URL.revokeObjectURL calls and safety timeouts to avoid resource leaks.
- Removed context menu items that relied on the raw PDF URL (Open in New Tab, Copy PDF URL); Download/Print actions now await the async handlers.
## 2026-01-28 - 3.41.2 - fix(dees-pdf-viewer)
account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays
- Multiply canvas.width and canvas.height by window.devicePixelRatio (dpr) and use Math.floor to set the actual pixel buffer size
- Call ctx.scale(dpr, dpr) so drawing is rendered at device pixels while keeping CSS display size unchanged
- Apply the same high-DPI adjustments to both main page rendering and thumbnail generation
- Keep canvas.style.width and canvas.style.height set to viewport dimensions to preserve layout
## 2026-01-27 - 3.41.1 - fix(dataview-codebox)
fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies
- Updated CSS in ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts: added height:100%, box-sizing, display:flex and flex-direction:column on container, set flex-shrink on header elements, made code grid overflow:auto with flex:1 and min-height:0 to prevent overflow issues.
- Bumped dependencies in package.json: @design.estate/dees-domtools from ^2.3.7 to ^2.3.8 and @design.estate/dees-element from ^2.1.5 to ^2.1.6.
- Non-breaking visual/layout fix — suitable for a patch release.
## 2026-01-27 - 3.41.0 - feat(docs)
document new media & tile components and expand Workspace/IDE component docs; update component count to 90+
- Updated README component count from 70+ to 90+.
- Added Media & Tile components documentation (DeesTilePdf, DeesTileImage, DeesTileAudio, DeesTileVideo, DeesTileNote, DeesTileFolder, DeesPreview, DeesPdfViewer, DeesImageViewer, DeesAudioViewer, DeesVideoViewer).
- Expanded Workspace/IDE documentation and introduced workspace subcomponents (DeesWorkspace, DeesWorkspaceMonaco, DeesWorkspaceDiffEditor, DeesWorkspaceFiletree, DeesWorkspaceTerminal, DeesWorkspaceTerminalPreview, DeesWorkspaceMarkdown, DeesWorkspaceMarkdownoutlet, DeesWorkspaceBottombar).
- Enhanced Contextmenu docs to demonstrate nested submenus and programmatic API usage.
- Added ITileFolderItem interface example for tile folder items.
## 2026-01-27 - 3.40.0 - feat(dees-tile)
unify tile badge styling and markup; replace component-specific badge classes with shared tile-badge classes and update related imports/tests
- Add shared badge styles: .tile-badge-corner, .tile-badge-topright, .tile-badge and layout rules in dees-tile-shared/styles.ts
- Update tile components (audio, video, image, folder, note, pdf) to use new badge markup and remove duplicated badge CSS
- Shift badge positioning when a tile label is present (.tile-container:has(.tile-label))
- Remove older per-component badge rules (duration-badge, item-count-badge, dimension-badge, note-language/note-line-indicator, preview-page-indicator) and replace with unified classes
- Update tests to import dees-contextmenu and dees-dashboardgrid from new 00group-overlay / 00group-layout paths to reflect folder reorganization
## 2026-01-27 - 3.39.1 - fix(dees-tile-note)
use horizontal pointer position to scroll note body by computing percentage from clientX and element width instead of clientY and height
- Changed ts_web/elements/00group-media/dees-tile-note/component.ts: use x = e.clientX - rect.left and percentage = x / rect.width to drive scrollTop calculation instead of using vertical coordinates
- Fixes incorrect scroll mapping where vertical mouse position was used for horizontal scrolling interaction
## 2026-01-27 - 3.39.0 - feat(components)
add large set of new UI components and demos, reorganize groups, and bump a few dependencies
- Add Media viewers: dees-image-viewer, dees-audio-viewer, dees-video-viewer, DeesPdf/DeesPdfViewer and related PDF utilities (CanvasPool, PdfManager) and demos
- Introduce dees-preview composite that auto-detects content and delegates to appropriate viewers
- New Tile system (DeesTileBase) and tile components: dees-tile-pdf, dees-tile-image, dees-tile-audio, dees-tile-video, dees-tile-note, dees-tile-folder plus demos
- Add dashboard/grid system: dees-dashboardgrid with drag/resize, collision resolution, layout helpers, demos and utilities
- Data view additions: dees-table with lucene-like query parser, column utilities, and dees-statsgrid enhancements (including cpuCores visualization)
- New feedback & overlay components: dees-toast, dees-actionbar, dees-progressbar, dees-spinner, dees-badge and supporting demos/interfaces
- Many layout and utility components added or improved: dees-panel, dees-chips, dees-heading, dees-label, dees-pagination, dees-stepper, and associated demos
- Refactor project structure: components organized into 00group-* directories and demo metadata migrated from demoGroup to demoGroups (string[]); many import path updates to match new grouping
- Deprecations/notes: dees-pdf-preview and legacy dees-pdf marked as deprecated in favor of new viewers/tiles
- Dependency bumps in package.json: @design.estate/dees-wcctools -> ^3.8.0, @git.zone/tstest -> ^3.1.8
## 2026-01-26 - 3.38.0 - feat(appui)
add app shell and bottom bar APIs, new input components, and update README component listings and docs
- README: updated components count (80+ → 70+), reorganized categories (added App Shell / Pre-built Templates, renamed Workspace/IDE) and improved typography
- Document listing: added DeesActionbar, DeesInputToggle, DeesInputCode, DeesAppuiBottombar and other component entries
- Menu item interface: added closeable and onClose properties for dismissible menu items
- IViewDefinition: expanded content type to accept element constructors and async content, added badgeVariant and cache flags
- New interfaces added: IBottomBarWidget, IBottomBarAction, IViewActivationContext to support bottom bar widgets/actions and view activation context
## 2026-01-25 - 3.37.1 - fix(editor)
fix monaco/editor layout, update dev deps, simplify watch script and remove Playwright snapshots
- Make dees-input-code host and inner wrappers flex so Monaco editor grows to fill available height (set flex, min-height and height:100% where needed).
- Set dees-workspace-monaco and editor-wrapper to stretch so the embedded Monaco editor fills the component.
- Bump dev dependencies: @git.zone/tsbuild -> ^4.1.2, @git.zone/tsbundle -> ^2.8.3, @git.zone/tstest -> ^3.1.7, @git.zone/tswatch -> ^3.0.1; also bump @types/node -> ^25.0.10 and lucide -> ^0.563.0.
- Change npm script: watch from "tswatch element" to "tswatch" and add @git.zone/tswatch preset in npmextra.json.
- Add .playwright-mcp/ to .gitignore and remove many Playwright screenshot/test artifact PNGs to keep repo tidy.
## 2026-01-13 - 3.37.0 - feat(dees-button,dees-statsgrid)
add unified icon property and icon-position support to dees-button; add partition and disk tile types to dees-statsgrid
- dees-button: introduce icon (string) and iconPosition ('left'|'right') properties; extractLightDom() migrates legacy <dees-icon> slotted usage into properties and supports icon-only buttons
- dees-button: render left/right icon, hide text for icon-only size; preserve backward compatibility for old type mappings
- Demo updates: convert iconFA attributes to new icon syntax (e.g. 'fa:plus', 'lucide:Search') and add new icon-via-property examples and event logging
- dees-statsgrid: add IPartitionData and IDiskData interfaces and new tile types 'partition' and 'disk' with rendering, styles and thresholds (usage, filesystem, mountpoint, capacity, iops, health)
- dees-statsgrid.demo: add Disk & Storage demo panel with sample partition and disk tiles and configuration notes
- misc: added/updated many demo images and metadata to reflect new icon/tile features
## 2026-01-12 - 3.36.0 - feat(dees-chart-log) ## 2026-01-12 - 3.36.0 - feat(dees-chart-log)
add xterm search addon support and enhance chart log demo with structured/raw (Docker-like) logs and themeable styles add xterm search addon support and enhance chart log demo with structured/raw (Docker-like) logs and themeable styles

View File

@@ -59,5 +59,8 @@
"bundler": "esbuild" "bundler": "esbuild"
} }
] ]
},
"@git.zone/tswatch": {
"preset": "element"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.36.0", "version": "3.48.4",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@@ -9,20 +9,20 @@
"scripts": { "scripts": {
"test": "tstest test/ --web --verbose --timeout 30 --logfile", "test": "tstest test/ --web --verbose --timeout 30 --logfile",
"build": "tsbuild tsfolders --allowimplicitany && tsbundle", "build": "tsbuild tsfolders --allowimplicitany && tsbundle",
"watch": "tswatch element", "watch": "tswatch",
"buildDocs": "tsdoc", "buildDocs": "tsdoc",
"postinstall": "node scripts/update-monaco-version.cjs" "postinstall": "node scripts/update-monaco-version.cjs"
}, },
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.7", "@design.estate/dees-domtools": "^2.5.1",
"@design.estate/dees-element": "^2.1.5", "@design.estate/dees-element": "^2.2.2",
"@design.estate/dees-wcctools": "^3.7.1", "@design.estate/dees-wcctools": "^3.8.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.2.0",
"@push.rocks/smarti18n": "^1.0.4", "@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0", "@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.1.0", "@push.rocks/smartstring": "^4.1.0",
@@ -34,22 +34,22 @@
"@tiptap/extension-underline": "^2.23.0", "@tiptap/extension-underline": "^2.23.0",
"@tiptap/starter-kit": "^2.23.0", "@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.3.0", "@tsclass/tsclass": "^9.3.0",
"apexcharts": "^5.3.6", "apexcharts": "^5.10.3",
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"ibantools": "^4.5.1", "ibantools": "^4.5.1",
"lucide": "^0.562.0", "lucide": "^0.577.0",
"monaco-editor": "0.55.1", "monaco-editor": "0.55.1",
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0" "xterm-addon-fit": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.1.0", "@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.8.1", "@git.zone/tsbundle": "^2.9.1",
"@git.zone/tstest": "^3.1.4", "@git.zone/tstest": "^3.3.2",
"@git.zone/tswatch": "^2.3.13", "@git.zone/tswatch": "^3.3.0",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.6" "@types/node": "^25.4.0"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

6674
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -912,6 +912,93 @@ appui.getBottomBarVisible();
- `ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts` - Demo - `ts_web/elements/00group-appui/dees-appui-bottombar/dees-appui-bottombar.demo.ts` - Demo
- `ts_web/elements/interfaces/appconfig.ts` - New interfaces added - `ts_web/elements/interfaces/appconfig.ts` - New interfaces added
## Media Components (2026-01-26)
New media viewer components and a unified preview composite component.
### Directory: `ts_web/elements/00group-media/`
#### dees-image-viewer
- Image display with zoom, pan, fit, and download controls
- Properties: `src`, `alt`, `fit` ('contain'|'cover'|'actual'), `showToolbar`
- Features: mouse wheel zoom, click-drag pan, double-click toggle, checkerboard transparency background
- Toolbar matches PDF viewer pattern (48px height, 32px buttons, 16px icons, 6px border-radius)
#### dees-audio-viewer
- Audio player with waveform visualization via Web Audio API
- Properties: `src`, `title`, `artist`, `showWaveform`, `autoplay`, `loop`
- Features: canvas waveform rendering, play/pause, seek, volume control, mute toggle, loop toggle
- Uses `HTMLAudioElement` for playback, `AudioContext.decodeAudioData` for waveform data
#### dees-video-viewer
- Video player with custom overlay controls
- Properties: `src`, `poster`, `showControls`, `autoplay`, `loop`, `muted`
- Features: custom controls bar with gradient, seekbar, volume slider, fullscreen toggle, auto-hide controls, 16:9 aspect ratio
### dees-preview (Composite Component)
- Auto-detects content type and delegates to the appropriate viewer
- Directory: `ts_web/elements/dees-preview/`
- Properties: `url`, `file` (File object), `base64`, `textContent`, `contentType` (override), `language`, `mimeType`, `filename`, `showToolbar`, `showFilename`
- Content type detection priority: explicit override → MIME type → file extension → fallback
- Renders: image→DeesImageViewer, pdf→DeesPdfViewer, code→DeesDataviewCodebox, audio→DeesAudioViewer, video→DeesVideoViewer, text→pre, unknown→placeholder
- Header bar with file type icon, filename, and type badge
### dees-dataview-codebox modification
- Removed `<dees-windowcontrols>` elements from the appbar (Step 1 of the plan)
- Now shows clean centered filename title bar without fake window buttons
### Icon Sizing Convention
- All `dees-icon` elements in buttons need explicit `font-size: 16px` CSS rule
- Toolbar buttons: 32px × 32px, border-radius: 6px
- Placeholder/error icons: `font-size: 32px`
- Pattern: `.button-class dees-icon { font-size: 16px; }`
## Tile Component System (2026-01-27)
A family of 200×260px content preview cards with a shared abstract base class. All tiles support lazy loading (IntersectionObserver with 200px margin), hover lift effect, click events, loading/error states, and three sizes (small: 150×195, default: 200×260, large: 250×325).
### Architecture
- **DeesTileBase** (`dees-tile-shared/DeesTileBase.ts`) — Abstract base class extending DeesElement
- Common properties: `clickable`, `loading`, `error`, `size`, `label`
- IntersectionObserver lazy loading via `onBecameVisible()` hook
- Click dispatch via `tile-click` CustomEvent (detail from `getTileClickDetail()`)
- Subclasses implement `renderTileContent(): TemplateResult`
### Components
| Tag | Class | Description |
|-----|-------|-------------|
| `dees-tile-pdf` | `DeesTilePdf` | PDF page thumbnail with hover-to-browse pages. Canvas-rendered via PDF.js/CanvasPool. |
| `dees-tile-image` | `DeesTileImage` | Image thumbnail with `object-fit: cover`, dimension detection on load |
| `dees-tile-audio` | `DeesTileAudio` | Music icon + mini waveform (AudioContext decode), duration badge |
| `dees-tile-video` | `DeesTileVideo` | Auto-captured first frame, duration badge, hover muted auto-preview |
| `dees-tile-note` | `DeesTileNote` | First ~12 lines of text in monospace, gradient fade, optional language badge |
| `dees-tile-folder` | `DeesTileFolder` | 2×2 grid of mini-previews (thumbnails or type icons), item count badge |
### Deprecations
- `dees-pdf-preview` → Use `dees-tile-pdf` instead. Old tag still works as a thin wrapper with console warning.
- `dees-pdf` deprecation comment updated to reference `DeesTilePdf`.
### File Structure
All tile components live in `ts_web/elements/00group-media/dees-tile-*/`:
- `component.ts` — Main component class
- `demo.ts` — Demo function
- `index.ts` — Re-export
- `styles.ts` — (PDF tile only) Component-specific styles
- Shared base: `dees-tile-shared/{DeesTileBase,styles,index}.ts`
### Interface: ITileFolderItem
```typescript
interface ITileFolderItem {
type: 'pdf' | 'image' | 'audio' | 'video' | 'note' | 'folder' | 'unknown';
thumbnailSrc?: string;
name: string;
}
```
## StatsGrid Enhancements (2026-01-12) ## StatsGrid Enhancements (2026-01-12)
### Column Spanning ### Column Spanning
@@ -973,4 +1060,51 @@ Features:
- `trend` - Sparkline with recent data - `trend` - Sparkline with recent data
- `text` - Text value display - `text` - Text value display
- `multiPercentage` - Multiple progress bars - `multiPercentage` - Multiple progress bars
- `cpuCores` - Vertical bar visualization for CPU cores - `cpuCores` - Vertical bar visualization for CPU cores
## Component Group Taxonomy (2026-01-27)
All components are organized into `00group-*` directories with 14 groups visible in the wcctools sidebar. The `demoGroups` property (plural, `string[]`) replaces the old `demoGroup` (singular). Components can belong to multiple groups.
### Group Directories
| Directory | Group Name | Count |
|-----------|-----------|-------|
| `00group-appui` | App UI | 10 |
| `00group-button` | Button | 3 |
| `00group-chart` | Chart | 2 |
| `00group-dataview` | Data View | 4 |
| `00group-feedback` | Feedback | 6 |
| `00group-form` | Form | 2 |
| `00group-input` | Input | 18 |
| `00group-layout` | Layout | 7 |
| `00group-media` | Media | 14 (viewers + PDF + tiles) |
| `00group-overlay` | Overlay | 4 |
| `00group-simple` | Simple | 3 |
| `00group-utility` | Utility | 5 |
| `00group-workspace` | Workspace | 9 |
| `00group-runtime` | (internal) | - |
### Multi-Group Components
Some components appear in multiple groups via `demoGroups = ['Primary', 'Secondary']`:
- `dees-chart-log`: Chart, Workspace
- `dees-dataview-codebox`: Data View, Workspace
- `dees-input-code`: Input, Workspace
- `dees-input-wysiwyg`: Input, Workspace
- `dees-form-submit`: Form, Button
- `dees-preview`: Media, Data View
- `dees-pdf*` / `dees-tile-pdf`: Media, PDF
- `dees-stepper`: Layout, Form
- `dees-label`: Layout, Input
- `dees-toast`: Feedback, Overlay
- `dees-actionbar`: Feedback, Overlay
### Import Conventions
- Within same group: `import '../sibling-component/file.js'`
- Cross-group (from depth-2): `import '../../00group-X/component/file.js'`
- Shared utilities: `import '../../00plugins.js'`, `import '../../00theme.js'`, etc.
### Key Notes
- The old `demoGroup` property (singular, string) is fully removed
- All 79 components with demos use `demoGroups` (plural, string[])
- `00group-pdf` no longer exists; PDF components are in `00group-media`
- `dees-search` and `dees-tooltip` remain standalone (no demos)

660
readme.md
View File

@@ -1,6 +1,6 @@
# @design.estate/dees-catalog # @design.estate/dees-catalog
A comprehensive web components library built with TypeScript and LitElement, providing **80+ production-ready UI components** for building modern web applications with consistent design and behavior. 🚀 A comprehensive web components library built with TypeScript and LitElement, providing **90+ production-ready UI components** for building modern web applications with consistent design and behavior. 🚀
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
[![LitElement](https://img.shields.io/badge/LitElement-4.0+-orange.svg)](https://lit.dev/) [![LitElement](https://img.shields.io/badge/LitElement-4.0+-orange.svg)](https://lit.dev/)
@@ -11,12 +11,15 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
## ✨ Features ## ✨ Features
- 🎨 **Consistent Design System** - Beautiful, cohesive components following modern UI/UX principles - 🎨 **Consistent Design System** Beautiful, cohesive components following modern UI/UX principles
- 🌙 **Dark/Light Theme Support** - All components automatically adapt to your theme - 🌙 **Dark/Light Theme Support** All components automatically adapt to your theme
- ⌨️ **Keyboard Accessible** - Full keyboard navigation and ARIA support - ⌨️ **Keyboard Accessible** Full keyboard navigation and ARIA support
- 📱 **Responsive** - Mobile-first design that works across all screen sizes - 📱 **Responsive** Mobile-first design that works across all screen sizes
- 🔧 **TypeScript-First** - Fully typed APIs with excellent IDE support - 🔧 **TypeScript-First** Fully typed APIs with excellent IDE support
- 🧩 **Modular** - Use only what you need, tree-shakeable architecture - 🧩 **Modular** Use only what you need, tree-shakeable architecture
- 🏗️ **Full App Shell**`dees-appui` provides a complete application framework with menus, routing, activity log, and bottom bar
- 🎬 **Media Components** — Rich tile-based previews for PDFs, images, audio, video, notes, and folders
- 💻 **IDE Workspace** — Full workspace component with Monaco editor, file tree, terminal, and diff viewer
## 📦 Installation ## 📦 Installation
@@ -52,16 +55,17 @@ For developers working on this library, please refer to the [UI Components Playb
| Category | Components | | Category | Components |
|----------|------------| |----------|------------|
| **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols) | | **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols), [`DeesActionbar`](#deesactionbar) |
| **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesFormSubmit`](#deesformsubmit) | | **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputToggle`](#deesinputtoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesInputCode`](#deesinputcode), [`DeesFormSubmit`](#deesformsubmit) |
| **Layout** | [`DeesAppui`](#deesappui), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) | | **App Shell (Layout)** | [`DeesAppui`](#deesappui-), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiBottombar`](#deesappuibottombar), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) | | **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) |
| **Media & Tiles** | [`DeesTilePdf`](#deestilepdf), [`DeesTileImage`](#deestileimage), [`DeesTileAudio`](#deestileaudio), [`DeesTileVideo`](#deestilevideo), [`DeesTileNote`](#deestilenote), [`DeesTileFolder`](#deestilefolder), [`DeesPreview`](#deespreview), [`DeesPdfViewer`](#deespdfviewer), [`DeesPdfPreview`](#deespdfpreview), [`DeesImageViewer`](#deesimageviewer), [`DeesAudioViewer`](#deesaudioviewer), [`DeesVideoViewer`](#deesvideoviewer) |
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) | | **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
| **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) | | **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
| **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) | | **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
| **Development** | [`DeesEditor`](#deeseditor), [`DeesEditorMarkdown`](#deeseditormarkdown), [`DeesEditorMarkdownoutlet`](#deeseditormarkdownoutlet), [`DeesTerminal`](#deesterminal), [`DeesUpdater`](#deesupdater) | | **Workspace / IDE** | [`DeesWorkspace`](#deesworkspace), [`DeesWorkspaceMonaco`](#deesworkspacemonaco), [`DeesWorkspaceDiffEditor`](#deesworkspacediffeditor), [`DeesWorkspaceFiletree`](#deesworkspacefiletree), [`DeesWorkspaceTerminal`](#deesworkspaceterminal), [`DeesWorkspaceTerminalPreview`](#deesworkspaceterminalpreview), [`DeesWorkspaceMarkdown`](#deesworkspacemarkdown), [`DeesWorkspaceMarkdownoutlet`](#deesworkspacemarkdownoutlet), [`DeesWorkspaceBottombar`](#deesworkspacebottombar) |
| **Theming** | [`DeesTheme`](#deestheme) | | **Theming** | [`DeesTheme`](#deestheme) |
| **Auth & Utilities** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) | | **Pre-built Templates** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) |
| **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) | | **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) |
--- ---
@@ -117,14 +121,14 @@ Interactive chips/tags with selection capabilities.
Display icons from FontAwesome and Lucide icon libraries with library prefixes. Display icons from FontAwesome and Lucide icon libraries with library prefixes.
```typescript ```typescript
// FontAwesome icons - use 'fa:' prefix // FontAwesome icons use 'fa:' prefix
<dees-icon <dees-icon
icon="fa:check" // FontAwesome icon with fa: prefix icon="fa:check" // FontAwesome icon with fa: prefix
iconSize="24" // Size in pixels iconSize="24" // Size in pixels
color="#22c55e" // Optional: custom color color="#22c55e" // Optional: custom color
></dees-icon> ></dees-icon>
// Lucide icons - use 'lucide:' prefix // Lucide icons use 'lucide:' prefix
<dees-icon <dees-icon
icon="lucide:menu" // Lucide icon with lucide: prefix icon="lucide:menu" // Lucide icon with lucide: prefix
iconSize="24" // Size in pixels iconSize="24" // Size in pixels
@@ -134,7 +138,7 @@ Display icons from FontAwesome and Lucide icon libraries with library prefixes.
// Legacy API (deprecated but still supported) // Legacy API (deprecated but still supported)
<dees-icon <dees-icon
iconFA="check" // Without prefix - assumes FontAwesome iconFA="check" // Without prefix assumes FontAwesome
></dees-icon> ></dees-icon>
``` ```
@@ -287,6 +291,18 @@ Window control buttons (minimize, maximize, close) for desktop-like applications
></dees-windowcontrols> ></dees-windowcontrols>
``` ```
#### `DeesActionbar`
Floating action bar for contextual actions.
```typescript
<dees-actionbar
.actions=${[
{ icon: 'lucide:save', label: 'Save', action: () => handleSave() },
{ icon: 'lucide:trash', label: 'Delete', action: () => handleDelete() }
]}
></dees-actionbar>
```
--- ---
### Form Components ### Form Components
@@ -331,6 +347,18 @@ Checkbox input component for boolean values.
></dees-input-checkbox> ></dees-input-checkbox>
``` ```
#### `DeesInputToggle`
Toggle switch component for boolean on/off states.
```typescript
<dees-input-toggle
key="darkMode"
label="Enable Dark Mode"
.value=${true}
@change=${handleToggle}
></dees-input-toggle>
```
#### `DeesInputDropdown` #### `DeesInputDropdown`
Dropdown selection component with search and filtering capabilities. Dropdown selection component with search and filtering capabilities.
@@ -658,6 +686,19 @@ Advanced block-based editor with slash commands and rich content blocks.
- Collaborative editing ready - Collaborative editing ready
- Extensible block types - Extensible block types
#### `DeesInputCode`
Code input component for editing source code with syntax highlighting.
```typescript
<dees-input-code
key="snippet"
label="Code Snippet"
.value=${codeString}
language="typescript"
@change=${handleCodeChange}
></dees-input-code>
```
#### `DeesFormSubmit` #### `DeesFormSubmit`
Submit button component specifically designed for `DeesForm`. Submit button component specifically designed for `DeesForm`.
@@ -670,10 +711,10 @@ Submit button component specifically designed for `DeesForm`.
--- ---
### Layout Components ### App Shell (Layout) Components
#### `DeesAppui` #### `DeesAppui` 🏗️
A comprehensive application shell component providing a complete UI framework with navigation, menus, activity logging, and view management. A comprehensive application shell component providing a complete UI framework with navigation, menus, activity logging, bottom bar, and view management.
> **Full API Documentation**: See [ts_web/elements/00group-appui/dees-appui/readme.md](./ts_web/elements/00group-appui/dees-appui/readme.md) for complete documentation including all programmatic APIs, view lifecycle hooks, and TypeScript interfaces. > **Full API Documentation**: See [ts_web/elements/00group-appui/dees-appui/readme.md](./ts_web/elements/00group-appui/dees-appui/readme.md) for complete documentation including all programmatic APIs, view lifecycle hooks, and TypeScript interfaces.
@@ -690,7 +731,6 @@ class MyApp extends DeesElement {
async firstUpdated() { async firstUpdated() {
this.appui = this.shadowRoot.querySelector('dees-appui'); this.appui = this.shadowRoot.querySelector('dees-appui');
// Configure with views and menu
this.appui.configure({ this.appui.configure({
branding: { logoIcon: 'lucide:box', logoText: 'My App' }, branding: { logoIcon: 'lucide:box', logoText: 'My App' },
views: [ views: [
@@ -700,7 +740,13 @@ class MyApp extends DeesElement {
mainMenu: { mainMenu: {
sections: [{ name: 'Main', views: ['dashboard', 'settings'] }] sections: [{ name: 'Main', views: ['dashboard', 'settings'] }]
}, },
defaultView: 'dashboard' defaultView: 'dashboard',
bottomBar: {
visible: true,
widgets: [
{ id: 'status', iconName: 'lucide:activity', label: 'Online', status: 'success' }
]
}
}); });
} }
@@ -710,43 +756,123 @@ class MyApp extends DeesElement {
} }
``` ```
**Key Features:** **Architecture Overview:**
- **Configure API**: Single `configure()` method for complete app setup
- **View Management**: Automatic view caching, lazy loading, and lifecycle hooks
- **Programmatic APIs**: Full control over AppBar, Main Menu, Secondary Menu, Content Tabs, and Activity Log
- **View Lifecycle Hooks**: `onActivate()`, `onDeactivate()`, and `canDeactivate()` for view components
- **Hash-based Routing**: Automatic URL synchronization with view navigation
- **RxJS Observables**: `viewChanged$` and `viewLifecycle$` for reactive programming
- **TypeScript-first**: Typed `IViewActivationContext` passed to views on activation
- **Activity Log Toggle**: Built-in appbar button to show/hide activity panel with entry count badge
**Programmatic APIs include:** ```
- `navigateToView(viewId, params?)` - Navigate between views ┌─────────────────────────────────────────────────────────────────────┐
- `setAppBarMenus()`, `setBreadcrumbs()`, `setUser()` - Control the app bar │ AppBar (dees-appui-appbar) │
- `setMainMenu()`, `setMainMenuSelection()`, `setMainMenuBadge()` - Control main navigation │ ├── Menus (File, Edit, View...) │
- `setMainMenuCollapsed()`, `setMainMenuVisible()` - Control main menu visibility │ ├── Breadcrumbs │
- `setSecondaryMenu()`, `setSecondaryMenuCollapsed()`, `setSecondaryMenuVisible()` - Control secondary menu │ ├── User Profile + Dropdown │
- `setContentTabs()`, `setContentTabsVisible()` - Control view-specific UI │ └── Activity Log Toggle │
- `activityLog.add()`, `activityLog.addMany()`, `activityLog.clear()` - Manage activity entries ├─────────────┬───────────────────────────────────┬───────────────────┤
- `setActivityLogVisible()`, `toggleActivityLog()`, `getActivityLogVisible()` - Control activity panel │ Main Menu │ Content Area │ Activity Log
│ (collapsed/ │ ├── Content Tabs │ (slide panel) │
│ expanded) │ │ (closable, from tables/lists)│ │
│ │ └── View Container │ │
│ ┌─────────┐ │ └── Active View │ │
│ │ 🏠 Home │ ├─────────────────────────────────┐ │ │
│ │ 📁 Files│ │ Secondary Menu │ │ │
│ │ ⚙ Set.. │ │ ├── Collapsible Groups │ │ │
│ │ │ │ │ ├── Tabs / Actions │ │ │
│ └─────────┘ │ │ ├── Filters / Links │ │ │
│ │ │ └── Dividers / Headers │ │ │
├─────────────┴──┴───────────────────────────────┴───────────────────┤
│ Bottom Bar (dees-appui-bottombar) — 24px status bar │
│ ├── Status widgets (left/right) │
│ └── Action buttons (left/right) │
└─────────────────────────────────────────────────────────────────────┘
```
**Configuration (`IAppConfig`):**
**View Visibility Control:**
```typescript ```typescript
// In your view's onActivate hook interface IAppConfig {
onActivate(context: IViewActivationContext) { branding?: { logoIcon?: string; logoText?: string };
// Hide secondary menu for a fullscreen view appBar?: IAppBarConfig;
context.appui.setSecondaryMenuVisible(false); views: IViewDefinition[];
mainMenu?: IMainMenuConfig;
// Hide content tabs defaultView?: string;
context.appui.setContentTabsVisible(false); activityLog?: IActivityLogConfig;
bottomBar?: IBottomBarConfig;
// Collapse main menu to give more space onViewChange?: (viewId: string, view: IViewDefinition) => void;
context.appui.setMainMenuCollapsed(true); onSearch?: (query: string) => void;
} }
``` ```
**Key Features:**
- 🔧 **Configure API** — Single `configure()` method for complete app setup
- 📄 **View Management** — Automatic view caching, lazy loading, and lifecycle hooks (`onActivate`, `onDeactivate`, `canDeactivate`)
- 🧭 **Hash-based Routing** — Automatic URL synchronization with view navigation and parameterized routes
- 📊 **Activity Log** — Slide-out panel with stacked entries, date grouping, search, and filtering
- 📌 **Bottom Status Bar** — Configurable widgets and actions with status colors and loading states
- 🎯 **RxJS Observables**`viewChanged$` and `viewLifecycle$` for reactive programming
- 🏷️ **TypeScript-first** — Typed `IViewActivationContext` passed to views on activation
**Programmatic APIs:**
| Area | Methods |
|------|---------|
| **Navigation** | `navigateToView(viewId, params?)`, `getCurrentView()`, `getViewRegistry()` |
| **App Bar** | `setAppBarMenus()`, `updateAppBarMenu()`, `setBreadcrumbs()`, `setUser()`, `setProfileMenuItems()`, `setSearchVisible()`, `onSearch()`, `setWindowControlsVisible()` |
| **Main Menu** | `setMainMenu()`, `updateMainMenuGroup()`, `addMainMenuItem()`, `removeMainMenuItem()`, `setMainMenuSelection()`, `setMainMenuCollapsed()`, `setMainMenuVisible()`, `setMainMenuBadge()`, `clearMainMenuBadge()` |
| **Secondary Menu** | `setSecondaryMenu()`, `updateSecondaryMenuGroup()`, `addSecondaryMenuItem()`, `setSecondaryMenuSelection()`, `setSecondaryMenuCollapsed()`, `setSecondaryMenuVisible()`, `clearSecondaryMenu()` |
| **Content Tabs** | `setContentTabs()`, `addContentTab()`, `removeContentTab()`, `selectContentTab()`, `getSelectedContentTab()`, `setContentTabsVisible()`, `setContentTabsAutoHide()` |
| **Activity Log** | `activityLog.add()`, `activityLog.addMany()`, `activityLog.clear()`, `activityLog.getEntries()`, `activityLog.filter()`, `activityLog.search()`, `setActivityLogVisible()`, `toggleActivityLog()`, `getActivityLogVisible()` |
| **Bottom Bar** | `bottomBar.addWidget()`, `bottomBar.updateWidget()`, `bottomBar.removeWidget()`, `bottomBar.getWidget()`, `bottomBar.clearWidgets()`, `bottomBar.addAction()`, `bottomBar.removeAction()`, `bottomBar.clearActions()`, `setBottomBarVisible()`, `getBottomBarVisible()` |
| **Observables** | `viewChanged$`, `viewLifecycle$` |
**View Lifecycle Hooks:**
```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 becomes visible
async onActivate(context: IViewActivationContext) {
const { appui, viewId, params } = context;
// Set view-specific secondary menu
appui.setSecondaryMenu({
heading: 'Settings',
groups: [{ name: 'Options', items: [...] }]
});
// Control visibility of other shell parts
appui.setContentTabsVisible(false);
appui.setSecondaryMenuVisible(true);
}
// Called when navigating away
onDeactivate() { /* cleanup */ }
// Return false or a message string to block navigation
canDeactivate(): boolean | string {
if (this.hasUnsavedChanges) return 'You have unsaved changes. Leave anyway?';
return true;
}
}
```
**Secondary Menu Item Types:**
The secondary menu supports **8 distinct item types** for building rich contextual sidebars:
| Type | Description |
|------|-------------|
| **Tab** (default) | Selectable item that stays highlighted |
| **Action** | Executes on click without staying selected (blue styling) |
| **Filter** | Checkbox toggle for filtering |
| **MultiFilter** | Collapsible multi-select filter box |
| **Divider** | Visual separator line |
| **Header** | Non-interactive section label |
| **Link** | Opens an external URL |
| **Danger Action** | Red-styled action with optional confirmation |
#### `DeesAppuiMainmenu` #### `DeesAppuiMainmenu`
Main navigation menu component for application-wide navigation. Main navigation menu component for application-wide navigation. Supports collapsed (icon-only) mode.
```typescript ```typescript
<dees-appui-mainmenu <dees-appui-mainmenu
@@ -759,12 +885,12 @@ Main navigation menu component for application-wide navigation.
] ]
} }
]} ]}
collapsed // Optional: show collapsed version collapsed // Optional: show collapsed icon-only version
></dees-appui-mainmenu> ></dees-appui-mainmenu>
``` ```
#### `DeesAppuiSecondarymenu` #### `DeesAppuiSecondarymenu`
Secondary navigation component for sub-section selection with collapsible groups and badges. Secondary navigation component for sub-section selection with collapsible groups, badges, and 8 item types.
```typescript ```typescript
<dees-appui-secondarymenu <dees-appui-secondarymenu
@@ -806,28 +932,12 @@ Professional application bar component with hierarchical menus, breadcrumb navig
.menuItems=${[ .menuItems=${[
{ {
name: 'File', name: 'File',
action: async () => {}, // No-op for parent menu items action: async () => {},
submenu: [ submenu: [
{ { name: 'New File', shortcut: 'Cmd+N', iconName: 'file-plus', action: async () => handleNewFile() },
name: 'New File', { name: 'Open...', shortcut: 'Cmd+O', iconName: 'folder-open', action: async () => handleOpen() },
shortcut: 'Cmd+N', { divider: true },
iconName: 'file-plus', { name: 'Save', shortcut: 'Cmd+S', iconName: 'save', action: async () => handleSave(), disabled: true }
action: async () => handleNewFile()
},
{
name: 'Open...',
shortcut: 'Cmd+O',
iconName: 'folder-open',
action: async () => handleOpen()
},
{ divider: true }, // Menu separator
{
name: 'Save',
shortcut: 'Cmd+S',
iconName: 'save',
action: async () => handleSave(),
disabled: true // Disabled state
}
] ]
} }
]} ]}
@@ -849,12 +959,12 @@ Professional application bar component with hierarchical menus, breadcrumb navig
``` ```
**Key Features:** **Key Features:**
- **Hierarchical Menu System** - Top-level menus with dropdown submenus, icons, and keyboard shortcuts - **Hierarchical Menu System** Top-level menus with dropdown submenus, icons, and keyboard shortcuts
- **Keyboard Navigation** - Full keyboard support (Tab, Arrow keys, Enter, Escape) - **Keyboard Navigation** Full keyboard support (Tab, Arrow keys, Enter, Escape)
- **Breadcrumb Navigation** - Customizable breadcrumb trail with click events - **Breadcrumb Navigation** Customizable breadcrumb trail with click events
- **User Account Section** - Avatar with status indicator and profile dropdown - **User Account Section** Avatar with status indicator and profile dropdown
- **Activity Log Toggle** - Button with badge count to show/hide activity panel - **Activity Log Toggle** Button with badge count to show/hide activity panel
- **Accessibility** - Full ARIA support with menubar roles - **Accessibility** Full ARIA support with menubar roles
#### `DeesAppuiActivitylog` #### `DeesAppuiActivitylog`
Real-time activity log panel for displaying user actions and system events. Real-time activity log panel for displaying user actions and system events.
@@ -886,6 +996,61 @@ activityLog.search('settings'); // Search by message
- Animated slide-in/out panel - Animated slide-in/out panel
- Theme-aware styling - Theme-aware styling
#### `DeesAppuiBottombar`
A 24px fixed-height status bar at the bottom of the application shell. Supports status widgets and action buttons positioned left or right.
```typescript
// Configure via DeesAppui
appui.configure({
bottomBar: {
visible: true,
widgets: [
{
id: 'status',
iconName: 'lucide:activity',
label: 'System Online',
status: 'success', // 'idle' | 'active' | 'success' | 'warning' | 'error'
tooltip: 'All systems operational',
onClick: () => console.log('Status clicked'),
},
{
id: 'version',
iconName: 'lucide:gitBranch',
label: 'v1.2.3',
position: 'right',
}
],
actions: [
{
id: 'terminal',
iconName: 'lucide:terminal',
tooltip: 'Open Terminal',
position: 'right',
onClick: () => console.log('Terminal clicked'),
}
]
}
});
// Programmatic updates
appui.bottomBar.addWidget({ id: 'build', iconName: 'lucide:hammer', label: 'Building...', loading: true, status: 'active' });
appui.bottomBar.updateWidget('build', { label: 'Build complete', loading: false, status: 'success' });
appui.bottomBar.removeWidget('build');
appui.bottomBar.addAction({ id: 'refresh', iconName: 'lucide:refreshCw', onClick: () => location.reload() });
appui.bottomBar.removeAction('refresh');
appui.setBottomBarVisible(false);
```
**Key Features:**
- Configurable status widgets with icons, labels, and colored status indicators
- Loading spinner state for widgets
- Contextual actions with icon buttons
- Left/right positioning for both widgets and actions
- Tooltips on hover
- Context menu support per widget
#### `DeesAppuiTabs` #### `DeesAppuiTabs`
Reusable tab component with horizontal/vertical layout support. Reusable tab component with horizontal/vertical layout support.
@@ -1007,7 +1172,7 @@ A responsive grid component for displaying statistical data with various visuali
value: 125420, value: 125420,
unit: '$', unit: '$',
type: 'number', type: 'number',
icon: 'faDollarSign', icon: 'lucide:dollarSign',
description: '+12.5% from last month', description: '+12.5% from last month',
color: '#22c55e' color: '#22c55e'
}, },
@@ -1016,7 +1181,7 @@ A responsive grid component for displaying statistical data with various visuali
title: 'CPU Usage', title: 'CPU Usage',
value: 73, value: 73,
type: 'gauge', type: 'gauge',
icon: 'faMicrochip', icon: 'lucide:cpu',
gaugeOptions: { gaugeOptions: {
min: 0, max: 100, min: 0, max: 100,
thresholds: [ thresholds: [
@@ -1032,8 +1197,22 @@ A responsive grid component for displaying statistical data with various visuali
value: '1.2k', value: '1.2k',
unit: '/min', unit: '/min',
type: 'trend', type: 'trend',
icon: 'faServer', icon: 'lucide:server',
trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92] trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92]
},
{
id: 'cores',
title: 'CPU Cores',
value: 0,
type: 'cpuCores',
icon: 'lucide:cpu',
columnSpan: 2,
coresData: [
{ id: 0, usage: 45, label: '0' },
{ id: 1, usage: 72, label: '1' },
{ id: 2, usage: 30, label: '2' },
{ id: 3, usage: 88, label: '3' }
]
} }
]} ]}
.minTileWidth=${250} .minTileWidth=${250}
@@ -1041,6 +1220,8 @@ A responsive grid component for displaying statistical data with various visuali
></dees-statsgrid> ></dees-statsgrid>
``` ```
**Tile Types:** `number`, `gauge`, `percentage`, `trend`, `text`, `multiPercentage`, `cpuCores`
#### `DeesPagination` #### `DeesPagination`
Pagination component for navigating through large datasets. Pagination component for navigating through large datasets.
@@ -1056,6 +1237,146 @@ Pagination component for navigating through large datasets.
--- ---
### Media & Tile Components 🎬
A rich collection of tile-based preview components for displaying media files in grids. All tiles share a consistent base class (`DeesTileBase`) with lazy loading via `IntersectionObserver`, hover interactions, click events, context menus, and three size variants (`small`, `default`, `large`).
All tile badges use a unified styling system with label-awareness — when a `label` is set, bottom badges automatically shift up to avoid overlapping.
#### `DeesTilePdf`
PDF document tile with live page preview on hover.
```typescript
<dees-tile-pdf
pdfUrl="/documents/report.pdf"
label="Annual Report"
clickable
@tile-click=${handleClick}
></dees-tile-pdf>
```
**Key Features:**
- Renders first page as canvas preview
- Hover to scrub through pages (mouse X position maps to page number)
- Shows page count badge, hover page indicator
- Detects A4/Letter vs non-standard aspect ratios
#### `DeesTileImage`
Image tile with lazy loading and dimension display.
```typescript
<dees-tile-image
src="/photos/landscape.jpg"
alt="Mountain landscape"
label="landscape.jpg"
clickable
@tile-click=${handleClick}
></dees-tile-image>
```
**Key Features:**
- Lazy loads image on scroll into view
- Shows image dimensions on hover (e.g. "1920 × 1080")
- Checkerboard background for transparent images
#### `DeesTileAudio`
Audio file tile with waveform visualization.
```typescript
<dees-tile-audio
src="/music/track.mp3"
title="Summer Vibes"
artist="DJ Example"
clickable
@tile-click=${handleClick}
></dees-tile-audio>
```
**Key Features:**
- Generates waveform visualization from audio data
- Shows duration badge (e.g. "3:42")
- Displays title and artist metadata
- Play overlay on hover
#### `DeesTileVideo`
Video tile with thumbnail capture and hover preview.
```typescript
<dees-tile-video
src="/videos/intro.mp4"
poster="/thumbs/intro.jpg"
label="Introduction"
clickable
@tile-click=${handleClick}
></dees-tile-video>
```
**Key Features:**
- Auto-captures first frame as thumbnail (or uses provided `poster`)
- Plays video preview on hover
- Shows duration badge
- Play button overlay
#### `DeesTileNote`
Code/text snippet tile with syntax-aware display.
```typescript
<dees-tile-note
title="config.ts"
language="TypeScript"
.content=${codeString}
clickable
@tile-click=${handleClick}
></dees-tile-note>
```
**Key Features:**
- Monospace font with line numbers
- Language badge (top-right)
- Scrollable content on hover (mouse X position controls scroll)
- Line indicator badge while scrolling
- Gradient fade at bottom
#### `DeesTileFolder`
Folder tile with 2×2 content preview grid.
```typescript
<dees-tile-folder
name="Project Assets"
.items=${[
{ type: 'image', name: 'logo.png', thumbnailSrc: '/thumbs/logo.png' },
{ type: 'pdf', name: 'spec.pdf' },
{ type: 'audio', name: 'jingle.mp3' },
{ type: 'video', name: 'demo.mp4' },
]}
clickable
@tile-click=${handleClick}
></dees-tile-folder>
```
**Key Features:**
- 2×2 preview grid showing first 4 items (thumbnails or type icons)
- Item count badge (e.g. "12 items")
- Folder icon header with name
- Supports: `pdf`, `image`, `audio`, `video`, `note`, `folder`, `unknown` types
#### `DeesPreview`
Unified preview component that auto-selects the right tile type based on content.
#### `DeesPdfViewer` / `DeesPdfPreview`
Full PDF viewing components with navigation controls.
#### `DeesImageViewer`
Full-screen image viewer with zoom and pan.
#### `DeesAudioViewer`
Audio playback component with waveform and controls.
#### `DeesVideoViewer`
Video playback component with standard controls.
---
### Visualization Components ### Visualization Components
#### `DeesChartArea` #### `DeesChartArea`
@@ -1116,16 +1437,41 @@ DeesModal.createAndShow({
``` ```
#### `DeesContextmenu` #### `DeesContextmenu`
Context menu component for right-click actions. Context menu component for right-click actions with nested submenu support.
```typescript ```typescript
<dees-contextmenu // Programmatic usage
.items=${[ DeesContextmenu.openContextMenuWithOptions(mouseEvent, [
{ label: 'Edit', icon: 'edit', action: () => handleEdit() }, {
{ label: 'Delete', icon: 'trash', action: () => handleDelete() } name: 'Edit',
]} iconName: 'lucide:edit',
position="right" action: async () => handleEdit()
></dees-contextmenu> },
{ divider: true },
{
name: 'More Options',
iconName: 'lucide:moreHorizontal',
submenu: [
{ name: 'Duplicate', iconName: 'lucide:copy', action: async () => handleDuplicate() },
{ name: 'Archive', iconName: 'lucide:archive', action: async () => handleArchive() },
]
},
{
name: 'Delete',
iconName: 'lucide:trash2',
action: async () => handleDelete()
}
]);
// Component-based (implement getContextMenuItems on any element)
class MyComponent extends DeesElement {
getContextMenuItems() {
return [
{ name: 'View Details', iconName: 'lucide:eye', action: async () => { ... } },
{ name: 'Edit', iconName: 'lucide:edit', action: async () => { ... } },
];
}
}
``` ```
#### `DeesSpeechbubble` #### `DeesSpeechbubble`
@@ -1189,7 +1535,7 @@ Progress indicator component for tracking completion status.
Theme provider component that wraps children and provides CSS custom properties for consistent theming. Theme provider component that wraps children and provides CSS custom properties for consistent theming.
```typescript ```typescript
// Basic usage - wrap your app // Basic usage wrap your app
<dees-theme> <dees-theme>
<my-app></my-app> <my-app></my-app>
</dees-theme> </dees-theme>
@@ -1217,76 +1563,71 @@ Theme provider component that wraps children and provides CSS custom properties
--- ---
### Development Components ### Workspace / IDE Components 💻
#### `DeesEditor` A full-featured IDE workspace component suite for building browser-based code editors, terminal interfaces, and documentation viewers.
Code editor component with syntax highlighting and code completion, powered by Monaco Editor.
#### `DeesWorkspace`
Top-level workspace shell that composes editor, file tree, terminal, and bottom bar into an IDE-like layout.
```typescript ```typescript
<dees-editor <dees-workspace></dees-workspace>
```
#### `DeesWorkspaceMonaco`
Monaco Editor integration for code editing with full IntelliSense, syntax highlighting, and language support.
```typescript
<dees-workspace-monaco
.value=${code} .value=${code}
@change=${handleCodeChange}
.language=${'typescript'} .language=${'typescript'}
.theme=${'vs-dark'} @change=${handleCodeChange}
.options=${{ ></dees-workspace-monaco>
lineNumbers: true,
minimap: { enabled: true },
autoClosingBrackets: 'always'
}}
></dees-editor>
``` ```
#### `DeesEditorMarkdown` #### `DeesWorkspaceDiffEditor`
Markdown editor component with live preview. Side-by-side diff editor powered by Monaco for comparing file versions.
```typescript ```typescript
<dees-editor-markdown <dees-workspace-diff-editor
.value=${markdown} .originalValue=${originalCode}
@change=${handleMarkdownChange} .modifiedValue=${modifiedCode}
.options=${{ preview: true, toolbar: true, spellcheck: true }} .language=${'typescript'}
></dees-editor-markdown> ></dees-workspace-diff-editor>
``` ```
#### `DeesEditorMarkdownoutlet` #### `DeesWorkspaceFiletree`
Markdown preview component for rendering markdown content. File tree navigation component with expand/collapse, file icons, and selection.
```typescript ```typescript
<dees-editor-markdownoutlet <dees-workspace-filetree
.markdown=${markdownContent} .files=${fileTreeData}
.theme=${'github'} @file-select=${handleFileSelect}
allowHtml={false} ></dees-workspace-filetree>
></dees-editor-markdownoutlet>
``` ```
#### `DeesTerminal` #### `DeesWorkspaceTerminal`
Terminal emulator component for command-line interface. Terminal emulator component powered by xterm.js.
```typescript ```typescript
<dees-terminal <dees-workspace-terminal></dees-workspace-terminal>
.commands=${{
'echo': (args) => `Echo: ${args.join(' ')}`,
'help': () => 'Available commands: echo, help'
}}
.prompt=${'$'}
.welcomeMessage=${'Welcome! Type "help" for available commands.'}
></dees-terminal>
``` ```
#### `DeesUpdater` #### `DeesWorkspaceTerminalPreview`
Component for managing application updates and version control. Terminal with integrated preview pane for output visualization.
```typescript #### `DeesWorkspaceMarkdown`
<dees-updater Markdown editor with live preview.
.currentVersion=${'1.5.2'}
.checkInterval=${3600000} #### `DeesWorkspaceMarkdownoutlet`
.autoUpdate=${false} Read-only markdown renderer for documentation display.
@update-available=${handleUpdateAvailable}
></dees-updater> #### `DeesWorkspaceBottombar`
``` IDE-style bottom status bar for the workspace.
--- ---
### Auth & Utilities Components ### Pre-built Templates
#### `DeesSimpleAppdash` #### `DeesSimpleAppdash`
Simple application dashboard component for quick prototyping. Simple application dashboard component for quick prototyping.
@@ -1360,6 +1701,8 @@ interface IMenuItem {
action: () => void; action: () => void;
badge?: string | number; badge?: string | number;
badgeVariant?: 'default' | 'success' | 'warning' | 'error'; badgeVariant?: 'default' | 'success' | 'warning' | 'error';
closeable?: boolean;
onClose?: () => void;
} }
// Menu group interface for organized menus // Menu group interface for organized menus
@@ -1375,11 +1718,13 @@ interface IViewDefinition {
id: string; id: string;
name: string; name: string;
iconName?: string; iconName?: string;
content: string | HTMLElement | (() => TemplateResult); content: string | (new () => HTMLElement) | (() => TemplateResult) | (() => Promise<any>);
secondaryMenu?: ISecondaryMenuGroup[]; secondaryMenu?: ISecondaryMenuGroup[];
contentTabs?: IMenuItem[]; contentTabs?: IMenuItem[];
route?: string; route?: string;
badge?: string | number; badge?: string | number;
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
cache?: boolean;
} }
// Activity log entry // Activity log entry
@@ -1392,6 +1737,43 @@ interface IActivityEntry {
iconName?: string; iconName?: string;
data?: Record<string, unknown>; data?: Record<string, unknown>;
} }
// Bottom bar widget
interface IBottomBarWidget {
id: string;
iconName?: string;
label?: string;
status?: 'idle' | 'active' | 'success' | 'warning' | 'error';
tooltip?: string;
loading?: boolean;
onClick?: () => void;
position?: 'left' | 'right';
order?: number;
}
// Bottom bar action button
interface IBottomBarAction {
id: string;
iconName: string;
tooltip?: string;
onClick: () => void | Promise<void>;
disabled?: boolean;
position?: 'left' | 'right';
}
// View activation context (passed to onActivate)
interface IViewActivationContext {
appui: DeesAppui;
viewId: string;
params?: Record<string, string>;
}
// Tile folder item (for DeesTileFolder)
interface ITileFolderItem {
type: 'pdf' | 'image' | 'audio' | 'video' | 'note' | 'folder' | 'unknown';
thumbnailSrc?: string;
name: string;
}
``` ```
--- ---

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,8 @@ import { tap, expect } from '@git.zone/tstest/tapbundle';
import { import {
resolveWidgetPlacement, resolveWidgetPlacement,
collectCollisions, collectCollisions,
} from '../ts_web/elements/dees-dashboardgrid/layout.ts'; } from '../ts_web/elements/00group-layout/dees-dashboardgrid/layout.ts';
import type { DashboardWidget } from '../ts_web/elements/dees-dashboardgrid/types.ts'; import type { DashboardWidget } from '../ts_web/elements/00group-layout/dees-dashboardgrid/types.ts';
tap.test('dashboardgrid does not overlap widgets after swap attempt', async () => { tap.test('dashboardgrid does not overlap widgets after swap attempt', async () => {
const widgets: DashboardWidget[] = [ const widgets: DashboardWidget[] = [

View File

@@ -0,0 +1,79 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import * as deesCatalog from '../ts_web/index.js';
tap.test('PDF viewer should render text layer', async () => {
const viewer = await webhelpers.fixture(
webhelpers.html`
<dees-pdf-viewer
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
initialZoom="page-fit"
style="height: 600px; width: 100%;"
></dees-pdf-viewer>
`
) as deesCatalog.DeesPdfViewer;
// Wait for PDF to load and render
await new Promise(resolve => setTimeout(resolve, 5000));
await viewer.updateComplete;
expect(viewer.totalPages).toBeGreaterThan(0);
const textLayer = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"]');
expect(textLayer).toBeTruthy();
const textSpans = textLayer?.querySelectorAll('span');
expect(textSpans?.length).toBeGreaterThan(0);
console.log(`Text layer has ${textSpans?.length} spans`);
});
tap.test('Text should be selectable', async () => {
const viewer = await webhelpers.fixture(
webhelpers.html`
<dees-pdf-viewer
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
initialZoom="page-fit"
style="height: 600px; width: 100%;"
></dees-pdf-viewer>
`
) as deesCatalog.DeesPdfViewer;
// Wait for PDF to load and render
await new Promise(resolve => setTimeout(resolve, 5000));
const textLayer = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"]');
const firstSpan = textLayer?.querySelector('span') as HTMLElement;
if (firstSpan?.textContent) {
const range = document.createRange();
const textNode = firstSpan.firstChild;
if (textNode) {
range.setStart(textNode, 0);
range.setEnd(textNode, Math.min(5, textNode.textContent?.length || 0));
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
expect(selection?.toString().length).toBeGreaterThan(0);
console.log('Selected text:', selection?.toString());
}
}
});
tap.test('endOfContent element exists for selection boundary', async () => {
const viewer = await webhelpers.fixture(
webhelpers.html`
<dees-pdf-viewer
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
initialZoom="page-fit"
style="height: 600px; width: 100%;"
></dees-pdf-viewer>
`
) as deesCatalog.DeesPdfViewer;
// Wait for PDF to load and render
await new Promise(resolve => setTimeout(resolve, 5000));
const endOfContent = viewer.shadowRoot?.querySelector('.text-layer[data-page="1"] .endOfContent');
expect(endOfContent).toBeTruthy();
});
export default tap.start();

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,8 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import '../../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import type { IActivityEntry, IActivityLogAPI } from '../../interfaces/appconfig.js'; import type { IActivityEntry, IActivityLogAPI } from '../../interfaces/appconfig.js';
import { demoFunc } from './dees-appui-activitylog.demo.js'; import { demoFunc } from './dees-appui-activitylog.demo.js';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
@@ -20,7 +20,7 @@ import { themeDefaultStyles } from '../../00theme.js';
export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI { export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI {
// STATIC // STATIC
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE PROPERTIES // INSTANCE PROPERTIES
@state() @state()

View File

@@ -15,8 +15,8 @@ import { appuiAppbarStyles } from './styles.js';
import { renderAppuiAppbar } from './template.js'; import { renderAppuiAppbar } from './template.js';
// Import required components // Import required components
import '../../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import '../../dees-windowcontrols/dees-windowcontrols.js'; import '../../00group-utility/dees-windowcontrols/dees-windowcontrols.js';
import '../dees-appui-profiledropdown/dees-appui-profiledropdown.js'; import '../dees-appui-profiledropdown/dees-appui-profiledropdown.js';
declare global { declare global {
@@ -28,7 +28,7 @@ declare global {
@customElement('dees-appui-appbar') @customElement('dees-appui-appbar')
export class DeesAppuiBar extends DeesElement { export class DeesAppuiBar extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE PROPERTIES // INSTANCE PROPERTIES
@property({ type: Array }) @property({ type: Array })

View File

@@ -8,8 +8,8 @@ import {
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
import '../../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import type { import type {
IBottomBarWidget, IBottomBarWidget,
IBottomBarAction, IBottomBarAction,
@@ -26,7 +26,7 @@ declare global {
@customElement('dees-appui-bottombar') @customElement('dees-appui-bottombar')
export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI { export class DeesAppuiBottombar extends DeesElement implements IBottomBarAPI {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE PROPERTIES // INSTANCE PROPERTIES
@state() @state()

View File

@@ -31,7 +31,7 @@ export class DeesAppuiMaincontent extends DeesElement {
</div> </div>
</dees-appui-maincontent> </dees-appui-maincontent>
`; `;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE // INSTANCE
@property({ @property({
@@ -53,6 +53,12 @@ export class DeesAppuiMaincontent extends DeesElement {
@property({ type: Number }) @property({ type: Number })
accessor tabsAutoHideThreshold: number = 0; accessor tabsAutoHideThreshold: number = 0;
@property({ type: Array })
accessor tabActionsLeft: interfaces.ITabAction[] = [];
@property({ type: Array })
accessor tabActionsRight: interfaces.ITabAction[] = [];
public static styles = [ public static styles = [
themeDefaultStyles, themeDefaultStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
@@ -106,6 +112,8 @@ export class DeesAppuiMaincontent extends DeesElement {
.tabStyle=${'horizontal'} .tabStyle=${'horizontal'}
.autoHide=${this.tabsAutoHide} .autoHide=${this.tabsAutoHide}
.autoHideThreshold=${this.tabsAutoHideThreshold} .autoHideThreshold=${this.tabsAutoHideThreshold}
.actionsLeft=${this.tabActionsLeft}
.actionsRight=${this.tabActionsRight}
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)} @tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleTabClose(e)} @tab-close=${(e: CustomEvent) => this.handleTabClose(e)}
></dees-appui-tabs> ></dees-appui-tabs>

View File

@@ -11,7 +11,7 @@ import {
css, css,
cssManager, cssManager,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import { demoFunc } from './dees-appui-mainmenu.demo.js'; import { demoFunc } from './dees-appui-mainmenu.demo.js';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
@@ -22,7 +22,7 @@ import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-mainmenu') @customElement('dees-appui-mainmenu')
export class DeesAppuiMainmenu extends DeesElement { export class DeesAppuiMainmenu extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE // INSTANCE

View File

@@ -35,7 +35,7 @@ export class DeesAppuiProfileDropdown extends DeesElement {
.isOpen=${true} .isOpen=${true}
></dees-appui-profiledropdown> ></dees-appui-profiledropdown>
`; `;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
@property({ type: Object }) @property({ type: Object })
accessor user: { accessor user: {

View File

@@ -1,8 +1,8 @@
import * as plugins from '../../00plugins.js'; import * as plugins from '../../00plugins.js';
import * as interfaces from '../../interfaces/index.js'; import * as interfaces from '../../interfaces/index.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import '../../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import { import {
DeesElement, DeesElement,
@@ -33,7 +33,7 @@ import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-secondarymenu') @customElement('dees-appui-secondarymenu')
export class DeesAppuiSecondarymenu extends DeesElement { export class DeesAppuiSecondarymenu extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE // INSTANCE

View File

@@ -2,7 +2,7 @@ import { html, cssManager, css, DeesElement, customElement, state } from '@desig
import * as interfaces from '../../interfaces/index.js'; import * as interfaces from '../../interfaces/index.js';
import type { DeesAppuiTabs } from './dees-appui-tabs.js'; import type { DeesAppuiTabs } from './dees-appui-tabs.js';
// Interactive demo component for closeable tabs // Interactive demo component for closeable tabs with action buttons
@customElement('demo-closeable-tabs') @customElement('demo-closeable-tabs')
class DemoCloseableTabs extends DeesElement { class DemoCloseableTabs extends DeesElement {
@state() @state()
@@ -18,24 +18,6 @@ class DemoCloseableTabs extends DeesElement {
:host { :host {
display: block; display: block;
} }
.controls {
display: flex;
gap: 8px;
margin-top: 16px;
}
button {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
border: 1px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.3)', 'rgba(59, 130, 246, 0.3)')};
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s ease;
}
button:hover {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
}
.info { .info {
margin-top: 16px; margin-top: 16px;
padding: 12px 16px; padding: 12px 16px;
@@ -66,17 +48,27 @@ class DemoCloseableTabs extends DeesElement {
this.tabs = this.tabs.filter(t => t.key !== tabKey); this.tabs = this.tabs.filter(t => t.key !== tabKey);
} }
private clearAll() {
const tabsEl = this.shadowRoot!.querySelector('dees-appui-tabs') as DeesAppuiTabs;
tabsEl?.clear();
this.tabs = [];
this.tabCounter = 0;
}
render() { render() {
const rightActions: interfaces.ITabAction[] = [
{ id: 'add', iconName: 'lucide:plus', action: () => this.addTab(), tooltip: 'New Tab' },
{ id: 'clear', iconName: 'lucide:trash2', action: () => this.clearAll(), tooltip: 'Clear All Tabs' },
];
return html` return html`
<dees-appui-tabs <dees-appui-tabs
.tabs=${this.tabs} .tabs=${this.tabs}
.actionsRight=${rightActions}
@tab-close=${(e: CustomEvent) => this.removeTab(e.detail.tab.key)} @tab-close=${(e: CustomEvent) => this.removeTab(e.detail.tab.key)}
></dees-appui-tabs> ></dees-appui-tabs>
<div class="controls">
<button @click=${() => this.addTab()}>+ Add New Tab</button>
</div>
<div class="info"> <div class="info">
Click the X button on tabs to close them. The "Main" tab is not closeable. Click the X button on tabs to close them. Use the + button to add tabs and the trash button to clear all.
<br>Current tabs: ${this.tabs.length} <br>Current tabs: ${this.tabs.length}
</div> </div>
`; `;
@@ -232,6 +224,16 @@ export const demoFunc = () => {
{ key: 'Archived', action: () => console.log('Archived clicked') }, { key: 'Archived', action: () => console.log('Archived clicked') },
]; ];
const actionsLeft: interfaces.ITabAction[] = [
{ id: 'back', iconName: 'lucide:arrowLeft', action: () => console.log('Back'), tooltip: 'Go Back' },
];
const actionsRight: interfaces.ITabAction[] = [
{ id: 'add', iconName: 'lucide:plus', action: () => console.log('Add tab'), tooltip: 'New Tab' },
{ id: 'search', iconName: 'lucide:search', action: () => console.log('Search'), tooltip: 'Search Tabs' },
{ id: 'disabled', iconName: 'lucide:lock', action: () => {}, tooltip: 'Disabled Action', disabled: true },
];
const demoContent = (text: string) => html` const demoContent = (text: string) => html`
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};"> <div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
${text} ${text}
@@ -279,7 +281,17 @@ export const demoFunc = () => {
</div> </div>
<div class="section"> <div class="section">
<div class="section-title">Closeable Tabs (Browser-style)</div> <div class="section-title">Tabs with Action Buttons</div>
<dees-appui-tabs
.tabs=${horizontalTabs}
.actionsLeft=${actionsLeft}
.actionsRight=${actionsRight}
></dees-appui-tabs>
${demoContent('Action buttons can be placed on either side of the tab bar. They remain fixed while tabs scroll. The lock icon shows a disabled action.')}
</div>
<div class="section">
<div class="section-title">Closeable Tabs with Actions</div>
<demo-closeable-tabs></demo-closeable-tabs> <demo-closeable-tabs></demo-closeable-tabs>
</div> </div>

View File

@@ -18,7 +18,7 @@ import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-appui-tabs') @customElement('dees-appui-tabs')
export class DeesAppuiTabs extends DeesElement { export class DeesAppuiTabs extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// INSTANCE // INSTANCE
@property({ @property({
@@ -41,6 +41,12 @@ export class DeesAppuiTabs extends DeesElement {
@property({ type: Number }) @property({ type: Number })
accessor autoHideThreshold: number = 0; accessor autoHideThreshold: number = 0;
@property({ type: Array })
accessor actionsLeft: interfaces.ITabAction[] = [];
@property({ type: Array })
accessor actionsRight: interfaces.ITabAction[] = [];
// Scroll state for fade indicators // Scroll state for fade indicators
@state() @state()
private accessor canScrollLeft: boolean = false; private accessor canScrollLeft: boolean = false;
@@ -73,6 +79,8 @@ export class DeesAppuiTabs extends DeesElement {
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
display: flex;
align-items: stretch;
} }
/* Scroll fade indicators */ /* Scroll fade indicators */
@@ -105,6 +113,72 @@ export class DeesAppuiTabs extends DeesElement {
opacity: 1; opacity: 1;
} }
.scroll-area {
position: relative;
flex: 1;
min-width: 0;
overflow: hidden;
display: flex;
}
/* Tab action buttons */
.tab-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
padding: 0 4px;
}
.tab-actions.left {
padding-left: 12px;
padding-right: 8px;
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
}
.tab-actions.right {
padding-right: 12px;
padding-left: 8px;
border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
}
.tab-action-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
background: transparent;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
flex-shrink: 0;
}
.tab-action-button:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.tab-action-button:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
}
.tab-action-button.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.tab-action-button.disabled:hover {
background: transparent;
color: ${cssManager.bdTheme('#71717a', '#71717a')};
}
.tab-action-button dees-icon {
font-size: 16px;
}
.tabsContainer { .tabsContainer {
position: relative; position: relative;
user-select: none; user-select: none;
@@ -121,12 +195,14 @@ export class DeesAppuiTabs extends DeesElement {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: transparent transparent; scrollbar-color: transparent transparent;
height: 100%; height: 100%;
width: 100%;
padding: 0 16px; padding: 0 16px;
gap: 4px; gap: 4px;
} }
/* Show scrollbar on hover */ /* Show scrollbar on hover */
.tabs-wrapper:hover .tabsContainer.horizontal { .tabs-wrapper:hover .tabsContainer.horizontal,
.scroll-area:hover .tabsContainer.horizontal {
scrollbar-color: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')} transparent; scrollbar-color: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')} transparent;
} }
@@ -144,11 +220,13 @@ export class DeesAppuiTabs extends DeesElement {
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb { .tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb,
.scroll-area:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')}; background: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')};
} }
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover { .tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover,
.scroll-area:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('rgba(0,0,0,0.35)', 'rgba(255,255,255,0.35)')}; background: ${cssManager.bdTheme('rgba(0,0,0,0.35)', 'rgba(255,255,255,0.35)')};
} }
@@ -331,13 +409,20 @@ export class DeesAppuiTabs extends DeesElement {
const containerClass = `tabsContainer ${this.tabStyle}`; const containerClass = `tabsContainer ${this.tabStyle}`;
if (isHorizontal) { if (isHorizontal) {
const hasLeftActions = this.actionsLeft && this.actionsLeft.length > 0;
const hasRightActions = this.actionsRight && this.actionsRight.length > 0;
return html` return html`
<div class="${wrapperClass}"> <div class="${wrapperClass}">
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div> ${hasLeftActions ? this.renderActions(this.actionsLeft, 'left') : ''}
<div class="${containerClass}" @scroll=${this.handleScroll}> <div class="scroll-area">
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))} <div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
<div class="${containerClass}" @scroll=${this.handleScroll}>
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
</div>
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
</div> </div>
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div> ${hasRightActions ? this.renderActions(this.actionsRight, 'right') : ''}
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''} ${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
</div> </div>
`; `;
@@ -353,6 +438,22 @@ export class DeesAppuiTabs extends DeesElement {
`; `;
} }
private renderActions(actions: interfaces.ITabAction[], position: 'left' | 'right'): TemplateResult {
return html`
<div class="tab-actions ${position}">
${actions.map(action => html`
<div
class="tab-action-button ${action.disabled ? 'disabled' : ''}"
title="${action.tooltip || action.id}"
@click=${() => !action.disabled && action.action()}
>
<dees-icon .icon=${action.iconName}></dees-icon>
</div>
`)}
</div>
`;
}
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult { private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
const isSelected = tab === this.selectedTab; const isSelected = tab === this.selectedTab;
const classes = `tab ${isSelected ? 'selectedTab' : ''}`; const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
@@ -406,6 +507,14 @@ export class DeesAppuiTabs extends DeesElement {
})); }));
} }
/**
* Clear all tabs and reset selection.
*/
public clear(): void {
this.tabs = [];
this.selectedTab = null;
}
private closeTab(e: Event, tab: interfaces.IMenuItem) { private closeTab(e: Event, tab: interfaces.IMenuItem) {
e.stopPropagation(); // Don't select tab when closing e.stopPropagation(); // Don't select tab when closing
@@ -423,14 +532,9 @@ export class DeesAppuiTabs extends DeesElement {
} }
firstUpdated() { firstUpdated() {
if (this.tabs && this.tabs.length > 0) { // Tab selection is handled by updated() lifecycle
this.selectTab(this.tabs[0]);
}
// Set up ResizeObserver for scroll state updates
this.setupResizeObserver(); this.setupResizeObserver();
// Initial scroll state check
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.updateScrollState(); this.updateScrollState();
}); });
@@ -503,8 +607,24 @@ export class DeesAppuiTabs extends DeesElement {
async updated(changedProperties: Map<string, any>) { async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) { if (changedProperties.has('tabs')) {
this.selectTab(this.tabs[0]); if (!this.tabs || this.tabs.length === 0) {
// Tabs are empty => reset selection
if (this.selectedTab !== null) {
this.selectedTab = null;
this.dispatchEvent(new CustomEvent('tab-select', {
detail: { tab: null },
bubbles: true,
composed: true,
}));
}
} else if (this.selectedTab && !this.tabs.includes(this.selectedTab)) {
// Selected tab was removed => select first available
this.selectTab(this.tabs[0]);
} else if (!this.selectedTab) {
// Tabs exist but nothing selected => select first
this.selectTab(this.tabs[0]);
}
} }
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) { if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {

View File

@@ -3,8 +3,8 @@ import type { DeesAppui } from './dees-appui.js';
import type { IAppConfig, IViewActivationContext } from '../../interfaces/appconfig.js'; import type { IAppConfig, IViewActivationContext } from '../../interfaces/appconfig.js';
import type * as interfaces from '../../interfaces/index.js'; import type * as interfaces from '../../interfaces/index.js';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import '../../dees-statsgrid/dees-statsgrid.js'; import '../../00group-dataview/dees-statsgrid/dees-statsgrid.js';
import type { IStatsTile } from '../../dees-statsgrid/dees-statsgrid.js'; import type { IStatsTile } from '../../00group-dataview/dees-statsgrid/dees-statsgrid.js';
// Demo view component with lifecycle hooks // Demo view component with lifecycle hooks
@customElement('demo-dashboard-view') @customElement('demo-dashboard-view')

View File

@@ -39,7 +39,7 @@ declare global {
@customElement('dees-appui') @customElement('dees-appui')
export class DeesAppui extends DeesElement { export class DeesAppui extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'App UI'; public static demoGroups = ['App UI'];
// ========================================== // ==========================================
// REACTIVE OBSERVABLES (RxJS Subjects) // REACTIVE OBSERVABLES (RxJS Subjects)
@@ -143,6 +143,12 @@ export class DeesAppui extends DeesElement {
@property({ type: Object }) @property({ type: Object })
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined; accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
@property({ type: Array })
accessor contentTabActionsLeft: interfaces.ITabAction[] = [];
@property({ type: Array })
accessor contentTabActionsRight: interfaces.ITabAction[] = [];
// References to child components // References to child components
@state() @state()
accessor appbar: DeesAppuiBar | undefined = undefined; accessor appbar: DeesAppuiBar | undefined = undefined;
@@ -213,11 +219,13 @@ export class DeesAppui extends DeesElement {
.maingrid > dees-appui-mainmenu { .maingrid > dees-appui-mainmenu {
position: relative; position: relative;
z-index: 3; z-index: 3;
min-height: 0;
} }
.maingrid > dees-appui-secondarymenu { .maingrid > dees-appui-secondarymenu {
position: relative; position: relative;
z-index: 2; z-index: 2;
min-height: 0;
} }
.maingrid > dees-appui-maincontent { .maingrid > dees-appui-maincontent {
@@ -306,6 +314,8 @@ export class DeesAppui extends DeesElement {
.showTabs=${this.maincontentTabsVisible} .showTabs=${this.maincontentTabsVisible}
.tabsAutoHide=${this.contentTabsAutoHide} .tabsAutoHide=${this.contentTabsAutoHide}
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold} .tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
.tabActionsLeft=${this.contentTabActionsLeft}
.tabActionsRight=${this.contentTabActionsRight}
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)} @tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)} @tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
> >
@@ -699,6 +709,20 @@ export class DeesAppui extends DeesElement {
return this.maincontentSelectedTab; return this.maincontentSelectedTab;
} }
/**
* Set content tab action buttons on the left side
*/
public setContentTabActionsLeft(actions: interfaces.ITabAction[]): void {
this.contentTabActionsLeft = [...actions];
}
/**
* Set content tab action buttons on the right side
*/
public setContentTabActionsRight(actions: interfaces.ITabAction[]): void {
this.contentTabActionsRight = [...actions];
}
// ========================================== // ==========================================
// PROGRAMMATIC API: ACTIVITY LOG // PROGRAMMATIC API: ACTIVITY LOG
// ========================================== // ==========================================
@@ -853,8 +877,13 @@ export class DeesAppui extends DeesElement {
try { try {
await this.loadView(view, params); await this.loadView(view, params);
// Update URL hash // Update URL hash (substitute params into route pattern)
const route = view.route || viewId; let route = view.route || viewId;
if (params) {
for (const [key, val] of Object.entries(params)) {
route = route.replace(`:${key}`, val);
}
}
const newHash = `#${route}`; const newHash = `#${route}`;
if (window.location.hash !== newHash) { if (window.location.hash !== newHash) {
window.history.pushState({ viewId }, '', newHash); window.history.pushState({ viewId }, '', newHash);

View File

@@ -1,6 +1,6 @@
import { html, css } from '@design.estate/dees-element'; import { html, css } from '@design.estate/dees-element';
import '../00group-button/dees-button/dees-button.js'; import '../00group-button/dees-button/dees-button.js';
import '../dees-panel/dees-panel.js'; import '../../00group-layout/dees-panel/dees-panel.js';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => html` export const demoFunc = () => html`

View File

@@ -1,6 +1,6 @@
import * as plugins from '../00plugins.js'; import * as plugins from '../../00plugins.js';
import { zIndexRegistry } from '../00zindex.js'; import { zIndexRegistry } from '../../00zindex.js';
import { cssGeistFontFamily } from '../00fonts.js'; import { cssGeistFontFamily } from '../../00fonts.js';
import { import {
cssManager, cssManager,
css, css,
@@ -12,13 +12,14 @@ import {
property, property,
state, state,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { DeesWindowLayer } from '../dees-windowlayer/dees-windowlayer.js'; import { DeesWindowLayer } from '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
import '../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import { themeDefaultStyles } from '../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
@customElement('dees-mobilenavigation') @customElement('dees-mobilenavigation')
export class DeesMobilenavigation extends DeesElement { export class DeesMobilenavigation extends DeesElement {
// STATIC // STATIC
public static demoGroups = ['App UI'];
public static demo = () => html` public static demo = () => html`
<dees-button @click=${() => { <dees-button @click=${() => {
DeesMobilenavigation.createAndShow([ DeesMobilenavigation.createAndShow([

View File

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

View File

@@ -16,7 +16,7 @@ export class DeesButtonExit extends DeesElement {
public static demo = () => html` public static demo = () => html`
<dees-button-exit></dees-button-exit> <dees-button-exit></dees-button-exit>
`; `;
public static demoGroup = 'Button'; public static demoGroups = ['Button'];
// INSTANCE // INSTANCE
@property({ @property({

View File

@@ -21,7 +21,7 @@ declare global {
@customElement('dees-button-group') @customElement('dees-button-group')
export class DeesButtonGroup extends DeesElement { export class DeesButtonGroup extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'Button'; public static demoGroups = ['Button'];
@property() @property()
accessor label: string = ''; accessor label: string = '';

View File

@@ -1,10 +1,10 @@
import { html, css, cssManager, domtools } from '@design.estate/dees-element'; import { html, css, cssManager, domtools } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import '../../dees-panel/dees-panel.js'; import '../../00group-layout/dees-panel/dees-panel.js';
import '../../00group-form/dees-form/dees-form.js'; import '../../00group-form/dees-form/dees-form.js';
import '../../00group-form/dees-form-submit/dees-form-submit.js'; import '../../00group-form/dees-form-submit/dees-form-submit.js';
import '../../00group-input/dees-input-text/dees-input-text.js'; import '../../00group-input/dees-input-text/dees-input-text.js';
import '../../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import type { DeesButton } from '../dees-button/dees-button.js'; import type { DeesButton } from '../dees-button/dees-button.js';
export const demoFunc = () => html` export const demoFunc = () => html`
@@ -142,54 +142,95 @@ export const demoFunc = () => html`
<dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}> <dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}>
<div class="icon-row"> <div class="icon-row">
<dees-button> <dees-button>
<dees-icon iconFA="faPlus"></dees-icon> <dees-icon icon="fa:plus"></dees-icon>
Add Item Add Item
</dees-button> </dees-button>
<dees-button type="destructive"> <dees-button type="destructive">
<dees-icon iconFA="faTrash"></dees-icon> <dees-icon icon="fa:trash"></dees-icon>
Delete Delete
</dees-button> </dees-button>
<dees-button type="outline"> <dees-button type="outline">
<dees-icon iconFA="faDownload"></dees-icon> <dees-icon icon="lucide:Download"></dees-icon>
Download Download
</dees-button> </dees-button>
</div> </div>
<div class="icon-row"> <div class="icon-row">
<dees-button type="secondary" size="sm"> <dees-button type="secondary" size="sm">
<dees-icon iconFA="faCog"></dees-icon> <dees-icon icon="fa:gear"></dees-icon>
Settings Settings
</dees-button> </dees-button>
<dees-button type="ghost"> <dees-button type="ghost">
<dees-icon iconFA="faChevronLeft"></dees-icon> <dees-icon icon="fa:caretLeft"></dees-icon>
Back Back
</dees-button> </dees-button>
<dees-button type="ghost"> <dees-button type="ghost">
Next Next
<dees-icon iconFA="faChevronRight"></dees-icon> <dees-icon icon="fa:caretRight"></dees-icon>
</dees-button> </dees-button>
</div> </div>
<div class="icon-row"> <div class="icon-row">
<dees-button size="icon" type="default"> <dees-button size="icon" type="default">
<dees-icon iconFA="faPlus"></dees-icon> <dees-icon icon="fa:plus"></dees-icon>
</dees-button> </dees-button>
<dees-button size="icon" type="secondary"> <dees-button size="icon" type="secondary">
<dees-icon iconFA="faCog"></dees-icon> <dees-icon icon="fa:gear"></dees-icon>
</dees-button> </dees-button>
<dees-button size="icon" type="outline"> <dees-button size="icon" type="outline">
<dees-icon iconFA="faSearch"></dees-icon> <dees-icon icon="lucide:Search"></dees-icon>
</dees-button> </dees-button>
<dees-button size="icon" type="ghost"> <dees-button size="icon" type="ghost">
<dees-icon iconFA="faEllipsisV"></dees-icon> <dees-icon icon="lucide:MoreVertical"></dees-icon>
</dees-button> </dees-button>
<dees-button size="icon" type="destructive"> <dees-button size="icon" type="destructive">
<dees-icon iconFA="faTrash"></dees-icon> <dees-icon icon="fa:trash"></dees-icon>
</dees-button> </dees-button>
</div> </div>
</dees-panel> </dees-panel>
</dees-demowrapper> </dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Track icon property button clicks
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button) => {
button.addEventListener('clicked', () => {
const icon = button.getAttribute('icon') || 'none';
const position = button.getAttribute('iconPosition') || 'left';
console.log(`Icon property button: icon=${icon}, position=${position}`);
});
});
}}>
<dees-panel .title=${'4. Icons via Property'} .subtitle=${'Simplified icon syntax using the icon property'}>
<div class="icon-row">
<dees-button icon="fa:plus">Add Item</dees-button>
<dees-button type="destructive" icon="fa:trash">Delete</dees-button>
<dees-button type="outline" icon="lucide:Download">Download</dees-button>
</div>
<div class="icon-row">
<dees-button type="secondary" size="sm" icon="fa:gear">Settings</dees-button>
<dees-button type="ghost" icon="fa:caretLeft">Back</dees-button>
<dees-button type="ghost" icon="fa:caretRight" iconPosition="right">Next</dees-button>
</div>
<div class="icon-row">
<dees-button size="icon" type="default" icon="fa:plus"></dees-button>
<dees-button size="icon" type="secondary" icon="lucide:Settings"></dees-button>
<dees-button size="icon" type="outline" icon="lucide:Search"></dees-button>
<dees-button size="icon" type="ghost" icon="lucide:MoreVertical"></dees-button>
<dees-button size="icon" type="destructive" icon="fa:trash"></dees-button>
</div>
<div style="margin-top: 16px;">
<div class="code-snippet">
&lt;dees-button icon="fa:plus"&gt;Add Item&lt;/dees-button&gt;<br>
&lt;dees-button icon="fa:caretRight" iconPosition="right"&gt;Next&lt;/dees-button&gt;
</div>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => { <dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate status changes // Demonstrate status changes
const pendingButton = elementArg.querySelector('dees-button[status="pending"]'); const pendingButton = elementArg.querySelector('dees-button[status="pending"]');
@@ -215,7 +256,7 @@ export const demoFunc = () => html`
}); });
} }
}}> }}>
<dees-panel .title=${'4. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}> <dees-panel .title=${'5. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}>
<div class="button-group"> <div class="button-group">
<dees-button status="normal">Normal</dees-button> <dees-button status="normal">Normal</dees-button>
<dees-button status="pending">Processing...</dees-button> <dees-button status="pending">Processing...</dees-button>
@@ -260,7 +301,7 @@ export const demoFunc = () => html`
}); });
} }
}}> }}>
<dees-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}> <dees-panel .title=${'6. Event Handling'} .subtitle=${'Interactive examples with click event handling'}>
<div class="button-group"> <div class="button-group">
<dees-button>Click Me</dees-button> <dees-button>Click Me</dees-button>
<dees-button type="secondary" .eventDetailData=${'custom-data-123'}> <dees-button type="secondary" .eventDetailData=${'custom-data-123'}>
@@ -303,7 +344,7 @@ export const demoFunc = () => html`
}); });
} }
}}> }}>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}> <dees-panel .title=${'7. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}>
<dees-form> <dees-form>
<dees-input-text label="Name" key="name" required></dees-input-text> <dees-input-text label="Name" key="name" required></dees-input-text>
<dees-input-text label="Email" key="email" type="email" required></dees-input-text> <dees-input-text label="Email" key="email" type="email" required></dees-input-text>
@@ -330,7 +371,7 @@ export const demoFunc = () => html`
} }
}); });
}}> }}>
<dees-panel .title=${'7. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}> <dees-panel .title=${'8. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}>
<div class="button-group"> <div class="button-group">
<dees-button type="normal">Normal → Default</dees-button> <dees-button type="normal">Normal → Default</dees-button>
<dees-button type="highlighted">Highlighted → Destructive</dees-button> <dees-button type="highlighted">Highlighted → Destructive</dees-button>
@@ -371,36 +412,36 @@ export const demoFunc = () => html`
}); });
} }
}}> }}>
<dees-panel .title=${'8. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}> <dees-panel .title=${'9. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}>
<div class="horizontal-group"> <div class="horizontal-group">
<div class="vertical-group"> <div class="vertical-group">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Action Group</h4> <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Action Group</h4>
<dees-button type="default" size="sm"> <dees-button type="default" size="sm">
<dees-icon iconFA="faSave"></dees-icon> <dees-icon icon="lucide:Save"></dees-icon>
Save Changes Save Changes
</dees-button> </dees-button>
<dees-button type="secondary" size="sm"> <dees-button type="secondary" size="sm">
<dees-icon iconFA="faUndo"></dees-icon> <dees-icon icon="lucide:Undo2"></dees-icon>
Discard Discard
</dees-button> </dees-button>
<dees-button type="ghost" size="sm"> <dees-button type="ghost" size="sm">
<dees-icon iconFA="faQuestionCircle"></dees-icon> <dees-icon icon="lucide:HelpCircle"></dees-icon>
Help Help
</dees-button> </dees-button>
</div> </div>
<div class="vertical-group"> <div class="vertical-group">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Danger Zone</h4> <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Danger Zone</h4>
<dees-button type="destructive" size="sm"> <dees-button type="destructive" size="sm">
<dees-icon iconFA="faTrash"></dees-icon> <dees-icon icon="fa:trash"></dees-icon>
Delete Account Delete Account
</dees-button> </dees-button>
<dees-button type="outline" size="sm"> <dees-button type="outline" size="sm">
<dees-icon iconFA="faArchive"></dees-icon> <dees-icon icon="lucide:Archive"></dees-icon>
Archive Data Archive Data
</dees-button> </dees-button>
<dees-button type="ghost" size="sm" disabled> <dees-button type="ghost" size="sm" disabled>
<dees-icon iconFA="faBan"></dees-icon> <dees-icon icon="lucide:Ban"></dees-icon>
Not Available Not Available
</dees-button> </dees-button>
</div> </div>
@@ -409,8 +450,7 @@ export const demoFunc = () => html`
<div style="margin-top: 24px;"> <div style="margin-top: 24px;">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Code Example:</h4> <h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Code Example:</h4>
<div class="code-snippet"> <div class="code-snippet">
&lt;dees-button type="default" size="sm" @clicked="\${handleClick}"&gt;<br> &lt;dees-button type="default" size="sm" icon="lucide:Save" @clicked="\${handleClick}"&gt;<br>
&nbsp;&nbsp;&lt;dees-icon iconFA="faSave"&gt;&lt;/dees-icon&gt;<br>
&nbsp;&nbsp;Save Changes<br> &nbsp;&nbsp;Save Changes<br>
&lt;/dees-button&gt; &lt;/dees-button&gt;
</div> </div>

View File

@@ -23,7 +23,7 @@ declare global {
@customElement('dees-button') @customElement('dees-button')
export class DeesButton extends DeesElement { export class DeesButton extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'Button'; public static demoGroups = ['Button'];
@property({ @property({
reflect: true, reflect: true,
@@ -68,6 +68,12 @@ export class DeesButton extends DeesElement {
}) })
accessor insideForm: boolean = false; accessor insideForm: boolean = false;
@property({ type: String, reflect: true })
accessor icon: string;
@property({ type: String, reflect: true })
accessor iconPosition: 'left' | 'right' = 'left';
constructor() { constructor() {
super(); super();
} }
@@ -340,9 +346,62 @@ export class DeesButton extends DeesElement {
height: 18px; height: 18px;
} }
/* Text alignment */
.textbox {
display: flex;
align-items: center;
}
`, `,
]; ];
/**
* Extracts icon and text from light DOM and sets properties
*/
private extractLightDom(): void {
const iconElement = this.querySelector('dees-icon') as any;
// Get all text content from light DOM
const textContent = Array.from(this.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent?.trim())
.filter(Boolean)
.join(' ');
if (textContent && !this.text) {
this.text = textContent;
}
if (iconElement) {
// Get icon value
const iconValue = iconElement.icon || iconElement.getAttribute('icon') ||
(iconElement.iconFA ? `fa:${iconElement.iconFA}` : null);
if (iconValue) {
// Determine position based on DOM order
const children = Array.from(this.childNodes);
const iconIndex = children.indexOf(iconElement);
const textNodes = children.filter(node =>
node.nodeType === Node.TEXT_NODE && node.textContent?.trim()
);
if (textNodes.length > 0) {
const firstTextIndex = children.indexOf(textNodes[0]);
this.iconPosition = iconIndex < firstTextIndex ? 'left' : 'right';
}
// Set the icon property
this.icon = iconValue;
}
// Remove the light DOM icon element
iconElement.remove();
}
// Clear all remaining light DOM
this.innerHTML = '';
}
public render(): TemplateResult { public render(): TemplateResult {
// Map old types to new types for backward compatibility // Map old types to new types for backward compatibility
const typeMap: {[key: string]: string} = { const typeMap: {[key: string]: string} = {
@@ -351,10 +410,20 @@ export class DeesButton extends DeesElement {
'discreet': 'outline', 'discreet': 'outline',
'big': 'default' // Will use size instead 'big': 'default' // Will use size instead
}; };
const actualType = typeMap[this.type] || this.type; const actualType = typeMap[this.type] || this.type;
const actualSize = this.type === 'big' ? 'lg' : this.size; const actualSize = this.type === 'big' ? 'lg' : this.size;
const leftIcon = this.iconPosition === 'left' && this.icon
? html`<dees-icon .icon=${this.icon}></dees-icon>`
: '';
const rightIcon = this.iconPosition === 'right' && this.icon
? html`<dees-icon .icon=${this.icon}></dees-icon>`
: '';
// For icon-only buttons, hide the textbox
const isIconOnly = actualSize === 'icon' && this.icon;
return html` return html`
<div <div
class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} ${this.status} ${this.disabled class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} ${this.status} ${this.disabled
@@ -363,13 +432,15 @@ export class DeesButton extends DeesElement {
@click="${this.dispatchClick}" @click="${this.dispatchClick}"
> >
${this.status === 'normal' ? html``: html` ${this.status === 'normal' ? html``: html`
<dees-spinner <dees-spinner
.bnw=${true} .bnw=${true}
status="${this.status}" status="${this.status}"
size="${actualSize === 'sm' ? 14 : actualSize === 'lg' ? 18 : 16}" size="${actualSize === 'sm' ? 14 : actualSize === 'lg' ? 18 : 16}"
></dees-spinner> ></dees-spinner>
`} `}
<div class="textbox">${this.text || html`<slot>Button</slot>`}</div> ${leftIcon}
${isIconOnly ? '' : html`<div class="textbox">${this.text || 'Button'}</div>`}
${rightIcon}
</div> </div>
`; `;
} }
@@ -390,6 +461,7 @@ export class DeesButton extends DeesElement {
} }
public async firstUpdated() { public async firstUpdated() {
// Don't set default text here as it interferes with slotted content // Extract light DOM content (icon + text) and set as properties
this.extractLightDom();
} }
} }

View File

@@ -12,6 +12,16 @@ import { chartAreaStyles } from './styles.js';
import { renderChartArea } from './template.js'; import { renderChartArea } from './template.js';
import type ApexCharts from 'apexcharts'; import type ApexCharts from 'apexcharts';
type ApexAxisChartSeries = {
name?: string;
type?: string;
color?: string;
group?: string;
hidden?: boolean;
zIndex?: number;
data: (number | null)[] | { x: any; y: any; [key: string]: any }[] | [number, number | null][] | number[][];
}[];
import { DeesServiceLibLoader } from '../../../services/index.js'; import { DeesServiceLibLoader } from '../../../services/index.js';
declare global { declare global {
@@ -23,7 +33,7 @@ declare global {
@customElement('dees-chart-area') @customElement('dees-chart-area')
export class DeesChartArea extends DeesElement { export class DeesChartArea extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'Chart'; public static demoGroups = ['Chart'];
// instance // instance
@state() @state()

View File

@@ -44,7 +44,7 @@ export interface ILogMetrics {
@customElement('dees-chart-log') @customElement('dees-chart-log')
export class DeesChartLog extends DeesElement { export class DeesChartLog extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'Chart'; public static demoGroups = ['Chart', 'Workspace'];
@property() @property()
accessor label: string = 'Server Logs'; accessor label: string = 'Server Logs';
@@ -385,11 +385,23 @@ export class DeesChartLog extends DeesElement {
this.domtoolsInstance = await this.domtoolsPromise; this.domtoolsInstance = await this.domtoolsPromise;
await this.initializeTerminal(); await this.initializeTerminal();
// Process any initial log entries // initializeTerminal() already replayed logBuffer (from addLog/updateLog).
if (this.logEntries.length > 0) { // Now handle logEntries set via property binding before terminal was ready.
if (this.logEntries.length > 0 && this.logBuffer.length === 0) {
this.logBuffer = [...this.logEntries];
for (const entry of this.logEntries) { for (const entry of this.logEntries) {
this.updateMetrics(entry.level);
this.writeLogEntry(entry); this.writeLogEntry(entry);
} }
} else if (this.logEntries.length > 0 && this.logBuffer.length > 0) {
const bufferSet = new Set(this.logBuffer.map(e => `${e.timestamp}|${e.message}`));
for (const entry of this.logEntries) {
if (!bufferSet.has(`${entry.timestamp}|${entry.message}`)) {
this.logBuffer.push(entry);
this.updateMetrics(entry.level);
this.writeLogEntry(entry);
}
}
} }
} }
@@ -445,6 +457,58 @@ export class DeesChartLog extends DeesElement {
this.rateInterval = setInterval(() => this.calculateRate(), 1000); this.rateInterval = setInterval(() => this.calculateRate(), 1000);
this.terminalReady = true; this.terminalReady = true;
// Replay any entries that arrived via updateLog()/addLog() before terminal was ready
for (const entry of this.logBuffer) {
this.writeLogEntry(entry);
}
}
public updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('logEntries') && this.terminalReady && this.logEntries.length > 0) {
const oldEntries: ILogEntry[] = changedProperties.get('logEntries') || [];
const newEntries = this.logEntries;
// Same content? Skip entirely.
if (
oldEntries.length === newEntries.length &&
oldEntries.length > 0 &&
oldEntries[oldEntries.length - 1].timestamp === newEntries[newEntries.length - 1].timestamp &&
oldEntries[oldEntries.length - 1].message === newEntries[newEntries.length - 1].message
) {
return;
}
// Append-only? Write only the new tail entries incrementally.
if (
newEntries.length > oldEntries.length &&
oldEntries.length > 0 &&
oldEntries[oldEntries.length - 1].timestamp === newEntries[oldEntries.length - 1].timestamp &&
oldEntries[oldEntries.length - 1].message === newEntries[oldEntries.length - 1].message
) {
const tailEntries = newEntries.slice(oldEntries.length);
for (const entry of tailEntries) {
this.logBuffer.push(entry);
this.updateMetrics(entry.level);
// Enforce maxEntries
if (this.logBuffer.length > this.maxEntries) {
this.logBuffer.shift();
}
// Respect filter mode
if (!this.filterMode || !this.searchQuery || this.entryMatchesFilter(entry)) {
this.writeLogEntry(entry);
}
}
return;
}
// Different content — full re-render
this.logBuffer = [...newEntries];
this.reRenderFilteredLogs();
}
} }
private getTerminalTheme() { private getTerminalTheme() {

View File

@@ -15,7 +15,7 @@ import type { HLJSApi } from 'highlight.js';
import * as smartstring from '@push.rocks/smartstring'; import * as smartstring from '@push.rocks/smartstring';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import { DeesServiceLibLoader } from '../../../services/index.js'; import { DeesServiceLibLoader } from '../../../services/index.js';
declare global { declare global {
@@ -27,7 +27,7 @@ declare global {
@customElement('dees-dataview-codebox') @customElement('dees-dataview-codebox')
export class DeesDataviewCodebox extends DeesElement { export class DeesDataviewCodebox extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'Data View'; public static demoGroups = ['Data View', 'Workspace'];
@property() @property()
accessor progLang: string = 'typescript'; accessor progLang: string = 'typescript';
@@ -52,6 +52,8 @@ export class DeesDataviewCodebox extends DeesElement {
text-align: left; text-align: left;
font-size: 16px; font-size: 16px;
font-family: ${cssGeistFontFamily}; font-family: ${cssGeistFontFamily};
height: 100%;
box-sizing: border-box;
} }
.mainbox { .mainbox {
position: relative; position: relative;
@@ -61,6 +63,10 @@ export class DeesDataviewCodebox extends DeesElement {
background: ${cssManager.bdTheme('#ffffff', '#09090b')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
} }
.appbar { .appbar {
@@ -74,6 +80,7 @@ export class DeesDataviewCodebox extends DeesElement {
line-height: 32px; line-height: 32px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-shrink: 0;
} }
.appbar .fileName { .appbar .fileName {
@@ -95,6 +102,7 @@ export class DeesDataviewCodebox extends DeesElement {
justify-content: flex-end; justify-content: flex-end;
align-items: stretch; align-items: stretch;
overflow: hidden; overflow: hidden;
flex-shrink: 0;
} }
.spacesLabel { .spacesLabel {
@@ -121,7 +129,9 @@ export class DeesDataviewCodebox extends DeesElement {
.codegrid { .codegrid {
display: grid; display: grid;
grid-template-columns: 50px auto; grid-template-columns: 50px auto;
overflow: hidden; overflow: auto;
flex: 1;
min-height: 0;
} }
.lineNumbers { .lineNumbers {
@@ -206,9 +216,7 @@ export class DeesDataviewCodebox extends DeesElement {
}}" }}"
> >
<div class="appbar"> <div class="appbar">
<dees-windowcontrols type="mac" position="left"></dees-windowcontrols>
<div class="fileName">index.ts</div> <div class="fileName">index.ts</div>
<dees-windowcontrols type="mac" position="right"></dees-windowcontrols>
</div> </div>
<div class="codegrid"> <div class="codegrid">
<div class="lineNumbers"> <div class="lineNumbers">

View File

@@ -15,7 +15,7 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as tsclass from '@tsclass/tsclass'; import * as tsclass from '@tsclass/tsclass';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
declare global { declare global {
@@ -27,7 +27,7 @@ declare global {
@customElement('dees-dataview-statusobject') @customElement('dees-dataview-statusobject')
export class DeesDataviewStatusobject extends DeesElement { export class DeesDataviewStatusobject extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroup = 'Data View'; public static demoGroups = ['Data View'];
@property({ type: Object }) accessor statusObject: tsclass.code.IStatusObject; @property({ type: Object }) accessor statusObject: tsclass.code.IStatusObject;

View File

@@ -1,7 +1,7 @@
import { html, css, cssManager } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import '../dees-panel/dees-panel.js'; import '../../00group-layout/dees-panel/dees-panel.js';
import type { IStatsTile, ICpuCore } from '../dees-statsgrid/dees-statsgrid.js'; import type { IStatsTile, ICpuCore, IPartitionData, IDiskData } from '../dees-statsgrid/dees-statsgrid.js';
// Helper function to generate random CPU core data // Helper function to generate random CPU core data
const generateCpuCores = (count: number): ICpuCore[] => { const generateCpuCores = (count: number): ICpuCore[] => {
@@ -601,8 +601,136 @@ html\`
></dees-statsgrid> ></dees-statsgrid>
\`;`}</div> \`;`}</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'7. Disk & Storage Tiles'} .subtitle=${'Partition and physical disk visualization tiles'}>
<dees-statsgrid
.tiles=${[
{
id: 'root-partition',
title: 'Root Partition',
value: 0,
type: 'partition',
icon: 'lucide:folder-root',
partitionData: {
used: 698_341_425_152, // ~650 GB
total: 1_073_741_824_000, // ~1 TB
filesystem: 'ext4',
mountPoint: '/'
}
},
{
id: 'home-partition',
title: 'Home Partition',
value: 0,
type: 'partition',
icon: 'lucide:home',
partitionData: {
used: 214_748_364_800, // ~200 GB
total: 536_870_912_000, // ~500 GB
filesystem: 'ext4',
mountPoint: '/home'
}
},
{
id: 'data-partition',
title: 'Data Partition',
value: 0,
type: 'partition',
icon: 'lucide:database',
partitionData: {
used: 1_932_735_283_200, // ~1.8 TB (90% - critical)
total: 2_147_483_648_000, // ~2 TB
filesystem: 'xfs',
mountPoint: '/data'
}
},
{
id: 'nvme-ssd',
title: 'Primary NVMe',
value: 0,
type: 'disk',
icon: 'lucide:hard-drive',
columnSpan: 2,
diskData: {
capacity: 2_000_000_000_000, // 2 TB
model: 'Samsung 990 Pro',
type: 'nvme',
iops: {
read: 7450,
write: 6900
},
health: 98
}
},
{
id: 'sata-ssd',
title: 'Secondary SSD',
value: 0,
type: 'disk',
icon: 'lucide:hard-drive',
diskData: {
capacity: 1_000_000_000_000, // 1 TB
model: 'Crucial MX500',
type: 'ssd',
iops: {
read: 560,
write: 510
},
health: 85
}
},
{
id: 'hdd-storage',
title: 'Backup HDD',
value: 0,
type: 'disk',
icon: 'lucide:archive',
diskData: {
capacity: 8_000_000_000_000, // 8 TB
model: 'Seagate IronWolf',
type: 'hdd',
iops: {
read: 210,
write: 195
},
health: 42
}
}
]}
.minTileWidth=${280}
.gap=${16}
></dees-statsgrid>
<div class="tile-config">
<div class="config-section">
<div class="config-title">Partition Tile Properties</div>
<div class="config-description">
<ul style="margin: 8px 0; padding-left: 20px;">
<li><strong>partitionData.used:</strong> Used space in bytes (auto-formatted)</li>
<li><strong>partitionData.total:</strong> Total capacity in bytes</li>
<li><strong>partitionData.filesystem:</strong> Filesystem type (ext4, xfs, ntfs)</li>
<li><strong>partitionData.mountPoint:</strong> Mount point path (optional)</li>
</ul>
Color thresholds: Normal (&lt;75%), Warning (75-90%), Critical (&gt;90%)
</div>
</div>
<div class="config-section">
<div class="config-title">Disk Tile Properties</div>
<div class="config-description">
<ul style="margin: 8px 0; padding-left: 20px;">
<li><strong>diskData.capacity:</strong> Total capacity in bytes</li>
<li><strong>diskData.model:</strong> Disk model name (optional)</li>
<li><strong>diskData.type:</strong> Disk type: 'ssd', 'hdd', or 'nvme'</li>
<li><strong>diskData.iops:</strong> Read/write IOPS (optional)</li>
<li><strong>diskData.health:</strong> Health percentage 0-100 (optional)</li>
</ul>
Health thresholds: Good (70-100%), Warning (30-70%), Critical (&lt;30%)
</div>
</div>
</div>
</dees-panel>
</div> </div>
<script> <script>
// Cleanup live updates on page unload // Cleanup live updates on page unload
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {

View File

@@ -1,6 +1,6 @@
import { demoFunc } from './dees-statsgrid.demo.js'; import { demoFunc } from './dees-statsgrid.demo.js';
import * as plugins from '../00plugins.js'; import * as plugins from '../../00plugins.js';
import { cssGeistFontFamily } from '../00fonts.js'; import { cssGeistFontFamily } from '../../00fonts.js';
import { import {
customElement, customElement,
html, html,
@@ -10,13 +10,14 @@ import {
css, css,
unsafeCSS, unsafeCSS,
cssManager, cssManager,
containerResponsive,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import type { TemplateResult } from '@design.estate/dees-element'; import type { TemplateResult } from '@design.estate/dees-element';
import '../dees-icon/dees-icon.js'; import '../../00group-utility/dees-icon/dees-icon.js';
import '../dees-contextmenu/dees-contextmenu.js'; import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import '../00group-button/dees-button/dees-button.js'; import '../../00group-button/dees-button/dees-button.js';
import { themeDefaultStyles } from '../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -30,12 +31,30 @@ export interface ICpuCore {
label?: string; label?: string;
} }
export interface IPartitionData {
used: number; // bytes
total: number; // bytes
filesystem: string; // e.g., 'ext4', 'NTFS', 'btrfs'
mountPoint?: string; // e.g., '/', '/home', 'C:'
}
export interface IDiskData {
capacity: number; // bytes
model?: string; // e.g., 'Samsung 970 EVO Plus'
type?: 'ssd' | 'hdd' | 'nvme';
iops?: {
read: number;
write: number;
};
health?: number; // 0-100 (100 = new, 0 = end of life)
}
export interface IStatsTile { export interface IStatsTile {
id: string; id: string;
title: string; title: string;
value: number | string; value: number | string;
unit?: string; unit?: string;
type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text' | 'multiPercentage' | 'cpuCores'; type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text' | 'multiPercentage' | 'cpuCores' | 'partition' | 'disk';
// Layout options // Layout options
columnSpan?: number; // Number of columns to span (default: 1) columnSpan?: number; // Number of columns to span (default: 1)
@@ -60,6 +79,12 @@ export interface IStatsTile {
// For cpuCores type // For cpuCores type
coresData?: ICpuCore[]; coresData?: ICpuCore[];
// For partition type
partitionData?: IPartitionData;
// For disk type
diskData?: IDiskData;
// Visual customization // Visual customization
color?: string; color?: string;
icon?: string; icon?: string;
@@ -69,9 +94,11 @@ export interface IStatsTile {
actions?: plugins.tsclass.website.IMenuItem[]; actions?: plugins.tsclass.website.IMenuItem[];
} }
@containerResponsive()
@customElement('dees-statsgrid') @customElement('dees-statsgrid')
export class DeesStatsGrid extends DeesElement { export class DeesStatsGrid extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroups = ['Data View'];
@property({ type: Array }) @property({ type: Array })
accessor tiles: IStatsTile[] = []; accessor tiles: IStatsTile[] = [];
@@ -485,6 +512,219 @@ export class DeesStatsGrid extends DeesElement {
max-width: 100%; max-width: 100%;
} }
/* Partition Styles */
.partition-wrapper {
width: 100%;
display: flex;
flex-direction: column;
flex: 1;
gap: 8px;
}
.partition-header {
display: flex;
align-items: baseline;
gap: 8px;
}
.partition-percentage {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
}
.partition-bar {
width: 100%;
height: 6px;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
border-radius: 3px;
overflow: hidden;
}
.partition-bar-fill {
height: 100%;
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 3px;
}
.partition-bar-fill.warning {
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
}
.partition-bar-fill.critical {
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
}
.partition-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
}
.partition-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.partition-stat-label {
font-size: 10px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
text-transform: uppercase;
letter-spacing: 0.02em;
}
.partition-stat-value {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
letter-spacing: -0.01em;
}
.partition-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
}
.partition-filesystem {
font-size: 11px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
padding: 2px 6px;
border-radius: 3px;
}
.partition-mountpoint {
font-size: 11px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
/* Disk Styles */
.disk-wrapper {
width: 100%;
display: flex;
flex-direction: column;
flex: 1;
gap: 8px;
}
.disk-capacity {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
}
.disk-model {
font-size: 12px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
display: flex;
align-items: center;
gap: 6px;
}
.disk-type-badge {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
padding: 2px 6px;
border-radius: 3px;
}
.disk-metrics {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: auto;
}
.disk-iops {
display: flex;
align-items: center;
gap: 12px;
}
.disk-iops-item {
display: flex;
align-items: baseline;
gap: 4px;
}
.disk-iops-label {
font-size: 10px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
text-transform: uppercase;
}
.disk-iops-value {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.disk-health {
display: flex;
flex-direction: column;
gap: 4px;
}
.disk-health-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.disk-health-label {
font-size: 10px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
text-transform: uppercase;
}
.disk-health-value {
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.disk-health-bar {
width: 100%;
height: 4px;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
border-radius: 2px;
overflow: hidden;
}
.disk-health-fill {
height: 100%;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 2px;
}
.disk-health-fill.good {
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
}
.disk-health-fill.warning {
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
}
.disk-health-fill.critical {
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
}
/* Trend Styles */ /* Trend Styles */
.trend-container { .trend-container {
width: 100%; width: 100%;
@@ -563,6 +803,38 @@ export class DeesStatsGrid extends DeesElement {
z-index: 1000; z-index: 1000;
} }
`, `,
// Container-responsive: when this statsgrid is narrow
cssManager.cssForTablet(css`
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
`, this),
cssManager.cssForPhablet(css`
:host {
--tile-padding: 12px;
--value-font-size: 22px;
--title-font-size: 12px;
--grid-gap: 8px;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 8px;
}
.stats-tile {
grid-column: span 1 !important;
}
`, this),
cssManager.cssForPhone(css`
:host {
--tile-padding: 10px;
--value-font-size: 20px;
--header-spacing: 8px;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 6px;
}
`, this),
]; ];
constructor() { constructor() {
@@ -659,6 +931,12 @@ export class DeesStatsGrid extends DeesElement {
case 'cpuCores': case 'cpuCores':
return this.renderCpuCores(tile); return this.renderCpuCores(tile);
case 'partition':
return this.renderPartition(tile);
case 'disk':
return this.renderDisk(tile);
case 'text': case 'text':
return html` return html`
<div class="text-value" style="${tile.color ? `color: ${tile.color}` : ''}"> <div class="text-value" style="${tile.color ? `color: ${tile.color}` : ''}">
@@ -876,6 +1154,114 @@ export class DeesStatsGrid extends DeesElement {
`; `;
} }
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
private renderPartition(tile: IStatsTile): TemplateResult {
if (!tile.partitionData) {
return html`<div class="tile-value">${tile.value}</div>`;
}
const { used, total, filesystem, mountPoint } = tile.partitionData;
const percentage = Math.min(100, Math.max(0, (used / total) * 100));
const free = total - used;
// Determine color class based on usage
const getColorClass = (): string => {
if (percentage >= 90) return 'critical';
if (percentage >= 75) return 'warning';
return '';
};
return html`
<div class="partition-wrapper">
<div class="partition-header">
<span class="partition-percentage">${Math.round(percentage)}%</span>
</div>
<div class="partition-bar">
<div
class="partition-bar-fill ${getColorClass()}"
style="width: ${percentage}%"
></div>
</div>
<div class="partition-stats">
<div class="partition-stat">
<span class="partition-stat-label">Used</span>
<span class="partition-stat-value">${this.formatBytes(used)}</span>
</div>
<div class="partition-stat">
<span class="partition-stat-label">Free</span>
<span class="partition-stat-value">${this.formatBytes(free)}</span>
</div>
</div>
<div class="partition-meta">
<span class="partition-filesystem">${filesystem}</span>
${mountPoint ? html`<span class="partition-mountpoint">${mountPoint}</span>` : ''}
</div>
</div>
`;
}
private renderDisk(tile: IStatsTile): TemplateResult {
if (!tile.diskData) {
return html`<div class="tile-value">${tile.value}</div>`;
}
const { capacity, model, type, iops, health } = tile.diskData;
// Determine health color class (inverted - high is good)
const getHealthClass = (value: number): string => {
if (value >= 70) return 'good';
if (value >= 30) return 'warning';
return 'critical';
};
return html`
<div class="disk-wrapper">
<div class="disk-capacity">${this.formatBytes(capacity)}</div>
${model || type ? html`
<div class="disk-model">
${model ? html`<span>${model}</span>` : ''}
${type ? html`<span class="disk-type-badge">${type}</span>` : ''}
</div>
` : ''}
<div class="disk-metrics">
${iops ? html`
<div class="disk-iops">
<div class="disk-iops-item">
<span class="disk-iops-label">Read</span>
<span class="disk-iops-value">${iops.read.toLocaleString()}</span>
</div>
<div class="disk-iops-item">
<span class="disk-iops-label">Write</span>
<span class="disk-iops-value">${iops.write.toLocaleString()}</span>
</div>
</div>
` : ''}
${health !== undefined ? html`
<div class="disk-health">
<div class="disk-health-header">
<span class="disk-health-label">Health</span>
<span class="disk-health-value">${health}%</span>
</div>
<div class="disk-health-bar">
<div
class="disk-health-fill ${getHealthClass(health)}"
style="width: ${health}%"
></div>
</div>
</div>
` : ''}
</div>
</div>
`;
}
private async handleGridAction(action: plugins.tsclass.website.IMenuItem) { private async handleGridAction(action: plugins.tsclass.website.IMenuItem) {
if (action.action) { if (action.action) {
await action.action(); await action.action();

View File

@@ -0,0 +1,156 @@
import { html } from '@design.estate/dees-element';
import type { IStorageDataProvider, IStorageObject } from './interfaces.js';
import './dees-storage-browser.js';
// Mock in-memory storage data provider for demo purposes
class MockStorageDataProvider implements IStorageDataProvider {
private objects: Map<string, { content: string; contentType: string; size: number; lastModified: string }> = new Map();
constructor() {
const now = new Date().toISOString();
// Seed with sample data
this.objects.set('documents/readme.md', {
content: btoa('# Welcome\n\nThis is a demo Storage browser.\n'),
contentType: 'text/markdown',
size: 42,
lastModified: now,
});
this.objects.set('documents/config.json', {
content: btoa('{\n "name": "demo",\n "version": "1.0.0"\n}'),
contentType: 'application/json',
size: 48,
lastModified: now,
});
this.objects.set('documents/notes/todo.txt', {
content: btoa('Buy milk\nFix bug #42\nDeploy to production'),
contentType: 'text/plain',
size: 45,
lastModified: now,
});
this.objects.set('images/logo.png', {
content: btoa('fake-png-data'),
contentType: 'image/png',
size: 24500,
lastModified: now,
});
this.objects.set('images/banner.jpg', {
content: btoa('fake-jpg-data'),
contentType: 'image/jpeg',
size: 156000,
lastModified: now,
});
this.objects.set('scripts/deploy.sh', {
content: btoa('#!/bin/bash\necho "Deploying..."\n'),
contentType: 'text/plain',
size: 34,
lastModified: now,
});
this.objects.set('index.html', {
content: btoa('<!DOCTYPE html>\n<html>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>'),
contentType: 'text/html',
size: 72,
lastModified: now,
});
this.objects.set('styles.css', {
content: btoa('body { margin: 0; font-family: sans-serif; }'),
contentType: 'text/css',
size: 44,
lastModified: now,
});
}
async listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IStorageObject[]; prefixes: string[] }> {
const pfx = prefix || '';
const objects: IStorageObject[] = [];
const prefixes = new Set<string>();
for (const [key, data] of this.objects) {
if (!key.startsWith(pfx)) continue;
const rest = key.slice(pfx.length);
if (delimiter) {
const slashIndex = rest.indexOf(delimiter);
if (slashIndex >= 0) {
prefixes.add(pfx + rest.slice(0, slashIndex + 1));
} else {
objects.push({ key, size: data.size, lastModified: data.lastModified });
}
} else {
objects.push({ key, size: data.size, lastModified: data.lastModified });
}
}
return { objects, prefixes: Array.from(prefixes).sort() };
}
async getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }> {
const obj = this.objects.get(key);
if (!obj) throw new Error('Not found');
return { ...obj };
}
async putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean> {
this.objects.set(key, {
content: base64Content,
contentType,
size: atob(base64Content).length,
lastModified: new Date().toISOString(),
});
return true;
}
async deleteObject(bucket: string, key: string): Promise<boolean> {
return this.objects.delete(key);
}
async deletePrefix(bucket: string, prefix: string): Promise<boolean> {
for (const key of this.objects.keys()) {
if (key.startsWith(prefix)) {
this.objects.delete(key);
}
}
return true;
}
async getObjectUrl(bucket: string, key: string): Promise<string> {
const obj = this.objects.get(key);
if (!obj) return '';
const blob = new Blob([Uint8Array.from(atob(obj.content), c => c.charCodeAt(0))], { type: obj.contentType });
return URL.createObjectURL(blob);
}
async moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }> {
const obj = this.objects.get(sourceKey);
if (!obj) return { success: false, error: 'Source not found' };
this.objects.set(destKey, { ...obj, lastModified: new Date().toISOString() });
this.objects.delete(sourceKey);
return { success: true };
}
async movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }> {
let count = 0;
const toMove = Array.from(this.objects.entries()).filter(([k]) => k.startsWith(sourcePrefix));
for (const [key, data] of toMove) {
const newKey = destPrefix + key.slice(sourcePrefix.length);
this.objects.set(newKey, { ...data, lastModified: new Date().toISOString() });
this.objects.delete(key);
count++;
}
return { success: true, movedCount: count };
}
}
export const demoFunc = () => html`
<style>
.demo-container {
height: 600px;
padding: 16px;
}
</style>
<div class="demo-container">
<dees-storage-browser
.dataProvider=${new MockStorageDataProvider()}
.bucketName=${'demo-bucket'}
></dees-storage-browser>
</div>
`;

View File

@@ -0,0 +1,439 @@
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
import { demoFunc } from './dees-storage-browser.demo.js';
import type { IStorageDataProvider, IStorageChangeEvent } from './interfaces.js';
import './dees-storage-columns.js';
import './dees-storage-keys.js';
import './dees-storage-preview.js';
declare global {
interface HTMLElementTagNameMap {
'dees-storage-browser': DeesStorageBrowser;
}
}
type TViewType = 'columns' | 'keys';
@customElement('dees-storage-browser')
export class DeesStorageBrowser extends DeesElement {
public static demo = demoFunc;
public static demoGroups = ['Data View'];
@property({ type: Object })
public accessor dataProvider: IStorageDataProvider | null = null;
@property({ type: String })
public accessor bucketName: string = '';
/**
* Optional change stream subscription.
* Pass a function that takes a callback and returns an unsubscribe function.
*/
@property({ type: Object })
public accessor onChangeEvent: ((callback: (event: IStorageChangeEvent) => void) => (() => void)) | null = null;
@state()
private accessor viewType: TViewType = 'columns';
@state()
private accessor currentPrefix: string = '';
@state()
private accessor selectedKey: string = '';
@state()
private accessor refreshKey: number = 0;
@state()
private accessor previewWidth: number = 700;
@state()
private accessor isResizingPreview: boolean = false;
@state()
private accessor recentChangeCount: number = 0;
@state()
private accessor isStreamConnected: boolean = false;
private changeUnsubscribe: (() => void) | null = null;
public static styles = [
cssManager.defaultStyles,
themeDefaultStyles,
css`
:host {
display: block;
height: 100%;
}
.browser-container {
display: flex;
flex-direction: column;
height: 100%;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')};
border-radius: 8px;
margin-bottom: 16px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#999')};
}
.breadcrumb-item {
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.15s;
}
.breadcrumb-item:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
color: ${cssManager.bdTheme('#18181b', '#fff')};
}
.breadcrumb-separator {
color: ${cssManager.bdTheme('#d4d4d8', '#555')};
}
.view-toggle {
display: flex;
gap: 4px;
}
.view-btn {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')};
color: ${cssManager.bdTheme('#71717a', '#888')};
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s;
}
.view-btn:hover {
border-color: ${cssManager.bdTheme('#a1a1aa', '#666')};
color: ${cssManager.bdTheme('#3f3f46', '#aaa')};
}
.view-btn.active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
border-color: ${cssManager.bdTheme('#a1a1aa', '#404040')};
color: ${cssManager.bdTheme('#18181b', '#e0e0e0')};
}
.content {
flex: 1;
display: grid;
grid-template-columns: 1fr;
gap: 0;
overflow: hidden;
}
.content.has-preview {
grid-template-columns: 1fr 4px var(--preview-width, 700px);
}
.resize-divider {
width: 4px;
background: transparent;
cursor: col-resize;
transition: background 0.2s;
}
.resize-divider:hover,
.resize-divider.active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')};
}
.main-view {
overflow: auto;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
border-radius: 8px;
}
.preview-panel {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
border-radius: 8px;
overflow: hidden;
margin-left: 12px;
}
@media (max-width: 1024px) {
.content,
.content.has-preview {
grid-template-columns: 1fr;
}
.preview-panel,
.resize-divider {
display: none;
}
}
.stream-status {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: ${cssManager.bdTheme('#71717a', '#888')};
margin-left: auto;
margin-right: 12px;
}
.stream-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: ${cssManager.bdTheme('#a1a1aa', '#888')};
}
.stream-dot.connected {
background: #22c55e;
}
.change-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: rgba(245, 158, 11, 0.2);
border-radius: 4px;
font-size: 11px;
color: #f59e0b;
margin-right: 12px;
}
.change-indicator.pulse {
animation: pulse-orange 1s ease-in-out;
}
@keyframes pulse-orange {
0% { background: rgba(245, 158, 11, 0.4); }
100% { background: rgba(245, 158, 11, 0.2); }
}
`,
];
async connectedCallback() {
super.connectedCallback();
this.subscribeToChanges();
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.unsubscribeFromChanges();
}
/**
* Public method to trigger a refresh of child components
*/
public refresh() {
this.refreshKey++;
}
private setViewType(type: TViewType) {
this.viewType = type;
}
private navigateToPrefix(prefix: string) {
this.currentPrefix = prefix;
this.selectedKey = '';
}
private handleKeySelected(e: CustomEvent) {
this.selectedKey = e.detail.key;
}
private handleNavigate(e: CustomEvent) {
this.navigateToPrefix(e.detail.prefix);
}
private handleObjectDeleted(e: CustomEvent) {
this.selectedKey = '';
this.refreshKey++;
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('bucketName')) {
this.selectedKey = '';
this.currentPrefix = '';
this.recentChangeCount = 0;
this.unsubscribeFromChanges();
this.subscribeToChanges();
}
if (changedProperties.has('onChangeEvent')) {
this.unsubscribeFromChanges();
this.subscribeToChanges();
}
}
private subscribeToChanges() {
if (!this.onChangeEvent) {
this.isStreamConnected = false;
return;
}
try {
this.changeUnsubscribe = this.onChangeEvent((event: IStorageChangeEvent) => {
this.handleChange(event);
});
this.isStreamConnected = true;
} catch (error) {
console.warn('[StorageBrowser] Failed to subscribe to changes:', error);
this.isStreamConnected = false;
}
}
private unsubscribeFromChanges() {
if (this.changeUnsubscribe) {
this.changeUnsubscribe();
this.changeUnsubscribe = null;
}
this.isStreamConnected = false;
}
private handleChange(event: IStorageChangeEvent) {
this.recentChangeCount++;
this.refreshKey++;
}
private startPreviewResize = (e: MouseEvent) => {
e.preventDefault();
this.isResizingPreview = true;
document.addEventListener('mousemove', this.handlePreviewResize);
document.addEventListener('mouseup', this.endPreviewResize);
};
private handlePreviewResize = (e: MouseEvent) => {
if (!this.isResizingPreview) return;
const contentEl = this.shadowRoot?.querySelector('.content');
if (!contentEl) return;
const containerRect = contentEl.getBoundingClientRect();
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000);
this.previewWidth = newWidth;
};
private endPreviewResize = () => {
this.isResizingPreview = false;
document.removeEventListener('mousemove', this.handlePreviewResize);
document.removeEventListener('mouseup', this.endPreviewResize);
};
render() {
const breadcrumbParts = this.currentPrefix
? this.currentPrefix.split('/').filter(Boolean)
: [];
return html`
<div class="browser-container">
<div class="toolbar">
<div class="breadcrumb">
<span
class="breadcrumb-item"
@click=${() => this.navigateToPrefix('')}
>
${this.bucketName}
</span>
${breadcrumbParts.map((part, index) => {
const prefix = breadcrumbParts.slice(0, index + 1).join('/') + '/';
return html`
<span class="breadcrumb-separator">/</span>
<span
class="breadcrumb-item"
@click=${() => this.navigateToPrefix(prefix)}
>
${part}
</span>
`;
})}
</div>
${this.onChangeEvent ? html`
<div class="stream-status">
<span class="stream-dot ${this.isStreamConnected ? 'connected' : ''}"></span>
${this.isStreamConnected ? 'Live' : 'Offline'}
</div>
` : ''}
${this.recentChangeCount > 0
? html`
<div class="change-indicator pulse">
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
</div>
`
: ''}
<div class="view-toggle">
<button
class="view-btn ${this.viewType === 'columns' ? 'active' : ''}"
@click=${() => this.setViewType('columns')}
>
Columns
</button>
<button
class="view-btn ${this.viewType === 'keys' ? 'active' : ''}"
@click=${() => this.setViewType('keys')}
>
List
</button>
</div>
</div>
<div class="content ${this.selectedKey ? 'has-preview' : ''}" style="--preview-width: ${this.previewWidth}px">
<div class="main-view">
${this.viewType === 'columns'
? html`
<dees-storage-columns
.dataProvider=${this.dataProvider}
.bucketName=${this.bucketName}
.currentPrefix=${this.currentPrefix}
.refreshKey=${this.refreshKey}
@key-selected=${this.handleKeySelected}
@navigate=${this.handleNavigate}
></dees-storage-columns>
`
: html`
<dees-storage-keys
.dataProvider=${this.dataProvider}
.bucketName=${this.bucketName}
.currentPrefix=${this.currentPrefix}
.refreshKey=${this.refreshKey}
@key-selected=${this.handleKeySelected}
@navigate=${this.handleNavigate}
></dees-storage-keys>
`}
</div>
${this.selectedKey
? html`
<div
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
@mousedown=${this.startPreviewResize}
></div>
<div class="preview-panel">
<dees-storage-preview
.dataProvider=${this.dataProvider}
.bucketName=${this.bucketName}
.objectKey=${this.selectedKey}
@object-deleted=${this.handleObjectDeleted}
></dees-storage-preview>
</div>
`
: ''}
</div>
</div>
`;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,540 @@
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
import type { IStorageDataProvider } from './interfaces.js';
import { formatSize, getFileName } from './utilities.js';
declare global {
interface HTMLElementTagNameMap {
'dees-storage-preview': DeesStoragePreview;
}
}
@customElement('dees-storage-preview')
export class DeesStoragePreview extends DeesElement {
@property({ type: Object })
public accessor dataProvider: IStorageDataProvider | null = null;
@property({ type: String })
public accessor bucketName: string = '';
@property({ type: String })
public accessor objectKey: string = '';
@state()
private accessor loading: boolean = false;
@state()
private accessor saving: boolean = false;
@state()
private accessor content: string = '';
@state()
private accessor originalTextContent: string = '';
@state()
private accessor hasChanges: boolean = false;
@state()
private accessor editing: boolean = false;
@state()
private accessor contentType: string = '';
@state()
private accessor fileSize: number = 0;
@state()
private accessor lastModified: string = '';
@state()
private accessor error: string = '';
public static styles = [
cssManager.defaultStyles,
themeDefaultStyles,
css`
:host {
display: block;
height: 100%;
}
.preview-container {
display: flex;
flex-direction: column;
height: 100%;
}
.preview-header {
padding: 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
}
.preview-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
word-break: break-all;
}
.preview-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#888')};
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.preview-content {
flex: 1;
overflow: hidden;
}
.preview-content dees-preview {
width: 100%;
height: 100%;
}
.preview-content.code-editor {
padding: 0;
overflow: hidden;
}
.preview-content.code-editor dees-input-code {
height: 100%;
}
.preview-actions {
padding: 12px;
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
display: flex;
gap: 8px;
}
.action-btn {
flex: 1;
padding: 8px 16px;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#404040')};
color: ${cssManager.bdTheme('#3f3f46', '#e0e0e0')};
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.15s;
}
.action-btn:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.15)')};
}
.action-btn.danger {
background: rgba(239, 68, 68, 0.2);
border-color: #ef4444;
color: #f87171;
}
.action-btn.danger:hover {
background: rgba(239, 68, 68, 0.3);
}
.action-btn.primary {
background: rgba(59, 130, 246, 0.3);
border-color: #3b82f6;
color: #60a5fa;
}
.action-btn.primary:hover {
background: rgba(59, 130, 246, 0.4);
}
.action-btn.primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-btn.secondary {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
border-color: ${cssManager.bdTheme('#d4d4d8', '#555')};
color: ${cssManager.bdTheme('#71717a', '#aaa')};
}
.action-btn.secondary:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
color: ${cssManager.bdTheme('#18181b', '#fff')};
}
.unsaved-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: rgba(251, 191, 36, 0.1);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: 4px;
font-size: 12px;
color: #fbbf24;
}
.unsaved-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #fbbf24;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: ${cssManager.bdTheme('#a1a1aa', '#666')};
text-align: center;
padding: 24px;
}
.empty-state svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: ${cssManager.bdTheme('#71717a', '#888')};
}
.error-state {
padding: 16px;
color: #f87171;
text-align: center;
}
`,
];
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('objectKey') || changedProperties.has('bucketName')) {
if (this.objectKey) {
this.loadObject();
} else {
this.content = '';
this.contentType = '';
this.error = '';
this.originalTextContent = '';
this.hasChanges = false;
this.editing = false;
}
}
}
private async loadObject() {
if (!this.objectKey || !this.bucketName || !this.dataProvider) return;
this.loading = true;
this.error = '';
this.hasChanges = false;
this.editing = false;
try {
const result = await this.dataProvider.getObject(this.bucketName, this.objectKey);
if (!result) {
this.error = 'Object not found';
this.loading = false;
return;
}
this.content = result.content || '';
this.contentType = result.contentType || '';
this.fileSize = result.size || 0;
this.lastModified = result.lastModified || '';
if (this.isText()) {
this.originalTextContent = this.getTextContent();
}
} catch (err) {
console.error('Error loading object:', err);
this.error = 'Failed to load object';
}
this.loading = false;
}
private formatDate(dateStr: string): string {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString();
}
private isImage(): boolean {
return this.contentType.startsWith('image/');
}
private isText(): boolean {
return (
this.contentType.startsWith('text/') ||
this.contentType === 'application/json' ||
this.contentType === 'application/xml' ||
this.contentType === 'application/javascript'
);
}
private getTextContent(): string {
try {
const binaryString = atob(this.content);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new TextDecoder('utf-8').decode(bytes);
} catch {
return 'Unable to decode content';
}
}
private async handleDownload() {
try {
const blob = new Blob([Uint8Array.from(atob(this.content), (c) => c.charCodeAt(0))], {
type: this.contentType,
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = getFileName(this.objectKey);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (err) {
console.error('Error downloading:', err);
}
}
private async handleDelete() {
if (!this.dataProvider) return;
if (!confirm(`Delete "${getFileName(this.objectKey)}"?`)) return;
try {
await this.dataProvider.deleteObject(this.bucketName, this.objectKey);
this.dispatchEvent(
new CustomEvent('object-deleted', {
detail: { key: this.objectKey },
bubbles: true,
composed: true,
})
);
} catch (err) {
console.error('Error deleting object:', err);
}
}
private getLanguage(): string {
const ext = this.objectKey.split('.').pop()?.toLowerCase() || '';
const languageMap: Record<string, string> = {
ts: 'typescript',
tsx: 'typescript',
js: 'javascript',
jsx: 'javascript',
mjs: 'javascript',
cjs: 'javascript',
json: 'json',
html: 'html',
htm: 'html',
css: 'css',
scss: 'scss',
sass: 'scss',
less: 'less',
md: 'markdown',
markdown: 'markdown',
xml: 'xml',
yaml: 'yaml',
yml: 'yaml',
py: 'python',
rb: 'ruby',
go: 'go',
rs: 'rust',
java: 'java',
c: 'c',
cpp: 'cpp',
h: 'c',
hpp: 'cpp',
cs: 'csharp',
php: 'php',
sh: 'shell',
bash: 'shell',
zsh: 'shell',
sql: 'sql',
graphql: 'graphql',
gql: 'graphql',
dockerfile: 'dockerfile',
txt: 'plaintext',
};
return languageMap[ext] || 'plaintext';
}
private handleContentChange(event: CustomEvent) {
const newValue = event.detail as string;
this.hasChanges = newValue !== this.originalTextContent;
}
private handleEdit() {
this.editing = true;
}
private handleDiscard() {
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
if (codeEditor) {
codeEditor.value = this.originalTextContent;
}
this.hasChanges = false;
this.editing = false;
}
private async handleSave() {
if (!this.hasChanges || this.saving || !this.dataProvider) return;
this.saving = true;
try {
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
const currentContent = codeEditor?.value ?? '';
const encoder = new TextEncoder();
const bytes = encoder.encode(currentContent);
const base64Content = btoa(String.fromCharCode(...bytes));
const success = await this.dataProvider.putObject(
this.bucketName,
this.objectKey,
base64Content,
this.contentType
);
if (success) {
this.originalTextContent = currentContent;
this.hasChanges = false;
this.editing = false;
this.content = base64Content;
}
} catch (err) {
console.error('Error saving object:', err);
}
this.saving = false;
}
render() {
if (!this.objectKey) {
return html`
<div class="preview-container">
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<p>Select a file to preview</p>
</div>
</div>
`;
}
if (this.loading) {
return html`
<div class="preview-container">
<div class="loading-state">Loading...</div>
</div>
`;
}
if (this.error) {
return html`
<div class="preview-container">
<div class="error-state">${this.error}</div>
</div>
`;
}
return html`
<div class="preview-container">
<div class="preview-header">
<div class="preview-title">${getFileName(this.objectKey)}</div>
<div class="preview-meta">
<span class="meta-item">${this.contentType}</span>
<span class="meta-item">${formatSize(this.fileSize)}</span>
<span class="meta-item">${this.formatDate(this.lastModified)}</span>
${this.hasChanges ? html`
<span class="unsaved-indicator">
<span class="unsaved-dot"></span>
Unsaved changes
</span>
` : ''}
</div>
</div>
<div class="preview-content ${this.editing ? 'code-editor' : ''}">
${this.editing
? html`
<dees-input-code
.value=${this.originalTextContent}
.language=${this.getLanguage()}
height="100%"
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
></dees-input-code>
`
: this.isText()
? html`
<dees-preview
.textContent=${this.originalTextContent}
.filename=${getFileName(this.objectKey)}
.language=${this.getLanguage()}
.showToolbar=${true}
.showFilename=${false}
></dees-preview>
`
: html`
<dees-preview
.base64=${this.content}
.mimeType=${this.contentType}
.filename=${getFileName(this.objectKey)}
.showToolbar=${true}
.showFilename=${false}
></dees-preview>
`
}
</div>
<div class="preview-actions">
${this.editing
? html`
<button class="action-btn secondary" @click=${this.handleDiscard}>
${this.hasChanges ? 'Discard' : 'Cancel'}
</button>
<button
class="action-btn primary"
@click=${this.handleSave}
?disabled=${this.saving || !this.hasChanges}
>
${this.saving ? 'Saving...' : 'Save'}
</button>
`
: html`
${this.isText()
? html`<button class="action-btn" @click=${this.handleEdit}>Edit</button>`
: ''}
<button class="action-btn" @click=${this.handleDownload}>Download</button>
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
`
}
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,6 @@
export * from './dees-storage-browser.js';
export * from './dees-storage-columns.js';
export * from './dees-storage-keys.js';
export * from './dees-storage-preview.js';
export * from './interfaces.js';
export { formatSize, formatCount, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';

View File

@@ -0,0 +1,37 @@
/**
* Storage Data Provider interface - implement this to connect the storage browser to your backend
*/
export interface IStorageObject {
key: string;
size?: number;
lastModified?: string;
isPrefix?: boolean;
}
export interface IStorageChangeEvent {
type: 'add' | 'modify' | 'delete';
key: string;
bucket: string;
size?: number;
lastModified?: Date;
}
export interface IStorageDataProvider {
listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IStorageObject[]; prefixes: string[] }>;
getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }>;
putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean>;
deleteObject(bucket: string, key: string): Promise<boolean>;
deletePrefix(bucket: string, prefix: string): Promise<boolean>;
getObjectUrl(bucket: string, key: string): Promise<string>;
moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }>;
movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }>;
}
export interface IColumn {
prefix: string;
objects: IStorageObject[];
prefixes: string[];
selectedItem: string | null;
width: number;
}

View File

@@ -0,0 +1,120 @@
/**
* Shared utilities for Storage browser components
*/
export interface IMoveValidation {
valid: boolean;
error?: string;
}
/**
* Format a byte size into a human-readable string
*/
export function formatSize(bytes?: number): string {
if (bytes === undefined || bytes === null) return '-';
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
}
/**
* Format a count into a compact human-readable string
*/
export function formatCount(count?: number): string {
if (count === undefined || count === null) return '';
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
return count.toString();
}
/**
* Extract the file name from a path
*/
export function getFileName(path: string): string {
const parts = path.replace(/\/$/, '').split('/');
return parts[parts.length - 1] || path;
}
/**
* Validates if a move operation is allowed
*/
export function validateMove(sourceKey: string, destPrefix: string): IMoveValidation {
if (sourceKey.endsWith('/')) {
if (destPrefix.startsWith(sourceKey)) {
return { valid: false, error: 'Cannot move a folder into itself' };
}
}
const sourceParent = getParentPrefix(sourceKey);
if (sourceParent === destPrefix) {
return { valid: false, error: 'Item is already in this location' };
}
return { valid: true };
}
/**
* Gets the parent prefix (directory) of a given key
*/
export function getParentPrefix(key: string): string {
const trimmed = key.endsWith('/') ? key.slice(0, -1) : key;
const lastSlash = trimmed.lastIndexOf('/');
return lastSlash >= 0 ? trimmed.substring(0, lastSlash + 1) : '';
}
/**
* Get content type from file extension
*/
export function getContentType(ext: string): string {
const contentTypes: Record<string, string> = {
json: 'application/json',
txt: 'text/plain',
html: 'text/html',
css: 'text/css',
js: 'application/javascript',
ts: 'text/typescript',
md: 'text/markdown',
xml: 'application/xml',
yaml: 'text/yaml',
yml: 'text/yaml',
csv: 'text/csv',
};
return contentTypes[ext] || 'application/octet-stream';
}
/**
* Get default content for a new file based on extension
*/
export function getDefaultContent(ext: string): string {
const defaults: Record<string, string> = {
json: '{\n \n}',
html: '<!DOCTYPE html>\n<html>\n<head>\n <title></title>\n</head>\n<body>\n \n</body>\n</html>',
md: '# Title\n\n',
txt: '',
};
return defaults[ext] || '';
}
/**
* Parse a prefix into cumulative path segments
*/
export function getPathSegments(prefix: string): string[] {
if (!prefix) return [];
const parts = prefix.split('/').filter(p => p);
const segments: string[] = [];
let cumulative = '';
for (const part of parts) {
cumulative += part + '/';
segments.push(cumulative);
}
return segments;
}

View File

@@ -1,5 +1,5 @@
import { type ITableAction } from './dees-table.js'; import { type ITableAction } from './dees-table.js';
import * as plugins from '../00plugins.js'; import * as plugins from '../../00plugins.js';
import { html, css, cssManager } from '@design.estate/dees-element'; import { html, css, cssManager } from '@design.estate/dees-element';
interface ITableDemoData { interface ITableDemoData {

View File

@@ -1,10 +1,10 @@
import * as plugins from '../00plugins.js'; import * as plugins from '../../00plugins.js';
import { demoFunc } from './dees-table.demo.js'; import { demoFunc } from './dees-table.demo.js';
import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element'; import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element';
import { DeesContextmenu } from '../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { type TIconKey } from '../dees-icon/dees-icon.js'; import { type TIconKey } from '../../00group-utility/dees-icon/dees-icon.js';
import { tableStyles } from './styles.js'; import { tableStyles } from './styles.js';
import type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js'; import type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
import { import {
@@ -14,7 +14,7 @@ import {
getViewData as getViewDataFn, getViewData as getViewDataFn,
} from './data.js'; } from './data.js';
import { compileLucenePredicate } from './lucene.js'; import { compileLucenePredicate } from './lucene.js';
import { themeDefaultStyles } from '../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js'; export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
@@ -30,6 +30,7 @@ declare global {
@customElement('dees-table') @customElement('dees-table')
export class DeesTable<T> extends DeesElement { export class DeesTable<T> extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroups = ['Data View'];
// INSTANCE // INSTANCE
@property({ @property({
@@ -231,7 +232,7 @@ export class DeesTable<T> extends DeesElement {
${directives.resolveExec(async () => { ${directives.resolveExec(async () => {
const resultArray: TemplateResult[] = []; const resultArray: TemplateResult[] = [];
for (const action of this.dataActions) { for (const action of this.dataActions) {
if (!action.type.includes('header')) continue; if (!action.type?.includes('header')) continue;
resultArray.push( resultArray.push(
html`<div html`<div
class="headerAction" class="headerAction"
@@ -449,7 +450,7 @@ export class DeesTable<T> extends DeesElement {
<td <td
@dblclick=${(e: Event) => { @dblclick=${(e: Event) => {
const dblAction = this.dataActions.find((actionArg) => const dblAction = this.dataActions.find((actionArg) =>
actionArg.type.includes('doubleClick') actionArg.type?.includes('doubleClick')
); );
if (this.editableFields.includes(editKey)) { if (this.editableFields.includes(editKey)) {
this.handleCellEditing(e, itemArg, editKey); this.handleCellEditing(e, itemArg, editKey);
@@ -505,7 +506,7 @@ export class DeesTable<T> extends DeesElement {
${directives.resolveExec(async () => { ${directives.resolveExec(async () => {
const resultArray: TemplateResult[] = []; const resultArray: TemplateResult[] = [];
for (const action of this.dataActions) { for (const action of this.dataActions) {
if (!action.type.includes('footer')) continue; if (!action.type?.includes('footer')) continue;
resultArray.push( resultArray.push(
html`<div html`<div
class="footerAction" class="footerAction"
@@ -539,11 +540,11 @@ export class DeesTable<T> extends DeesElement {
super.updated(changedProperties); super.updated(changedProperties);
this.determineColumnWidths(); this.determineColumnWidths();
if (this.searchable) { if (this.searchable) {
const existing = this.dataActions.find((actionArg) => actionArg.type.includes('header') && actionArg.name === 'Search'); const existing = this.dataActions.find((actionArg) => actionArg.type?.includes('header') && actionArg.name === 'Search');
if (!existing) { if (!existing) {
this.dataActions.unshift({ this.dataActions.unshift({
name: 'Search', name: 'Search',
iconName: 'magnifyingGlass', iconName: 'lucide:Search',
type: ['header'], type: ['header'],
actionFunc: async () => { actionFunc: async () => {
console.log('open search'); console.log('open search');
@@ -622,7 +623,7 @@ export class DeesTable<T> extends DeesElement {
const width = window.getComputedStyle(cell).width; const width = window.getComputedStyle(cell).width;
if (cell.textContent.includes('Actions')) { if (cell.textContent.includes('Actions')) {
const neededWidth = const neededWidth =
this.dataActions.filter((actionArg) => actionArg.type.includes('inRow')).length * 36; this.dataActions.filter((actionArg) => actionArg.type?.includes('inRow')).length * 36;
cell.style.width = `${Math.max(neededWidth, 68)}px`; cell.style.width = `${Math.max(neededWidth, 68)}px`;
} else { } else {
cell.style.width = width; cell.style.width = width;
@@ -794,7 +795,7 @@ export class DeesTable<T> extends DeesElement {
getActionsForType(typeArg: ITableAction['type'][0]) { getActionsForType(typeArg: ITableAction['type'][0]) {
const actions: ITableAction[] = []; const actions: ITableAction[] = [];
for (const action of this.dataActions) { for (const action of this.dataActions) {
if (!action.type.includes(typeArg)) continue; if (!action.type?.includes(typeArg)) continue;
actions.push(action); actions.push(action);
} }
return actions; return actions;

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