Compare commits

...

37 Commits

Author SHA1 Message Date
09741e0b37 v3.44.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-10 12:39:21 +00:00
5cadd1fc7f feat(appui-tabs): add support for left/right tab action buttons and content tab action APIs 2026-03-10 12:39:21 +00:00
1795235c6d v3.43.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-09 17:37:50 +00:00
ba7d387acb fix(media): remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies 2026-03-09 17:37:50 +00:00
26ca16a284 v3.43.3
Some checks failed
Default (tags) / security (push) Failing after 2s
Default (tags) / test (push) Failing after 2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 18:25:54 +00:00
3ab3eb5e5e fix(dees-table): use lucide icon identifier for Search action in dees-table 2026-02-24 18:25:54 +00:00
da5dbc70e2 v3.43.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-21 21:12:18 +00:00
19b7981542 fix(dees-chart-log): avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs 2026-02-21 21:12:18 +00:00
bad105074e v3.43.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-21 18:06:42 +00:00
f124091784 fix(dees-chart-log): replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs 2026-02-21 18:06:42 +00:00
68790a26ed v3.43.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-17 17:34:53 +00:00
2abf7e356d feat(dees-form): add layout styles to dees-form and standardize demo input grouping 2026-02-17 17:34:53 +00:00
ecd35683e6 v3.42.2
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-16 17:16:49 +00:00
93cb632448 fix(dees-chart-area): add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data 2026-02-16 17:16:49 +00:00
047e42c6a3 v3.42.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-16 16:03:23 +00:00
59efa8cff0 fix(dees-table): Guard against undefined action.type in dees-table by using optional chaining and update several dependencies 2026-02-16 16:03:23 +00:00
09f0aa97dd v3.42.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-02 14:32:20 +00:00
7c62f45d77 feat(dees-form-submit): forward button properties to internal dees-button, use property bindings, add demo and styles 2026-02-02 14:32:20 +00:00
b123768474 v3.41.6
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-02 09:27:53 +00:00
f292e7a7f4 fix(dees-simple-appdash): respect selectedView when loading initial view, falling back to the first tab 2026-02-02 09:27:53 +00:00
d82e5603a7 v3.41.5
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-01 23:51:53 +00:00
7e2386bcdf fix(dees-service-lib-loader): prevent horizontal scrollbar by offsetting xterm WidthCache measurement container 2026-02-01 23:51:53 +00:00
eba2a03355 v3.41.4
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-29 13:21:30 +00:00
06c01f0690 fix(): no changes 2026-01-29 13:21:30 +00:00
91e03eb9c4 v3.41.3
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-29 07:46:19 +00:00
b7f3f47c61 fix(dees-pdf-viewer): use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling 2026-01-29 07:46:19 +00:00
0a83f0e136 update 2026-01-28 17:10:37 +00:00
2b048cf34f update 2026-01-28 17:02:52 +00:00
7e50b8cb3f update 2026-01-28 16:46:07 +00:00
b97601a876 v3.41.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-28 16:17:05 +00:00
5ddeb8fe7c fix(dees-pdf-viewer): account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays 2026-01-28 16:17:05 +00:00
25f46162c5 v3.41.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 14:02:25 +00:00
b84b0e7ce6 fix(dataview-codebox): fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies 2026-01-27 14:02:25 +00:00
69840de3a6 v3.41.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 12:36:46 +00:00
85badfbd21 feat(docs): document new media & tile components and expand Workspace/IDE component docs; update component count to 90+ 2026-01-27 12:36:46 +00:00
264e460365 v3.40.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-27 11:36:24 +00:00
bfda6b75e7 feat(dees-tile): unify tile badge styling and markup; replace component-specific badge classes with shared tile-badge classes and update related imports/tests 2026-01-27 11:36:24 +00:00
54 changed files with 4006 additions and 3627 deletions

View File

@@ -1,5 +1,139 @@
# Changelog # Changelog
## 2026-03-10 - 3.44.0 - feat(appui-tabs)
add support for left/right tab action buttons and content tab action APIs
- Introduce ITabAction interface and add actionsLeft/actionsRight properties to dees-appui-tabs, dees-appui-maincontent, and dees-appui.
- Render action buttons with new styles and renderActions() helper, including disabled state and click handlers; wire actions into tab components.
- Add public clear() on dees-appui-tabs and improve tab selection logic to reset selection when tabs become empty or when the selected tab is removed.
- Expose setContentTabActionsLeft and setContentTabActionsRight on the DeesAppui programmatic API and update interfaces/appconfig accordingly.
- Update demos to showcase action buttons, add clear-all behavior, and adjust layout/styling for action areas.
## 2026-03-09 - 3.43.4 - fix(media)
remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies
- Removed deprecated PDF components and related demos/styles: ts_web/elements/00group-media/dees-pdf/* and ts_web/elements/00group-media/dees-pdf-preview/*
- Removed exports for dees-pdf and dees-pdf-preview from ts_web/elements/00group-media/index.ts (public API removal)
- Dependency upgrades: @design.estate/dees-domtools → ^2.3.9, apexcharts → ^5.10.3, lucide → ^0.577.0, @fortawesome/* → ^7.2.0
- DevDependency upgrades: @git.zone/tsbuild → ^4.3.0, @git.zone/tsbundle → ^2.9.1, @git.zone/tstest → ^3.3.2, @git.zone/tswatch → ^3.2.5, @types/node → ^25.3.5
- Updated ts_web/services/versions.ts to align CDN/version constants (apexcharts, tiptap → 2.27.2, fontawesome)
## 2026-02-24 - 3.43.3 - fix(dees-table)
use lucide icon identifier for Search action in dees-table
- Replaced iconName 'magnifyingGlass' with 'lucide:Search' in ts_web/elements/00group-dataview/dees-table/dees-table.ts
- Updates the icon identifier for the header 'Search' action; no functional behavior changed
## 2026-02-21 - 3.43.2 - fix(dees-chart-log)
avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs
- Prevent property-bound logEntries from duplicating entries already present in logBuffer by deduplicating on timestamp|message
- Call updateMetrics() when replaying or appending log entries so metrics stay accurate
- Skip processing entirely when the incoming logEntries array is unchanged
- Optimize append-only updates by writing only the new tail entries instead of full re-render
- Enforce maxEntries when appending to the logBuffer to maintain buffer size
- Respect filterMode and searchQuery when deciding whether to write appended entries to the terminal
## 2026-02-21 - 3.43.1 - fix(dees-chart-log)
replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs
- Replay entries stored in logBuffer after terminal initialization to avoid losing entries that arrived early
- Add updated() lifecycle hook to copy logEntries into logBuffer and call reRenderFilteredLogs when terminalReady
- Call super.updated(changedProperties) to preserve base class behavior
## 2026-02-17 - 3.43.0 - feat(dees-form)
add layout styles to dees-form and standardize demo input grouping
- Add static CSS to dees-form: default column layout with gap and support for [horizontal-layout] (row wrapping, alignment and gap).
- Remove inline <style> from dees-form render to centralize styling.
- Simplify dees-input-base styles by removing host margins and making spacing container-driven.
- Update multiple demo files to wrap related inputs in a new .input-group container and include .input-group CSS for consistent vertical spacing.
## 2026-02-16 - 3.42.2 - fix(dees-chart-area)
add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data
- Introduces ApexAxisChartSeries type alias with support for number arrays, [x,y] tuples, and object {x,y,...} series entries
- Type-only change — no runtime or API behavior modified
- File changed: ts_web/elements/00group-chart/dees-chart-area/component.ts
## 2026-02-16 - 3.42.1 - fix(dees-table)
Guard against undefined action.type in dees-table by using optional chaining and update several dependencies
- Use optional chaining (action.type?.includes(...)) in ts_web/elements/.../dees-table.ts to prevent runtime errors when action.type is undefined
- Bump dependency apexcharts from ^5.3.6 to ^5.5.0
- Bump dependency lucide from ^0.563.0 to ^0.564.0
- Bump devDependency @git.zone/tswatch from ^3.0.1 to ^3.1.0
- Bump devDependency @types/node from ^25.0.10 to ^25.2.3
## 2026-02-02 - 3.42.0 - feat(dees-form-submit)
forward button properties to internal dees-button, use property bindings, add demo and styles
- Added forwarded properties: type, size, icon, iconPosition (with defaults) and preserved text/status/disabled
- Changed template to use property bindings (.prop) for dees-button instead of string attributes
- Switched internal event handler to listen for dees-button's @clicked event (was @click)
- Added component styles (:host display and dees-button width:100%) and improved layout
- Expanded demo with multiple usage examples (basic, icons, types, sizes, states, and a form context)
## 2026-02-02 - 3.41.6 - fix(dees-simple-appdash)
respect selectedView when loading initial view, falling back to the first tab
- firstUpdated now loads this.selectedView if set, otherwise loads the first view tab
- Prevents always loading the first tab and preserves a previously selected view on initial render
## 2026-02-01 - 3.41.5 - fix(dees-service-lib-loader)
prevent horizontal scrollbar by offsetting xterm WidthCache measurement container
- Injects additional CSS into DeesServiceLibLoader to move xterm.js WidthCache measurement div off-screen horizontally (selector: body > div[style*="top: -50000px"][style*="width: 50000px"])
- Fixes root cause where xterm creates a large-width measurement container (width: 50000px) on document.body that expands scrollWidth and causes a horizontal scrollbar
- Change applied in ts_web/services/DeesServiceLibLoader.ts by concatenating the fix CSS into the injected stylesheet
## 2026-01-29 - 3.41.4 - fix()
no changes
- No files changed in this commit; no code or documentation modified
- No release required
## 2026-01-29 - 3.41.3 - fix(dees-pdf-viewer)
use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling
- Download and Print now use pdfDocument.getData() to create Blob URLs so in-memory PDFs (pdf.js) can be saved/printed.
- Print flow now opens an HTML wrapper with an iframe to allow onafterprint handling, auto-close, popup-fallback and timed cleanup of Blob URLs.
- Added try/catch logging, URL.revokeObjectURL calls and safety timeouts to avoid resource leaks.
- Removed context menu items that relied on the raw PDF URL (Open in New Tab, Copy PDF URL); Download/Print actions now await the async handlers.
## 2026-01-28 - 3.41.2 - fix(dees-pdf-viewer)
account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays
- Multiply canvas.width and canvas.height by window.devicePixelRatio (dpr) and use Math.floor to set the actual pixel buffer size
- Call ctx.scale(dpr, dpr) so drawing is rendered at device pixels while keeping CSS display size unchanged
- Apply the same high-DPI adjustments to both main page rendering and thumbnail generation
- Keep canvas.style.width and canvas.style.height set to viewport dimensions to preserve layout
## 2026-01-27 - 3.41.1 - fix(dataview-codebox)
fix dees-dataview codebox layout to ensure full-height, proper flex behavior and scrolling; bump internal dependencies
- Updated CSS in ts_web/elements/00group-dataview/dees-dataview-codebox/dees-dataview-codebox.ts: added height:100%, box-sizing, display:flex and flex-direction:column on container, set flex-shrink on header elements, made code grid overflow:auto with flex:1 and min-height:0 to prevent overflow issues.
- Bumped dependencies in package.json: @design.estate/dees-domtools from ^2.3.7 to ^2.3.8 and @design.estate/dees-element from ^2.1.5 to ^2.1.6.
- Non-breaking visual/layout fix — suitable for a patch release.
## 2026-01-27 - 3.41.0 - feat(docs)
document new media & tile components and expand Workspace/IDE component docs; update component count to 90+
- Updated README component count from 70+ to 90+.
- Added Media & Tile components documentation (DeesTilePdf, DeesTileImage, DeesTileAudio, DeesTileVideo, DeesTileNote, DeesTileFolder, DeesPreview, DeesPdfViewer, DeesImageViewer, DeesAudioViewer, DeesVideoViewer).
- Expanded Workspace/IDE documentation and introduced workspace subcomponents (DeesWorkspace, DeesWorkspaceMonaco, DeesWorkspaceDiffEditor, DeesWorkspaceFiletree, DeesWorkspaceTerminal, DeesWorkspaceTerminalPreview, DeesWorkspaceMarkdown, DeesWorkspaceMarkdownoutlet, DeesWorkspaceBottombar).
- Enhanced Contextmenu docs to demonstrate nested submenus and programmatic API usage.
- Added ITileFolderItem interface example for tile folder items.
## 2026-01-27 - 3.40.0 - feat(dees-tile)
unify tile badge styling and markup; replace component-specific badge classes with shared tile-badge classes and update related imports/tests
- Add shared badge styles: .tile-badge-corner, .tile-badge-topright, .tile-badge and layout rules in dees-tile-shared/styles.ts
- Update tile components (audio, video, image, folder, note, pdf) to use new badge markup and remove duplicated badge CSS
- Shift badge positioning when a tile label is present (.tile-container:has(.tile-label))
- Remove older per-component badge rules (duration-badge, item-count-badge, dimension-badge, note-language/note-line-indicator, preview-page-indicator) and replace with unified classes
- Update tests to import dees-contextmenu and dees-dashboardgrid from new 00group-overlay / 00group-layout paths to reflect folder reorganization
## 2026-01-27 - 3.39.1 - fix(dees-tile-note) ## 2026-01-27 - 3.39.1 - fix(dees-tile-note)
use horizontal pointer position to scroll note body by computing percentage from clientX and element width instead of clientY and height use horizontal pointer position to scroll note body by computing percentage from clientX and element width instead of clientY and height

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.39.1", "version": "3.44.0",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@@ -16,13 +16,13 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.3.7", "@design.estate/dees-domtools": "^2.3.9",
"@design.estate/dees-element": "^2.1.5", "@design.estate/dees-element": "^2.1.6",
"@design.estate/dees-wcctools": "^3.8.0", "@design.estate/dees-wcctools": "^3.8.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.2.0",
"@push.rocks/smarti18n": "^1.0.4", "@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0", "@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.1.0", "@push.rocks/smartstring": "^4.1.0",
@@ -34,22 +34,22 @@
"@tiptap/extension-underline": "^2.23.0", "@tiptap/extension-underline": "^2.23.0",
"@tiptap/starter-kit": "^2.23.0", "@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.3.0", "@tsclass/tsclass": "^9.3.0",
"apexcharts": "^5.3.6", "apexcharts": "^5.10.3",
"highlight.js": "11.11.1", "highlight.js": "11.11.1",
"ibantools": "^4.5.1", "ibantools": "^4.5.1",
"lucide": "^0.563.0", "lucide": "^0.577.0",
"monaco-editor": "0.55.1", "monaco-editor": "0.55.1",
"pdfjs-dist": "^4.10.38", "pdfjs-dist": "^4.10.38",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0" "xterm-addon-fit": "^0.8.0"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.8.3", "@git.zone/tsbundle": "^2.9.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.3.2",
"@git.zone/tswatch": "^3.0.1", "@git.zone/tswatch": "^3.2.5",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@types/node": "^25.0.10" "@types/node": "^25.3.5"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

4850
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

274
readme.md
View File

@@ -1,6 +1,6 @@
# @design.estate/dees-catalog # @design.estate/dees-catalog
A comprehensive web components library built with TypeScript and LitElement, providing **70+ production-ready UI components** for building modern web applications with consistent design and behavior. 🚀 A comprehensive web components library built with TypeScript and LitElement, providing **90+ production-ready UI components** for building modern web applications with consistent design and behavior. 🚀
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
[![LitElement](https://img.shields.io/badge/LitElement-4.0+-orange.svg)](https://lit.dev/) [![LitElement](https://img.shields.io/badge/LitElement-4.0+-orange.svg)](https://lit.dev/)
@@ -18,6 +18,8 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- 🔧 **TypeScript-First** — Fully typed APIs with excellent IDE support - 🔧 **TypeScript-First** — Fully typed APIs with excellent IDE support
- 🧩 **Modular** — Use only what you need, tree-shakeable architecture - 🧩 **Modular** — Use only what you need, tree-shakeable architecture
- 🏗️ **Full App Shell**`dees-appui` provides a complete application framework with menus, routing, activity log, and bottom bar - 🏗️ **Full App Shell**`dees-appui` provides a complete application framework with menus, routing, activity log, and bottom bar
- 🎬 **Media Components** — Rich tile-based previews for PDFs, images, audio, video, notes, and folders
- 💻 **IDE Workspace** — Full workspace component with Monaco editor, file tree, terminal, and diff viewer
## 📦 Installation ## 📦 Installation
@@ -55,12 +57,13 @@ For developers working on this library, please refer to the [UI Components Playb
|----------|------------| |----------|------------|
| **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols), [`DeesActionbar`](#deesactionbar) | | **Core UI** | [`DeesButton`](#deesbutton), [`DeesButtonExit`](#deesbuttonexit), [`DeesButtonGroup`](#deesbuttongroup), [`DeesBadge`](#deesbadge), [`DeesChips`](#deeschips), [`DeesHeading`](#deesheading), [`DeesHint`](#deeshint), [`DeesIcon`](#deesicon), [`DeesLabel`](#deeslabel), [`DeesPanel`](#deespanel), [`DeesSearchbar`](#deessearchbar), [`DeesSpinner`](#deesspinner), [`DeesToast`](#deestoast), [`DeesWindowcontrols`](#deeswindowcontrols), [`DeesActionbar`](#deesactionbar) |
| **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputToggle`](#deesinputtoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesInputCode`](#deesinputcode), [`DeesFormSubmit`](#deesformsubmit) | | **Forms** | [`DeesForm`](#deesform), [`DeesInputText`](#deesinputtext), [`DeesInputCheckbox`](#deesinputcheckbox), [`DeesInputDropdown`](#deesinputdropdown), [`DeesInputRadiogroup`](#deesinputradiogroup), [`DeesInputFileupload`](#deesinputfileupload), [`DeesInputIban`](#deesinputiban), [`DeesInputPhone`](#deesinputphone), [`DeesInputQuantitySelector`](#deesinputquantityselector), [`DeesInputMultitoggle`](#deesinputmultitoggle), [`DeesInputToggle`](#deesinputtoggle), [`DeesInputTags`](#deesinputtags), [`DeesInputTypelist`](#deesinputtypelist), [`DeesInputList`](#deesinputlist), [`DeesInputProfilepicture`](#deesinputprofilepicture), [`DeesInputRichtext`](#deesinputrichtext), [`DeesInputWysiwyg`](#deesinputwysiwyg), [`DeesInputDatepicker`](#deesinputdatepicker), [`DeesInputSearchselect`](#deesinputsearchselect), [`DeesInputCode`](#deesinputcode), [`DeesFormSubmit`](#deesformsubmit) |
| **App Shell (Layout)** | [`DeesAppui`](#deesappui-), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiBottombar`](#deesappuibottombar), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) | | **App Shell (Layout)** | [`DeesAppui`](#deesappui-), [`DeesAppuiMainmenu`](#deesappuimainmenu), [`DeesAppuiSecondarymenu`](#deesappuisecondarymenu), [`DeesAppuiMaincontent`](#deesappuimaincontent), [`DeesAppuiAppbar`](#deesappuiappbar), [`DeesAppuiActivitylog`](#deesappuiactivitylog), [`DeesAppuiBottombar`](#deesappuibottombar), [`DeesAppuiProfiledropdown`](#deesappuiprofiledropdown), [`DeesAppuiTabs`](#deesappuitabs), [`DeesMobileNavigation`](#deesmobilenavigation), [`DeesDashboardGrid`](#deesdashboardgrid) |
| **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) | | **Data Display** | [`DeesTable`](#deestable), [`DeesDataviewCodebox`](#deesdataviewcodebox), [`DeesDataviewStatusobject`](#deesdataviewstatusobject), [`DeesPdf`](#deespdf), [`DeesStatsGrid`](#deesstatsgrid), [`DeesPagination`](#deespagination) |
| **Media & Tiles** | [`DeesTilePdf`](#deestilepdf), [`DeesTileImage`](#deestileimage), [`DeesTileAudio`](#deestileaudio), [`DeesTileVideo`](#deestilevideo), [`DeesTileNote`](#deestilenote), [`DeesTileFolder`](#deestilefolder), [`DeesPreview`](#deespreview), [`DeesPdfViewer`](#deespdfviewer), [`DeesPdfPreview`](#deespdfpreview), [`DeesImageViewer`](#deesimageviewer), [`DeesAudioViewer`](#deesaudioviewer), [`DeesVideoViewer`](#deesvideoviewer) |
| **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) | | **Visualization** | [`DeesChartArea`](#deeschartarea), [`DeesChartLog`](#deeschartlog) |
| **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) | | **Dialogs & Overlays** | [`DeesModal`](#deesmodal), [`DeesContextmenu`](#deescontextmenu), [`DeesSpeechbubble`](#deesspeechbubble), [`DeesWindowlayer`](#deeswindowlayer) |
| **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) | | **Navigation** | [`DeesStepper`](#deesstepper), [`DeesProgressbar`](#deesprogressbar) |
| **Workspace / IDE** | [`DeesEditor`](#deeseditor), [`DeesTerminal`](#deesterminal), [`DeesUpdater`](#deesupdater) | | **Workspace / IDE** | [`DeesWorkspace`](#deesworkspace), [`DeesWorkspaceMonaco`](#deesworkspacemonaco), [`DeesWorkspaceDiffEditor`](#deesworkspacediffeditor), [`DeesWorkspaceFiletree`](#deesworkspacefiletree), [`DeesWorkspaceTerminal`](#deesworkspaceterminal), [`DeesWorkspaceTerminalPreview`](#deesworkspaceterminalpreview), [`DeesWorkspaceMarkdown`](#deesworkspacemarkdown), [`DeesWorkspaceMarkdownoutlet`](#deesworkspacemarkdownoutlet), [`DeesWorkspaceBottombar`](#deesworkspacebottombar) |
| **Theming** | [`DeesTheme`](#deestheme) | | **Theming** | [`DeesTheme`](#deestheme) |
| **Pre-built Templates** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) | | **Pre-built Templates** | [`DeesSimpleAppdash`](#deessimpleappdash), [`DeesSimpleLogin`](#deessimplelogin) |
| **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) | | **Shopping** | [`DeesShoppingProductcard`](#deesshoppingproductcard) |
@@ -1234,6 +1237,146 @@ Pagination component for navigating through large datasets.
--- ---
### Media & Tile Components 🎬
A rich collection of tile-based preview components for displaying media files in grids. All tiles share a consistent base class (`DeesTileBase`) with lazy loading via `IntersectionObserver`, hover interactions, click events, context menus, and three size variants (`small`, `default`, `large`).
All tile badges use a unified styling system with label-awareness — when a `label` is set, bottom badges automatically shift up to avoid overlapping.
#### `DeesTilePdf`
PDF document tile with live page preview on hover.
```typescript
<dees-tile-pdf
pdfUrl="/documents/report.pdf"
label="Annual Report"
clickable
@tile-click=${handleClick}
></dees-tile-pdf>
```
**Key Features:**
- Renders first page as canvas preview
- Hover to scrub through pages (mouse X position maps to page number)
- Shows page count badge, hover page indicator
- Detects A4/Letter vs non-standard aspect ratios
#### `DeesTileImage`
Image tile with lazy loading and dimension display.
```typescript
<dees-tile-image
src="/photos/landscape.jpg"
alt="Mountain landscape"
label="landscape.jpg"
clickable
@tile-click=${handleClick}
></dees-tile-image>
```
**Key Features:**
- Lazy loads image on scroll into view
- Shows image dimensions on hover (e.g. "1920 × 1080")
- Checkerboard background for transparent images
#### `DeesTileAudio`
Audio file tile with waveform visualization.
```typescript
<dees-tile-audio
src="/music/track.mp3"
title="Summer Vibes"
artist="DJ Example"
clickable
@tile-click=${handleClick}
></dees-tile-audio>
```
**Key Features:**
- Generates waveform visualization from audio data
- Shows duration badge (e.g. "3:42")
- Displays title and artist metadata
- Play overlay on hover
#### `DeesTileVideo`
Video tile with thumbnail capture and hover preview.
```typescript
<dees-tile-video
src="/videos/intro.mp4"
poster="/thumbs/intro.jpg"
label="Introduction"
clickable
@tile-click=${handleClick}
></dees-tile-video>
```
**Key Features:**
- Auto-captures first frame as thumbnail (or uses provided `poster`)
- Plays video preview on hover
- Shows duration badge
- Play button overlay
#### `DeesTileNote`
Code/text snippet tile with syntax-aware display.
```typescript
<dees-tile-note
title="config.ts"
language="TypeScript"
.content=${codeString}
clickable
@tile-click=${handleClick}
></dees-tile-note>
```
**Key Features:**
- Monospace font with line numbers
- Language badge (top-right)
- Scrollable content on hover (mouse X position controls scroll)
- Line indicator badge while scrolling
- Gradient fade at bottom
#### `DeesTileFolder`
Folder tile with 2×2 content preview grid.
```typescript
<dees-tile-folder
name="Project Assets"
.items=${[
{ type: 'image', name: 'logo.png', thumbnailSrc: '/thumbs/logo.png' },
{ type: 'pdf', name: 'spec.pdf' },
{ type: 'audio', name: 'jingle.mp3' },
{ type: 'video', name: 'demo.mp4' },
]}
clickable
@tile-click=${handleClick}
></dees-tile-folder>
```
**Key Features:**
- 2×2 preview grid showing first 4 items (thumbnails or type icons)
- Item count badge (e.g. "12 items")
- Folder icon header with name
- Supports: `pdf`, `image`, `audio`, `video`, `note`, `folder`, `unknown` types
#### `DeesPreview`
Unified preview component that auto-selects the right tile type based on content.
#### `DeesPdfViewer` / `DeesPdfPreview`
Full PDF viewing components with navigation controls.
#### `DeesImageViewer`
Full-screen image viewer with zoom and pan.
#### `DeesAudioViewer`
Audio playback component with waveform and controls.
#### `DeesVideoViewer`
Video playback component with standard controls.
---
### Visualization Components ### Visualization Components
#### `DeesChartArea` #### `DeesChartArea`
@@ -1294,16 +1437,41 @@ DeesModal.createAndShow({
``` ```
#### `DeesContextmenu` #### `DeesContextmenu`
Context menu component for right-click actions. Context menu component for right-click actions with nested submenu support.
```typescript ```typescript
<dees-contextmenu // Programmatic usage
.items=${[ DeesContextmenu.openContextMenuWithOptions(mouseEvent, [
{ label: 'Edit', icon: 'edit', action: () => handleEdit() }, {
{ label: 'Delete', icon: 'trash', action: () => handleDelete() } name: 'Edit',
]} iconName: 'lucide:edit',
position="right" action: async () => handleEdit()
></dees-contextmenu> },
{ divider: true },
{
name: 'More Options',
iconName: 'lucide:moreHorizontal',
submenu: [
{ name: 'Duplicate', iconName: 'lucide:copy', action: async () => handleDuplicate() },
{ name: 'Archive', iconName: 'lucide:archive', action: async () => handleArchive() },
]
},
{
name: 'Delete',
iconName: 'lucide:trash2',
action: async () => handleDelete()
}
]);
// Component-based (implement getContextMenuItems on any element)
class MyComponent extends DeesElement {
getContextMenuItems() {
return [
{ name: 'View Details', iconName: 'lucide:eye', action: async () => { ... } },
{ name: 'Edit', iconName: 'lucide:edit', action: async () => { ... } },
];
}
}
``` ```
#### `DeesSpeechbubble` #### `DeesSpeechbubble`
@@ -1395,51 +1563,68 @@ Theme provider component that wraps children and provides CSS custom properties
--- ---
### Workspace / IDE Components ### Workspace / IDE Components 💻
#### `DeesEditor` A full-featured IDE workspace component suite for building browser-based code editors, terminal interfaces, and documentation viewers.
Code editor component with syntax highlighting and code completion, powered by Monaco Editor.
#### `DeesWorkspace`
Top-level workspace shell that composes editor, file tree, terminal, and bottom bar into an IDE-like layout.
```typescript ```typescript
<dees-editor <dees-workspace></dees-workspace>
```
#### `DeesWorkspaceMonaco`
Monaco Editor integration for code editing with full IntelliSense, syntax highlighting, and language support.
```typescript
<dees-workspace-monaco
.value=${code} .value=${code}
@change=${handleCodeChange}
.language=${'typescript'} .language=${'typescript'}
.theme=${'vs-dark'} @change=${handleCodeChange}
.options=${{ ></dees-workspace-monaco>
lineNumbers: true,
minimap: { enabled: true },
autoClosingBrackets: 'always'
}}
></dees-editor>
``` ```
#### `DeesTerminal` #### `DeesWorkspaceDiffEditor`
Terminal emulator component for command-line interface. Side-by-side diff editor powered by Monaco for comparing file versions.
```typescript ```typescript
<dees-terminal <dees-workspace-diff-editor
.commands=${{ .originalValue=${originalCode}
'echo': (args) => `Echo: ${args.join(' ')}`, .modifiedValue=${modifiedCode}
'help': () => 'Available commands: echo, help' .language=${'typescript'}
}} ></dees-workspace-diff-editor>
.prompt=${'$'}
.welcomeMessage=${'Welcome! Type "help" for available commands.'}
></dees-terminal>
``` ```
#### `DeesUpdater` #### `DeesWorkspaceFiletree`
Component for managing application updates and version control. File tree navigation component with expand/collapse, file icons, and selection.
```typescript ```typescript
<dees-updater <dees-workspace-filetree
.currentVersion=${'1.5.2'} .files=${fileTreeData}
.checkInterval=${3600000} @file-select=${handleFileSelect}
.autoUpdate=${false} ></dees-workspace-filetree>
@update-available=${handleUpdateAvailable}
></dees-updater>
``` ```
#### `DeesWorkspaceTerminal`
Terminal emulator component powered by xterm.js.
```typescript
<dees-workspace-terminal></dees-workspace-terminal>
```
#### `DeesWorkspaceTerminalPreview`
Terminal with integrated preview pane for output visualization.
#### `DeesWorkspaceMarkdown`
Markdown editor with live preview.
#### `DeesWorkspaceMarkdownoutlet`
Read-only markdown renderer for documentation display.
#### `DeesWorkspaceBottombar`
IDE-style bottom status bar for the workspace.
--- ---
### Pre-built Templates ### Pre-built Templates
@@ -1582,6 +1767,13 @@ interface IViewActivationContext {
viewId: string; viewId: string;
params?: Record<string, string>; params?: Record<string, string>;
} }
// Tile folder item (for DeesTileFolder)
interface ITileFolderItem {
type: 'pdf' | 'image' | 'audio' | 'video' | 'note' | 'folder' | 'unknown';
thumbnailSrc?: string;
name: string;
}
``` ```
--- ---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,6 +143,12 @@ export class DeesAppui extends DeesElement {
@property({ type: Object }) @property({ type: Object })
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined; accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
@property({ type: Array })
accessor contentTabActionsLeft: interfaces.ITabAction[] = [];
@property({ type: Array })
accessor contentTabActionsRight: interfaces.ITabAction[] = [];
// References to child components // References to child components
@state() @state()
accessor appbar: DeesAppuiBar | undefined = undefined; accessor appbar: DeesAppuiBar | undefined = undefined;
@@ -306,6 +312,8 @@ export class DeesAppui extends DeesElement {
.showTabs=${this.maincontentTabsVisible} .showTabs=${this.maincontentTabsVisible}
.tabsAutoHide=${this.contentTabsAutoHide} .tabsAutoHide=${this.contentTabsAutoHide}
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold} .tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
.tabActionsLeft=${this.contentTabActionsLeft}
.tabActionsRight=${this.contentTabActionsRight}
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)} @tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)} @tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
> >
@@ -699,6 +707,20 @@ export class DeesAppui extends DeesElement {
return this.maincontentSelectedTab; return this.maincontentSelectedTab;
} }
/**
* Set content tab action buttons on the left side
*/
public setContentTabActionsLeft(actions: interfaces.ITabAction[]): void {
this.contentTabActionsLeft = [...actions];
}
/**
* Set content tab action buttons on the right side
*/
public setContentTabActionsRight(actions: interfaces.ITabAction[]): void {
this.contentTabActionsRight = [...actions];
}
// ========================================== // ==========================================
// PROGRAMMATIC API: ACTIVITY LOG // PROGRAMMATIC API: ACTIVITY LOG
// ========================================== // ==========================================

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,85 @@
import { html } from '@design.estate/dees-element'; 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, css,
cssManager, cssManager,
property, property,
type TemplateResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import type { DeesForm } from '../dees-form/dees-form.js'; import type { DeesForm } from '../dees-form/dees-form.js';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
@@ -21,38 +22,61 @@ export class DeesFormSubmit extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
public static demoGroups = ['Form', 'Button']; public static demoGroups = ['Form', 'Button'];
@property({ // =============================================
type: Boolean, // Properties forwarded to internal dees-button
reflect: true, // =============================================
})
@property({ type: Boolean, reflect: true })
accessor disabled = false; accessor disabled = false;
@property({ @property({ type: String })
type: String,
})
accessor text: string; accessor text: string;
@property({ @property({ type: String })
type: String,
})
accessor status: 'normal' | 'pending' | 'success' | 'error' = 'normal'; 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() { constructor() {
super(); 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` return html`
<dees-button <dees-button
status="${this.status}" .status=${this.status}
@click="${this.submit}" .type=${this.type}
?disabled="${this.disabled}" .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> </dees-button>
`; `;
} }

View File

@@ -1,6 +1,7 @@
import { import {
customElement, customElement,
html, html,
css,
type TemplateResult, type TemplateResult,
DeesElement, DeesElement,
type CSSResult, type CSSResult,
@@ -81,13 +82,25 @@ export class DeesForm extends DeesElement {
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' }) @property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
accessor horizontalLayout: boolean = false; 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 { public render(): TemplateResult {
return html` return html`
<style>
:host {
display: contents;
}
</style>
<slot></slot> <slot></slot>
`; `;
} }

View File

@@ -54,37 +54,20 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
/* CSS Variables for consistent spacing */ /* CSS Variables for consistent spacing */
:host { :host {
--dees-input-spacing-unit: 8px; --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 */ --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 { :host {
display: block; display: block;
margin: 0; 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 */ /* Horizontal layout mode - activated by attribute */
:host([layout-mode="horizontal"]) { :host([layout-mode="horizontal"]) {
display: inline-block; 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 */ /* Label position variations */
:host([label-position="left"]) .input-wrapper { :host([label-position="left"]) .input-wrapper {
display: grid; display: grid;

View File

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

View File

@@ -13,6 +13,12 @@ export const demoFunc = () => html`
margin: 0 auto; margin: 0 auto;
} }
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.payment-group { .payment-group {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -24,16 +30,18 @@ export const demoFunc = () => html`
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}> <dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
<dees-input-iban <div class="input-group">
.label=${'Bank Account IBAN'} <dees-input-iban
.description=${'Enter your International Bank Account Number'} .label=${'Bank Account IBAN'}
></dees-input-iban> .description=${'Enter your International Bank Account Number'}
></dees-input-iban>
<dees-input-iban
.label=${'Verified IBAN'} <dees-input-iban
.description=${'This IBAN has been verified'} .label=${'Verified IBAN'}
.value=${'DE89370400440532013000'} .description=${'This IBAN has been verified'}
></dees-input-iban> .value=${'DE89370400440532013000'}
></dees-input-iban>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Payment Information'} .subtitle=${'IBAN input with horizontal layout for payment forms'}> <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>
<dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}> <dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}>
<dees-input-iban <div class="input-group">
.label=${'Payment Account'} <dees-input-iban
.description=${'Required for processing payments'} .label=${'Payment Account'}
.required=${true} .description=${'Required for processing payments'}
></dees-input-iban> .required=${true}
></dees-input-iban>
<dees-input-iban
.label=${'Locked IBAN'} <dees-input-iban
.description=${'This IBAN cannot be changed'} .label=${'Locked IBAN'}
.value=${'FR1420041010050500013M02606'} .description=${'This IBAN cannot be changed'}
.disabled=${true} .value=${'FR1420041010050500013M02606'}
></dees-input-iban> .disabled=${true}
></dees-input-iban>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Bank Transfer Form'} .subtitle=${'Complete form example with IBAN validation'}> <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; margin: 0 auto;
} }
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.horizontal-group { .horizontal-group {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -24,18 +30,20 @@ export const demoFunc = () => html`
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}> <dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}>
<dees-input-phone <div class="input-group">
.label=${'Phone Number'} <dees-input-phone
.description=${'Enter your phone number with country code'} .label=${'Phone Number'}
.value=${'5551234567'} .description=${'Enter your phone number with country code'}
></dees-input-phone> .value=${'5551234567'}
></dees-input-phone>
<dees-input-phone
.label=${'Contact Phone'} <dees-input-phone
.description=${'Required for account verification'} .label=${'Contact Phone'}
.required=${true} .description=${'Required for account verification'}
.placeholder=${'+1 (555) 000-0000'} .required=${true}
></dees-input-phone> .placeholder=${'+1 (555) 000-0000'}
></dees-input-phone>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}> <dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}>
@@ -55,17 +63,19 @@ export const demoFunc = () => html`
</dees-panel> </dees-panel>
<dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}> <dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}>
<dees-input-phone <div class="input-group">
.label=${'International Contact'} <dees-input-phone
.description=${'Automatically formats international numbers'} .label=${'International Contact'}
.value=${'441234567890'} .description=${'Automatically formats international numbers'}
></dees-input-phone> .value=${'441234567890'}
></dees-input-phone>
<dees-input-phone
.label=${'Emergency Contact'} <dees-input-phone
.value=${'911'} .label=${'Emergency Contact'}
.disabled=${true} .value=${'911'}
></dees-input-phone> .disabled=${true}
></dees-input-phone>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Form Integration'} .subtitle=${'Phone input as part of a contact form'}> <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; margin: 0 auto;
} }
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.shopping-grid { .shopping-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -60,17 +66,19 @@ export const demoFunc = () => html`
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}> <dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}>
<dees-input-quantityselector <div class="input-group">
.label=${'Quantity'} <dees-input-quantityselector
.description=${'Select the desired quantity'} .label=${'Quantity'}
.value=${1} .description=${'Select the desired quantity'}
></dees-input-quantityselector> .value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Items in Cart'} <dees-input-quantityselector
.description=${'Adjust the quantity of items'} .label=${'Items in Cart'}
.value=${3} .description=${'Adjust the quantity of items'}
></dees-input-quantityselector> .value=${3}
></dees-input-quantityselector>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Modern e-commerce product cards with interactive quantity selectors'} .runAfterRender=${async (elementArg: HTMLElement) => { <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>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}> <dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}>
<dees-input-quantityselector <div class="input-group">
.label=${'Number of Licenses'} <dees-input-quantityselector
.description=${'Select how many licenses you need'} .label=${'Number of Licenses'}
.required=${true} .description=${'Select how many licenses you need'}
.value=${1} .required=${true}
></dees-input-quantityselector> .value=${1}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Fixed Quantity'} <dees-input-quantityselector
.description=${'This quantity cannot be changed'} .label=${'Fixed Quantity'}
.disabled=${true} .description=${'This quantity cannot be changed'}
.value=${5} .disabled=${true}
></dees-input-quantityselector> .value=${5}
></dees-input-quantityselector>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Order Form'} .subtitle=${'Complete order form with quantity selection'}> <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; margin-bottom: 0;
} }
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.demo-grid { .demo-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@@ -60,20 +66,22 @@ export const demoFunc = () => html`
</dees-panel> </dees-panel>
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}> <dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
<dees-input-radiogroup <div class="input-group">
.label=${'Do you agree with the terms?'} <dees-input-radiogroup
.options=${['Yes', 'No', 'Maybe']} .label=${'Do you agree with the terms?'}
.direction=${'horizontal'} .options=${['Yes', 'No', 'Maybe']}
.selectedOption=${'Yes'} .direction=${'horizontal'}
></dees-input-radiogroup> .selectedOption=${'Yes'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Experience Level'} <dees-input-radiogroup
.options=${['Beginner', 'Intermediate', 'Expert']} .label=${'Experience Level'}
.direction=${'horizontal'} .options=${['Beginner', 'Intermediate', 'Expert']}
.selectedOption=${'Intermediate'} .direction=${'horizontal'}
.description=${'Select your experience level with web development'} .selectedOption=${'Intermediate'}
></dees-input-radiogroup> .description=${'Select your experience level with web development'}
></dees-input-radiogroup>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}> <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>
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}> <dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
<dees-input-radiogroup <div class="input-group">
.label=${'Theme Preference'} <dees-input-radiogroup
.options=${[ .label=${'Theme Preference'}
{ option: 'Light Theme', key: 'light', payload: 'light' }, .options=${[
{ option: 'Dark Theme', key: 'dark', payload: 'dark' }, { option: 'Light Theme', key: 'light', payload: 'light' },
{ option: 'System Default', key: 'system', payload: 'auto' } { option: 'Dark Theme', key: 'dark', payload: 'dark' },
]} { option: 'System Default', key: 'system', payload: 'auto' }
.selectedOption=${'dark'} ]}
.description=${'Choose how the application should appear'} .selectedOption=${'dark'}
></dees-input-radiogroup> .description=${'Choose how the application should appear'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Notification Frequency'} <dees-input-radiogroup
.options=${['All Notifications', 'Important Only', 'None']} .label=${'Notification Frequency'}
.selectedOption=${'Important Only'} .options=${['All Notifications', 'Important Only', 'None']}
.description=${'Control how often you receive notifications'} .selectedOption=${'Important Only'}
></dees-input-radiogroup> .description=${'Control how often you receive notifications'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Language'} <dees-input-radiogroup
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']} .label=${'Language'}
.selectedOption=${'English'} .options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
.direction=${'horizontal'} .selectedOption=${'English'}
></dees-input-radiogroup> .direction=${'horizontal'}
></dees-input-radiogroup>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}> <dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>

View File

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

View File

@@ -13,6 +13,12 @@ export const demoFunc = () => html`
margin: 0 auto; margin: 0 auto;
} }
.input-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.horizontal-group { .horizontal-group {
display: flex; display: flex;
gap: 24px; gap: 24px;
@@ -39,27 +45,30 @@ export const demoFunc = () => html`
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}> <dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}>
<dees-input-typelist <div class="input-group">
.label=${'Tags'} <dees-input-typelist
.description=${'Add tags by typing and pressing Enter'} .label=${'Tags'}
.value=${['javascript', 'typescript', 'web-components']} .description=${'Add tags by typing and pressing Enter'}
></dees-input-typelist> .value=${['javascript', 'typescript', 'web-components']}
></dees-input-typelist>
<dees-input-typelist
.label=${'Team Members'} <dees-input-typelist
.description=${'Add email addresses of team members'} .label=${'Team Members'}
.value=${['alice@example.com', 'bob@example.com']} .description=${'Add email addresses of team members'}
></dees-input-typelist> .value=${['alice@example.com', 'bob@example.com']}
></dees-input-typelist>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}> <dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}>
<dees-input-typelist <div class="input-group">
.label=${'Your Skills'} <dees-input-typelist
.description=${'List your professional skills'} .label=${'Your Skills'}
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']} .description=${'List your professional skills'}
></dees-input-typelist> .value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
></dees-input-typelist>
<div class="horizontal-group">
<div class="horizontal-group">
<dees-input-typelist <dees-input-typelist
.label=${'Categories'} .label=${'Categories'}
.layoutMode=${'horizontal'} .layoutMode=${'horizontal'}
@@ -72,22 +81,25 @@ export const demoFunc = () => html`
.value=${['innovation', 'startup', 'growth']} .value=${['innovation', 'startup', 'growth']}
></dees-input-typelist> ></dees-input-typelist>
</div> </div>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}> <dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}>
<dees-input-typelist <div class="input-group">
.label=${'Project Dependencies'} <dees-input-typelist
.description=${'List all required npm packages'} .label=${'Project Dependencies'}
.required=${true} .description=${'List all required npm packages'}
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']} .required=${true}
></dees-input-typelist> .value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
></dees-input-typelist>
<dees-input-typelist
.label=${'System Tags'} <dees-input-typelist
.description=${'These tags are managed by the system'} .label=${'System Tags'}
.disabled=${true} .description=${'These tags are managed by the system'}
.value=${['system', 'protected', 'readonly']} .disabled=${true}
></dees-input-typelist> .value=${['system', 'protected', 'readonly']}
></dees-input-typelist>
</div>
</dees-panel> </dees-panel>
<dees-panel .title=${'Article Publishing Form'} .subtitle=${'Complete form with tag management'}> <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}> = []; accessor thumbnailData: Array<{page: number, rendered: boolean}> = [];
@property({ type: Array }) @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 pdfDocument: any;
private renderState: RenderState = 'idle'; private renderState: RenderState = 'idle';
@@ -63,6 +63,7 @@ export class DeesPdfViewer extends DeesElement {
private currentRenderPromise: Promise<void> | null = null; private currentRenderPromise: Promise<void> | null = null;
private thumbnailRenderTasks: any[] = []; private thumbnailRenderTasks: any[] = [];
private pageRenderTasks: Map<number, any> = new Map(); private pageRenderTasks: Map<number, any> = new Map();
private textLayerRenderTasks: Map<number, any> = new Map();
private canvas: HTMLCanvasElement | undefined; private canvas: HTMLCanvasElement | undefined;
private ctx: CanvasRenderingContext2D | undefined; private ctx: CanvasRenderingContext2D | undefined;
private viewerMain: HTMLElement | null = null; private viewerMain: HTMLElement | null = null;
@@ -230,6 +231,7 @@ export class DeesPdfViewer extends DeesElement {
<div class="page-wrapper" data-page="${item.page}"> <div class="page-wrapper" data-page="${item.page}">
<div class="canvas-container"> <div class="canvas-container">
<canvas class="page-canvas" data-page="${item.page}"></canvas> <canvas class="page-canvas" data-page="${item.page}"></canvas>
<div class="text-layer" data-page="${item.page}"></div>
</div> </div>
</div> </div>
` `
@@ -330,7 +332,8 @@ export class DeesPdfViewer extends DeesElement {
this.pageData = Array.from({length: this.totalPages}, (_, i) => ({ this.pageData = Array.from({length: this.totalPages}, (_, i) => ({
page: i + 1, page: i + 1,
rendered: false, rendered: false,
rendering: false rendering: false,
textLayerRendered: false,
})); }));
// Set loading to false to render the pages // 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 page = await this.pdfDocument.getPage(pageNum);
const viewport = this.computeViewport(page); const viewport = this.computeViewport(page);
// Set canvas dimensions // Set canvas dimensions with device pixel ratio for sharp rendering
canvas.height = viewport.height; const dpr = window.devicePixelRatio || 1;
canvas.width = viewport.width; canvas.width = Math.floor(viewport.width * dpr);
canvas.height = Math.floor(viewport.height * dpr);
canvas.style.width = `${viewport.width}px`; canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`; canvas.style.height = `${viewport.height}px`;
@@ -457,6 +461,9 @@ export class DeesPdfViewer extends DeesElement {
return; return;
} }
// Scale context for high-DPI displays
ctx.scale(dpr, dpr);
const renderContext = { const renderContext = {
canvasContext: ctx, canvasContext: ctx,
viewport: viewport, viewport: viewport,
@@ -472,6 +479,9 @@ export class DeesPdfViewer extends DeesElement {
pageInfo.rendering = false; pageInfo.rendering = false;
this.pageRenderTasks.delete(pageNum); this.pageRenderTasks.delete(pageNum);
// Render text layer for selection
await this.renderTextLayer(pageNum);
// Update page data to reflect rendered state // Update page data to reflect rendered state
this.requestUpdate('pageData'); this.requestUpdate('pageData');
} catch (error: any) { } 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 = () => { private handleScroll = () => {
// Throttle scroll events // Throttle scroll events
if (this.scrollThrottleTimeout) { if (this.scrollThrottleTimeout) {
@@ -652,9 +788,10 @@ export class DeesPdfViewer extends DeesElement {
const scale = maxThumbnailWidth / initialViewport.width; const scale = maxThumbnailWidth / initialViewport.width;
const viewport = page.getViewport({ scale }); const viewport = page.getViewport({ scale });
// Set canvas dimensions to actual render size // Set canvas dimensions with device pixel ratio for sharp thumbnails
canvas.width = viewport.width; const dpr = window.devicePixelRatio || 1;
canvas.height = viewport.height; canvas.width = Math.floor(viewport.width * dpr);
canvas.height = Math.floor(viewport.height * dpr);
// Set the display size via style to ensure proper display // Set the display size via style to ensure proper display
canvas.style.width = `${viewport.width}px`; canvas.style.width = `${viewport.width}px`;
@@ -670,6 +807,9 @@ export class DeesPdfViewer extends DeesElement {
continue; continue;
} }
// Scale context for high-DPI displays
context.scale(dpr, dpr);
const renderContext = { const renderContext = {
canvasContext: context, canvasContext: context,
viewport: viewport, viewport: viewport,
@@ -763,6 +903,7 @@ export class DeesPdfViewer extends DeesElement {
this.pageData.forEach(page => { this.pageData.forEach(page => {
page.rendered = false; page.rendered = false;
page.rendering = false; page.rendering = false;
page.textLayerRendered = false;
}); });
// Cancel any ongoing render tasks // Cancel any ongoing render tasks
@@ -775,6 +916,16 @@ export class DeesPdfViewer extends DeesElement {
}); });
this.pageRenderTasks.clear(); 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 // Request update to re-render pages
this.requestUpdate(); this.requestUpdate();
@@ -784,52 +935,138 @@ export class DeesPdfViewer extends DeesElement {
}); });
} }
private downloadPdf() { private async downloadPdf() {
const link = document.createElement('a'); if (!this.pdfDocument) return;
link.href = this.pdfUrl;
link.download = this.pdfUrl.split('/').pop() || 'document.pdf'; try {
link.click(); // 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() { private async printPdf() {
window.open(this.pdfUrl, '_blank')?.print(); 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 * Provide context menu items for right-click functionality
*/ */
public getContextMenuItems() { public getContextMenuItems() {
return [ const items: any[] = [];
{
name: 'Open PDF in New Tab', // Add copy option if text is selected
iconName: 'lucide:ExternalLink', const selection = window.getSelection();
action: async () => { const selectedText = selection?.toString() || '';
window.open(this.pdfUrl, '_blank'); if (selectedText) {
} items.push({
}, name: 'Copy',
{ divider: true },
{
name: 'Copy PDF URL',
iconName: 'lucide:Copy', iconName: 'lucide:Copy',
action: async () => { action: async () => {
await navigator.clipboard.writeText(this.pdfUrl); await navigator.clipboard.writeText(selectedText);
} }
}, });
items.push({ divider: true });
}
items.push(
{ {
name: 'Download PDF', name: 'Download PDF',
iconName: 'lucide:Download', iconName: 'lucide:Download',
action: async () => { action: async () => {
this.downloadPdf(); await this.downloadPdf();
} }
}, },
{ {
name: 'Print PDF', name: 'Print PDF',
iconName: 'lucide:Printer', iconName: 'lucide:Printer',
action: async () => { action: async () => {
this.printPdf(); await this.printPdf();
} }
} }
]; );
return items;
} }
private get canZoomIn(): boolean { private get canZoomIn(): boolean {
@@ -988,6 +1225,16 @@ export class DeesPdfViewer extends DeesElement {
}); });
this.pageRenderTasks.clear(); 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 // Cancel any thumbnail render tasks
for (const task of (this.thumbnailRenderTasks || [])) { for (const task of (this.thumbnailRenderTasks || [])) {
try { try {

View File

@@ -276,6 +276,7 @@ export const viewerStyles = [
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
position: relative;
} }
.page-canvas { .page-canvas {
@@ -284,6 +285,52 @@ export const viewerStyles = [
image-rendering: crisp-edges; 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 { .pdf-viewer.with-sidebar .viewer-main {
margin-left: 0; 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

@@ -84,20 +84,6 @@ export class DeesTileAudio extends DeesTileBase {
display: block; display: block;
} }
.duration-badge {
position: absolute;
bottom: 8px;
right: 8px;
padding: 3px 8px;
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
border-radius: 4px;
font-size: 10px;
font-weight: 600;
font-variant-numeric: tabular-nums;
backdrop-filter: blur(8px);
z-index: 10;
}
.play-overlay { .play-overlay {
position: absolute; position: absolute;
@@ -177,7 +163,7 @@ export class DeesTileAudio extends DeesTileBase {
</div> </div>
${this.duration > 0 ? html` ${this.duration > 0 ? html`
<div class="duration-badge">${this.formatTime(this.duration)}</div> <div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
` : ''} ` : ''}
<div class="play-overlay"> <div class="play-overlay">

View File

@@ -109,19 +109,6 @@ export class DeesTileFolder extends DeesTileBase {
background: ${cssManager.bdTheme('hsl(215 15% 96%)', 'hsl(215 20% 16%)')}; background: ${cssManager.bdTheme('hsl(215 15% 96%)', 'hsl(215 20% 16%)')};
} }
.item-count-badge {
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;
backdrop-filter: blur(8px);
z-index: 10;
}
`, `,
] as any; ] as any;
@@ -158,7 +145,7 @@ export class DeesTileFolder extends DeesTileBase {
</div> </div>
</div> </div>
<div class="item-count-badge"> <div class="tile-badge-corner">
${this.items.length} item${this.items.length !== 1 ? 's' : ''} ${this.items.length} item${this.items.length !== 1 ? 's' : ''}
</div> </div>

View File

@@ -55,24 +55,12 @@ export class DeesTileImage extends DeesTileBase {
opacity: 0; opacity: 0;
} }
.dimension-badge { .tile-badge-topright.dimension-badge {
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;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
.tile-container.clickable:hover .dimension-badge { .tile-container.clickable:hover .tile-badge-topright.dimension-badge {
opacity: 1; opacity: 1;
} }
`, `,
@@ -110,7 +98,7 @@ export class DeesTileImage extends DeesTileBase {
</div> </div>
${this.imageWidth > 0 && this.imageHeight > 0 ? html` ${this.imageWidth > 0 && this.imageHeight > 0 ? html`
<div class="dimension-badge"> <div class="tile-badge-topright dimension-badge">
${this.imageWidth} × ${this.imageHeight} ${this.imageWidth} × ${this.imageHeight}
</div> </div>
` : ''} ` : ''}

View File

@@ -81,16 +81,10 @@ export class DeesTileNote extends DeesTileBase {
pointer-events: none; pointer-events: none;
} }
.note-language { .tile-badge-topright.note-language {
position: absolute;
top: 8px;
right: 8px;
padding: 2px 6px;
background: ${cssManager.bdTheme('hsl(215 20% 92%)', 'hsl(215 20% 88%)')}; background: ${cssManager.bdTheme('hsl(215 20% 92%)', 'hsl(215 20% 88%)')};
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 40%)')}; color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 40%)')};
border-radius: 3px;
font-size: 9px; font-size: 9px;
font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
z-index: 5; z-index: 5;
} }
@@ -116,21 +110,6 @@ export class DeesTileNote extends DeesTileBase {
padding-right: 6px; padding-right: 6px;
} }
.note-line-indicator {
position: absolute;
bottom: 8px;
right: 8px;
padding: 3px 8px;
background: rgba(0, 0, 0, 0.6);
color: white;
border-radius: 4px;
font-size: 9px;
font-weight: 600;
font-variant-numeric: tabular-nums;
backdrop-filter: blur(8px);
z-index: 10;
pointer-events: none;
}
`, `,
] as any; ] as any;
@@ -154,7 +133,7 @@ export class DeesTileNote extends DeesTileBase {
return html` return html`
<div class="note-content"> <div class="note-content">
${this.language ? html` ${this.language ? html`
<div class="note-language">${this.language}</div> <div class="tile-badge-topright note-language">${this.language}</div>
` : ''} ` : ''}
${this.title ? html` ${this.title ? html`
@@ -169,7 +148,7 @@ export class DeesTileNote extends DeesTileBase {
</div> </div>
${this.isHovering && lines.length > 12 ? html` ${this.isHovering && lines.length > 12 ? html`
<div class="note-line-indicator"> <div class="tile-badge-corner">
Line ${this.getVisibleLineRange(lines.length)} Line ${this.getVisibleLineRange(lines.length)}
</div> </div>
` : ''} ` : ''}

View File

@@ -55,15 +55,14 @@ export class DeesTilePdf extends DeesTileBase {
</div> </div>
${this.pageCount > 1 && this.isHovering ? html` ${this.pageCount > 1 && this.isHovering ? html`
<div class="preview-page-indicator"> <div class="tile-badge">
Page ${this.currentPreviewPage} of ${this.pageCount} Page ${this.currentPreviewPage} of ${this.pageCount}
</div> </div>
` : ''} ` : ''}
${this.pageCount > 0 && !this.isHovering ? html` ${this.pageCount > 0 && !this.isHovering ? html`
<div class="tile-info"> <div class="tile-badge-corner">
<dees-icon icon="lucide:FileText"></dees-icon> ${this.pageCount} page${this.pageCount > 1 ? 's' : ''}
<span class="tile-info-text">${this.pageCount} page${this.pageCount > 1 ? 's' : ''}</span>
</div> </div>
` : ''} ` : ''}

View File

@@ -35,24 +35,6 @@ export const tilePdfStyles = css`
border-radius: 4px; border-radius: 4px;
} }
.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;
}
/* Grid optimizations */ /* Grid optimizations */
:host([grid-mode]) .preview-canvas { :host([grid-mode]) .preview-canvas {
image-rendering: -webkit-optimize-contrast; image-rendering: -webkit-optimize-contrast;

View File

@@ -117,6 +117,46 @@ export const tileBaseStyles = [
animation: fadeIn 0.2s ease; animation: fadeIn 0.2s ease;
} }
.tile-badge-corner {
position: absolute;
bottom: 8px;
right: 8px;
padding: 3px 8px;
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
border-radius: 4px;
font-size: 10px;
font-weight: 600;
font-variant-numeric: tabular-nums;
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;
}
/* Shift bottom badges up when label is present */
.tile-container:has(.tile-label) .tile-badge-corner {
bottom: 33px;
}
.tile-container:has(.tile-label) .tile-info {
bottom: 33px;
}
.tile-loading, .tile-loading,
.tile-error { .tile-error {
position: absolute; position: absolute;

View File

@@ -54,20 +54,6 @@ export class DeesTileVideo extends DeesTileBase {
display: block; display: block;
} }
.duration-badge {
position: absolute;
bottom: 8px;
right: 8px;
padding: 3px 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
font-variant-numeric: tabular-nums;
backdrop-filter: blur(8px);
z-index: 10;
}
.play-overlay { .play-overlay {
position: absolute; position: absolute;
@@ -155,7 +141,7 @@ export class DeesTileVideo extends DeesTileBase {
</div> </div>
${this.duration > 0 ? html` ${this.duration > 0 ? html`
<div class="duration-badge">${this.formatTime(this.duration)}</div> <div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
` : ''} ` : ''}
${!this.isHovering ? html` ${!this.isHovering ? html`

View File

@@ -5,8 +5,6 @@ export * from './dees-video-viewer/index.js';
export * from './dees-preview/index.js'; export * from './dees-preview/index.js';
// PDF Components // 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-shared/index.js';
export * from './dees-pdf-viewer/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; const domtools = await this.domtoolsPromise;
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
if (this.viewTabs && this.viewTabs.length > 0) { 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 { TemplateResult } from '@design.estate/dees-element';
import type { IAppBarMenuItem } from './appbarmenuitem.js'; 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 { IMenuGroup } from './menugroup.js';
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from './secondarymenu.js'; import type { ISecondaryMenuGroup, ISecondaryMenuItem } from './secondarymenu.js';
@@ -134,6 +134,8 @@ export type TDeesAppui = HTMLElement & {
removeContentTab: (tabKey: string) => void; removeContentTab: (tabKey: string) => void;
selectContentTab: (tabKey: string) => void; selectContentTab: (tabKey: string) => void;
getSelectedContentTab: () => IMenuItem | undefined; getSelectedContentTab: () => IMenuItem | undefined;
setContentTabActionsLeft: (actions: ITabAction[]) => void;
setContentTabActionsRight: (actions: ITabAction[]) => void;
activityLog: IActivityLogAPI; activityLog: IActivityLogAPI;
setActivityLogVisible: (visible: boolean) => void; setActivityLogVisible: (visible: boolean) => void;
toggleActivityLog: () => void; toggleActivityLog: () => void;

View File

@@ -7,3 +7,11 @@ export interface IMenuItem {
closeable?: boolean; closeable?: boolean;
onClose?: () => void; 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 response = await fetch(cssUrl);
const cssText = await response.text(); 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'); const style = document.createElement('style');
style.id = styleId; style.id = styleId;
style.textContent = cssText; style.textContent = cssText + xtermMeasurementFix;
document.head.appendChild(style); document.head.appendChild(style);
} }

View File

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