Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3285cbf0e7 | |||
| a2d750b2f6 | |||
| d4276710e6 | |||
| 66d64bf476 | |||
| 2504251707 | |||
| fed130f291 | |||
| 4f05b5907b | |||
| e517320dcd | |||
| ade5a25b3a | |||
| a396dfea12 | |||
| d0105e1b80 | |||
| 1eeebb35e6 | |||
| 14e8b8c533 | |||
| eaf327ea75 | |||
| 09741e0b37 | |||
| 5cadd1fc7f | |||
| 1795235c6d | |||
| ba7d387acb | |||
| 26ca16a284 | |||
| 3ab3eb5e5e | |||
| da5dbc70e2 | |||
| 19b7981542 | |||
| bad105074e | |||
| f124091784 | |||
| 68790a26ed | |||
| 2abf7e356d | |||
| ecd35683e6 | |||
| 93cb632448 | |||
| 047e42c6a3 | |||
| 59efa8cff0 | |||
| 09f0aa97dd | |||
| 7c62f45d77 | |||
| b123768474 | |||
| f292e7a7f4 | |||
| d82e5603a7 | |||
| 7e2386bcdf | |||
| eba2a03355 | |||
| 06c01f0690 | |||
| 91e03eb9c4 | |||
| b7f3f47c61 | |||
| 0a83f0e136 | |||
| 2b048cf34f | |||
| 7e50b8cb3f |
150
changelog.md
150
changelog.md
@@ -1,5 +1,155 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-11 - 3.47.2 - fix(deps)
|
||||||
|
bump @design.estate/dees-domtools and @design.estate/dees-element dependencies
|
||||||
|
|
||||||
|
- update @design.estate/dees-domtools from ^2.3.9 to ^2.5.1
|
||||||
|
- update @design.estate/dees-element from ^2.2.1 to ^2.2.2
|
||||||
|
|
||||||
|
## 2026-03-11 - 3.47.1 - fix(dees-statsgrid)
|
||||||
|
add tablet breakpoint to render stats grid as three columns
|
||||||
|
|
||||||
|
- Added cssManager.cssForTablet rule to set .stats-grid grid-template-columns: repeat(3, 1fr).
|
||||||
|
- Improves responsive layout on tablet devices for dees-statsgrid tiles.
|
||||||
|
- Change made in ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
|
||||||
|
|
||||||
|
## 2026-03-11 - 3.47.0 - feat(dees-statsgrid)
|
||||||
|
add container-responsive behavior and responsive CSS to dees-statsgrid; bump @design.estate/dees-element dependency to ^2.2.1
|
||||||
|
|
||||||
|
- Added @containerResponsive decorator and import to dees-statsgrid
|
||||||
|
- Added cssManager.cssForPhablet and cssManager.cssForPhone responsive style blocks to adjust layout, spacing and font sizes on smaller viewports
|
||||||
|
- Bumped dependency @design.estate/dees-element from ^2.1.6 to ^2.2.1
|
||||||
|
|
||||||
|
## 2026-03-10 - 3.46.1 - fix(dees-appui)
|
||||||
|
add min-height: 0 to mainmenu and secondarymenu to prevent unintended container height and fix layout stacking
|
||||||
|
|
||||||
|
- Modified ts_web/elements/00group-appui/dees-appui/dees-appui.ts: added min-height: 0 to .maingrid > dees-appui-mainmenu and .maingrid > dees-appui-secondarymenu
|
||||||
|
- Fixes layout issues where children or flexbox-derived min-height could cause menu containers to expand and interfere with z-index stacking
|
||||||
|
|
||||||
|
## 2026-03-10 - 3.46.0 - feat(dees-tile)
|
||||||
|
unify tile metadata into a consistent bottom info bar and add PDF file-size display
|
||||||
|
|
||||||
|
- Introduce renderBottomBar() hook in DeesTileBase and remove per-component bottom badges/labels in favor of a unified info bar.
|
||||||
|
- Implement renderBottomBar in audio, video, image, folder, note and pdf tiles to show label, counts, dimensions, duration, language/line info and page counts.
|
||||||
|
- PDF tile: add fileSize state, attempt to read download info and display formatted file size in the info bar; show currentPreviewPage/pageCount when hovering.
|
||||||
|
- Styling changes: replace legacy badges/labels with .tile-info-bar (.info-label, .info-detail, .info-spacer); adjust padding, font sizing, z-index, and remove hover translate for clickable tiles.
|
||||||
|
- PDF demo and styles: use cssManager theming for demo colors and adjust preview padding.
|
||||||
|
- Bump devDependencies: @git.zone/tswatch -> ^3.3.0 and @types/node -> ^25.4.0
|
||||||
|
|
||||||
|
## 2026-03-10 - 3.45.1 - fix(dees-appui)
|
||||||
|
substitute route params into URL hash when navigating
|
||||||
|
|
||||||
|
- Replaces :param placeholders in view.route with provided params before updating the URL hash.
|
||||||
|
- Ensures window.history.pushState is called with the resolved route so URLs do not contain literal parameter tokens.
|
||||||
|
|
||||||
|
## 2026-03-10 - 3.45.0 - feat(dees-form)
|
||||||
|
register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates
|
||||||
|
|
||||||
|
- Added imports and registration of DeesInputTags, DeesInputList, DeesInputWysiwyg, and DeesInputRichtext in dees-form
|
||||||
|
- Extended TFormInputElement union type to include the new input components
|
||||||
|
- DeesInputRichtext now calls changeSubject.next(this.value) in the editor onUpdate handler to propagate changes
|
||||||
|
|
||||||
|
## 2026-03-10 - 3.44.0 - feat(appui-tabs)
|
||||||
|
add support for left/right tab action buttons and content tab action APIs
|
||||||
|
|
||||||
|
- Introduce ITabAction interface and add actionsLeft/actionsRight properties to dees-appui-tabs, dees-appui-maincontent, and dees-appui.
|
||||||
|
- Render action buttons with new styles and renderActions() helper, including disabled state and click handlers; wire actions into tab components.
|
||||||
|
- Add public clear() on dees-appui-tabs and improve tab selection logic to reset selection when tabs become empty or when the selected tab is removed.
|
||||||
|
- Expose setContentTabActionsLeft and setContentTabActionsRight on the DeesAppui programmatic API and update interfaces/appconfig accordingly.
|
||||||
|
- Update demos to showcase action buttons, add clear-all behavior, and adjust layout/styling for action areas.
|
||||||
|
|
||||||
|
## 2026-03-09 - 3.43.4 - fix(media)
|
||||||
|
remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies
|
||||||
|
|
||||||
|
- Removed deprecated PDF components and related demos/styles: ts_web/elements/00group-media/dees-pdf/* and ts_web/elements/00group-media/dees-pdf-preview/*
|
||||||
|
- Removed exports for dees-pdf and dees-pdf-preview from ts_web/elements/00group-media/index.ts (public API removal)
|
||||||
|
- Dependency upgrades: @design.estate/dees-domtools → ^2.3.9, apexcharts → ^5.10.3, lucide → ^0.577.0, @fortawesome/* → ^7.2.0
|
||||||
|
- DevDependency upgrades: @git.zone/tsbuild → ^4.3.0, @git.zone/tsbundle → ^2.9.1, @git.zone/tstest → ^3.3.2, @git.zone/tswatch → ^3.2.5, @types/node → ^25.3.5
|
||||||
|
- Updated ts_web/services/versions.ts to align CDN/version constants (apexcharts, tiptap → 2.27.2, fontawesome)
|
||||||
|
|
||||||
|
## 2026-02-24 - 3.43.3 - fix(dees-table)
|
||||||
|
use lucide icon identifier for Search action in dees-table
|
||||||
|
|
||||||
|
- Replaced iconName 'magnifyingGlass' with 'lucide:Search' in ts_web/elements/00group-dataview/dees-table/dees-table.ts
|
||||||
|
- Updates the icon identifier for the header 'Search' action; no functional behavior changed
|
||||||
|
|
||||||
|
## 2026-02-21 - 3.43.2 - fix(dees-chart-log)
|
||||||
|
avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs
|
||||||
|
|
||||||
|
- Prevent property-bound logEntries from duplicating entries already present in logBuffer by deduplicating on timestamp|message
|
||||||
|
- Call updateMetrics() when replaying or appending log entries so metrics stay accurate
|
||||||
|
- Skip processing entirely when the incoming logEntries array is unchanged
|
||||||
|
- Optimize append-only updates by writing only the new tail entries instead of full re-render
|
||||||
|
- Enforce maxEntries when appending to the logBuffer to maintain buffer size
|
||||||
|
- Respect filterMode and searchQuery when deciding whether to write appended entries to the terminal
|
||||||
|
|
||||||
|
## 2026-02-21 - 3.43.1 - fix(dees-chart-log)
|
||||||
|
replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs
|
||||||
|
|
||||||
|
- Replay entries stored in logBuffer after terminal initialization to avoid losing entries that arrived early
|
||||||
|
- Add updated() lifecycle hook to copy logEntries into logBuffer and call reRenderFilteredLogs when terminalReady
|
||||||
|
- Call super.updated(changedProperties) to preserve base class behavior
|
||||||
|
|
||||||
|
## 2026-02-17 - 3.43.0 - feat(dees-form)
|
||||||
|
add layout styles to dees-form and standardize demo input grouping
|
||||||
|
|
||||||
|
- Add static CSS to dees-form: default column layout with gap and support for [horizontal-layout] (row wrapping, alignment and gap).
|
||||||
|
- Remove inline <style> from dees-form render to centralize styling.
|
||||||
|
- Simplify dees-input-base styles by removing host margins and making spacing container-driven.
|
||||||
|
- Update multiple demo files to wrap related inputs in a new .input-group container and include .input-group CSS for consistent vertical spacing.
|
||||||
|
|
||||||
|
## 2026-02-16 - 3.42.2 - fix(dees-chart-area)
|
||||||
|
add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data
|
||||||
|
|
||||||
|
- Introduces ApexAxisChartSeries type alias with support for number arrays, [x,y] tuples, and object {x,y,...} series entries
|
||||||
|
- Type-only change — no runtime or API behavior modified
|
||||||
|
- File changed: ts_web/elements/00group-chart/dees-chart-area/component.ts
|
||||||
|
|
||||||
|
## 2026-02-16 - 3.42.1 - fix(dees-table)
|
||||||
|
Guard against undefined action.type in dees-table by using optional chaining and update several dependencies
|
||||||
|
|
||||||
|
- Use optional chaining (action.type?.includes(...)) in ts_web/elements/.../dees-table.ts to prevent runtime errors when action.type is undefined
|
||||||
|
- Bump dependency apexcharts from ^5.3.6 to ^5.5.0
|
||||||
|
- Bump dependency lucide from ^0.563.0 to ^0.564.0
|
||||||
|
- Bump devDependency @git.zone/tswatch from ^3.0.1 to ^3.1.0
|
||||||
|
- Bump devDependency @types/node from ^25.0.10 to ^25.2.3
|
||||||
|
|
||||||
|
## 2026-02-02 - 3.42.0 - feat(dees-form-submit)
|
||||||
|
forward button properties to internal dees-button, use property bindings, add demo and styles
|
||||||
|
|
||||||
|
- Added forwarded properties: type, size, icon, iconPosition (with defaults) and preserved text/status/disabled
|
||||||
|
- Changed template to use property bindings (.prop) for dees-button instead of string attributes
|
||||||
|
- Switched internal event handler to listen for dees-button's @clicked event (was @click)
|
||||||
|
- Added component styles (:host display and dees-button width:100%) and improved layout
|
||||||
|
- Expanded demo with multiple usage examples (basic, icons, types, sizes, states, and a form context)
|
||||||
|
|
||||||
|
## 2026-02-02 - 3.41.6 - fix(dees-simple-appdash)
|
||||||
|
respect selectedView when loading initial view, falling back to the first tab
|
||||||
|
|
||||||
|
- firstUpdated now loads this.selectedView if set, otherwise loads the first view tab
|
||||||
|
- Prevents always loading the first tab and preserves a previously selected view on initial render
|
||||||
|
|
||||||
|
## 2026-02-01 - 3.41.5 - fix(dees-service-lib-loader)
|
||||||
|
prevent horizontal scrollbar by offsetting xterm WidthCache measurement container
|
||||||
|
|
||||||
|
- Injects additional CSS into DeesServiceLibLoader to move xterm.js WidthCache measurement div off-screen horizontally (selector: body > div[style*="top: -50000px"][style*="width: 50000px"])
|
||||||
|
- Fixes root cause where xterm creates a large-width measurement container (width: 50000px) on document.body that expands scrollWidth and causes a horizontal scrollbar
|
||||||
|
- Change applied in ts_web/services/DeesServiceLibLoader.ts by concatenating the fix CSS into the injected stylesheet
|
||||||
|
|
||||||
|
## 2026-01-29 - 3.41.4 - fix()
|
||||||
|
no changes
|
||||||
|
|
||||||
|
- No files changed in this commit; no code or documentation modified
|
||||||
|
- No release required
|
||||||
|
|
||||||
|
## 2026-01-29 - 3.41.3 - fix(dees-pdf-viewer)
|
||||||
|
use in-memory PDF data for download and print; add robust print wrapper, cleanup and error handling
|
||||||
|
|
||||||
|
- Download and Print now use pdfDocument.getData() to create Blob URLs so in-memory PDFs (pdf.js) can be saved/printed.
|
||||||
|
- Print flow now opens an HTML wrapper with an iframe to allow onafterprint handling, auto-close, popup-fallback and timed cleanup of Blob URLs.
|
||||||
|
- Added try/catch logging, URL.revokeObjectURL calls and safety timeouts to avoid resource leaks.
|
||||||
|
- Removed context menu items that relied on the raw PDF URL (Open in New Tab, Copy PDF URL); Download/Print actions now await the async handlers.
|
||||||
|
|
||||||
## 2026-01-28 - 3.41.2 - fix(dees-pdf-viewer)
|
## 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
|
account for devicePixelRatio when setting canvas dimensions and scale 2D context to render crisp PDF pages and thumbnails on high-DPI displays
|
||||||
|
|
||||||
|
|||||||
28
package.json
28
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-catalog",
|
"name": "@design.estate/dees-catalog",
|
||||||
"version": "3.41.2",
|
"version": "3.47.2",
|
||||||
"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.8",
|
"@design.estate/dees-domtools": "^2.5.1",
|
||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.2.2",
|
||||||
"@design.estate/dees-wcctools": "^3.8.0",
|
"@design.estate/dees-wcctools": "^3.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.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.3.0",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@types/node": "^25.0.10"
|
"@types/node": "^25.4.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
|||||||
4848
pnpm-lock.yaml
generated
4848
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
79
test/test.pdf-text-selection.chromium.ts
Normal file
79
test/test.pdf-text-selection.chromium.ts
Normal 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();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
name: '@design.estate/dees-catalog',
|
||||||
version: '3.41.2',
|
version: '3.47.2',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ export class DeesAppui extends DeesElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
|
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
accessor contentTabActionsLeft: interfaces.ITabAction[] = [];
|
||||||
|
|
||||||
|
@property({ type: Array })
|
||||||
|
accessor contentTabActionsRight: interfaces.ITabAction[] = [];
|
||||||
|
|
||||||
// References to child components
|
// References to child components
|
||||||
@state()
|
@state()
|
||||||
accessor appbar: DeesAppuiBar | undefined = undefined;
|
accessor appbar: DeesAppuiBar | undefined = undefined;
|
||||||
@@ -213,11 +219,13 @@ export class DeesAppui extends DeesElement {
|
|||||||
.maingrid > dees-appui-mainmenu {
|
.maingrid > dees-appui-mainmenu {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maingrid > dees-appui-secondarymenu {
|
.maingrid > dees-appui-secondarymenu {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maingrid > dees-appui-maincontent {
|
.maingrid > dees-appui-maincontent {
|
||||||
@@ -306,6 +314,8 @@ export class DeesAppui extends DeesElement {
|
|||||||
.showTabs=${this.maincontentTabsVisible}
|
.showTabs=${this.maincontentTabsVisible}
|
||||||
.tabsAutoHide=${this.contentTabsAutoHide}
|
.tabsAutoHide=${this.contentTabsAutoHide}
|
||||||
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
|
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
|
||||||
|
.tabActionsLeft=${this.contentTabActionsLeft}
|
||||||
|
.tabActionsRight=${this.contentTabActionsRight}
|
||||||
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
||||||
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
|
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
|
||||||
>
|
>
|
||||||
@@ -699,6 +709,20 @@ export class DeesAppui extends DeesElement {
|
|||||||
return this.maincontentSelectedTab;
|
return this.maincontentSelectedTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set content tab action buttons on the left side
|
||||||
|
*/
|
||||||
|
public setContentTabActionsLeft(actions: interfaces.ITabAction[]): void {
|
||||||
|
this.contentTabActionsLeft = [...actions];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set content tab action buttons on the right side
|
||||||
|
*/
|
||||||
|
public setContentTabActionsRight(actions: interfaces.ITabAction[]): void {
|
||||||
|
this.contentTabActionsRight = [...actions];
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// PROGRAMMATIC API: ACTIVITY LOG
|
// PROGRAMMATIC API: ACTIVITY LOG
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -853,8 +877,13 @@ export class DeesAppui extends DeesElement {
|
|||||||
try {
|
try {
|
||||||
await this.loadView(view, params);
|
await this.loadView(view, params);
|
||||||
|
|
||||||
// Update URL hash
|
// Update URL hash (substitute params into route pattern)
|
||||||
const route = view.route || viewId;
|
let route = view.route || viewId;
|
||||||
|
if (params) {
|
||||||
|
for (const [key, val] of Object.entries(params)) {
|
||||||
|
route = route.replace(`:${key}`, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
const newHash = `#${route}`;
|
const newHash = `#${route}`;
|
||||||
if (window.location.hash !== newHash) {
|
if (window.location.hash !== newHash) {
|
||||||
window.history.pushState({ viewId }, '', newHash);
|
window.history.pushState({ viewId }, '', newHash);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
cssManager,
|
cssManager,
|
||||||
|
containerResponsive,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import type { TemplateResult } from '@design.estate/dees-element';
|
import type { TemplateResult } from '@design.estate/dees-element';
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ export interface IStatsTile {
|
|||||||
actions?: plugins.tsclass.website.IMenuItem[];
|
actions?: plugins.tsclass.website.IMenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@containerResponsive()
|
||||||
@customElement('dees-statsgrid')
|
@customElement('dees-statsgrid')
|
||||||
export class DeesStatsGrid extends DeesElement {
|
export class DeesStatsGrid extends DeesElement {
|
||||||
public static demo = demoFunc;
|
public static demo = demoFunc;
|
||||||
@@ -801,6 +803,38 @@ export class DeesStatsGrid extends DeesElement {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
// Container-responsive: when this statsgrid is narrow
|
||||||
|
cssManager.cssForTablet(css`
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
`, this),
|
||||||
|
cssManager.cssForPhablet(css`
|
||||||
|
:host {
|
||||||
|
--tile-padding: 12px;
|
||||||
|
--value-font-size: 22px;
|
||||||
|
--title-font-size: 12px;
|
||||||
|
--grid-gap: 8px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.stats-tile {
|
||||||
|
grid-column: span 1 !important;
|
||||||
|
}
|
||||||
|
`, this),
|
||||||
|
cssManager.cssForPhone(css`
|
||||||
|
:host {
|
||||||
|
--tile-padding: 10px;
|
||||||
|
--value-font-size: 20px;
|
||||||
|
--header-spacing: 8px;
|
||||||
|
}
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
`, this),
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
css,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
DeesElement,
|
DeesElement,
|
||||||
type CSSResult,
|
type CSSResult,
|
||||||
@@ -21,6 +22,10 @@ import { DeesInputMultitoggle } from '../../00group-input/dees-input-multitoggle
|
|||||||
import { DeesInputPhone } from '../../00group-input/dees-input-phone/dees-input-phone.js';
|
import { DeesInputPhone } from '../../00group-input/dees-input-phone/dees-input-phone.js';
|
||||||
import { DeesInputToggle } from '../../00group-input/dees-input-toggle/dees-input-toggle.js';
|
import { DeesInputToggle } from '../../00group-input/dees-input-toggle/dees-input-toggle.js';
|
||||||
import { DeesInputTypelist } from '../../00group-input/dees-input-typelist/dees-input-typelist.js';
|
import { DeesInputTypelist } from '../../00group-input/dees-input-typelist/dees-input-typelist.js';
|
||||||
|
import { DeesInputTags } from '../../00group-input/dees-input-tags/dees-input-tags.js';
|
||||||
|
import { DeesInputList } from '../../00group-input/dees-input-list/dees-input-list.js';
|
||||||
|
import { DeesInputWysiwyg } from '../../00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
|
||||||
|
import { DeesInputRichtext } from '../../00group-input/dees-input-richtext/component.js';
|
||||||
import { DeesFormSubmit } from '../dees-form-submit/dees-form-submit.js';
|
import { DeesFormSubmit } from '../dees-form-submit/dees-form-submit.js';
|
||||||
import { DeesTable } from '../../00group-dataview/dees-table/index.js';
|
import { DeesTable } from '../../00group-dataview/dees-table/index.js';
|
||||||
import { demoFunc } from './dees-form.demo.js';
|
import { demoFunc } from './dees-form.demo.js';
|
||||||
@@ -40,6 +45,10 @@ const FORM_INPUT_TYPES = [
|
|||||||
DeesInputText,
|
DeesInputText,
|
||||||
DeesInputToggle,
|
DeesInputToggle,
|
||||||
DeesInputTypelist,
|
DeesInputTypelist,
|
||||||
|
DeesInputTags,
|
||||||
|
DeesInputList,
|
||||||
|
DeesInputWysiwyg,
|
||||||
|
DeesInputRichtext,
|
||||||
DeesTable,
|
DeesTable,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -57,6 +66,10 @@ export type TFormInputElement =
|
|||||||
| DeesInputText
|
| DeesInputText
|
||||||
| DeesInputToggle
|
| DeesInputToggle
|
||||||
| DeesInputTypelist
|
| DeesInputTypelist
|
||||||
|
| DeesInputTags
|
||||||
|
| DeesInputList
|
||||||
|
| DeesInputWysiwyg
|
||||||
|
| DeesInputRichtext
|
||||||
| DeesTable<any>;
|
| DeesTable<any>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -81,13 +94,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
|||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
this.value = editor.getHTML();
|
this.value = editor.getHTML();
|
||||||
this.updateWordCount();
|
this.updateWordCount();
|
||||||
|
this.changeSubject.next(this.value);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('input', {
|
new CustomEvent('input', {
|
||||||
detail: { value: this.value },
|
detail: { value: this.value },
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './component.js';
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
@@ -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
|
||||||
@@ -476,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) {
|
||||||
@@ -487,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) {
|
||||||
@@ -771,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
|
||||||
@@ -783,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();
|
||||||
|
|
||||||
@@ -792,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 {
|
||||||
@@ -996,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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './component.js';
|
|
||||||
@@ -162,10 +162,6 @@ export class DeesTileAudio extends DeesTileBase {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.duration > 0 ? html`
|
|
||||||
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<div class="play-overlay">
|
<div class="play-overlay">
|
||||||
<div class="play-circle">
|
<div class="play-circle">
|
||||||
<dees-icon icon="lucide:Play"></dees-icon>
|
<dees-icon icon="lucide:Play"></dees-icon>
|
||||||
@@ -181,6 +177,17 @@ export class DeesTileAudio extends DeesTileBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
if (!this.label && !this.duration) return '';
|
||||||
|
return html`
|
||||||
|
<div class="tile-info-bar">
|
||||||
|
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||||
|
<span class="info-spacer"></span>
|
||||||
|
${this.duration > 0 ? html`<span class="info-detail">${this.formatTime(this.duration)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTileClickDetail(): Record<string, unknown> {
|
protected getTileClickDetail(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
|
|||||||
@@ -145,10 +145,6 @@ export class DeesTileFolder extends DeesTileBase {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tile-badge-corner">
|
|
||||||
${this.items.length} item${this.items.length !== 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.clickable ? html`
|
${this.clickable ? html`
|
||||||
<div class="tile-overlay">
|
<div class="tile-overlay">
|
||||||
<dees-icon icon="lucide:FolderOpen"></dees-icon>
|
<dees-icon icon="lucide:FolderOpen"></dees-icon>
|
||||||
@@ -158,6 +154,17 @@ export class DeesTileFolder extends DeesTileBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
if (!this.label && !this.items.length) return '';
|
||||||
|
return html`
|
||||||
|
<div class="tile-info-bar">
|
||||||
|
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||||
|
<span class="info-spacer"></span>
|
||||||
|
<span class="info-detail">${this.items.length} item${this.items.length !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTileClickDetail(): Record<string, unknown> {
|
protected getTileClickDetail(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
|
|||||||
@@ -55,14 +55,6 @@ export class DeesTileImage extends DeesTileBase {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-badge-topright.dimension-badge {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-container.clickable:hover .tile-badge-topright.dimension-badge {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
] as any;
|
] as any;
|
||||||
|
|
||||||
@@ -97,19 +89,6 @@ export class DeesTileImage extends DeesTileBase {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.imageWidth > 0 && this.imageHeight > 0 ? html`
|
|
||||||
<div class="tile-badge-topright dimension-badge">
|
|
||||||
${this.imageWidth} × ${this.imageHeight}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.imageLoaded ? html`
|
|
||||||
<div class="tile-info">
|
|
||||||
<dees-icon icon="lucide:Image"></dees-icon>
|
|
||||||
<span class="tile-info-text">${this.imageWidth} × ${this.imageHeight}</span>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.clickable ? html`
|
${this.clickable ? html`
|
||||||
<div class="tile-overlay">
|
<div class="tile-overlay">
|
||||||
<dees-icon icon="lucide:Eye"></dees-icon>
|
<dees-icon icon="lucide:Eye"></dees-icon>
|
||||||
@@ -119,6 +98,19 @@ export class DeesTileImage extends DeesTileBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
if (!this.label && !(this.imageWidth > 0)) return '';
|
||||||
|
return html`
|
||||||
|
<div class="tile-info-bar">
|
||||||
|
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||||
|
<span class="info-spacer"></span>
|
||||||
|
${this.imageWidth > 0 && this.imageHeight > 0
|
||||||
|
? html`<span class="info-detail">${this.imageWidth} × ${this.imageHeight}</span>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTileClickDetail(): Record<string, unknown> {
|
protected getTileClickDetail(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
|
|||||||
@@ -81,14 +81,6 @@ export class DeesTileNote extends DeesTileBase {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-badge-topright.note-language {
|
|
||||||
background: ${cssManager.bdTheme('hsl(215 20% 92%)', 'hsl(215 20% 88%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 40%)')};
|
|
||||||
font-size: 9px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-lines {
|
.note-lines {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -132,10 +124,6 @@ export class DeesTileNote extends DeesTileBase {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="note-content">
|
<div class="note-content">
|
||||||
${this.language ? html`
|
|
||||||
<div class="tile-badge-topright note-language">${this.language}</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.title ? html`
|
${this.title ? html`
|
||||||
<div class="note-header">
|
<div class="note-header">
|
||||||
<div class="note-title">${this.title}</div>
|
<div class="note-title">${this.title}</div>
|
||||||
@@ -147,11 +135,6 @@ export class DeesTileNote extends DeesTileBase {
|
|||||||
${!this.isHovering ? html`<div class="note-fade"></div>` : ''}
|
${!this.isHovering ? html`<div class="note-fade"></div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.isHovering && lines.length > 12 ? html`
|
|
||||||
<div class="tile-badge-corner">
|
|
||||||
Line ${this.getVisibleLineRange(lines.length)}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.clickable ? html`
|
${this.clickable ? html`
|
||||||
@@ -163,6 +146,21 @@ export class DeesTileNote extends DeesTileBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
const lines = this.content.split('\n');
|
||||||
|
if (!this.label && !this.language && !lines.length) return '';
|
||||||
|
return html`
|
||||||
|
<div class="tile-info-bar">
|
||||||
|
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||||
|
<span class="info-spacer"></span>
|
||||||
|
${this.language ? html`<span class="info-detail">${this.language.toUpperCase()}</span>` : ''}
|
||||||
|
${this.isHovering && lines.length > 12
|
||||||
|
? html`<span class="info-detail">Line ${this.getVisibleLineRange(lines.length)}</span>`
|
||||||
|
: html`<span class="info-detail">${lines.length} lines</span>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTileClickDetail(): Record<string, unknown> {
|
protected getTileClickDetail(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { property, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element';
|
import { property, state, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element';
|
||||||
import { DeesTileBase } from '../dees-tile-shared/DeesTileBase.js';
|
import { DeesTileBase } from '../dees-tile-shared/DeesTileBase.js';
|
||||||
import { tileBaseStyles } from '../dees-tile-shared/styles.js';
|
import { tileBaseStyles } from '../dees-tile-shared/styles.js';
|
||||||
import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
|
import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
|
||||||
import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js';
|
import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js';
|
||||||
import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js';
|
import { PerformanceMonitor, throttle, formatFileSize } from '../dees-pdf-shared/utils.js';
|
||||||
import { tilePdfStyles } from './styles.js';
|
import { tilePdfStyles } from './styles.js';
|
||||||
import { demo as demoFunc } from './demo.js';
|
import { demo as demoFunc } from './demo.js';
|
||||||
|
|
||||||
@@ -37,6 +37,9 @@ export class DeesTilePdf extends DeesTileBase {
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
accessor isA4Format: boolean = true;
|
accessor isA4Format: boolean = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor fileSize: number = 0;
|
||||||
|
|
||||||
private renderPagesTask: Promise<void> | null = null;
|
private renderPagesTask: Promise<void> | null = null;
|
||||||
private renderPagesQueued: boolean = false;
|
private renderPagesQueued: boolean = false;
|
||||||
private pdfDocument: any;
|
private pdfDocument: any;
|
||||||
@@ -54,18 +57,6 @@ export class DeesTilePdf extends DeesTileBase {
|
|||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.pageCount > 1 && this.isHovering ? html`
|
|
||||||
<div class="tile-badge">
|
|
||||||
Page ${this.currentPreviewPage} of ${this.pageCount}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.pageCount > 0 && !this.isHovering ? html`
|
|
||||||
<div class="tile-badge-corner">
|
|
||||||
${this.pageCount} page${this.pageCount > 1 ? 's' : ''}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${this.clickable ? html`
|
${this.clickable ? html`
|
||||||
<div class="tile-overlay">
|
<div class="tile-overlay">
|
||||||
<dees-icon icon="lucide:Eye"></dees-icon>
|
<dees-icon icon="lucide:Eye"></dees-icon>
|
||||||
@@ -75,6 +66,22 @@ export class DeesTilePdf extends DeesTileBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
if (!this.pageCount && !this.label) return '';
|
||||||
|
return html`
|
||||||
|
<div class="tile-info-bar">
|
||||||
|
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||||
|
<span class="info-spacer"></span>
|
||||||
|
${this.pageCount > 1 && this.isHovering
|
||||||
|
? html`<span class="info-detail">${this.currentPreviewPage}/${this.pageCount}</span>`
|
||||||
|
: this.pageCount > 0
|
||||||
|
? html`<span class="info-detail">${this.pageCount} pg</span>`
|
||||||
|
: ''}
|
||||||
|
${this.fileSize > 0 ? html`<span class="info-detail">${formatFileSize(this.fileSize)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTileClickDetail(): Record<string, unknown> {
|
protected getTileClickDetail(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
pdfUrl: this.pdfUrl,
|
pdfUrl: this.pdfUrl,
|
||||||
@@ -141,6 +148,13 @@ export class DeesTilePdf extends DeesTileBase {
|
|||||||
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
|
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
|
||||||
this.pageCount = this.pdfDocument.numPages;
|
this.pageCount = this.pdfDocument.numPages;
|
||||||
this.currentPreviewPage = 1;
|
this.currentPreviewPage = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const downloadInfo = await this.pdfDocument.getDownloadInfo();
|
||||||
|
this.fileSize = downloadInfo.length;
|
||||||
|
} catch {
|
||||||
|
// File size unavailable — not critical
|
||||||
|
}
|
||||||
this.loadedPdfUrl = this.pdfUrl;
|
this.loadedPdfUrl = this.pdfUrl;
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { html } from '@design.estate/dees-element';
|
import { html, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
export const demo = () => {
|
export const demo = () => {
|
||||||
const samplePdfs = [
|
const samplePdfs = [
|
||||||
@@ -29,7 +29,7 @@ export const demo = () => {
|
|||||||
<style>
|
<style>
|
||||||
.demo-container {
|
.demo-container {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
background: #f5f5f5;
|
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-section {
|
.demo-section {
|
||||||
@@ -40,6 +40,7 @@ export const demo = () => {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-grid {
|
.preview-grid {
|
||||||
@@ -59,6 +60,7 @@ export const demo = () => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ export const tilePdfStyles = css`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 8px 8px 28px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-stack.non-a4 {
|
.preview-stack.non-a4 {
|
||||||
padding: 12px;
|
padding: 12px 12px 28px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-canvas {
|
.preview-canvas {
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ export abstract class DeesTileBase extends DeesElement {
|
|||||||
|
|
||||||
${!this.loading && !this.error ? this.renderTileContent() : ''}
|
${!this.loading && !this.error ? this.renderTileContent() : ''}
|
||||||
|
|
||||||
${this.label ? html`
|
${this.renderBottomBar()}
|
||||||
<div class="tile-label">${this.label}</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -69,6 +67,11 @@ export abstract class DeesTileBase extends DeesElement {
|
|||||||
/** Subclasses implement this to render their specific content */
|
/** Subclasses implement this to render their specific content */
|
||||||
protected abstract renderTileContent(): TemplateResult;
|
protected abstract renderTileContent(): TemplateResult;
|
||||||
|
|
||||||
|
/** Subclasses override this to render a bottom info bar with metadata */
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
public async connectedCallback(): Promise<void> {
|
public async connectedCallback(): Promise<void> {
|
||||||
await super.connectedCallback();
|
await super.connectedCallback();
|
||||||
this.setupIntersectionObserver();
|
this.setupIntersectionObserver();
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ export const tileBaseStyles = [
|
|||||||
background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(215 20% 14%)')};
|
background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(215 20% 14%)')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: box-shadow 0.2s ease;
|
||||||
box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')};
|
box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-container.clickable {
|
.tile-container.clickable {
|
||||||
@@ -24,7 +25,6 @@ export const tileBaseStyles = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tile-container.clickable:hover {
|
.tile-container.clickable:hover {
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 24px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.3)')};
|
box-shadow: 0 8px 24px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.3)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,90 +71,39 @@ export const tileBaseStyles = [
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-info {
|
.tile-info-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 8px;
|
bottom: 0;
|
||||||
left: 8px;
|
left: 0;
|
||||||
right: 8px;
|
right: 0;
|
||||||
padding: 6px 10px;
|
padding: 4px 8px;
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.92)', 'hsl(215 20% 12% / 0.92)')};
|
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.95)', 'hsl(215 20% 12% / 0.95)')};
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')};
|
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')};
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
z-index: 25;
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-info dees-icon {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-info-text {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
padding: 5px 8px;
|
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.7)', 'hsl(0 0% 100% / 0.9)')};
|
|
||||||
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
z-index: 15;
|
|
||||||
pointer-events: none;
|
|
||||||
animation: fadeIn 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-badge-corner {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 8px;
|
|
||||||
right: 8px;
|
|
||||||
padding: 3px 8px;
|
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
|
|
||||||
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
z-index: 10;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-badge-topright {
|
.info-label {
|
||||||
position: absolute;
|
white-space: nowrap;
|
||||||
top: 8px;
|
overflow: hidden;
|
||||||
right: 8px;
|
text-overflow: ellipsis;
|
||||||
padding: 3px 8px;
|
min-width: 0;
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
|
|
||||||
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
z-index: 15;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shift bottom badges up when label is present */
|
.info-spacer {
|
||||||
.tile-container:has(.tile-label) .tile-badge-corner {
|
flex: 1;
|
||||||
bottom: 33px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-container:has(.tile-label) .tile-info {
|
.info-detail {
|
||||||
bottom: 33px;
|
white-space: nowrap;
|
||||||
|
opacity: 0.7;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-loading,
|
.tile-loading,
|
||||||
@@ -200,40 +149,12 @@ export const tileBaseStyles = [
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.95)', 'hsl(215 20% 12% / 0.95)')};
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('hsl(215 16% 35%)', 'hsl(215 16% 75%)')};
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
z-index: 10;
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Size variants */
|
/* Size variants */
|
||||||
:host([size="small"]) .tile-container {
|
:host([size="small"]) .tile-container {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
|||||||
@@ -140,10 +140,6 @@ export class DeesTileVideo extends DeesTileBase {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${this.duration > 0 ? html`
|
|
||||||
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
${!this.isHovering ? html`
|
${!this.isHovering ? html`
|
||||||
<div class="play-overlay">
|
<div class="play-overlay">
|
||||||
<dees-icon icon="lucide:Play"></dees-icon>
|
<dees-icon icon="lucide:Play"></dees-icon>
|
||||||
@@ -159,6 +155,17 @@ export class DeesTileVideo extends DeesTileBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderBottomBar(): TemplateResult | string {
|
||||||
|
if (!this.label && !this.duration) return '';
|
||||||
|
return html`
|
||||||
|
<div class="tile-info-bar">
|
||||||
|
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||||
|
<span class="info-spacer"></span>
|
||||||
|
${this.duration > 0 ? html`<span class="info-detail">${this.formatTime(this.duration)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTileClickDetail(): Record<string, unknown> {
|
protected getTileClickDetail(): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user