Compare commits

..

20 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
25 changed files with 4342 additions and 258 deletions

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.45.1",
"version": "3.48.4",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",
@@ -16,8 +16,8 @@
"author": "Lossless GmbH",
"license": "MIT",
"dependencies": {
"@design.estate/dees-domtools": "^2.3.9",
"@design.estate/dees-element": "^2.1.6",
"@design.estate/dees-domtools": "^2.5.1",
"@design.estate/dees-element": "^2.2.2",
"@design.estate/dees-wcctools": "^3.8.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
@@ -47,9 +47,9 @@
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1",
"@git.zone/tstest": "^3.3.2",
"@git.zone/tswatch": "^3.2.5",
"@git.zone/tswatch": "^3.3.0",
"@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.3.5"
"@types/node": "^25.4.0"
},
"files": [
"ts/**/*",

146
pnpm-lock.yaml generated
View File

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

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '3.45.1',
version: '3.48.4',
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 {
position: relative;
z-index: 3;
min-height: 0;
}
.maingrid > dees-appui-secondarymenu {
position: relative;
z-index: 2;
min-height: 0;
}
.maingrid > dees-appui-maincontent {

View File

@@ -10,6 +10,7 @@ import {
css,
unsafeCSS,
cssManager,
containerResponsive,
} 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[];
}
@containerResponsive()
@customElement('dees-statsgrid')
export class DeesStatsGrid extends DeesElement {
public static demo = demoFunc;
@@ -801,6 +803,38 @@ export class DeesStatsGrid extends DeesElement {
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() {

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

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

View File

@@ -162,10 +162,6 @@ export class DeesTileAudio extends DeesTileBase {
` : ''}
</div>
${this.duration > 0 ? html`
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
` : ''}
<div class="play-overlay">
<div class="play-circle">
<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> {
return {
src: this.src,

View File

@@ -145,10 +145,6 @@ export class DeesTileFolder extends DeesTileBase {
</div>
</div>
<div class="tile-badge-corner">
${this.items.length} item${this.items.length !== 1 ? 's' : ''}
</div>
${this.clickable ? html`
<div class="tile-overlay">
<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> {
return {
name: this.name,

View File

@@ -55,14 +55,6 @@ export class DeesTileImage extends DeesTileBase {
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;
@@ -97,19 +89,6 @@ export class DeesTileImage extends DeesTileBase {
` : ''}
</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`
<div class="tile-overlay">
<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> {
return {
src: this.src,

View File

@@ -81,14 +81,6 @@ export class DeesTileNote extends DeesTileBase {
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 {
position: absolute;
top: 0;
@@ -132,10 +124,6 @@ export class DeesTileNote extends DeesTileBase {
return html`
<div class="note-content">
${this.language ? html`
<div class="tile-badge-topright note-language">${this.language}</div>
` : ''}
${this.title ? html`
<div class="note-header">
<div class="note-title">${this.title}</div>
@@ -147,11 +135,6 @@ export class DeesTileNote extends DeesTileBase {
${!this.isHovering ? html`<div class="note-fade"></div>` : ''}
</div>
${this.isHovering && lines.length > 12 ? html`
<div class="tile-badge-corner">
Line ${this.getVisibleLineRange(lines.length)}
</div>
` : ''}
</div>
${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> {
return {
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 { tileBaseStyles } from '../dees-tile-shared/styles.js';
import { PdfManager } from '../dees-pdf-shared/PdfManager.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 { demo as demoFunc } from './demo.js';
@@ -37,6 +37,9 @@ export class DeesTilePdf extends DeesTileBase {
@property({ type: Boolean })
accessor isA4Format: boolean = true;
@state()
accessor fileSize: number = 0;
private renderPagesTask: Promise<void> | null = null;
private renderPagesQueued: boolean = false;
private pdfDocument: any;
@@ -54,18 +57,6 @@ export class DeesTilePdf extends DeesTileBase {
></canvas>
</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`
<div class="tile-overlay">
<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> {
return {
pdfUrl: this.pdfUrl,
@@ -141,6 +148,13 @@ export class DeesTilePdf extends DeesTileBase {
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
this.pageCount = this.pdfDocument.numPages;
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.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 = () => {
const samplePdfs = [
@@ -29,7 +29,7 @@ export const demo = () => {
<style>
.demo-container {
padding: 40px;
background: #f5f5f5;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
}
.demo-section {
@@ -40,6 +40,7 @@ export const demo = () => {
margin-bottom: 20px;
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.preview-grid {
@@ -59,6 +60,7 @@ export const demo = () => {
font-size: 14px;
font-weight: 500;
min-width: 100px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
</style>

View File

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

View File

@@ -59,9 +59,7 @@ export abstract class DeesTileBase extends DeesElement {
${!this.loading && !this.error ? this.renderTileContent() : ''}
${this.label ? html`
<div class="tile-label">${this.label}</div>
` : ''}
${this.renderBottomBar()}
</div>
`;
}
@@ -69,6 +67,11 @@ export abstract class DeesTileBase extends DeesElement {
/** Subclasses implement this to render their specific content */
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> {
await super.connectedCallback();
this.setupIntersectionObserver();

View File

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

View File

@@ -140,10 +140,6 @@ export class DeesTileVideo extends DeesTileBase {
` : ''}
</div>
${this.duration > 0 ? html`
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
` : ''}
${!this.isHovering ? html`
<div class="play-overlay">
<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> {
return {
src: this.src,