Compare commits

...

16 Commits

Author SHA1 Message Date
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
25 changed files with 4337 additions and 260 deletions

View File

@@ -1,5 +1,60 @@
# Changelog # Changelog
## 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) ## 2026-03-10 - 3.45.0 - feat(dees-form)
register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.45.0", "version": "3.48.1",
"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",
@@ -16,8 +16,8 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.9", "@design.estate/dees-domtools": "^2.5.1",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.2.2",
"@design.estate/dees-wcctools": "^3.8.0", "@design.estate/dees-wcctools": "^3.8.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-brands-svg-icons": "^7.2.0",
@@ -47,9 +47,9 @@
"@git.zone/tsbuild": "^4.3.0", "@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1", "@git.zone/tsbundle": "^2.9.1",
"@git.zone/tstest": "^3.3.2", "@git.zone/tstest": "^3.3.2",
"@git.zone/tswatch": "^3.2.5", "@git.zone/tswatch": "^3.3.0",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.3.5" "@types/node": "^25.4.0"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

146
pnpm-lock.yaml generated
View File

@@ -9,11 +9,11 @@ importers:
.: .:
dependencies: dependencies:
'@design.estate/dees-domtools': '@design.estate/dees-domtools':
specifier: ^2.3.9 specifier: ^2.5.1
version: 2.3.9 version: 2.5.1
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.1.6 specifier: ^2.2.2
version: 2.1.6 version: 2.2.2
'@design.estate/dees-wcctools': '@design.estate/dees-wcctools':
specifier: ^3.8.0 specifier: ^3.8.0
version: 3.8.0 version: 3.8.0
@@ -97,14 +97,14 @@ importers:
specifier: ^3.3.2 specifier: ^3.3.2
version: 3.3.2(socks@2.8.7)(typescript@5.9.3) version: 3.3.2(socks@2.8.7)(typescript@5.9.3)
'@git.zone/tswatch': '@git.zone/tswatch':
specifier: ^3.2.5 specifier: ^3.3.0
version: 3.2.5(@tiptap/pm@2.27.2) version: 3.3.0(@tiptap/pm@2.27.2)
'@push.rocks/projectinfo': '@push.rocks/projectinfo':
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.2 version: 5.0.2
'@types/node': '@types/node':
specifier: ^25.3.5 specifier: ^25.4.0
version: 25.3.5 version: 25.4.0
packages: packages:
@@ -302,23 +302,23 @@ packages:
'@cfworker/json-schema@4.1.1': '@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@cloudflare/workers-types@4.20260307.1': '@cloudflare/workers-types@4.20260310.1':
resolution: {integrity: sha512-0PvWLVVD6Q64V/XhollYtc8H35Vxm2rZi8bkZbEr3lK+mNgd2FBBVhlZ6A3saAUq3giRF4US/UfU/3a8i1PEcg==} resolution: {integrity: sha512-Cg4gyGDtfimNMgBr2h06aGR5Bt8puUbblyzPNZN55mBfVYCTWwQiUd9PrbkcoddKrWHlsy0ACH/16dAeGf5BQg==}
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.43.3': '@design.estate/dees-catalog@3.45.1':
resolution: {integrity: sha512-GjTePdwqNBL4isMOx4Ibei6pgK55H+DccbtgyNqjHRBz3LL14mo809ebjY2IZOVobswyzuTcNFvhfiqFP4/HLg==} resolution: {integrity: sha512-XVRof0OrjG74Xo4q+RRiZq2TMwBzvxZGY8TAN8+ozPUU2QJiHyo7sZ8E2sSh0V1L3sEWDWAMr1i08RlHy3eQfw==}
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
'@design.estate/dees-domtools@2.3.9': '@design.estate/dees-domtools@2.5.1':
resolution: {integrity: sha512-tixdBPUbbQEg46QkUQw9XVgGH/OxVe68FwPjspczKVPDM/0CbJL76JGQuTySZTPe8F49f2Q2Ft257qEGBEEtGA==} resolution: {integrity: sha512-ojzRSkOpQvxpd4drCNF1wadvPwthI6xIJpYjBbOwlgxkFCrlgxlOxHzRKEVnj5wWeUPqykKhddKp33LKW9mydw==}
'@design.estate/dees-element@2.1.6': '@design.estate/dees-element@2.2.2':
resolution: {integrity: sha512-7zyHkUjB8UEQgT9VbB2IJtc/yuPt9CI5JGel3b6BxA1kecY64ceIjFvof1uIkc0QP8q2fMLLY45r1c+9zDTjzg==} resolution: {integrity: sha512-sfA01ClHpTQSDgxFqZlfWOF0OlWvx59S8rjPir3UERF+LgYTvxGlGofMkx1fUo1TE32/MR+LzejWnB6RA+16HQ==}
'@design.estate/dees-wcctools@3.8.0': '@design.estate/dees-wcctools@3.8.0':
resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==} resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==}
@@ -528,8 +528,8 @@ packages:
resolution: {integrity: sha512-1R3VMEg+VMeMlSTIzIYTAsRIHuZMlpWmG1j4Q1cPSSw3jOp79OD7sJxfHkqy4bO/nTTcKMGXs5DD1nhT7Xno8w==} resolution: {integrity: sha512-1R3VMEg+VMeMlSTIzIYTAsRIHuZMlpWmG1j4Q1cPSSw3jOp79OD7sJxfHkqy4bO/nTTcKMGXs5DD1nhT7Xno8w==}
hasBin: true hasBin: true
'@git.zone/tswatch@3.2.5': '@git.zone/tswatch@3.3.0':
resolution: {integrity: sha512-0T+B4ufh4TYG2LG90W0PIUUE2K5bsjVbo0jgHfMyYsfUl1E4kvR+kfeCbZ0fwSzG/1sgVKzSrr1SO5LXcOERlQ==} resolution: {integrity: sha512-2d5G4L6RpEGW7d16xz6Gg6P/JnrMncNRDy74WaFrNjdn2fe5yIPtqoiQ/9LTbxqk67snj0gN2xtlQTXiN+Xa/w==}
hasBin: true hasBin: true
'@happy-dom/global-registrator@15.11.7': '@happy-dom/global-registrator@15.11.7':
@@ -1278,9 +1278,6 @@ packages:
'@push.rocks/taskbuffer@3.5.0': '@push.rocks/taskbuffer@3.5.0':
resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==} resolution: {integrity: sha512-Y9WwIEIyp6oVFdj06j84tfrZIvjhbMb3DF52rYxlTeYLk3W7RPhSg1bGPCbtkXWeKdBrSe37V90BkOG7Qq8Pqg==}
'@push.rocks/taskbuffer@4.2.1':
resolution: {integrity: sha512-F3aizWLGWdAz7wSJqOzjwVgo1VQJcxTbHUjDN/Pqxw0WMQUwODRGbhgy4zLag7bOyE4tc8Jv7yid7Bjmn5hKdg==}
'@push.rocks/webrequest@4.0.5': '@push.rocks/webrequest@4.0.5':
resolution: {integrity: sha512-wVSCaXqJ9Vh+rbwVz0wDl46dYz4rnwwSrm5vbVXKbuH6oKTPF0YRoujeJPqRltIn64RVGdLeY9/6ix+ZCrzhsg==} resolution: {integrity: sha512-wVSCaXqJ9Vh+rbwVz0wDl46dYz4rnwwSrm5vbVXKbuH6oKTPF0YRoujeJPqRltIn64RVGdLeY9/6ix+ZCrzhsg==}
@@ -1945,8 +1942,8 @@ packages:
'@types/node@22.19.15': '@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
'@types/node@25.3.5': '@types/node@25.4.0':
resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -2499,8 +2496,8 @@ packages:
fast-json-stable-stringify@2.1.0: fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
fast-xml-builder@1.0.0: fast-xml-builder@1.1.0:
resolution: {integrity: sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==} resolution: {integrity: sha512-7mtITW/we2/wTUZqMyBOR2F8xP4CRxMiSEcQxPIqdRWdO2L/HZSOlzoNyghmyDwNB8BDxePooV1ZTJpkOUhdRg==}
fast-xml-parser@4.5.4: fast-xml-parser@4.5.4:
resolution: {integrity: sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==} resolution: {integrity: sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==}
@@ -2510,8 +2507,8 @@ packages:
resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==} resolution: {integrity: sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==}
hasBin: true hasBin: true
fast-xml-parser@5.4.2: fast-xml-parser@5.5.1:
resolution: {integrity: sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ==} resolution: {integrity: sha512-JTpMz8P5mDoNYzXTmTT/xzWjFiCWi0U+UQTJtrFH9muXsr2RqtXZPbnCW5h2mKsOd4u3XcPWCvDSrnaBPlUcMQ==}
hasBin: true hasBin: true
fault@2.0.1: fault@2.0.1:
@@ -2897,9 +2894,6 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'} engines: {node: '>=12'}
lucide@0.564.0:
resolution: {integrity: sha512-FasyXKHWon773WIl3HeCQpd5xS6E0aLjqxiQStlHNKktni+HDncc1sqY+6vRUbCfmDsIaKQz43EEQLAUDLZO0g==}
lucide@0.577.0: lucide@0.577.0:
resolution: {integrity: sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==} resolution: {integrity: sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==}
@@ -3300,6 +3294,10 @@ packages:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'} engines: {node: '>=8'}
path-expression-matcher@1.1.2:
resolution: {integrity: sha512-LXWqJmcpp2BKOEmgt4CyuESFmBfPuhJlAHKJsFzuJU6CxErWk75BrO+Ni77M9OxHN6dCYKM4vj+21Z6cOL96YQ==}
engines: {node: '>=14.0.0'}
path-is-absolute@1.0.1: path-is-absolute@1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -4044,8 +4042,8 @@ snapshots:
'@api.global/typedrequest': 3.3.0 '@api.global/typedrequest': 3.3.0
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.2(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260307.1 '@cloudflare/workers-types': 4.20260310.1
'@design.estate/dees-catalog': 3.43.3(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.45.1(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.3.1 '@push.rocks/lik': 6.3.1
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -4555,16 +4553,16 @@ snapshots:
'@cfworker/json-schema@4.1.1': {} '@cfworker/json-schema@4.1.1': {}
'@cloudflare/workers-types@4.20260307.1': {} '@cloudflare/workers-types@4.20260310.1': {}
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.43.3(@tiptap/pm@2.27.2)': '@design.estate/dees-catalog@3.45.1(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.9 '@design.estate/dees-domtools': 2.5.1
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.2.2
'@design.estate/dees-wcctools': 3.8.0 '@design.estate/dees-wcctools': 3.8.0
'@fortawesome/fontawesome-svg-core': 7.2.0 '@fortawesome/fontawesome-svg-core': 7.2.0
'@fortawesome/free-brands-svg-icons': 7.2.0 '@fortawesome/free-brands-svg-icons': 7.2.0
@@ -4584,7 +4582,7 @@ snapshots:
apexcharts: 5.10.3 apexcharts: 5.10.3
highlight.js: 11.11.1 highlight.js: 11.11.1
ibantools: 4.5.1 ibantools: 4.5.1
lucide: 0.564.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
@@ -4603,7 +4601,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
broadcast-channel: 7.3.0 broadcast-channel: 7.3.0
'@design.estate/dees-domtools@2.3.9': '@design.estate/dees-domtools@2.5.1':
dependencies: dependencies:
'@api.global/typedrequest': 3.3.0 '@api.global/typedrequest': 3.3.0
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
@@ -4629,9 +4627,9 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@design.estate/dees-element@2.1.6': '@design.estate/dees-element@2.2.2':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.9 '@design.estate/dees-domtools': 2.5.1
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
lit: 3.3.2 lit: 3.3.2
@@ -4643,8 +4641,8 @@ snapshots:
'@design.estate/dees-wcctools@3.8.0': '@design.estate/dees-wcctools@3.8.0':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.9 '@design.estate/dees-domtools': 2.5.1
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
lit: 3.3.2 lit: 3.3.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -4891,7 +4889,7 @@ snapshots:
- utf-8-validate - utf-8-validate
- vue - vue
'@git.zone/tswatch@3.2.5(@tiptap/pm@2.27.2)': '@git.zone/tswatch@3.3.0(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@api.global/typedserver': 8.4.2(@tiptap/pm@2.27.2) '@api.global/typedserver': 8.4.2(@tiptap/pm@2.27.2)
'@git.zone/tsbundle': 2.9.1 '@git.zone/tsbundle': 2.9.1
@@ -4908,7 +4906,6 @@ snapshots:
'@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartshell': 3.3.7 '@push.rocks/smartshell': 3.3.7
'@push.rocks/smartwatch': 6.3.0 '@push.rocks/smartwatch': 6.3.0
'@push.rocks/taskbuffer': 4.2.1
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
- '@swc/helpers' - '@swc/helpers'
@@ -6077,7 +6074,7 @@ snapshots:
'@push.rocks/smartntml@2.0.8': '@push.rocks/smartntml@2.0.8':
dependencies: dependencies:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.2.2
'@happy-dom/global-registrator': 15.11.7 '@happy-dom/global-registrator': 15.11.7
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
fake-indexeddb: 6.2.5 fake-indexeddb: 6.2.5
@@ -6298,7 +6295,7 @@ snapshots:
'@push.rocks/smartxml@2.0.0': '@push.rocks/smartxml@2.0.0':
dependencies: dependencies:
fast-xml-parser: 5.4.2 fast-xml-parser: 5.5.1
'@push.rocks/smartyaml@2.0.5': '@push.rocks/smartyaml@2.0.5':
dependencies: dependencies:
@@ -6311,23 +6308,7 @@ snapshots:
'@push.rocks/taskbuffer@3.5.0': '@push.rocks/taskbuffer@3.5.0':
dependencies: dependencies:
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.2.2
'@push.rocks/lik': 6.3.1
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.2.1
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9
transitivePeerDependencies:
- '@nuxt/kit'
- react
- supports-color
- vue
'@push.rocks/taskbuffer@4.2.1':
dependencies:
'@design.estate/dees-element': 2.1.6
'@push.rocks/lik': 6.3.1 '@push.rocks/lik': 6.3.1
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -7041,7 +7022,7 @@ snapshots:
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
source-map: 0.6.1 source-map: 0.6.1
'@types/debug@4.1.12': '@types/debug@4.1.12':
@@ -7050,17 +7031,17 @@ snapshots:
'@types/from2@2.3.6': '@types/from2@2.3.6':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -7080,7 +7061,7 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
@@ -7103,11 +7084,11 @@ snapshots:
'@types/mute-stream@0.0.4': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/node-forge@1.3.14': '@types/node-forge@1.3.14':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/node@16.9.1': {} '@types/node@16.9.1': {}
@@ -7115,7 +7096,7 @@ snapshots:
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
'@types/node@25.3.5': '@types/node@25.4.0':
dependencies: dependencies:
undici-types: 7.18.2 undici-types: 7.18.2
@@ -7131,11 +7112,11 @@ snapshots:
'@types/tar-stream@3.1.4': '@types/tar-stream@3.1.4':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -7161,11 +7142,11 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.4.0
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -7636,7 +7617,9 @@ snapshots:
fast-json-stable-stringify@2.1.0: {} fast-json-stable-stringify@2.1.0: {}
fast-xml-builder@1.0.0: {} fast-xml-builder@1.1.0:
dependencies:
path-expression-matcher: 1.1.2
fast-xml-parser@4.5.4: fast-xml-parser@4.5.4:
dependencies: dependencies:
@@ -7644,12 +7627,13 @@ snapshots:
fast-xml-parser@5.4.1: fast-xml-parser@5.4.1:
dependencies: dependencies:
fast-xml-builder: 1.0.0 fast-xml-builder: 1.1.0
strnum: 2.2.0 strnum: 2.2.0
fast-xml-parser@5.4.2: fast-xml-parser@5.5.1:
dependencies: dependencies:
fast-xml-builder: 1.0.0 fast-xml-builder: 1.1.0
path-expression-matcher: 1.1.2
strnum: 2.2.0 strnum: 2.2.0
fault@2.0.1: fault@2.0.1:
@@ -8110,8 +8094,6 @@ snapshots:
lru-cache@7.18.3: {} lru-cache@7.18.3: {}
lucide@0.564.0: {}
lucide@0.577.0: {} lucide@0.577.0: {}
make-dir@3.1.0: make-dir@3.1.0:
@@ -8698,6 +8680,8 @@ snapshots:
path-exists@4.0.0: {} path-exists@4.0.0: {}
path-expression-matcher@1.1.2: {}
path-is-absolute@1.0.1: {} path-is-absolute@1.0.1: {}
path-key@3.1.1: {} path-key@3.1.1: {}

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '3.45.0', version: '3.48.1',
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

@@ -219,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 {
@@ -875,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

@@ -0,0 +1,156 @@
import { html } from '@design.estate/dees-element';
import type { IS3DataProvider, IS3Object } from './interfaces.js';
import './dees-s3-browser.js';
// Mock in-memory S3 data provider for demo purposes
class MockS3DataProvider implements IS3DataProvider {
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 S3 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: IS3Object[]; prefixes: string[] }> {
const pfx = prefix || '';
const objects: IS3Object[] = [];
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-s3-browser
.dataProvider=${new MockS3DataProvider()}
.bucketName=${'demo-bucket'}
></dees-s3-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-s3-browser.demo.js';
import type { IS3DataProvider, IS3ChangeEvent } from './interfaces.js';
import './dees-s3-columns.js';
import './dees-s3-keys.js';
import './dees-s3-preview.js';
declare global {
interface HTMLElementTagNameMap {
'dees-s3-browser': DeesS3Browser;
}
}
type TViewType = 'columns' | 'keys';
@customElement('dees-s3-browser')
export class DeesS3Browser extends DeesElement {
public static demo = demoFunc;
public static demoGroups = ['Data View'];
@property({ type: Object })
public accessor dataProvider: IS3DataProvider | 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: IS3ChangeEvent) => 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: IS3ChangeEvent) => {
this.handleChange(event);
});
this.isStreamConnected = true;
} catch (error) {
console.warn('[S3Browser] 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: IS3ChangeEvent) {
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-s3-columns
.dataProvider=${this.dataProvider}
.bucketName=${this.bucketName}
.currentPrefix=${this.currentPrefix}
.refreshKey=${this.refreshKey}
@key-selected=${this.handleKeySelected}
@navigate=${this.handleNavigate}
></dees-s3-columns>
`
: html`
<dees-s3-keys
.dataProvider=${this.dataProvider}
.bucketName=${this.bucketName}
.currentPrefix=${this.currentPrefix}
.refreshKey=${this.refreshKey}
@key-selected=${this.handleKeySelected}
@navigate=${this.handleNavigate}
></dees-s3-keys>
`}
</div>
${this.selectedKey
? html`
<div
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
@mousedown=${this.startPreviewResize}
></div>
<div class="preview-panel">
<dees-s3-preview
.dataProvider=${this.dataProvider}
.bucketName=${this.bucketName}
.objectKey=${this.selectedKey}
@object-deleted=${this.handleObjectDeleted}
></dees-s3-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 { IS3DataProvider } from './interfaces.js';
import { formatSize, getFileName } from './utilities.js';
declare global {
interface HTMLElementTagNameMap {
'dees-s3-preview': DeesS3Preview;
}
}
@customElement('dees-s3-preview')
export class DeesS3Preview extends DeesElement {
@property({ type: Object })
public accessor dataProvider: IS3DataProvider | 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-s3-browser.js';
export * from './dees-s3-columns.js';
export * from './dees-s3-keys.js';
export * from './dees-s3-preview.js';
export * from './interfaces.js';
export { formatSize, formatCount, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';

View File

@@ -0,0 +1,37 @@
/**
* S3 Data Provider interface - implement this to connect the S3 browser to your backend
*/
export interface IS3Object {
key: string;
size?: number;
lastModified?: string;
isPrefix?: boolean;
}
export interface IS3ChangeEvent {
type: 'add' | 'modify' | 'delete';
key: string;
bucket: string;
size?: number;
lastModified?: Date;
}
export interface IS3DataProvider {
listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IS3Object[]; 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: IS3Object[];
prefixes: string[];
selectedItem: string | null;
width: number;
}

View File

@@ -0,0 +1,120 @@
/**
* Shared utilities for S3 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

@@ -10,6 +10,7 @@ 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';
@@ -93,6 +94,7 @@ 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;
@@ -801,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() {

View File

@@ -3,3 +3,4 @@ export * from './dees-dataview-codebox/index.js';
export * from './dees-dataview-statusobject/index.js'; export * from './dees-dataview-statusobject/index.js';
export * from './dees-table/index.js'; export * from './dees-table/index.js';
export * from './dees-statsgrid/index.js'; export * from './dees-statsgrid/index.js';
export * from './dees-s3-browser/index.js';

View File

@@ -162,10 +162,6 @@ export class DeesTileAudio extends DeesTileBase {
` : ''} ` : ''}
</div> </div>
${this.duration > 0 ? html`
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
` : ''}
<div class="play-overlay"> <div class="play-overlay">
<div class="play-circle"> <div class="play-circle">
<dees-icon icon="lucide:Play"></dees-icon> <dees-icon icon="lucide:Play"></dees-icon>
@@ -181,6 +177,17 @@ export class DeesTileAudio extends DeesTileBase {
`; `;
} }
protected renderBottomBar(): TemplateResult | string {
if (!this.label && !this.duration) return '';
return html`
<div class="tile-info-bar">
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
<span class="info-spacer"></span>
${this.duration > 0 ? html`<span class="info-detail">${this.formatTime(this.duration)}</span>` : ''}
</div>
`;
}
protected getTileClickDetail(): Record<string, unknown> { protected getTileClickDetail(): Record<string, unknown> {
return { return {
src: this.src, src: this.src,

View File

@@ -145,10 +145,6 @@ export class DeesTileFolder extends DeesTileBase {
</div> </div>
</div> </div>
<div class="tile-badge-corner">
${this.items.length} item${this.items.length !== 1 ? 's' : ''}
</div>
${this.clickable ? html` ${this.clickable ? html`
<div class="tile-overlay"> <div class="tile-overlay">
<dees-icon icon="lucide:FolderOpen"></dees-icon> <dees-icon icon="lucide:FolderOpen"></dees-icon>
@@ -158,6 +154,17 @@ export class DeesTileFolder extends DeesTileBase {
`; `;
} }
protected renderBottomBar(): TemplateResult | string {
if (!this.label && !this.items.length) return '';
return html`
<div class="tile-info-bar">
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
<span class="info-spacer"></span>
<span class="info-detail">${this.items.length} item${this.items.length !== 1 ? 's' : ''}</span>
</div>
`;
}
protected getTileClickDetail(): Record<string, unknown> { protected getTileClickDetail(): Record<string, unknown> {
return { return {
name: this.name, name: this.name,

View File

@@ -55,14 +55,6 @@ export class DeesTileImage extends DeesTileBase {
opacity: 0; opacity: 0;
} }
.tile-badge-topright.dimension-badge {
opacity: 0;
transition: opacity 0.2s ease;
}
.tile-container.clickable:hover .tile-badge-topright.dimension-badge {
opacity: 1;
}
`, `,
] as any; ] as any;
@@ -97,19 +89,6 @@ export class DeesTileImage extends DeesTileBase {
` : ''} ` : ''}
</div> </div>
${this.imageWidth > 0 && this.imageHeight > 0 ? html`
<div class="tile-badge-topright dimension-badge">
${this.imageWidth} × ${this.imageHeight}
</div>
` : ''}
${this.imageLoaded ? html`
<div class="tile-info">
<dees-icon icon="lucide:Image"></dees-icon>
<span class="tile-info-text">${this.imageWidth} × ${this.imageHeight}</span>
</div>
` : ''}
${this.clickable ? html` ${this.clickable ? html`
<div class="tile-overlay"> <div class="tile-overlay">
<dees-icon icon="lucide:Eye"></dees-icon> <dees-icon icon="lucide:Eye"></dees-icon>
@@ -119,6 +98,19 @@ export class DeesTileImage extends DeesTileBase {
`; `;
} }
protected renderBottomBar(): TemplateResult | string {
if (!this.label && !(this.imageWidth > 0)) return '';
return html`
<div class="tile-info-bar">
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
<span class="info-spacer"></span>
${this.imageWidth > 0 && this.imageHeight > 0
? html`<span class="info-detail">${this.imageWidth} × ${this.imageHeight}</span>`
: ''}
</div>
`;
}
protected getTileClickDetail(): Record<string, unknown> { protected getTileClickDetail(): Record<string, unknown> {
return { return {
src: this.src, src: this.src,

View File

@@ -81,14 +81,6 @@ export class DeesTileNote extends DeesTileBase {
pointer-events: none; pointer-events: none;
} }
.tile-badge-topright.note-language {
background: ${cssManager.bdTheme('hsl(215 20% 92%)', 'hsl(215 20% 88%)')};
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 40%)')};
font-size: 9px;
text-transform: uppercase;
z-index: 5;
}
.note-lines { .note-lines {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -132,10 +124,6 @@ export class DeesTileNote extends DeesTileBase {
return html` return html`
<div class="note-content"> <div class="note-content">
${this.language ? html`
<div class="tile-badge-topright note-language">${this.language}</div>
` : ''}
${this.title ? html` ${this.title ? html`
<div class="note-header"> <div class="note-header">
<div class="note-title">${this.title}</div> <div class="note-title">${this.title}</div>
@@ -147,11 +135,6 @@ export class DeesTileNote extends DeesTileBase {
${!this.isHovering ? html`<div class="note-fade"></div>` : ''} ${!this.isHovering ? html`<div class="note-fade"></div>` : ''}
</div> </div>
${this.isHovering && lines.length > 12 ? html`
<div class="tile-badge-corner">
Line ${this.getVisibleLineRange(lines.length)}
</div>
` : ''}
</div> </div>
${this.clickable ? html` ${this.clickable ? html`
@@ -163,6 +146,21 @@ export class DeesTileNote extends DeesTileBase {
`; `;
} }
protected renderBottomBar(): TemplateResult | string {
const lines = this.content.split('\n');
if (!this.label && !this.language && !lines.length) return '';
return html`
<div class="tile-info-bar">
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
<span class="info-spacer"></span>
${this.language ? html`<span class="info-detail">${this.language.toUpperCase()}</span>` : ''}
${this.isHovering && lines.length > 12
? html`<span class="info-detail">Line ${this.getVisibleLineRange(lines.length)}</span>`
: html`<span class="info-detail">${lines.length} lines</span>`}
</div>
`;
}
protected getTileClickDetail(): Record<string, unknown> { protected getTileClickDetail(): Record<string, unknown> {
return { return {
title: this.title, title: this.title,

View File

@@ -1,9 +1,9 @@
import { property, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element'; import { property, state, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element';
import { DeesTileBase } from '../dees-tile-shared/DeesTileBase.js'; import { DeesTileBase } from '../dees-tile-shared/DeesTileBase.js';
import { tileBaseStyles } from '../dees-tile-shared/styles.js'; import { tileBaseStyles } from '../dees-tile-shared/styles.js';
import { PdfManager } from '../dees-pdf-shared/PdfManager.js'; import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js'; import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js';
import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js'; import { PerformanceMonitor, throttle, formatFileSize } from '../dees-pdf-shared/utils.js';
import { tilePdfStyles } from './styles.js'; import { tilePdfStyles } from './styles.js';
import { demo as demoFunc } from './demo.js'; import { demo as demoFunc } from './demo.js';
@@ -37,6 +37,9 @@ export class DeesTilePdf extends DeesTileBase {
@property({ type: Boolean }) @property({ type: Boolean })
accessor isA4Format: boolean = true; accessor isA4Format: boolean = true;
@state()
accessor fileSize: number = 0;
private renderPagesTask: Promise<void> | null = null; private renderPagesTask: Promise<void> | null = null;
private renderPagesQueued: boolean = false; private renderPagesQueued: boolean = false;
private pdfDocument: any; private pdfDocument: any;
@@ -54,18 +57,6 @@ export class DeesTilePdf extends DeesTileBase {
></canvas> ></canvas>
</div> </div>
${this.pageCount > 1 && this.isHovering ? html`
<div class="tile-badge">
Page ${this.currentPreviewPage} of ${this.pageCount}
</div>
` : ''}
${this.pageCount > 0 && !this.isHovering ? html`
<div class="tile-badge-corner">
${this.pageCount} page${this.pageCount > 1 ? 's' : ''}
</div>
` : ''}
${this.clickable ? html` ${this.clickable ? html`
<div class="tile-overlay"> <div class="tile-overlay">
<dees-icon icon="lucide:Eye"></dees-icon> <dees-icon icon="lucide:Eye"></dees-icon>
@@ -75,6 +66,22 @@ export class DeesTilePdf extends DeesTileBase {
`; `;
} }
protected renderBottomBar(): TemplateResult | string {
if (!this.pageCount && !this.label) return '';
return html`
<div class="tile-info-bar">
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
<span class="info-spacer"></span>
${this.pageCount > 1 && this.isHovering
? html`<span class="info-detail">${this.currentPreviewPage}/${this.pageCount}</span>`
: this.pageCount > 0
? html`<span class="info-detail">${this.pageCount} pg</span>`
: ''}
${this.fileSize > 0 ? html`<span class="info-detail">${formatFileSize(this.fileSize)}</span>` : ''}
</div>
`;
}
protected getTileClickDetail(): Record<string, unknown> { protected getTileClickDetail(): Record<string, unknown> {
return { return {
pdfUrl: this.pdfUrl, pdfUrl: this.pdfUrl,
@@ -141,6 +148,13 @@ export class DeesTilePdf extends DeesTileBase {
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl); this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
this.pageCount = this.pdfDocument.numPages; this.pageCount = this.pdfDocument.numPages;
this.currentPreviewPage = 1; this.currentPreviewPage = 1;
try {
const downloadInfo = await this.pdfDocument.getDownloadInfo();
this.fileSize = downloadInfo.length;
} catch {
// File size unavailable — not critical
}
this.loadedPdfUrl = this.pdfUrl; this.loadedPdfUrl = this.pdfUrl;
this.loading = false; this.loading = false;

View File

@@ -1,4 +1,4 @@
import { html } from '@design.estate/dees-element'; import { html, cssManager } from '@design.estate/dees-element';
export const demo = () => { export const demo = () => {
const samplePdfs = [ const samplePdfs = [
@@ -29,7 +29,7 @@ export const demo = () => {
<style> <style>
.demo-container { .demo-container {
padding: 40px; padding: 40px;
background: #f5f5f5; background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
} }
.demo-section { .demo-section {
@@ -40,6 +40,7 @@ export const demo = () => {
margin-bottom: 20px; margin-bottom: 20px;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
.preview-grid { .preview-grid {
@@ -59,6 +60,7 @@ export const demo = () => {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
min-width: 100px; min-width: 100px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
} }
</style> </style>

View File

@@ -10,10 +10,11 @@ export const tilePdfStyles = css`
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
padding: 8px 8px 28px 8px;
} }
.preview-stack.non-a4 { .preview-stack.non-a4 {
padding: 12px; padding: 12px 12px 28px 12px;
} }
.preview-canvas { .preview-canvas {

View File

@@ -59,9 +59,7 @@ export abstract class DeesTileBase extends DeesElement {
${!this.loading && !this.error ? this.renderTileContent() : ''} ${!this.loading && !this.error ? this.renderTileContent() : ''}
${this.label ? html` ${this.renderBottomBar()}
<div class="tile-label">${this.label}</div>
` : ''}
</div> </div>
`; `;
} }
@@ -69,6 +67,11 @@ export abstract class DeesTileBase extends DeesElement {
/** Subclasses implement this to render their specific content */ /** Subclasses implement this to render their specific content */
protected abstract renderTileContent(): TemplateResult; protected abstract renderTileContent(): TemplateResult;
/** Subclasses override this to render a bottom info bar with metadata */
protected renderBottomBar(): TemplateResult | string {
return '';
}
public async connectedCallback(): Promise<void> { public async connectedCallback(): Promise<void> {
await super.connectedCallback(); await super.connectedCallback();
this.setupIntersectionObserver(); this.setupIntersectionObserver();

View File

@@ -15,8 +15,9 @@ export const tileBaseStyles = [
background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(215 20% 14%)')}; background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(215 20% 14%)')};
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: box-shadow 0.2s ease;
box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')}; box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')};
} }
.tile-container.clickable { .tile-container.clickable {
@@ -24,7 +25,6 @@ export const tileBaseStyles = [
} }
.tile-container.clickable:hover { .tile-container.clickable:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.3)')}; box-shadow: 0 8px 24px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.3)')};
} }
@@ -71,90 +71,39 @@ export const tileBaseStyles = [
color: white; color: white;
} }
.tile-info { .tile-info-bar {
position: absolute; position: absolute;
bottom: 8px; bottom: 0;
left: 8px; left: 0;
right: 8px; right: 0;
padding: 6px 10px; padding: 4px 8px;
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.92)', 'hsl(215 20% 12% / 0.92)')}; background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.95)', 'hsl(215 20% 12% / 0.95)')};
border-radius: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: 12px; font-size: 10px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')}; color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')};
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); z-index: 25;
z-index: 10;
}
.tile-info dees-icon {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
}
.tile-info-text {
font-weight: 500;
font-size: 11px;
}
.tile-badge {
position: absolute;
top: 8px;
left: 8px;
right: 8px;
padding: 5px 8px;
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.7)', 'hsl(0 0% 100% / 0.9)')};
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-align: center;
backdrop-filter: blur(12px);
z-index: 15;
pointer-events: none;
animation: fadeIn 0.2s ease;
}
.tile-badge-corner {
position: absolute;
bottom: 8px;
right: 8px;
padding: 3px 8px;
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
border-radius: 4px;
font-size: 10px;
font-weight: 600;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
backdrop-filter: blur(8px);
z-index: 10;
pointer-events: none;
} }
.tile-badge-topright { .info-label {
position: absolute; white-space: nowrap;
top: 8px; overflow: hidden;
right: 8px; text-overflow: ellipsis;
padding: 3px 8px; min-width: 0;
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
border-radius: 4px;
font-size: 10px;
font-weight: 600;
backdrop-filter: blur(8px);
z-index: 15;
pointer-events: none;
} }
/* Shift bottom badges up when label is present */ .info-spacer {
.tile-container:has(.tile-label) .tile-badge-corner { flex: 1;
bottom: 33px;
} }
.tile-container:has(.tile-label) .tile-info { .info-detail {
bottom: 33px; white-space: nowrap;
opacity: 0.7;
flex-shrink: 0;
} }
.tile-loading, .tile-loading,
@@ -200,40 +149,12 @@ export const tileBaseStyles = [
font-weight: 500; font-weight: 500;
} }
.tile-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 6px 10px;
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.95)', 'hsl(215 20% 12% / 0.95)')};
font-size: 11px;
font-weight: 500;
color: ${cssManager.bdTheme('hsl(215 16% 35%)', 'hsl(215 16% 75%)')};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
z-index: 10;
backdrop-filter: blur(12px);
}
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Size variants */ /* Size variants */
:host([size="small"]) .tile-container { :host([size="small"]) .tile-container {
width: 150px; width: 150px;

View File

@@ -140,10 +140,6 @@ export class DeesTileVideo extends DeesTileBase {
` : ''} ` : ''}
</div> </div>
${this.duration > 0 ? html`
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
` : ''}
${!this.isHovering ? html` ${!this.isHovering ? html`
<div class="play-overlay"> <div class="play-overlay">
<dees-icon icon="lucide:Play"></dees-icon> <dees-icon icon="lucide:Play"></dees-icon>
@@ -159,6 +155,17 @@ export class DeesTileVideo extends DeesTileBase {
`; `;
} }
protected renderBottomBar(): TemplateResult | string {
if (!this.label && !this.duration) return '';
return html`
<div class="tile-info-bar">
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
<span class="info-spacer"></span>
${this.duration > 0 ? html`<span class="info-detail">${this.formatTime(this.duration)}</span>` : ''}
</div>
`;
}
protected getTileClickDetail(): Record<string, unknown> { protected getTileClickDetail(): Record<string, unknown> {
return { return {
src: this.src, src: this.src,