Compare commits

...

57 Commits

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

View File

@@ -1,5 +1,201 @@
# Changelog
## 2026-03-14 - 3.48.4 - fix(storage-browser)
rename S3-specific storage browser interfaces to generic storage types
- Replaces IS3DataProvider, IS3Object, and IS3ChangeEvent with generic storage interface names across storage browser components
- Updates demo provider naming and user-facing demo text from S3 browser to Storage browser
- Aligns interface and utility comments with storage-agnostic terminology
## 2026-03-14 - 3.48.3 - fix(dataview)
rename dees-s3-browser exports and custom elements to dees-storage-browser
- Replaces the dees-s3-browser module path with dees-storage-browser in dataview exports
- Renames the main custom element from dees-s3-browser to dees-storage-browser
- Renames related columns, keys, preview, demo, interfaces, and utility entry points under the new storage-browser module
## 2026-03-12 - 3.48.2 - fix(repo)
no changes to commit
## 2026-03-12 - 3.48.1 - fix(repo)
no changes to commit
## 2026-03-12 - 3.48.0 - feat(dataview)
add an S3 browser component with column and list views, file preview, editing, and object management
- introduces a new dees-s3-browser module with shared interfaces, utilities, demo, and exports
- supports browsing S3-style prefixes in both column and list layouts with breadcrumb navigation
- adds file preview with text editing, download, and delete actions
- includes create, rename, move, delete, upload, and drag-and-drop handling for files and folders
- adds optional live change stream integration with refresh indicators
## 2026-03-11 - 3.47.2 - fix(deps)
bump @design.estate/dees-domtools and @design.estate/dees-element dependencies
- update @design.estate/dees-domtools from ^2.3.9 to ^2.5.1
- update @design.estate/dees-element from ^2.2.1 to ^2.2.2
## 2026-03-11 - 3.47.1 - fix(dees-statsgrid)
add tablet breakpoint to render stats grid as three columns
- Added cssManager.cssForTablet rule to set .stats-grid grid-template-columns: repeat(3, 1fr).
- Improves responsive layout on tablet devices for dees-statsgrid tiles.
- Change made in ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
## 2026-03-11 - 3.47.0 - feat(dees-statsgrid)
add container-responsive behavior and responsive CSS to dees-statsgrid; bump @design.estate/dees-element dependency to ^2.2.1
- Added @containerResponsive decorator and import to dees-statsgrid
- Added cssManager.cssForPhablet and cssManager.cssForPhone responsive style blocks to adjust layout, spacing and font sizes on smaller viewports
- Bumped dependency @design.estate/dees-element from ^2.1.6 to ^2.2.1
## 2026-03-10 - 3.46.1 - fix(dees-appui)
add min-height: 0 to mainmenu and secondarymenu to prevent unintended container height and fix layout stacking
- Modified ts_web/elements/00group-appui/dees-appui/dees-appui.ts: added min-height: 0 to .maingrid > dees-appui-mainmenu and .maingrid > dees-appui-secondarymenu
- Fixes layout issues where children or flexbox-derived min-height could cause menu containers to expand and interfere with z-index stacking
## 2026-03-10 - 3.46.0 - feat(dees-tile)
unify tile metadata into a consistent bottom info bar and add PDF file-size display
- Introduce renderBottomBar() hook in DeesTileBase and remove per-component bottom badges/labels in favor of a unified info bar.
- Implement renderBottomBar in audio, video, image, folder, note and pdf tiles to show label, counts, dimensions, duration, language/line info and page counts.
- PDF tile: add fileSize state, attempt to read download info and display formatted file size in the info bar; show currentPreviewPage/pageCount when hovering.
- Styling changes: replace legacy badges/labels with .tile-info-bar (.info-label, .info-detail, .info-spacer); adjust padding, font sizing, z-index, and remove hover translate for clickable tiles.
- PDF demo and styles: use cssManager theming for demo colors and adjust preview padding.
- Bump devDependencies: @git.zone/tswatch -> ^3.3.0 and @types/node -> ^25.4.0
## 2026-03-10 - 3.45.1 - fix(dees-appui)
substitute route params into URL hash when navigating
- Replaces :param placeholders in view.route with provided params before updating the URL hash.
- Ensures window.history.pushState is called with the resolved route so URLs do not contain literal parameter tokens.
## 2026-03-10 - 3.45.0 - feat(dees-form)
register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates
- Added imports and registration of DeesInputTags, DeesInputList, DeesInputWysiwyg, and DeesInputRichtext in dees-form
- Extended TFormInputElement union type to include the new input components
- DeesInputRichtext now calls changeSubject.next(this.value) in the editor onUpdate handler to propagate changes
## 2026-03-10 - 3.44.0 - feat(appui-tabs)
add support for left/right tab action buttons and content tab action APIs
- Introduce ITabAction interface and add actionsLeft/actionsRight properties to dees-appui-tabs, dees-appui-maincontent, and dees-appui.
- Render action buttons with new styles and renderActions() helper, including disabled state and click handlers; wire actions into tab components.
- Add public clear() on dees-appui-tabs and improve tab selection logic to reset selection when tabs become empty or when the selected tab is removed.
- Expose setContentTabActionsLeft and setContentTabActionsRight on the DeesAppui programmatic API and update interfaces/appconfig accordingly.
- Update demos to showcase action buttons, add clear-all behavior, and adjust layout/styling for action areas.
## 2026-03-09 - 3.43.4 - fix(media)
remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies
- Removed deprecated PDF components and related demos/styles: ts_web/elements/00group-media/dees-pdf/* and ts_web/elements/00group-media/dees-pdf-preview/*
- Removed exports for dees-pdf and dees-pdf-preview from ts_web/elements/00group-media/index.ts (public API removal)
- Dependency upgrades: @design.estate/dees-domtools → ^2.3.9, apexcharts → ^5.10.3, lucide → ^0.577.0, @fortawesome/* → ^7.2.0
- DevDependency upgrades: @git.zone/tsbuild → ^4.3.0, @git.zone/tsbundle → ^2.9.1, @git.zone/tstest → ^3.3.2, @git.zone/tswatch → ^3.2.5, @types/node → ^25.3.5
- Updated ts_web/services/versions.ts to align CDN/version constants (apexcharts, tiptap → 2.27.2, fontawesome)
## 2026-02-24 - 3.43.3 - fix(dees-table)
use lucide icon identifier for Search action in dees-table
- Replaced iconName 'magnifyingGlass' with 'lucide:Search' in ts_web/elements/00group-dataview/dees-table/dees-table.ts
- Updates the icon identifier for the header 'Search' action; no functional behavior changed
## 2026-02-21 - 3.43.2 - fix(dees-chart-log)
avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs
- Prevent property-bound logEntries from duplicating entries already present in logBuffer by deduplicating on timestamp|message
- Call updateMetrics() when replaying or appending log entries so metrics stay accurate
- Skip processing entirely when the incoming logEntries array is unchanged
- Optimize append-only updates by writing only the new tail entries instead of full re-render
- Enforce maxEntries when appending to the logBuffer to maintain buffer size
- Respect filterMode and searchQuery when deciding whether to write appended entries to the terminal
## 2026-02-21 - 3.43.1 - fix(dees-chart-log)
replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs
- Replay entries stored in logBuffer after terminal initialization to avoid losing entries that arrived early
- Add updated() lifecycle hook to copy logEntries into logBuffer and call reRenderFilteredLogs when terminalReady
- Call super.updated(changedProperties) to preserve base class behavior
## 2026-02-17 - 3.43.0 - feat(dees-form)
add layout styles to dees-form and standardize demo input grouping
- Add static CSS to dees-form: default column layout with gap and support for [horizontal-layout] (row wrapping, alignment and gap).
- Remove inline <style> from dees-form render to centralize styling.
- Simplify dees-input-base styles by removing host margins and making spacing container-driven.
- Update multiple demo files to wrap related inputs in a new .input-group container and include .input-group CSS for consistent vertical spacing.
## 2026-02-16 - 3.42.2 - fix(dees-chart-area)
add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data
- Introduces ApexAxisChartSeries type alias with support for number arrays, [x,y] tuples, and object {x,y,...} series entries
- Type-only change — no runtime or API behavior modified
- File changed: ts_web/elements/00group-chart/dees-chart-area/component.ts
## 2026-02-16 - 3.42.1 - fix(dees-table)
Guard against undefined action.type in dees-table by using optional chaining and update several dependencies
- Use optional chaining (action.type?.includes(...)) in ts_web/elements/.../dees-table.ts to prevent runtime errors when action.type is undefined
- Bump dependency apexcharts from ^5.3.6 to ^5.5.0
- Bump dependency lucide from ^0.563.0 to ^0.564.0
- Bump devDependency @git.zone/tswatch from ^3.0.1 to ^3.1.0
- Bump devDependency @types/node from ^25.0.10 to ^25.2.3
## 2026-02-02 - 3.42.0 - feat(dees-form-submit)
forward button properties to internal dees-button, use property bindings, add demo and styles
- Added forwarded properties: type, size, icon, iconPosition (with defaults) and preserved text/status/disabled
- Changed template to use property bindings (.prop) for dees-button instead of string attributes
- Switched internal event handler to listen for dees-button's @clicked event (was @click)
- Added component styles (:host display and dees-button width:100%) and improved layout
- Expanded demo with multiple usage examples (basic, icons, types, sizes, states, and a form context)
## 2026-02-02 - 3.41.6 - fix(dees-simple-appdash)
respect selectedView when loading initial view, falling back to the first tab
- firstUpdated now loads this.selectedView if set, otherwise loads the first view tab
- Prevents always loading the first tab and preserves a previously selected view on initial render
## 2026-02-01 - 3.41.5 - fix(dees-service-lib-loader)
prevent horizontal scrollbar by offsetting xterm WidthCache measurement container
- Injects additional CSS into DeesServiceLibLoader to move xterm.js WidthCache measurement div off-screen horizontally (selector: body > div[style*="top: -50000px"][style*="width: 50000px"])
- Fixes root cause where xterm creates a large-width measurement container (width: 50000px) on document.body that expands scrollWidth and causes a horizontal scrollbar
- Change applied in ts_web/services/DeesServiceLibLoader.ts by concatenating the fix CSS into the injected stylesheet
## 2026-01-29 - 3.41.4 - fix()
no changes
- No files changed in this commit; no code or documentation modified
- No release required
## 2026-01-29 - 3.41.3 - fix(dees-pdf-viewer)
use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling
- Download and Print now use pdfDocument.getData() to create Blob URLs so in-memory PDFs (pdf.js) can be saved/printed.
- Print flow now opens an HTML wrapper with an iframe to allow onafterprint handling, auto-close, popup-fallback and timed cleanup of Blob URLs.
- Added try/catch logging, URL.revokeObjectURL calls and safety timeouts to avoid resource leaks.
- Removed context menu items that relied on the raw PDF URL (Open in New Tab, Copy PDF URL); Download/Print actions now await the async handlers.
## 2026-01-28 - 3.41.2 - fix(dees-pdf-viewer)
account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays
- Multiply canvas.width and canvas.height by window.devicePixelRatio (dpr) and use Math.floor to set the actual pixel buffer size
- Call ctx.scale(dpr, dpr) so drawing is rendered at device pixels while keeping CSS display size unchanged
- Apply the same high-DPI adjustments to both main page rendering and thumbnail generation
- Keep canvas.style.width and canvas.style.height set to viewport dimensions to preserve layout
## 2026-01-27 - 3.41.1 - fix(dataview-codebox)
fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies
- Updated CSS in ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts: added height:100%, box-sizing, display:flex and flex-direction:column on container, set flex-shrink on header elements, made code grid overflow:auto with flex:1 and min-height:0 to prevent overflow issues.
- Bumped dependencies in package.json: @design.estate/dees-domtools from ^2.3.7 to ^2.3.8 and @design.estate/dees-element from ^2.1.5 to ^2.1.6.
- Non-breaking visual/layout fix — suitable for a patch release.
## 2026-01-27 - 3.41.0 - feat(docs)
document new media & tile components and expand Workspace/IDE component docs; update component count to 90+

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.41.0",
"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,13 +16,13 @@
"author": "Lossless GmbH",
"license": "MIT",
"dependencies": {
"@design.estate/dees-domtools": "^2.3.7",
"@design.estate/dees-element": "^2.1.5",
"@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.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-regular-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.1.0",
@@ -34,22 +34,22 @@
"@tiptap/extension-underline": "^2.23.0",
"@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.3.0",
"apexcharts": "^5.3.6",
"apexcharts": "^5.10.3",
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",
"lucide": "^0.563.0",
"lucide": "^0.577.0",
"monaco-editor": "0.55.1",
"pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.1.2",
"@git.zone/tsbundle": "^2.8.3",
"@git.zone/tstest": "^3.1.8",
"@git.zone/tswatch": "^3.0.1",
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1",
"@git.zone/tstest": "^3.3.2",
"@git.zone/tswatch": "^3.3.0",
"@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.10"
"@types/node": "^25.4.0"
},
"files": [
"ts/**/*",

4848
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,6 +143,12 @@ export class DeesAppui extends DeesElement {
@property({ type: Object })
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
@property({ type: Array })
accessor contentTabActionsLeft: interfaces.ITabAction[] = [];
@property({ type: Array })
accessor contentTabActionsRight: interfaces.ITabAction[] = [];
// References to child components
@state()
accessor appbar: DeesAppuiBar | undefined = undefined;
@@ -213,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 {
@@ -306,6 +314,8 @@ export class DeesAppui extends DeesElement {
.showTabs=${this.maincontentTabsVisible}
.tabsAutoHide=${this.contentTabsAutoHide}
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
.tabActionsLeft=${this.contentTabActionsLeft}
.tabActionsRight=${this.contentTabActionsRight}
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
>
@@ -699,6 +709,20 @@ export class DeesAppui extends DeesElement {
return this.maincontentSelectedTab;
}
/**
* Set content tab action buttons on the left side
*/
public setContentTabActionsLeft(actions: interfaces.ITabAction[]): void {
this.contentTabActionsLeft = [...actions];
}
/**
* Set content tab action buttons on the right side
*/
public setContentTabActionsRight(actions: interfaces.ITabAction[]): void {
this.contentTabActionsRight = [...actions];
}
// ==========================================
// PROGRAMMATIC API: ACTIVITY LOG
// ==========================================
@@ -853,8 +877,13 @@ export class DeesAppui extends DeesElement {
try {
await this.loadView(view, params);
// Update URL hash
const route = view.route || viewId;
// Update URL hash (substitute params into route pattern)
let route = view.route || viewId;
if (params) {
for (const [key, val] of Object.entries(params)) {
route = route.replace(`:${key}`, val);
}
}
const newHash = `#${route}`;
if (window.location.hash !== newHash) {
window.history.pushState({ viewId }, '', newHash);

View File

@@ -12,6 +12,16 @@ import { chartAreaStyles } from './styles.js';
import { renderChartArea } from './template.js';
import type ApexCharts from 'apexcharts';
type ApexAxisChartSeries = {
name?: string;
type?: string;
color?: string;
group?: string;
hidden?: boolean;
zIndex?: number;
data: (number | null)[] | { x: any; y: any; [key: string]: any }[] | [number, number | null][] | number[][];
}[];
import { DeesServiceLibLoader } from '../../../services/index.js';
declare global {

View File

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

View File

@@ -52,6 +52,8 @@ export class DeesDataviewCodebox extends DeesElement {
text-align: left;
font-size: 16px;
font-family: ${cssGeistFontFamily};
height: 100%;
box-sizing: border-box;
}
.mainbox {
position: relative;
@@ -61,6 +63,10 @@ export class DeesDataviewCodebox extends DeesElement {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border-radius: 6px;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
.appbar {
@@ -74,6 +80,7 @@ export class DeesDataviewCodebox extends DeesElement {
line-height: 32px;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.appbar .fileName {
@@ -95,6 +102,7 @@ export class DeesDataviewCodebox extends DeesElement {
justify-content: flex-end;
align-items: stretch;
overflow: hidden;
flex-shrink: 0;
}
.spacesLabel {
@@ -121,7 +129,9 @@ export class DeesDataviewCodebox extends DeesElement {
.codegrid {
display: grid;
grid-template-columns: 50px auto;
overflow: hidden;
overflow: auto;
flex: 1;
min-height: 0;
}
.lineNumbers {

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

@@ -232,7 +232,7 @@ export class DeesTable<T> extends DeesElement {
${directives.resolveExec(async () => {
const resultArray: TemplateResult[] = [];
for (const action of this.dataActions) {
if (!action.type.includes('header')) continue;
if (!action.type?.includes('header')) continue;
resultArray.push(
html`<div
class="headerAction"
@@ -450,7 +450,7 @@ export class DeesTable<T> extends DeesElement {
<td
@dblclick=${(e: Event) => {
const dblAction = this.dataActions.find((actionArg) =>
actionArg.type.includes('doubleClick')
actionArg.type?.includes('doubleClick')
);
if (this.editableFields.includes(editKey)) {
this.handleCellEditing(e, itemArg, editKey);
@@ -506,7 +506,7 @@ export class DeesTable<T> extends DeesElement {
${directives.resolveExec(async () => {
const resultArray: TemplateResult[] = [];
for (const action of this.dataActions) {
if (!action.type.includes('footer')) continue;
if (!action.type?.includes('footer')) continue;
resultArray.push(
html`<div
class="footerAction"
@@ -540,11 +540,11 @@ export class DeesTable<T> extends DeesElement {
super.updated(changedProperties);
this.determineColumnWidths();
if (this.searchable) {
const existing = this.dataActions.find((actionArg) => actionArg.type.includes('header') && actionArg.name === 'Search');
const existing = this.dataActions.find((actionArg) => actionArg.type?.includes('header') && actionArg.name === 'Search');
if (!existing) {
this.dataActions.unshift({
name: 'Search',
iconName: 'magnifyingGlass',
iconName: 'lucide:Search',
type: ['header'],
actionFunc: async () => {
console.log('open search');
@@ -623,7 +623,7 @@ export class DeesTable<T> extends DeesElement {
const width = window.getComputedStyle(cell).width;
if (cell.textContent.includes('Actions')) {
const neededWidth =
this.dataActions.filter((actionArg) => actionArg.type.includes('inRow')).length * 36;
this.dataActions.filter((actionArg) => actionArg.type?.includes('inRow')).length * 36;
cell.style.width = `${Math.max(neededWidth, 68)}px`;
} else {
cell.style.width = width;
@@ -795,7 +795,7 @@ export class DeesTable<T> extends DeesElement {
getActionsForType(typeArg: ITableAction['type'][0]) {
const actions: ITableAction[] = [];
for (const action of this.dataActions) {
if (!action.type.includes(typeArg)) continue;
if (!action.type?.includes(typeArg)) continue;
actions.push(action);
}
return actions;

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

@@ -1,3 +1,85 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`<dees-form-submit>Submit Form</dees-form-submit>`;
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
}
.demo-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.demo-section h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 500;
color: #888;
}
.demo-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Usage</h3>
<div class="demo-row">
<dees-form-submit>Submit Form</dees-form-submit>
<dees-form-submit text="With Text Property"></dees-form-submit>
</div>
</div>
<div class="demo-section">
<h3>With Icons (inherited from DeesButton)</h3>
<div class="demo-row">
<dees-form-submit icon="lucide:send">Submit</dees-form-submit>
<dees-form-submit icon="lucide:save" iconPosition="left">Save Form</dees-form-submit>
<dees-form-submit icon="lucide:arrow-right" iconPosition="right">Continue</dees-form-submit>
</div>
</div>
<div class="demo-section">
<h3>Button Types</h3>
<div class="demo-row">
<dees-form-submit type="highlighted" icon="lucide:send">Highlighted</dees-form-submit>
<dees-form-submit type="normal" icon="lucide:send">Normal</dees-form-submit>
<dees-form-submit type="discreet" icon="lucide:send">Discreet</dees-form-submit>
</div>
</div>
<div class="demo-section">
<h3>Sizes</h3>
<div class="demo-row">
<dees-form-submit size="small" icon="lucide:send">Small</dees-form-submit>
<dees-form-submit size="normal" icon="lucide:send">Normal</dees-form-submit>
<dees-form-submit size="large" icon="lucide:send">Large</dees-form-submit>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<div class="demo-row">
<dees-form-submit status="normal" icon="lucide:send">Normal</dees-form-submit>
<dees-form-submit status="pending" icon="lucide:send">Pending</dees-form-submit>
<dees-form-submit status="success" icon="lucide:check">Success</dees-form-submit>
<dees-form-submit status="error" icon="lucide:x">Error</dees-form-submit>
<dees-form-submit disabled icon="lucide:send">Disabled</dees-form-submit>
</div>
</div>
<div class="demo-section">
<h3>In a Form Context</h3>
<dees-form>
<dees-input-text label="Name" key="name"></dees-input-text>
<dees-input-text label="Email" key="email"></dees-input-text>
<dees-form-submit icon="lucide:send" type="highlighted">Submit Form</dees-form-submit>
</dees-form>
</div>
</div>
`;

View File

@@ -6,6 +6,7 @@ import {
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import type { DeesForm } from '../dees-form/dees-form.js';
import { themeDefaultStyles } from '../../00theme.js';
@@ -21,38 +22,61 @@ export class DeesFormSubmit extends DeesElement {
public static demo = demoFunc;
public static demoGroups = ['Form', 'Button'];
@property({
type: Boolean,
reflect: true,
})
// =============================================
// Properties forwarded to internal dees-button
// =============================================
@property({ type: Boolean, reflect: true })
accessor disabled = false;
@property({
type: String,
})
@property({ type: String })
accessor text: string;
@property({
type: String,
})
@property({ type: String })
accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal';
@property({ type: String, reflect: true })
accessor type: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default';
@property({ type: String, reflect: true })
accessor size: 'sm' | 'default' | 'lg' | 'icon' | 'small' | 'normal' | 'large' = 'default';
@property({ type: String })
accessor icon: string;
@property({ type: String })
accessor iconPosition: 'left' | 'right' = 'left';
constructor() {
super();
}
public static styles = [themeDefaultStyles, cssManager.defaultStyles, css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
`];
public render() {
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: inline-block;
}
dees-button {
width: 100%;
}
`,
];
public render(): TemplateResult {
return html`
<dees-button
status="${this.status}"
@click="${this.submit}"
?disabled="${this.disabled}"
.status=${this.status}
.type=${this.type}
.size=${this.size}
.icon=${this.icon}
.iconPosition=${this.iconPosition}
.text=${this.text}
?disabled=${this.disabled}
@clicked=${this.submit}
>
${this.text || html`<slot></slot>`}
<slot></slot>
</dees-button>
`;
}

View File

@@ -1,6 +1,7 @@
import {
customElement,
html,
css,
type TemplateResult,
DeesElement,
type CSSResult,
@@ -21,6 +22,10 @@ import { DeesInputMultitoggle } from '../../00group-input/dees-input-multitoggle
import { DeesInputPhone } from '../../00group-input/dees-input-phone/dees-input-phone.js';
import { DeesInputToggle } from '../../00group-input/dees-input-toggle/dees-input-toggle.js';
import { DeesInputTypelist } from '../../00group-input/dees-input-typelist/dees-input-typelist.js';
import { DeesInputTags } from '../../00group-input/dees-input-tags/dees-input-tags.js';
import { DeesInputList } from '../../00group-input/dees-input-list/dees-input-list.js';
import { DeesInputWysiwyg } from '../../00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
import { DeesInputRichtext } from '../../00group-input/dees-input-richtext/component.js';
import { DeesFormSubmit } from '../dees-form-submit/dees-form-submit.js';
import { DeesTable } from '../../00group-dataview/dees-table/index.js';
import { demoFunc } from './dees-form.demo.js';
@@ -40,6 +45,10 @@ const FORM_INPUT_TYPES = [
DeesInputText,
DeesInputToggle,
DeesInputTypelist,
DeesInputTags,
DeesInputList,
DeesInputWysiwyg,
DeesInputRichtext,
DeesTable,
];
@@ -57,6 +66,10 @@ export type TFormInputElement =
| DeesInputText
| DeesInputToggle
| DeesInputTypelist
| DeesInputTags
| DeesInputList
| DeesInputWysiwyg
| DeesInputRichtext
| DeesTable<any>;
declare global {
@@ -81,13 +94,25 @@ export class DeesForm extends DeesElement {
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
accessor horizontalLayout: boolean = false;
public static styles = [
css`
:host {
display: flex;
flex-direction: column;
gap: 16px;
}
:host([horizontal-layout]) {
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
gap: 16px;
}
`,
];
public render(): TemplateResult {
return html`
<style>
:host {
display: contents;
}
</style>
<slot></slot>
`;
}

View File

@@ -54,37 +54,20 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
/* CSS Variables for consistent spacing */
:host {
--dees-input-spacing-unit: 8px;
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
}
/* Default vertical stacking mode (for forms) */
/* Default block display with no margins - spacing is container-driven */
:host {
display: block;
margin: 0;
margin-bottom: var(--dees-input-vertical-gap);
}
/* Last child in container should have no bottom margin */
:host(:last-child) {
margin-bottom: 0;
}
/* Horizontal layout mode - activated by attribute */
:host([layout-mode="horizontal"]) {
display: inline-block;
margin: 0;
margin-right: var(--dees-input-horizontal-gap);
margin-bottom: 0;
}
:host([layout-mode="horizontal"]:last-child) {
margin-right: 0;
}
/* Auto mode - inherit from parent dees-form if present */
/* Label position variations */
:host([label-position="left"]) .input-wrapper {
display: grid;

View File

@@ -31,6 +31,12 @@ export const demoFunc = () => html`
flex-wrap: wrap;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.spacer {
height: 200px;
display: flex;
@@ -63,30 +69,32 @@ export const demoFunc = () => html`
}
}}>
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
<dees-input-dropdown
.label=${'Select Country'}
.options=${[
{ option: 'United States', key: 'us' },
{ option: 'Canada', key: 'ca' },
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
{ option: 'Australia', key: 'au' },
{ option: 'Japan', key: 'jp' },
{ option: 'Brazil', key: 'br' }
]}
.selectedOption=${{ option: 'United States', key: 'us' }}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Select Role'}
.options=${[
{ option: 'Administrator', key: 'admin' },
{ option: 'Editor', key: 'editor' },
{ option: 'Viewer', key: 'viewer' },
{ option: 'Guest', key: 'guest' }
]}
></dees-input-dropdown>
<div class="input-group">
<dees-input-dropdown
.label=${'Select Country'}
.options=${[
{ option: 'United States', key: 'us' },
{ option: 'Canada', key: 'ca' },
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
{ option: 'Australia', key: 'au' },
{ option: 'Japan', key: 'jp' },
{ option: 'Brazil', key: 'br' }
]}
.selectedOption=${{ option: 'United States', key: 'us' }}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Select Role'}
.options=${[
{ option: 'Administrator', key: 'admin' },
{ option: 'Editor', key: 'editor' },
{ option: 'Viewer', key: 'viewer' },
{ option: 'Guest', key: 'guest' }
]}
></dees-input-dropdown>
</div>
</dees-panel>
</dees-demowrapper>
@@ -176,24 +184,26 @@ export const demoFunc = () => html`
}
}}>
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
<dees-input-dropdown
.label=${'Required Field'}
.required=${true}
.options=${[
{ option: 'Option A', key: 'a' },
{ option: 'Option B', key: 'b' },
{ option: 'Option C', key: 'c' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Disabled Dropdown'}
.disabled=${true}
.options=${[
{ option: 'Cannot Select', key: 'disabled' }
]}
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
></dees-input-dropdown>
<div class="input-group">
<dees-input-dropdown
.label=${'Required Field'}
.required=${true}
.options=${[
{ option: 'Option A', key: 'a' },
{ option: 'Option B', key: 'b' },
{ option: 'Option C', key: 'c' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Disabled Dropdown'}
.disabled=${true}
.options=${[
{ option: 'Cannot Select', key: 'disabled' }
]}
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
></dees-input-dropdown>
</div>
</dees-panel>
</dees-demowrapper>

View File

@@ -13,6 +13,12 @@ export const demoFunc = () => html`
margin: 0 auto;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.payment-group {
display: flex;
align-items: center;
@@ -24,16 +30,18 @@ export const demoFunc = () => html`
<div class="demo-container">
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
<dees-input-iban
.label=${'Bank Account IBAN'}
.description=${'Enter your International Bank Account Number'}
></dees-input-iban>
<dees-input-iban
.label=${'Verified IBAN'}
.description=${'This IBAN has been verified'}
.value=${'DE89370400440532013000'}
></dees-input-iban>
<div class="input-group">
<dees-input-iban
.label=${'Bank Account IBAN'}
.description=${'Enter your International Bank Account Number'}
></dees-input-iban>
<dees-input-iban
.label=${'Verified IBAN'}
.description=${'This IBAN has been verified'}
.value=${'DE89370400440532013000'}
></dees-input-iban>
</div>
</dees-panel>
<dees-panel .title=${'Payment Information'} .subtitle=${'IBAN input with horizontal layout for payment forms'}>
@@ -53,18 +61,20 @@ export const demoFunc = () => html`
</dees-panel>
<dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}>
<dees-input-iban
.label=${'Payment Account'}
.description=${'Required for processing payments'}
.required=${true}
></dees-input-iban>
<dees-input-iban
.label=${'Locked IBAN'}
.description=${'This IBAN cannot be changed'}
.value=${'FR1420041010050500013M02606'}
.disabled=${true}
></dees-input-iban>
<div class="input-group">
<dees-input-iban
.label=${'Payment Account'}
.description=${'Required for processing payments'}
.required=${true}
></dees-input-iban>
<dees-input-iban
.label=${'Locked IBAN'}
.description=${'This IBAN cannot be changed'}
.value=${'FR1420041010050500013M02606'}
.disabled=${true}
></dees-input-iban>
</div>
</dees-panel>
<dees-panel .title=${'Bank Transfer Form'} .subtitle=${'Complete form example with IBAN validation'}>

View File

@@ -13,6 +13,12 @@ export const demoFunc = () => html`
margin: 0 auto;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.horizontal-group {
display: flex;
align-items: center;
@@ -24,18 +30,20 @@ export const demoFunc = () => html`
<div class="demo-container">
<dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}>
<dees-input-phone
.label=${'Phone Number'}
.description=${'Enter your phone number with country code'}
.value=${'5551234567'}
></dees-input-phone>
<dees-input-phone
.label=${'Contact Phone'}
.description=${'Required for account verification'}
.required=${true}
.placeholder=${'+1 (555) 000-0000'}
></dees-input-phone>
<div class="input-group">
<dees-input-phone
.label=${'Phone Number'}
.description=${'Enter your phone number with country code'}
.value=${'5551234567'}
></dees-input-phone>
<dees-input-phone
.label=${'Contact Phone'}
.description=${'Required for account verification'}
.required=${true}
.placeholder=${'+1 (555) 000-0000'}
></dees-input-phone>
</div>
</dees-panel>
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}>
@@ -55,17 +63,19 @@ export const demoFunc = () => html`
</dees-panel>
<dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}>
<dees-input-phone
.label=${'International Contact'}
.description=${'Automatically formats international numbers'}
.value=${'441234567890'}
></dees-input-phone>
<dees-input-phone
.label=${'Emergency Contact'}
.value=${'911'}
.disabled=${true}
></dees-input-phone>
<div class="input-group">
<dees-input-phone
.label=${'International Contact'}
.description=${'Automatically formats international numbers'}
.value=${'441234567890'}
></dees-input-phone>
<dees-input-phone
.label=${'Emergency Contact'}
.value=${'911'}
.disabled=${true}
></dees-input-phone>
</div>
</dees-panel>
<dees-panel .title=${'Form Integration'} .subtitle=${'Phone input as part of a contact form'}>

View File

@@ -14,6 +14,12 @@ export const demoFunc = () => html`
margin: 0 auto;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.shopping-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -60,17 +66,19 @@ export const demoFunc = () => html`
<div class="demo-container">
<dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}>
<dees-input-quantityselector
.label=${'Quantity'}
.description=${'Select the desired quantity'}
.value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Items in Cart'}
.description=${'Adjust the quantity of items'}
.value=${3}
></dees-input-quantityselector>
<div class="input-group">
<dees-input-quantityselector
.label=${'Quantity'}
.description=${'Select the desired quantity'}
.value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Items in Cart'}
.description=${'Adjust the quantity of items'}
.value=${3}
></dees-input-quantityselector>
</div>
</dees-panel>
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Modern e-commerce product cards with interactive quantity selectors'} .runAfterRender=${async (elementArg: HTMLElement) => {
@@ -169,19 +177,21 @@ export const demoFunc = () => html`
</dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}>
<dees-input-quantityselector
.label=${'Number of Licenses'}
.description=${'Select how many licenses you need'}
.required=${true}
.value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Fixed Quantity'}
.description=${'This quantity cannot be changed'}
.disabled=${true}
.value=${5}
></dees-input-quantityselector>
<div class="input-group">
<dees-input-quantityselector
.label=${'Number of Licenses'}
.description=${'Select how many licenses you need'}
.required=${true}
.value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Fixed Quantity'}
.description=${'This quantity cannot be changed'}
.disabled=${true}
.value=${5}
></dees-input-quantityselector>
</div>
</dees-panel>
<dees-panel .title=${'Order Form'} .subtitle=${'Complete order form with quantity selection'}>

View File

@@ -23,6 +23,12 @@ export const demoFunc = () => html`
margin-bottom: 0;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@@ -60,20 +66,22 @@ export const demoFunc = () => html`
</dees-panel>
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
<dees-input-radiogroup
.label=${'Do you agree with the terms?'}
.options=${['Yes', 'No', 'Maybe']}
.direction=${'horizontal'}
.selectedOption=${'Yes'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Experience Level'}
.options=${['Beginner', 'Intermediate', 'Expert']}
.direction=${'horizontal'}
.selectedOption=${'Intermediate'}
.description=${'Select your experience level with web development'}
></dees-input-radiogroup>
<div class="input-group">
<dees-input-radiogroup
.label=${'Do you agree with the terms?'}
.options=${['Yes', 'No', 'Maybe']}
.direction=${'horizontal'}
.selectedOption=${'Yes'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Experience Level'}
.options=${['Beginner', 'Intermediate', 'Expert']}
.direction=${'horizontal'}
.selectedOption=${'Intermediate'}
.description=${'Select your experience level with web development'}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}>
@@ -132,30 +140,32 @@ export const demoFunc = () => html`
</dees-panel>
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
<dees-input-radiogroup
.label=${'Theme Preference'}
.options=${[
{ option: 'Light Theme', key: 'light', payload: 'light' },
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
{ option: 'System Default', key: 'system', payload: 'auto' }
]}
.selectedOption=${'dark'}
.description=${'Choose how the application should appear'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Notification Frequency'}
.options=${['All Notifications', 'Important Only', 'None']}
.selectedOption=${'Important Only'}
.description=${'Control how often you receive notifications'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Language'}
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
.selectedOption=${'English'}
.direction=${'horizontal'}
></dees-input-radiogroup>
<div class="input-group">
<dees-input-radiogroup
.label=${'Theme Preference'}
.options=${[
{ option: 'Light Theme', key: 'light', payload: 'light' },
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
{ option: 'System Default', key: 'system', payload: 'auto' }
]}
.selectedOption=${'dark'}
.description=${'Choose how the application should appear'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Notification Frequency'}
.options=${['All Notifications', 'Important Only', 'None']}
.selectedOption=${'Important Only'}
.description=${'Control how often you receive notifications'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Language'}
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
.selectedOption=${'English'}
.direction=${'horizontal'}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>

View File

@@ -269,6 +269,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
onUpdate: ({ editor }) => {
this.value = editor.getHTML();
this.updateWordCount();
this.changeSubject.next(this.value);
this.dispatchEvent(
new CustomEvent('input', {
detail: { value: this.value },

View File

@@ -30,6 +30,12 @@ export const demoFunc = () => html`
flex-wrap: wrap;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -83,25 +89,27 @@ export const demoFunc = () => html`
}
}}>
<dees-panel .title=${'Basic Text Inputs'} .subtitle=${'Standard text inputs with labels and descriptions'}>
<dees-input-text
.label=${'Username'}
.value=${'johndoe'}
.key=${'username'}
></dees-input-text>
<dees-input-text
.label=${'Email Address'}
.value=${'john@example.com'}
.description=${'We will never share your email with anyone'}
.key=${'email'}
></dees-input-text>
<dees-input-text
.label=${'Password'}
.isPasswordBool=${true}
.value=${'secret123'}
.key=${'password'}
></dees-input-text>
<div class="input-group">
<dees-input-text
.label=${'Username'}
.value=${'johndoe'}
.key=${'username'}
></dees-input-text>
<dees-input-text
.label=${'Email Address'}
.value=${'john@example.com'}
.description=${'We will never share your email with anyone'}
.key=${'email'}
></dees-input-text>
<dees-input-text
.label=${'Password'}
.isPasswordBool=${true}
.value=${'secret123'}
.key=${'password'}
></dees-input-text>
</div>
</dees-panel>
</dees-demowrapper>
@@ -172,31 +180,33 @@ export const demoFunc = () => html`
}
}}>
<dees-panel .title=${'Label Positions'} .subtitle=${'Different label positioning options for various layouts'}>
<dees-input-text
.label=${'Label on Top (Default)'}
.value=${'Standard layout'}
.labelPosition=${'top'}
></dees-input-text>
<dees-input-text
.label=${'Label on Left'}
.value=${'Inline label'}
.labelPosition=${'left'}
></dees-input-text>
<div class="grid-layout">
<div class="input-group">
<dees-input-text
.label=${'Label on Top (Default)'}
.value=${'Standard layout'}
.labelPosition=${'top'}
></dees-input-text>
<dees-input-text
.label=${'Label on Left'}
.value=${'Inline label'}
.labelPosition=${'left'}
></dees-input-text>
<div class="grid-layout">
<dees-input-text
.label=${'City'}
.value=${'New York'}
.labelPosition=${'left'}
></dees-input-text>
<dees-input-text
.label=${'ZIP Code'}
.value=${'10001'}
<dees-input-text
.label=${'ZIP Code'}
.value=${'10001'}
.labelPosition=${'left'}
></dees-input-text>
</div>
</div>
</dees-panel>
</dees-demowrapper>
@@ -234,24 +244,26 @@ export const demoFunc = () => html`
}
}}>
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
<dees-input-text
.label=${'Required Field'}
.required=${true}
.key=${'requiredField'}
></dees-input-text>
<dees-input-text
.label=${'Disabled Field'}
.value=${'Cannot edit this'}
.disabled=${true}
></dees-input-text>
<dees-input-text
.label=${'Field with Error'}
.value=${'invalid@'}
.validationText=${'Please enter a valid email address'}
.validationState=${'invalid'}
></dees-input-text>
<div class="input-group">
<dees-input-text
.label=${'Required Field'}
.required=${true}
.key=${'requiredField'}
></dees-input-text>
<dees-input-text
.label=${'Disabled Field'}
.value=${'Cannot edit this'}
.disabled=${true}
></dees-input-text>
<dees-input-text
.label=${'Field with Error'}
.value=${'invalid@'}
.validationText=${'Please enter a valid email address'}
.validationState=${'invalid'}
></dees-input-text>
</div>
</dees-panel>
</dees-demowrapper>
@@ -279,19 +291,21 @@ export const demoFunc = () => html`
});
}}>
<dees-panel .title=${'Advanced Features'} .subtitle=${'Password visibility toggle and other advanced features'}>
<dees-input-text
.label=${'Password with Toggle'}
.isPasswordBool=${true}
.value=${'mySecurePassword123'}
.description=${'Click the eye icon to show/hide password'}
></dees-input-text>
<dees-input-text
.label=${'API Key'}
.isPasswordBool=${true}
.value=${'sk-1234567890abcdef'}
.description=${'Keep this key secure and never share it'}
></dees-input-text>
<div class="input-group">
<dees-input-text
.label=${'Password with Toggle'}
.isPasswordBool=${true}
.value=${'mySecurePassword123'}
.description=${'Click the eye icon to show/hide password'}
></dees-input-text>
<dees-input-text
.label=${'API Key'}
.isPasswordBool=${true}
.value=${'sk-1234567890abcdef'}
.description=${'Keep this key secure and never share it'}
></dees-input-text>
</div>
</dees-panel>
</dees-demowrapper>

View File

@@ -13,6 +13,12 @@ export const demoFunc = () => html`
margin: 0 auto;
}
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.horizontal-group {
display: flex;
gap: 24px;
@@ -39,27 +45,30 @@ export const demoFunc = () => html`
<div class="demo-container">
<dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}>
<dees-input-typelist
.label=${'Tags'}
.description=${'Add tags by typing and pressing Enter'}
.value=${['javascript', 'typescript', 'web-components']}
></dees-input-typelist>
<dees-input-typelist
.label=${'Team Members'}
.description=${'Add email addresses of team members'}
.value=${['alice@example.com', 'bob@example.com']}
></dees-input-typelist>
<div class="input-group">
<dees-input-typelist
.label=${'Tags'}
.description=${'Add tags by typing and pressing Enter'}
.value=${['javascript', 'typescript', 'web-components']}
></dees-input-typelist>
<dees-input-typelist
.label=${'Team Members'}
.description=${'Add email addresses of team members'}
.value=${['alice@example.com', 'bob@example.com']}
></dees-input-typelist>
</div>
</dees-panel>
<dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}>
<dees-input-typelist
.label=${'Your Skills'}
.description=${'List your professional skills'}
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
></dees-input-typelist>
<div class="horizontal-group">
<div class="input-group">
<dees-input-typelist
.label=${'Your Skills'}
.description=${'List your professional skills'}
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
></dees-input-typelist>
<div class="horizontal-group">
<dees-input-typelist
.label=${'Categories'}
.layoutMode=${'horizontal'}
@@ -72,22 +81,25 @@ export const demoFunc = () => html`
.value=${['innovation', 'startup', 'growth']}
></dees-input-typelist>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}>
<dees-input-typelist
.label=${'Project Dependencies'}
.description=${'List all required npm packages'}
.required=${true}
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
></dees-input-typelist>
<dees-input-typelist
.label=${'System Tags'}
.description=${'These tags are managed by the system'}
.disabled=${true}
.value=${['system', 'protected', 'readonly']}
></dees-input-typelist>
<div class="input-group">
<dees-input-typelist
.label=${'Project Dependencies'}
.description=${'List all required npm packages'}
.required=${true}
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
></dees-input-typelist>
<dees-input-typelist
.label=${'System Tags'}
.description=${'These tags are managed by the system'}
.disabled=${true}
.value=${['system', 'protected', 'readonly']}
></dees-input-typelist>
</div>
</dees-panel>
<dees-panel .title=${'Article Publishing Form'} .subtitle=${'Complete form with tag management'}>

View File

@@ -1,24 +0,0 @@
import { customElement } from '@design.estate/dees-element';
import { DeesTilePdf } from '../dees-tile-pdf/component.js';
declare global {
interface HTMLElementTagNameMap {
'dees-pdf-preview': DeesPdfPreview;
}
}
/**
* @deprecated Use <dees-tile-pdf> instead. This component will be removed in a future release.
*/
@customElement('dees-pdf-preview')
export class DeesPdfPreview extends DeesTilePdf {
public static demoGroups: never[] = []; // Hide from demo catalog
public connectedCallback(): Promise<void> {
console.warn(
'[dees-pdf-preview] is deprecated. Use <dees-tile-pdf> instead. ' +
'This component will be removed in a future release.'
);
return super.connectedCallback();
}
}

View File

@@ -1,189 +0,0 @@
import { html } from '@design.estate/dees-element';
export const demo = () => {
const samplePdfs = [
'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf',
'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf',
];
const generateGridItems = (count: number) => {
const items = [];
for (let i = 0; i < count; i++) {
const pdfUrl = samplePdfs[i % samplePdfs.length];
items.push(html`
<dees-pdf-preview
pdfUrl="${pdfUrl}"
maxPages="3"
stackOffset="6"
clickable="true"
grid-mode
@pdf-preview-click=${(e: CustomEvent) => {
console.log('PDF Preview clicked:', e.detail);
alert(`PDF clicked: ${e.detail.pageCount} pages`);
}}
></dees-pdf-preview>
`);
}
return items;
};
return html`
<style>
.demo-container {
padding: 40px;
background: #f5f5f5;
}
.demo-section {
margin-bottom: 60px;
}
h3 {
margin-bottom: 20px;
font-size: 18px;
font-weight: 600;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 24px;
}
.preview-row {
display: flex;
gap: 24px;
align-items: center;
margin-bottom: 20px;
}
.preview-label {
font-size: 14px;
font-weight: 500;
min-width: 100px;
}
.performance-stats {
margin-top: 20px;
padding: 16px;
background: white;
border-radius: 8px;
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
margin-top: 12px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
.stat-value {
font-size: 16px;
font-weight: 600;
}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Single PDF Preview with Stacked Pages</h3>
<dees-pdf-preview
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"
maxPages="3"
stackOffset="8"
clickable="true"
></dees-pdf-preview>
</div>
<div class="demo-section">
<h3>Different Sizes</h3>
<div class="preview-row">
<div class="preview-label">Small:</div>
<dees-pdf-preview
size="small"
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
maxPages="2"
stackOffset="6"
clickable="true"
></dees-pdf-preview>
</div>
<div class="preview-row">
<div class="preview-label">Default:</div>
<dees-pdf-preview
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
maxPages="3"
stackOffset="8"
clickable="true"
></dees-pdf-preview>
</div>
<div class="preview-row">
<div class="preview-label">Large:</div>
<dees-pdf-preview
size="large"
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
maxPages="4"
stackOffset="10"
clickable="true"
></dees-pdf-preview>
</div>
</div>
<div class="demo-section">
<h3>Non-Clickable Preview</h3>
<dees-pdf-preview
pdfUrl="https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
maxPages="3"
stackOffset="8"
clickable="false"
></dees-pdf-preview>
</div>
<div class="demo-section">
<h3>Performance Grid - 50 PDFs with Lazy Loading</h3>
<p style="margin-bottom: 20px; font-size: 14px; color: #666;">
This grid demonstrates the performance optimizations with 50 PDF previews.
Scroll to see lazy loading in action - previews render only when visible.
</p>
<div class="preview-grid">
${generateGridItems(50)}
</div>
<div class="performance-stats">
<h4>Performance Features</h4>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Lazy Loading</span>
<span class="stat-value">✓ Enabled</span>
</div>
<div class="stat-item">
<span class="stat-label">Canvas Pooling</span>
<span class="stat-value">✓ Active</span>
</div>
<div class="stat-item">
<span class="stat-label">Memory Management</span>
<span class="stat-value">✓ Optimized</span>
</div>
<div class="stat-item">
<span class="stat-label">Intersection Observer</span>
<span class="stat-value">200px margin</span>
</div>
</div>
</div>
</div>
</div>
`;
};

View File

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

View File

@@ -1,223 +0,0 @@
import { css, cssManager } from '@design.estate/dees-element';
export const previewStyles = [
cssManager.defaultStyles,
css`
:host {
display: inline-block;
position: relative;
}
.preview-container {
position: relative;
width: 200px;
height: 260px;
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;
box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')};
}
.preview-container.clickable {
cursor: pointer;
}
.preview-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)')};
}
.preview-container.clickable:hover .preview-overlay {
opacity: 1;
}
.preview-stack {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
}
.preview-stack.non-a4 {
padding: 12px;
}
.preview-canvas {
position: relative;
background: white;
display: block;
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
image-rendering: auto;
-webkit-font-smoothing: antialiased;
box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.3)')};
}
.non-a4 .preview-canvas {
border: 1px solid ${cssManager.bdTheme('hsl(214 31% 92%)', 'hsl(217 25% 24%)')};
border-radius: 4px;
}
.preview-info {
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;
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
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;
}
.preview-info dees-icon {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
}
.preview-pages {
font-weight: 500;
font-size: 11px;
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.7)', 'rgba(0, 0, 0, 0.8)')};
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 20;
}
.preview-overlay dees-icon {
font-size: 24px;
color: white;
}
.preview-overlay span {
font-size: 14px;
font-weight: 500;
color: white;
}
.preview-loading,
.preview-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')};
}
.preview-loading {
background: ${cssManager.bdTheme('hsl(0 0% 99%)', 'hsl(215 20% 14%)')};
}
.preview-error {
background: ${cssManager.bdTheme('hsl(0 72% 98%)', 'hsl(0 62% 20%)')};
color: ${cssManager.bdTheme('hsl(0 72% 40%)', 'hsl(0 70% 68%)')};
}
.preview-spinner {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('hsl(214 31% 86%)', 'hsl(217 25% 28%)')};
border-top-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.preview-text {
font-size: 13px;
font-weight: 500;
}
.preview-error dees-icon {
font-size: 32px;
}
.preview-page-indicator {
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;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive sizes */
:host([size="small"]) .preview-container {
width: 150px;
height: 195px;
}
:host([size="large"]) .preview-container {
width: 250px;
height: 325px;
}
/* Grid optimizations */
:host([grid-mode]) .preview-container {
will-change: auto;
}
:host([grid-mode]) .preview-canvas {
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
`,
];

View File

@@ -52,7 +52,7 @@ export class DeesPdfViewer extends DeesElement {
accessor thumbnailData: Array<{page: number, rendered: boolean}> = [];
@property({ type: Array })
accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean}> = [];
accessor pageData: Array<{page: number, rendered: boolean, rendering: boolean, textLayerRendered: boolean}> = [];
private pdfDocument: any;
private renderState: RenderState = 'idle';
@@ -63,6 +63,7 @@ export class DeesPdfViewer extends DeesElement {
private currentRenderPromise: Promise<void> | null = null;
private thumbnailRenderTasks: any[] = [];
private pageRenderTasks: Map<number, any> = new Map();
private textLayerRenderTasks: Map<number, any> = new Map();
private canvas: HTMLCanvasElement | undefined;
private ctx: CanvasRenderingContext2D | undefined;
private viewerMain: HTMLElement | null = null;
@@ -230,6 +231,7 @@ export class DeesPdfViewer extends DeesElement {
<div class="page-wrapper" data-page="${item.page}">
<div class="canvas-container">
<canvas class="page-canvas" data-page="${item.page}"></canvas>
<div class="text-layer" data-page="${item.page}"></div>
</div>
</div>
`
@@ -330,7 +332,8 @@ export class DeesPdfViewer extends DeesElement {
this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
page: i + 1,
rendered: false,
rendering: false
rendering: false,
textLayerRendered: false,
}));
// Set loading to false to render the pages
@@ -444,9 +447,10 @@ export class DeesPdfViewer extends DeesElement {
const page = await this.pdfDocument.getPage(pageNum);
const viewport = this.computeViewport(page);
// Set canvas dimensions
canvas.height = viewport.height;
canvas.width = viewport.width;
// Set canvas dimensions with device pixel ratio for sharp rendering
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * dpr);
canvas.height = Math.floor(viewport.height * dpr);
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
@@ -457,6 +461,9 @@ export class DeesPdfViewer extends DeesElement {
return;
}
// Scale context for high-DPI displays
ctx.scale(dpr, dpr);
const renderContext = {
canvasContext: ctx,
viewport: viewport,
@@ -472,6 +479,9 @@ export class DeesPdfViewer extends DeesElement {
pageInfo.rendering = false;
this.pageRenderTasks.delete(pageNum);
// Render text layer for selection
await this.renderTextLayer(pageNum);
// Update page data to reflect rendered state
this.requestUpdate('pageData');
} catch (error: any) {
@@ -483,6 +493,132 @@ export class DeesPdfViewer extends DeesElement {
}
}
private async renderTextLayer(pageNum: number): Promise<void> {
const pageInfo = this.pageData.find(p => p.page === pageNum);
if (!pageInfo || pageInfo.textLayerRendered) return;
try {
const textLayerDiv = this.shadowRoot?.querySelector(
`.text-layer[data-page="${pageNum}"]`
) as HTMLElement;
if (!textLayerDiv) return;
textLayerDiv.innerHTML = '';
const page = await this.pdfDocument.getPage(pageNum);
const textContent = await page.getTextContent();
const viewport = this.computeViewport(page);
// @ts-ignore - Dynamic import of pdfjs
const pdfjs = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm');
textLayerDiv.style.width = `${viewport.width}px`;
textLayerDiv.style.height = `${viewport.height}px`;
// Set the scale factor CSS variable - required by PDF.js text layer
textLayerDiv.style.setProperty('--scale-factor', String(viewport.scale));
const textLayerRenderTask = pdfjs.renderTextLayer({
textContentSource: textContent,
container: textLayerDiv,
viewport: viewport,
});
this.textLayerRenderTasks.set(pageNum, textLayerRenderTask);
await textLayerRenderTask.promise;
// Add endOfContent for selection boundary
const endOfContent = document.createElement('div');
endOfContent.className = 'endOfContent';
textLayerDiv.appendChild(endOfContent);
// Custom drag selection for Shadow DOM compatibility
// caretRangeFromPoint doesn't pierce shadow DOM, so we find spans manually
let isDragging = false;
let anchorNode: Node | null = null;
let anchorOffset = 0;
const getTextPositionFromPoint = (x: number, y: number): { node: Node; offset: number } | null => {
// Find span at coordinates by checking bounding rects
const spans = Array.from(textLayerDiv.querySelectorAll('span'));
for (const span of spans) {
const rect = span.getBoundingClientRect();
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
const textNode = span.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
// Calculate character offset based on x position
const text = textNode.textContent || '';
const charWidth = rect.width / text.length;
const relativeX = x - rect.left;
const offset = Math.min(Math.round(relativeX / charWidth), text.length);
return { node: textNode, offset };
}
}
}
return null;
};
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
anchorNode = null;
textLayerDiv.classList.remove('selecting');
}
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging || !anchorNode) return;
e.preventDefault();
const pos = getTextPositionFromPoint(e.clientX, e.clientY);
if (pos) {
const selection = window.getSelection();
if (selection) {
try {
selection.setBaseAndExtent(anchorNode, anchorOffset, pos.node, pos.offset);
} catch (err) {
// Ignore errors from invalid selections
}
}
}
};
textLayerDiv.addEventListener('mousedown', (e: MouseEvent) => {
if (e.button !== 0) return;
const pos = getTextPositionFromPoint(e.clientX, e.clientY);
if (pos) {
// Prevent native selection behavior
e.preventDefault();
isDragging = true;
anchorNode = pos.node;
anchorOffset = pos.offset;
textLayerDiv.classList.add('selecting');
// Clear existing selection
const selection = window.getSelection();
selection?.removeAllRanges();
// Add document-level listeners for drag
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
});
pageInfo.textLayerRendered = true;
page.cleanup?.();
this.textLayerRenderTasks.delete(pageNum);
} catch (error: any) {
if (error?.name !== 'RenderingCancelledException') {
console.error(`Error rendering text layer for page ${pageNum}:`, error);
}
this.textLayerRenderTasks.delete(pageNum);
}
}
private handleScroll = () => {
// Throttle scroll events
if (this.scrollThrottleTimeout) {
@@ -652,9 +788,10 @@ export class DeesPdfViewer extends DeesElement {
const scale = maxThumbnailWidth / initialViewport.width;
const viewport = page.getViewport({ scale });
// Set canvas dimensions to actual render size
canvas.width = viewport.width;
canvas.height = viewport.height;
// Set canvas dimensions with device pixel ratio for sharp thumbnails
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * dpr);
canvas.height = Math.floor(viewport.height * dpr);
// Set the display size via style to ensure proper display
canvas.style.width = `${viewport.width}px`;
@@ -670,6 +807,9 @@ export class DeesPdfViewer extends DeesElement {
continue;
}
// Scale context for high-DPI displays
context.scale(dpr, dpr);
const renderContext = {
canvasContext: context,
viewport: viewport,
@@ -763,6 +903,7 @@ export class DeesPdfViewer extends DeesElement {
this.pageData.forEach(page => {
page.rendered = false;
page.rendering = false;
page.textLayerRendered = false;
});
// Cancel any ongoing render tasks
@@ -775,6 +916,16 @@ export class DeesPdfViewer extends DeesElement {
});
this.pageRenderTasks.clear();
// Cancel text layer render tasks
this.textLayerRenderTasks.forEach(task => {
try {
task.cancel?.();
} catch (error) {
// Ignore cancellation errors
}
});
this.textLayerRenderTasks.clear();
// Request update to re-render pages
this.requestUpdate();
@@ -784,52 +935,138 @@ export class DeesPdfViewer extends DeesElement {
});
}
private downloadPdf() {
const link = document.createElement('a');
link.href = this.pdfUrl;
link.download = this.pdfUrl.split('/').pop() || 'document.pdf';
link.click();
private async downloadPdf() {
if (!this.pdfDocument) return;
try {
// Get raw PDF data from the loaded document
const data = await this.pdfDocument.getData();
const blob = new Blob([data.buffer], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = this.pdfUrl ? this.pdfUrl.split('/').pop() || 'document.pdf' : 'document.pdf';
link.click();
// Clean up blob URL after short delay
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
} catch (error) {
console.error('Error downloading PDF:', error);
}
}
private printPdf() {
window.open(this.pdfUrl, '_blank')?.print();
private async printPdf() {
if (!this.pdfDocument) return;
try {
// Get raw PDF data from the loaded document
const data = await this.pdfDocument.getData();
const blob = new Blob([data.buffer], { type: 'application/pdf' });
const pdfUrl = URL.createObjectURL(blob);
// Create an HTML wrapper page that embeds the PDF and handles print/close
// This gives us control over the afterprint event (direct PDF URLs don't support it)
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<title>Print PDF</title>
<style>
* { margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; overflow: hidden; }
iframe { width: 100%; height: 100%; border: none; }
@media print {
html, body, iframe { width: 100%; height: 100%; }
}
</style>
</head>
<body>
<iframe src="${pdfUrl}" type="application/pdf"></iframe>
<script>
window.onload = function() {
setTimeout(function() {
window.focus();
window.print();
}, 500);
};
window.onafterprint = function() {
window.close();
};
// Safety close after 2 minutes
setTimeout(function() { window.close(); }, 120000);
</script>
</body>
</html>
`;
const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
const htmlUrl = URL.createObjectURL(htmlBlob);
const printWindow = window.open(htmlUrl, '_blank', 'width=800,height=600');
if (printWindow) {
// Cleanup blob URLs when window closes
const checkClosed = setInterval(() => {
if (printWindow.closed) {
clearInterval(checkClosed);
URL.revokeObjectURL(pdfUrl);
URL.revokeObjectURL(htmlUrl);
}
}, 500);
// Safety cleanup after 2 minutes
setTimeout(() => {
clearInterval(checkClosed);
URL.revokeObjectURL(pdfUrl);
URL.revokeObjectURL(htmlUrl);
}, 120000);
} else {
// Popup blocked - fall back to direct navigation
window.open(pdfUrl, '_blank');
setTimeout(() => URL.revokeObjectURL(pdfUrl), 60000);
URL.revokeObjectURL(htmlUrl);
}
} catch (error) {
console.error('Error printing PDF:', error);
}
}
/**
* Provide context menu items for right-click functionality
*/
public getContextMenuItems() {
return [
{
name: 'Open PDF in New Tab',
iconName: 'lucide:ExternalLink',
action: async () => {
window.open(this.pdfUrl, '_blank');
}
},
{ divider: true },
{
name: 'Copy PDF URL',
const items: any[] = [];
// Add copy option if text is selected
const selection = window.getSelection();
const selectedText = selection?.toString() || '';
if (selectedText) {
items.push({
name: 'Copy',
iconName: 'lucide:Copy',
action: async () => {
await navigator.clipboard.writeText(this.pdfUrl);
await navigator.clipboard.writeText(selectedText);
}
},
});
items.push({ divider: true });
}
items.push(
{
name: 'Download PDF',
iconName: 'lucide:Download',
action: async () => {
this.downloadPdf();
await this.downloadPdf();
}
},
{
name: 'Print PDF',
iconName: 'lucide:Printer',
action: async () => {
this.printPdf();
await this.printPdf();
}
}
];
);
return items;
}
private get canZoomIn(): boolean {
@@ -988,6 +1225,16 @@ export class DeesPdfViewer extends DeesElement {
});
this.pageRenderTasks.clear();
// Cancel text layer render tasks
this.textLayerRenderTasks.forEach(task => {
try {
task.cancel?.();
} catch (error) {
// Ignore cancellation errors
}
});
this.textLayerRenderTasks.clear();
// Cancel any thumbnail render tasks
for (const task of (this.thumbnailRenderTasks || [])) {
try {

View File

@@ -276,6 +276,7 @@ export const viewerStyles = [
border-radius: 4px;
overflow: hidden;
display: inline-block;
position: relative;
}
.page-canvas {
@@ -284,6 +285,52 @@ export const viewerStyles = [
image-rendering: crisp-edges;
}
/* Text layer for selection */
.text-layer {
position: absolute;
inset: 0;
overflow: visible;
line-height: 1;
text-size-adjust: none;
forced-color-adjust: none;
transform-origin: 0 0;
z-index: 1;
user-select: text;
-webkit-user-select: text;
}
.text-layer span,
.text-layer br {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
user-select: text;
-webkit-user-select: text;
}
.text-layer ::selection {
background: rgba(0, 100, 200, 0.3);
}
.text-layer br::selection {
background: transparent;
}
.text-layer .endOfContent {
display: block;
position: absolute;
inset: 100% 0 0;
z-index: 0;
cursor: default;
user-select: none;
}
.text-layer.selecting .endOfContent {
top: 0;
}
.pdf-viewer.with-sidebar .viewer-main {
margin-left: 0;
}

View File

@@ -1,161 +0,0 @@
import { DeesElement, property, html, customElement, domtools, type TemplateResult, type CSSResult, } from '@design.estate/dees-element';
import { Deferred } from '@push.rocks/smartpromise';
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
import '../../00group-utility/dees-icon/dees-icon.js';
// import type pdfjsTypes from 'pdfjs-dist';
declare global {
interface HTMLElementTagNameMap {
'dees-pdf': DeesPdf;
}
}
/**
* @deprecated Use DeesPdfViewer or DeesTilePdf instead
* - DeesPdfViewer: Full-featured PDF viewing with controls, navigation, zoom
* - DeesTilePdf: Lightweight, performance-optimized tile preview for grids
*/
@customElement('dees-pdf')
export class DeesPdf extends DeesElement {
// DEMO
public static demo = () => html` <dees-pdf></dees-pdf> `;
public static demoGroups = ['Media', 'PDF'];
// INSTANCE
@property()
accessor pdfUrl: string =
'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf';
constructor() {
super();
// you have access to all kinds of things through this.
// this.setAttribute('gotIt','true');
}
public render(): TemplateResult {
return html`
<style>
:host {
font-family: 'Geist Sans', sans-serif;
display: block;
box-sizing: border-box;
max-width: 800px;
}
:host([hidden]) {
display: none;
}
#pdfcanvas {
box-shadow: 0px 0px 5px #ccc;
width: 100%;
cursor: pointer;
}
</style>
<canvas
id="pdfcanvas"
.height=${0}
.width=${0}
></canvas>
`;
}
public static pdfJsReady: Promise<any>;
public static pdfjsLib: any // typeof pdfjsTypes;
public async connectedCallback() {
super.connectedCallback();
if (!DeesPdf.pdfJsReady) {
const pdfJsReadyDeferred = domtools.plugins.smartpromise.defer();
DeesPdf.pdfJsReady = pdfJsReadyDeferred.promise;
// @ts-ignore
DeesPdf.pdfjsLib = await import('https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/+esm');
DeesPdf.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.worker.mjs';
pdfJsReadyDeferred.resolve();
}
await DeesPdf.pdfJsReady;
this.displayContent();
}
public async displayContent() {
await DeesPdf.pdfJsReady;
// Asynchronous download of PDF
const loadingTask = DeesPdf.pdfjsLib.getDocument(this.pdfUrl);
loadingTask.promise.then(
(pdf) => {
console.log('PDF loaded');
// Fetch the first page
const pageNumber = 1;
pdf.getPage(pageNumber).then((page) => {
console.log('Page loaded');
const scale = 10;
const viewport = page.getViewport({ scale: scale });
// Prepare canvas using PDF page dimensions
const canvas: any = this.shadowRoot.querySelector('#pdfcanvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render PDF page into canvas context
const renderContext = {
canvasContext: context,
viewport: viewport,
};
const renderTask = page.render(renderContext);
renderTask.promise.then(function () {
console.log('Page rendered');
});
});
},
(reason) => {
// PDF loading error
console.error(reason);
}
);
}
/**
* Provide context menu items for the global context menu handler
*/
public getContextMenuItems() {
return [
{
name: 'Open PDF in New Tab',
iconName: 'lucide:ExternalLink',
action: async () => {
window.open(this.pdfUrl, '_blank');
}
},
{ divider: true },
{
name: 'Copy PDF URL',
iconName: 'lucide:Copy',
action: async () => {
await navigator.clipboard.writeText(this.pdfUrl);
}
},
{
name: 'Download PDF',
iconName: 'lucide:Download',
action: async () => {
const link = document.createElement('a');
link.href = this.pdfUrl;
link.download = this.pdfUrl.split('/').pop() || 'document.pdf';
link.click();
}
}
];
}
}

View File

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

View File

@@ -5,8 +5,6 @@ export * from './dees-video-viewer/index.js';
export * from './dees-preview/index.js';
// PDF Components
export * from './dees-pdf/index.js'; // @deprecated - Use dees-pdf-viewer or dees-tile-pdf instead
export * from './dees-pdf-preview/index.js'; // @deprecated - Use dees-tile-pdf instead
export * from './dees-pdf-shared/index.js';
export * from './dees-pdf-viewer/index.js';

View File

@@ -390,7 +390,8 @@ export class DeesSimpleAppDash extends DeesElement {
const domtools = await this.domtoolsPromise;
super.firstUpdated(_changedProperties);
if (this.viewTabs && this.viewTabs.length > 0) {
await this.loadView(this.viewTabs[0]);
const viewToLoad = this.selectedView || this.viewTabs[0];
await this.loadView(viewToLoad);
}
}

View File

@@ -1,6 +1,6 @@
import type { TemplateResult } from '@design.estate/dees-element';
import type { IAppBarMenuItem } from './appbarmenuitem.js';
import type { IMenuItem } from './tab.js';
import type { IMenuItem, ITabAction } from './tab.js';
import type { IMenuGroup } from './menugroup.js';
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from './secondarymenu.js';
@@ -134,6 +134,8 @@ export type TDeesAppui = HTMLElement & {
removeContentTab: (tabKey: string) => void;
selectContentTab: (tabKey: string) => void;
getSelectedContentTab: () => IMenuItem | undefined;
setContentTabActionsLeft: (actions: ITabAction[]) => void;
setContentTabActionsRight: (actions: ITabAction[]) => void;
activityLog: IActivityLogAPI;
setActivityLogVisible: (visible: boolean) => void;
toggleActivityLog: () => void;

View File

@@ -7,3 +7,11 @@ export interface IMenuItem {
closeable?: boolean;
onClose?: () => void;
}
export interface ITabAction {
id: string;
iconName: string;
action: () => void | Promise<void>;
tooltip?: string;
disabled?: boolean;
}

View File

@@ -193,9 +193,20 @@ export class DeesServiceLibLoader {
const response = await fetch(cssUrl);
const cssText = await response.text();
// Fix for xterm.js WidthCache measurement container causing horizontal scrollbar
// xterm.js creates this on document.body with width: 50000px, top: -50000px
// Moving it off-screen horizontally prevents scrollWidth expansion
const xtermMeasurementFix = `
/* Fix xterm.js WidthCache measurement container causing horizontal scrollbar */
/* xterm creates this on document.body - move it off-screen horizontally too */
body > div[style*="top: -50000px"][style*="width: 50000px"] {
left: -50000px !important;
}
`;
const style = document.createElement('style');
style.id = styleId;
style.textContent = cssText;
style.textContent = cssText + xtermMeasurementFix;
document.head.appendChild(style);
}

View File

@@ -7,9 +7,9 @@ export const CDN_VERSIONS = {
xtermAddonFit: '0.8.0',
xtermAddonSearch: '0.13.0',
highlightJs: '11.11.1',
apexcharts: '5.3.6',
tiptap: '2.23.0',
fontawesome: '7.1.0',
apexcharts: '5.10.3',
tiptap: '2.27.2',
fontawesome: '7.2.0',
} as const;
/**