Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c6738ea15 | |||
| e7da1d8b44 | |||
| 358d82e7fa | |||
| 6452e05e1d | |||
| 07b536ea9a | |||
| 3fcb0cbf89 | |||
| 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 |
122
changelog.md
122
changelog.md
@@ -1,5 +1,127 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-12 - 3.48.2 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-12 - 3.48.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-03-12 - 3.48.0 - feat(dataview)
|
||||
add an S3 browser component with column and list views, file preview, editing, and object management
|
||||
|
||||
- introduces a new dees-s3-browser module with shared interfaces, utilities, demo, and exports
|
||||
- supports browsing S3-style prefixes in both column and list layouts with breadcrumb navigation
|
||||
- adds file preview with text editing, download, and delete actions
|
||||
- includes create, rename, move, delete, upload, and drag-and-drop handling for files and folders
|
||||
- adds optional live change stream integration with refresh indicators
|
||||
|
||||
## 2026-03-11 - 3.47.2 - fix(deps)
|
||||
bump @design.estate/dees-domtools and @design.estate/dees-element dependencies
|
||||
|
||||
- update @design.estate/dees-domtools from ^2.3.9 to ^2.5.1
|
||||
- update @design.estate/dees-element from ^2.2.1 to ^2.2.2
|
||||
|
||||
## 2026-03-11 - 3.47.1 - fix(dees-statsgrid)
|
||||
add tablet breakpoint to render stats grid as three columns
|
||||
|
||||
- Added cssManager.cssForTablet rule to set .stats-grid grid-template-columns: repeat(3, 1fr).
|
||||
- Improves responsive layout on tablet devices for dees-statsgrid tiles.
|
||||
- Change made in ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
|
||||
|
||||
## 2026-03-11 - 3.47.0 - feat(dees-statsgrid)
|
||||
add container-responsive behavior and responsive CSS to dees-statsgrid; bump @design.estate/dees-element dependency to ^2.2.1
|
||||
|
||||
- Added @containerResponsive decorator and import to dees-statsgrid
|
||||
- Added cssManager.cssForPhablet and cssManager.cssForPhone responsive style blocks to adjust layout, spacing and font sizes on smaller viewports
|
||||
- Bumped dependency @design.estate/dees-element from ^2.1.6 to ^2.2.1
|
||||
|
||||
## 2026-03-10 - 3.46.1 - fix(dees-appui)
|
||||
add min-height: 0 to mainmenu and secondarymenu to prevent unintended container height and fix layout stacking
|
||||
|
||||
- Modified ts_web/elements/00group-appui/dees-appui/dees-appui.ts: added min-height: 0 to .maingrid > dees-appui-mainmenu and .maingrid > dees-appui-secondarymenu
|
||||
- Fixes layout issues where children or flexbox-derived min-height could cause menu containers to expand and interfere with z-index stacking
|
||||
|
||||
## 2026-03-10 - 3.46.0 - feat(dees-tile)
|
||||
unify tile metadata into a consistent bottom info bar and add PDF file-size display
|
||||
|
||||
- Introduce renderBottomBar() hook in DeesTileBase and remove per-component bottom badges/labels in favor of a unified info bar.
|
||||
- Implement renderBottomBar in audio, video, image, folder, note and pdf tiles to show label, counts, dimensions, duration, language/line info and page counts.
|
||||
- PDF tile: add fileSize state, attempt to read download info and display formatted file size in the info bar; show currentPreviewPage/pageCount when hovering.
|
||||
- Styling changes: replace legacy badges/labels with .tile-info-bar (.info-label, .info-detail, .info-spacer); adjust padding, font sizing, z-index, and remove hover translate for clickable tiles.
|
||||
- PDF demo and styles: use cssManager theming for demo colors and adjust preview padding.
|
||||
- Bump devDependencies: @git.zone/tswatch -> ^3.3.0 and @types/node -> ^25.4.0
|
||||
|
||||
## 2026-03-10 - 3.45.1 - fix(dees-appui)
|
||||
substitute route params into URL hash when navigating
|
||||
|
||||
- Replaces :param placeholders in view.route with provided params before updating the URL hash.
|
||||
- Ensures window.history.pushState is called with the resolved route so URLs do not contain literal parameter tokens.
|
||||
|
||||
## 2026-03-10 - 3.45.0 - feat(dees-form)
|
||||
register new input components (tags, list, wysiwyg, richtext) and emit change notification for richtext updates
|
||||
|
||||
- Added imports and registration of DeesInputTags, DeesInputList, DeesInputWysiwyg, and DeesInputRichtext in dees-form
|
||||
- Extended TFormInputElement union type to include the new input components
|
||||
- DeesInputRichtext now calls changeSubject.next(this.value) in the editor onUpdate handler to propagate changes
|
||||
|
||||
## 2026-03-10 - 3.44.0 - feat(appui-tabs)
|
||||
add support for left/right tab action buttons and content tab action APIs
|
||||
|
||||
- Introduce ITabAction interface and add actionsLeft/actionsRight properties to dees-appui-tabs, dees-appui-maincontent, and dees-appui.
|
||||
- Render action buttons with new styles and renderActions() helper, including disabled state and click handlers; wire actions into tab components.
|
||||
- Add public clear() on dees-appui-tabs and improve tab selection logic to reset selection when tabs become empty or when the selected tab is removed.
|
||||
- Expose setContentTabActionsLeft and setContentTabActionsRight on the DeesAppui programmatic API and update interfaces/appconfig accordingly.
|
||||
- Update demos to showcase action buttons, add clear-all behavior, and adjust layout/styling for action areas.
|
||||
|
||||
## 2026-03-09 - 3.43.4 - fix(media)
|
||||
remove deprecated dees-pdf and dees-pdf-preview components and bump several dependencies
|
||||
|
||||
- Removed deprecated PDF components and related demos/styles: ts_web/elements/00group-media/dees-pdf/* and ts_web/elements/00group-media/dees-pdf-preview/*
|
||||
- Removed exports for dees-pdf and dees-pdf-preview from ts_web/elements/00group-media/index.ts (public API removal)
|
||||
- Dependency upgrades: @design.estate/dees-domtools → ^2.3.9, apexcharts → ^5.10.3, lucide → ^0.577.0, @fortawesome/* → ^7.2.0
|
||||
- DevDependency upgrades: @git.zone/tsbuild → ^4.3.0, @git.zone/tsbundle → ^2.9.1, @git.zone/tstest → ^3.3.2, @git.zone/tswatch → ^3.2.5, @types/node → ^25.3.5
|
||||
- Updated ts_web/services/versions.ts to align CDN/version constants (apexcharts, tiptap → 2.27.2, fontawesome)
|
||||
|
||||
## 2026-02-24 - 3.43.3 - fix(dees-table)
|
||||
use lucide icon identifier for Search action in dees-table
|
||||
|
||||
- Replaced iconName 'magnifyingGlass' with 'lucide:Search' in ts_web/elements/00group-dataview/dees-table/dees-table.ts
|
||||
- Updates the icon identifier for the header 'Search' action; no functional behavior changed
|
||||
|
||||
## 2026-02-21 - 3.43.2 - fix(dees-chart-log)
|
||||
avoid duplicate log entries, optimize incremental updates, enforce maxEntries, and respect filters when writing logs
|
||||
|
||||
- Prevent property-bound logEntries from duplicating entries already present in logBuffer by deduplicating on timestamp|message
|
||||
- Call updateMetrics() when replaying or appending log entries so metrics stay accurate
|
||||
- Skip processing entirely when the incoming logEntries array is unchanged
|
||||
- Optimize append-only updates by writing only the new tail entries instead of full re-render
|
||||
- Enforce maxEntries when appending to the logBuffer to maintain buffer size
|
||||
- Respect filterMode and searchQuery when deciding whether to write appended entries to the terminal
|
||||
|
||||
## 2026-02-21 - 3.43.1 - fix(dees-chart-log)
|
||||
replay buffered log entries when terminal becomes ready and sync logEntries updates to re-render filtered logs
|
||||
|
||||
- Replay entries stored in logBuffer after terminal initialization to avoid losing entries that arrived early
|
||||
- Add updated() lifecycle hook to copy logEntries into logBuffer and call reRenderFilteredLogs when terminalReady
|
||||
- Call super.updated(changedProperties) to preserve base class behavior
|
||||
|
||||
## 2026-02-17 - 3.43.0 - feat(dees-form)
|
||||
add layout styles to dees-form and standardize demo input grouping
|
||||
|
||||
- Add static CSS to dees-form: default column layout with gap and support for [horizontal-layout] (row wrapping, alignment and gap).
|
||||
- Remove inline <style> from dees-form render to centralize styling.
|
||||
- Simplify dees-input-base styles by removing host margins and making spacing container-driven.
|
||||
- Update multiple demo files to wrap related inputs in a new .input-group container and include .input-group CSS for consistent vertical spacing.
|
||||
|
||||
## 2026-02-16 - 3.42.2 - fix(dees-chart-area)
|
||||
add ApexAxisChartSeries type to dees-chart-area component to improve typing for ApexCharts series data
|
||||
|
||||
- Introduces ApexAxisChartSeries type alias with support for number arrays, [x,y] tuples, and object {x,y,...} series entries
|
||||
- Type-only change — no runtime or API behavior modified
|
||||
- File changed: ts_web/elements/00group-chart/dees-chart-area/component.ts
|
||||
|
||||
## 2026-02-16 - 3.42.1 - fix(dees-table)
|
||||
Guard against undefined action.type in dees-table by using optional chaining and update several dependencies
|
||||
|
||||
|
||||
28
package.json
28
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.42.1",
|
||||
"version": "3.48.2",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
@@ -16,13 +16,13 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.3.8",
|
||||
"@design.estate/dees-element": "^2.1.6",
|
||||
"@design.estate/dees-domtools": "^2.5.1",
|
||||
"@design.estate/dees-element": "^2.2.2",
|
||||
"@design.estate/dees-wcctools": "^3.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||
"@push.rocks/smarti18n": "^1.0.4",
|
||||
"@push.rocks/smartpromise": "^4.2.0",
|
||||
"@push.rocks/smartstring": "^4.1.0",
|
||||
@@ -34,22 +34,22 @@
|
||||
"@tiptap/extension-underline": "^2.23.0",
|
||||
"@tiptap/starter-kit": "^2.23.0",
|
||||
"@tsclass/tsclass": "^9.3.0",
|
||||
"apexcharts": "^5.5.0",
|
||||
"apexcharts": "^5.10.3",
|
||||
"highlight.js": "11.11.1",
|
||||
"ibantools": "^4.5.1",
|
||||
"lucide": "^0.564.0",
|
||||
"lucide": "^0.577.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"pdfjs-dist": "^4.10.38",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tswatch": "^3.1.0",
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
"@git.zone/tsbundle": "^2.9.1",
|
||||
"@git.zone/tstest": "^3.3.2",
|
||||
"@git.zone/tswatch": "^3.3.0",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.2.3"
|
||||
"@types/node": "^25.4.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
4849
pnpm-lock.yaml
generated
4849
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.42.1',
|
||||
version: '3.48.2',
|
||||
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 })
|
||||
accessor tabsAutoHideThreshold: number = 0;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor tabActionsLeft: interfaces.ITabAction[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor tabActionsRight: interfaces.ITabAction[] = [];
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
@@ -106,6 +112,8 @@ export class DeesAppuiMaincontent extends DeesElement {
|
||||
.tabStyle=${'horizontal'}
|
||||
.autoHide=${this.tabsAutoHide}
|
||||
.autoHideThreshold=${this.tabsAutoHideThreshold}
|
||||
.actionsLeft=${this.tabActionsLeft}
|
||||
.actionsRight=${this.tabActionsRight}
|
||||
@tab-select=${(e: CustomEvent) => this.handleTabSelect(e)}
|
||||
@tab-close=${(e: CustomEvent) => this.handleTabClose(e)}
|
||||
></dees-appui-tabs>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { html, cssManager, css, DeesElement, customElement, state } from '@desig
|
||||
import * as interfaces from '../../interfaces/index.js';
|
||||
import type { DeesAppuiTabs } from './dees-appui-tabs.js';
|
||||
|
||||
// Interactive demo component for closeable tabs
|
||||
// Interactive demo component for closeable tabs with action buttons
|
||||
@customElement('demo-closeable-tabs')
|
||||
class DemoCloseableTabs extends DeesElement {
|
||||
@state()
|
||||
@@ -18,24 +18,6 @@ class DemoCloseableTabs extends DeesElement {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
button {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.3)', 'rgba(59, 130, 246, 0.3)')};
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
button:hover {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
|
||||
}
|
||||
.info {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
@@ -66,17 +48,27 @@ class DemoCloseableTabs extends DeesElement {
|
||||
this.tabs = this.tabs.filter(t => t.key !== tabKey);
|
||||
}
|
||||
|
||||
private clearAll() {
|
||||
const tabsEl = this.shadowRoot!.querySelector('dees-appui-tabs') as DeesAppuiTabs;
|
||||
tabsEl?.clear();
|
||||
this.tabs = [];
|
||||
this.tabCounter = 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
const rightActions: interfaces.ITabAction[] = [
|
||||
{ id: 'add', iconName: 'lucide:plus', action: () => this.addTab(), tooltip: 'New Tab' },
|
||||
{ id: 'clear', iconName: 'lucide:trash2', action: () => this.clearAll(), tooltip: 'Clear All Tabs' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-appui-tabs
|
||||
.tabs=${this.tabs}
|
||||
.actionsRight=${rightActions}
|
||||
@tab-close=${(e: CustomEvent) => this.removeTab(e.detail.tab.key)}
|
||||
></dees-appui-tabs>
|
||||
<div class="controls">
|
||||
<button @click=${() => this.addTab()}>+ Add New Tab</button>
|
||||
</div>
|
||||
<div class="info">
|
||||
Click the X button on tabs to close them. The "Main" tab is not closeable.
|
||||
Click the X button on tabs to close them. Use the + button to add tabs and the trash button to clear all.
|
||||
<br>Current tabs: ${this.tabs.length}
|
||||
</div>
|
||||
`;
|
||||
@@ -232,6 +224,16 @@ export const demoFunc = () => {
|
||||
{ key: 'Archived', action: () => console.log('Archived clicked') },
|
||||
];
|
||||
|
||||
const actionsLeft: interfaces.ITabAction[] = [
|
||||
{ id: 'back', iconName: 'lucide:arrowLeft', action: () => console.log('Back'), tooltip: 'Go Back' },
|
||||
];
|
||||
|
||||
const actionsRight: interfaces.ITabAction[] = [
|
||||
{ id: 'add', iconName: 'lucide:plus', action: () => console.log('Add tab'), tooltip: 'New Tab' },
|
||||
{ id: 'search', iconName: 'lucide:search', action: () => console.log('Search'), tooltip: 'Search Tabs' },
|
||||
{ id: 'disabled', iconName: 'lucide:lock', action: () => {}, tooltip: 'Disabled Action', disabled: true },
|
||||
];
|
||||
|
||||
const demoContent = (text: string) => html`
|
||||
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
|
||||
${text}
|
||||
@@ -279,7 +281,17 @@ export const demoFunc = () => {
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Closeable Tabs (Browser-style)</div>
|
||||
<div class="section-title">Tabs with Action Buttons</div>
|
||||
<dees-appui-tabs
|
||||
.tabs=${horizontalTabs}
|
||||
.actionsLeft=${actionsLeft}
|
||||
.actionsRight=${actionsRight}
|
||||
></dees-appui-tabs>
|
||||
${demoContent('Action buttons can be placed on either side of the tab bar. They remain fixed while tabs scroll. The lock icon shows a disabled action.')}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Closeable Tabs with Actions</div>
|
||||
<demo-closeable-tabs></demo-closeable-tabs>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
@property({ type: Number })
|
||||
accessor autoHideThreshold: number = 0;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor actionsLeft: interfaces.ITabAction[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor actionsRight: interfaces.ITabAction[] = [];
|
||||
|
||||
// Scroll state for fade indicators
|
||||
@state()
|
||||
private accessor canScrollLeft: boolean = false;
|
||||
@@ -73,6 +79,8 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Scroll fade indicators */
|
||||
@@ -105,6 +113,72 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Tab action buttons */
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.tab-actions.left {
|
||||
padding-left: 12px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
}
|
||||
|
||||
.tab-actions.right {
|
||||
padding-right: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
}
|
||||
|
||||
.tab-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-action-button:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.06)')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.tab-action-button:active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
}
|
||||
|
||||
.tab-action-button.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tab-action-button.disabled:hover {
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
}
|
||||
|
||||
.tab-action-button dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
@@ -121,12 +195,14 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* Show scrollbar on hover */
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal {
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal,
|
||||
.scroll-area:hover .tabsContainer.horizontal {
|
||||
scrollbar-color: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')} transparent;
|
||||
}
|
||||
|
||||
@@ -144,11 +220,13 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb,
|
||||
.scroll-area:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')};
|
||||
}
|
||||
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
|
||||
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover,
|
||||
.scroll-area:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0,0,0,0.35)', 'rgba(255,255,255,0.35)')};
|
||||
}
|
||||
|
||||
@@ -331,13 +409,20 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
const containerClass = `tabsContainer ${this.tabStyle}`;
|
||||
|
||||
if (isHorizontal) {
|
||||
const hasLeftActions = this.actionsLeft && this.actionsLeft.length > 0;
|
||||
const hasRightActions = this.actionsRight && this.actionsRight.length > 0;
|
||||
|
||||
return html`
|
||||
<div class="${wrapperClass}">
|
||||
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
|
||||
<div class="${containerClass}" @scroll=${this.handleScroll}>
|
||||
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
|
||||
${hasLeftActions ? this.renderActions(this.actionsLeft, 'left') : ''}
|
||||
<div class="scroll-area">
|
||||
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
|
||||
<div class="${containerClass}" @scroll=${this.handleScroll}>
|
||||
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
|
||||
</div>
|
||||
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
|
||||
</div>
|
||||
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
|
||||
${hasRightActions ? this.renderActions(this.actionsRight, 'right') : ''}
|
||||
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
@@ -353,6 +438,22 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActions(actions: interfaces.ITabAction[], position: 'left' | 'right'): TemplateResult {
|
||||
return html`
|
||||
<div class="tab-actions ${position}">
|
||||
${actions.map(action => html`
|
||||
<div
|
||||
class="tab-action-button ${action.disabled ? 'disabled' : ''}"
|
||||
title="${action.tooltip || action.id}"
|
||||
@click=${() => !action.disabled && action.action()}
|
||||
>
|
||||
<dees-icon .icon=${action.iconName}></dees-icon>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
|
||||
const isSelected = tab === this.selectedTab;
|
||||
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
|
||||
@@ -406,6 +507,14 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tabs and reset selection.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.tabs = [];
|
||||
this.selectedTab = null;
|
||||
}
|
||||
|
||||
private closeTab(e: Event, tab: interfaces.IMenuItem) {
|
||||
e.stopPropagation(); // Don't select tab when closing
|
||||
|
||||
@@ -423,14 +532,9 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.tabs && this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
|
||||
// Set up ResizeObserver for scroll state updates
|
||||
// Tab selection is handled by updated() lifecycle
|
||||
this.setupResizeObserver();
|
||||
|
||||
// Initial scroll state check
|
||||
requestAnimationFrame(() => {
|
||||
this.updateScrollState();
|
||||
});
|
||||
@@ -503,8 +607,24 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
async updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
if (changedProperties.has('tabs')) {
|
||||
if (!this.tabs || this.tabs.length === 0) {
|
||||
// Tabs are empty => reset selection
|
||||
if (this.selectedTab !== null) {
|
||||
this.selectedTab = null;
|
||||
this.dispatchEvent(new CustomEvent('tab-select', {
|
||||
detail: { tab: null },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
} else if (this.selectedTab && !this.tabs.includes(this.selectedTab)) {
|
||||
// Selected tab was removed => select first available
|
||||
this.selectTab(this.tabs[0]);
|
||||
} else if (!this.selectedTab) {
|
||||
// Tabs exist but nothing selected => select first
|
||||
this.selectTab(this.tabs[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
||||
|
||||
@@ -143,6 +143,12 @@ export class DeesAppui extends DeesElement {
|
||||
@property({ type: Object })
|
||||
accessor maincontentSelectedTab: interfaces.IMenuItem | undefined = undefined;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor contentTabActionsLeft: interfaces.ITabAction[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor contentTabActionsRight: interfaces.ITabAction[] = [];
|
||||
|
||||
// References to child components
|
||||
@state()
|
||||
accessor appbar: DeesAppuiBar | undefined = undefined;
|
||||
@@ -213,11 +219,13 @@ export class DeesAppui extends DeesElement {
|
||||
.maingrid > dees-appui-mainmenu {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.maingrid > dees-appui-secondarymenu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.maingrid > dees-appui-maincontent {
|
||||
@@ -306,6 +314,8 @@ export class DeesAppui extends DeesElement {
|
||||
.showTabs=${this.maincontentTabsVisible}
|
||||
.tabsAutoHide=${this.contentTabsAutoHide}
|
||||
.tabsAutoHideThreshold=${this.contentTabsAutoHideThreshold}
|
||||
.tabActionsLeft=${this.contentTabActionsLeft}
|
||||
.tabActionsRight=${this.contentTabActionsRight}
|
||||
@tab-select=${(e: CustomEvent) => this.handleContentTabSelect(e)}
|
||||
@tab-close=${(e: CustomEvent) => this.handleContentTabClose(e)}
|
||||
>
|
||||
@@ -699,6 +709,20 @@ export class DeesAppui extends DeesElement {
|
||||
return this.maincontentSelectedTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content tab action buttons on the left side
|
||||
*/
|
||||
public setContentTabActionsLeft(actions: interfaces.ITabAction[]): void {
|
||||
this.contentTabActionsLeft = [...actions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content tab action buttons on the right side
|
||||
*/
|
||||
public setContentTabActionsRight(actions: interfaces.ITabAction[]): void {
|
||||
this.contentTabActionsRight = [...actions];
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PROGRAMMATIC API: ACTIVITY LOG
|
||||
// ==========================================
|
||||
@@ -853,8 +877,13 @@ export class DeesAppui extends DeesElement {
|
||||
try {
|
||||
await this.loadView(view, params);
|
||||
|
||||
// Update URL hash
|
||||
const route = view.route || viewId;
|
||||
// Update URL hash (substitute params into route pattern)
|
||||
let route = view.route || viewId;
|
||||
if (params) {
|
||||
for (const [key, val] of Object.entries(params)) {
|
||||
route = route.replace(`:${key}`, val);
|
||||
}
|
||||
}
|
||||
const newHash = `#${route}`;
|
||||
if (window.location.hash !== newHash) {
|
||||
window.history.pushState({ viewId }, '', newHash);
|
||||
|
||||
@@ -12,6 +12,16 @@ import { chartAreaStyles } from './styles.js';
|
||||
import { renderChartArea } from './template.js';
|
||||
|
||||
import type ApexCharts from 'apexcharts';
|
||||
|
||||
type ApexAxisChartSeries = {
|
||||
name?: string;
|
||||
type?: string;
|
||||
color?: string;
|
||||
group?: string;
|
||||
hidden?: boolean;
|
||||
zIndex?: number;
|
||||
data: (number | null)[] | { x: any; y: any; [key: string]: any }[] | [number, number | null][] | number[][];
|
||||
}[];
|
||||
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -385,11 +385,23 @@ export class DeesChartLog extends DeesElement {
|
||||
this.domtoolsInstance = await this.domtoolsPromise;
|
||||
await this.initializeTerminal();
|
||||
|
||||
// Process any initial log entries
|
||||
if (this.logEntries.length > 0) {
|
||||
// initializeTerminal() already replayed logBuffer (from addLog/updateLog).
|
||||
// Now handle logEntries set via property binding before terminal was ready.
|
||||
if (this.logEntries.length > 0 && this.logBuffer.length === 0) {
|
||||
this.logBuffer = [...this.logEntries];
|
||||
for (const entry of this.logEntries) {
|
||||
this.updateMetrics(entry.level);
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
} else if (this.logEntries.length > 0 && this.logBuffer.length > 0) {
|
||||
const bufferSet = new Set(this.logBuffer.map(e => `${e.timestamp}|${e.message}`));
|
||||
for (const entry of this.logEntries) {
|
||||
if (!bufferSet.has(`${entry.timestamp}|${entry.message}`)) {
|
||||
this.logBuffer.push(entry);
|
||||
this.updateMetrics(entry.level);
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +457,58 @@ export class DeesChartLog extends DeesElement {
|
||||
this.rateInterval = setInterval(() => this.calculateRate(), 1000);
|
||||
|
||||
this.terminalReady = true;
|
||||
|
||||
// Replay any entries that arrived via updateLog()/addLog() before terminal was ready
|
||||
for (const entry of this.logBuffer) {
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('logEntries') && this.terminalReady && this.logEntries.length > 0) {
|
||||
const oldEntries: ILogEntry[] = changedProperties.get('logEntries') || [];
|
||||
const newEntries = this.logEntries;
|
||||
|
||||
// Same content? Skip entirely.
|
||||
if (
|
||||
oldEntries.length === newEntries.length &&
|
||||
oldEntries.length > 0 &&
|
||||
oldEntries[oldEntries.length - 1].timestamp === newEntries[newEntries.length - 1].timestamp &&
|
||||
oldEntries[oldEntries.length - 1].message === newEntries[newEntries.length - 1].message
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Append-only? Write only the new tail entries incrementally.
|
||||
if (
|
||||
newEntries.length > oldEntries.length &&
|
||||
oldEntries.length > 0 &&
|
||||
oldEntries[oldEntries.length - 1].timestamp === newEntries[oldEntries.length - 1].timestamp &&
|
||||
oldEntries[oldEntries.length - 1].message === newEntries[oldEntries.length - 1].message
|
||||
) {
|
||||
const tailEntries = newEntries.slice(oldEntries.length);
|
||||
for (const entry of tailEntries) {
|
||||
this.logBuffer.push(entry);
|
||||
this.updateMetrics(entry.level);
|
||||
|
||||
// Enforce maxEntries
|
||||
if (this.logBuffer.length > this.maxEntries) {
|
||||
this.logBuffer.shift();
|
||||
}
|
||||
|
||||
// Respect filter mode
|
||||
if (!this.filterMode || !this.searchQuery || this.entryMatchesFilter(entry)) {
|
||||
this.writeLogEntry(entry);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Different content — full re-render
|
||||
this.logBuffer = [...newEntries];
|
||||
this.reRenderFilteredLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private getTerminalTheme() {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { IS3DataProvider, IS3Object } from './interfaces.js';
|
||||
import './dees-s3-browser.js';
|
||||
|
||||
// Mock in-memory S3 data provider for demo purposes
|
||||
class MockS3DataProvider implements IS3DataProvider {
|
||||
private objects: Map<string, { content: string; contentType: string; size: number; lastModified: string }> = new Map();
|
||||
|
||||
constructor() {
|
||||
const now = new Date().toISOString();
|
||||
// Seed with sample data
|
||||
this.objects.set('documents/readme.md', {
|
||||
content: btoa('# Welcome\n\nThis is a demo S3 browser.\n'),
|
||||
contentType: 'text/markdown',
|
||||
size: 42,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('documents/config.json', {
|
||||
content: btoa('{\n "name": "demo",\n "version": "1.0.0"\n}'),
|
||||
contentType: 'application/json',
|
||||
size: 48,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('documents/notes/todo.txt', {
|
||||
content: btoa('Buy milk\nFix bug #42\nDeploy to production'),
|
||||
contentType: 'text/plain',
|
||||
size: 45,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('images/logo.png', {
|
||||
content: btoa('fake-png-data'),
|
||||
contentType: 'image/png',
|
||||
size: 24500,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('images/banner.jpg', {
|
||||
content: btoa('fake-jpg-data'),
|
||||
contentType: 'image/jpeg',
|
||||
size: 156000,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('scripts/deploy.sh', {
|
||||
content: btoa('#!/bin/bash\necho "Deploying..."\n'),
|
||||
contentType: 'text/plain',
|
||||
size: 34,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('index.html', {
|
||||
content: btoa('<!DOCTYPE html>\n<html>\n<body>\n <h1>Hello World</h1>\n</body>\n</html>'),
|
||||
contentType: 'text/html',
|
||||
size: 72,
|
||||
lastModified: now,
|
||||
});
|
||||
this.objects.set('styles.css', {
|
||||
content: btoa('body { margin: 0; font-family: sans-serif; }'),
|
||||
contentType: 'text/css',
|
||||
size: 44,
|
||||
lastModified: now,
|
||||
});
|
||||
}
|
||||
|
||||
async listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IS3Object[]; prefixes: string[] }> {
|
||||
const pfx = prefix || '';
|
||||
const objects: IS3Object[] = [];
|
||||
const prefixes = new Set<string>();
|
||||
|
||||
for (const [key, data] of this.objects) {
|
||||
if (!key.startsWith(pfx)) continue;
|
||||
const rest = key.slice(pfx.length);
|
||||
|
||||
if (delimiter) {
|
||||
const slashIndex = rest.indexOf(delimiter);
|
||||
if (slashIndex >= 0) {
|
||||
prefixes.add(pfx + rest.slice(0, slashIndex + 1));
|
||||
} else {
|
||||
objects.push({ key, size: data.size, lastModified: data.lastModified });
|
||||
}
|
||||
} else {
|
||||
objects.push({ key, size: data.size, lastModified: data.lastModified });
|
||||
}
|
||||
}
|
||||
|
||||
return { objects, prefixes: Array.from(prefixes).sort() };
|
||||
}
|
||||
|
||||
async getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }> {
|
||||
const obj = this.objects.get(key);
|
||||
if (!obj) throw new Error('Not found');
|
||||
return { ...obj };
|
||||
}
|
||||
|
||||
async putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean> {
|
||||
this.objects.set(key, {
|
||||
content: base64Content,
|
||||
contentType,
|
||||
size: atob(base64Content).length,
|
||||
lastModified: new Date().toISOString(),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteObject(bucket: string, key: string): Promise<boolean> {
|
||||
return this.objects.delete(key);
|
||||
}
|
||||
|
||||
async deletePrefix(bucket: string, prefix: string): Promise<boolean> {
|
||||
for (const key of this.objects.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.objects.delete(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async getObjectUrl(bucket: string, key: string): Promise<string> {
|
||||
const obj = this.objects.get(key);
|
||||
if (!obj) return '';
|
||||
const blob = new Blob([Uint8Array.from(atob(obj.content), c => c.charCodeAt(0))], { type: obj.contentType });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }> {
|
||||
const obj = this.objects.get(sourceKey);
|
||||
if (!obj) return { success: false, error: 'Source not found' };
|
||||
this.objects.set(destKey, { ...obj, lastModified: new Date().toISOString() });
|
||||
this.objects.delete(sourceKey);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }> {
|
||||
let count = 0;
|
||||
const toMove = Array.from(this.objects.entries()).filter(([k]) => k.startsWith(sourcePrefix));
|
||||
for (const [key, data] of toMove) {
|
||||
const newKey = destPrefix + key.slice(sourcePrefix.length);
|
||||
this.objects.set(newKey, { ...data, lastModified: new Date().toISOString() });
|
||||
this.objects.delete(key);
|
||||
count++;
|
||||
}
|
||||
return { success: true, movedCount: count };
|
||||
}
|
||||
}
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
height: 600px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-s3-browser
|
||||
.dataProvider=${new MockS3DataProvider()}
|
||||
.bucketName=${'demo-bucket'}
|
||||
></dees-s3-browser>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,439 @@
|
||||
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import { demoFunc } from './dees-s3-browser.demo.js';
|
||||
import type { IS3DataProvider, IS3ChangeEvent } from './interfaces.js';
|
||||
import './dees-s3-columns.js';
|
||||
import './dees-s3-keys.js';
|
||||
import './dees-s3-preview.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-s3-browser': DeesS3Browser;
|
||||
}
|
||||
}
|
||||
|
||||
type TViewType = 'columns' | 'keys';
|
||||
|
||||
@customElement('dees-s3-browser')
|
||||
export class DeesS3Browser extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Data View'];
|
||||
|
||||
@property({ type: Object })
|
||||
public accessor dataProvider: IS3DataProvider | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor bucketName: string = '';
|
||||
|
||||
/**
|
||||
* Optional change stream subscription.
|
||||
* Pass a function that takes a callback and returns an unsubscribe function.
|
||||
*/
|
||||
@property({ type: Object })
|
||||
public accessor onChangeEvent: ((callback: (event: IS3ChangeEvent) => void) => (() => void)) | null = null;
|
||||
|
||||
@state()
|
||||
private accessor viewType: TViewType = 'columns';
|
||||
|
||||
@state()
|
||||
private accessor currentPrefix: string = '';
|
||||
|
||||
@state()
|
||||
private accessor selectedKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor refreshKey: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor previewWidth: number = 700;
|
||||
|
||||
@state()
|
||||
private accessor isResizingPreview: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor recentChangeCount: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor isStreamConnected: boolean = false;
|
||||
|
||||
private changeUnsubscribe: (() => void) | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.browser-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#999')};
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#fff')};
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: ${cssManager.bdTheme('#d4d4d8', '#555')};
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
border-color: ${cssManager.bdTheme('#a1a1aa', '#666')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#aaa')};
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border-color: ${cssManager.bdTheme('#a1a1aa', '#404040')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#e0e0e0')};
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr 4px var(--preview-width, 700px);
|
||||
}
|
||||
|
||||
.resize-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resize-divider:hover,
|
||||
.resize-divider.active {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')};
|
||||
}
|
||||
|
||||
.main-view {
|
||||
overflow: auto;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.content,
|
||||
.content.has-preview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel,
|
||||
.resize-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
margin-left: auto;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.stream-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: ${cssManager.bdTheme('#a1a1aa', '#888')};
|
||||
}
|
||||
|
||||
.stream-dot.connected {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.change-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: #f59e0b;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.change-indicator.pulse {
|
||||
animation: pulse-orange 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse-orange {
|
||||
0% { background: rgba(245, 158, 11, 0.4); }
|
||||
100% { background: rgba(245, 158, 11, 0.2); }
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
this.unsubscribeFromChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to trigger a refresh of child components
|
||||
*/
|
||||
public refresh() {
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
private setViewType(type: TViewType) {
|
||||
this.viewType = type;
|
||||
}
|
||||
|
||||
private navigateToPrefix(prefix: string) {
|
||||
this.currentPrefix = prefix;
|
||||
this.selectedKey = '';
|
||||
}
|
||||
|
||||
private handleKeySelected(e: CustomEvent) {
|
||||
this.selectedKey = e.detail.key;
|
||||
}
|
||||
|
||||
private handleNavigate(e: CustomEvent) {
|
||||
this.navigateToPrefix(e.detail.prefix);
|
||||
}
|
||||
|
||||
private handleObjectDeleted(e: CustomEvent) {
|
||||
this.selectedKey = '';
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('bucketName')) {
|
||||
this.selectedKey = '';
|
||||
this.currentPrefix = '';
|
||||
this.recentChangeCount = 0;
|
||||
this.unsubscribeFromChanges();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
if (changedProperties.has('onChangeEvent')) {
|
||||
this.unsubscribeFromChanges();
|
||||
this.subscribeToChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToChanges() {
|
||||
if (!this.onChangeEvent) {
|
||||
this.isStreamConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.changeUnsubscribe = this.onChangeEvent((event: IS3ChangeEvent) => {
|
||||
this.handleChange(event);
|
||||
});
|
||||
this.isStreamConnected = true;
|
||||
} catch (error) {
|
||||
console.warn('[S3Browser] Failed to subscribe to changes:', error);
|
||||
this.isStreamConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
private unsubscribeFromChanges() {
|
||||
if (this.changeUnsubscribe) {
|
||||
this.changeUnsubscribe();
|
||||
this.changeUnsubscribe = null;
|
||||
}
|
||||
this.isStreamConnected = false;
|
||||
}
|
||||
|
||||
private handleChange(event: IS3ChangeEvent) {
|
||||
this.recentChangeCount++;
|
||||
this.refreshKey++;
|
||||
}
|
||||
|
||||
private startPreviewResize = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.isResizingPreview = true;
|
||||
document.addEventListener('mousemove', this.handlePreviewResize);
|
||||
document.addEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
private handlePreviewResize = (e: MouseEvent) => {
|
||||
if (!this.isResizingPreview) return;
|
||||
const contentEl = this.shadowRoot?.querySelector('.content');
|
||||
if (!contentEl) return;
|
||||
const containerRect = contentEl.getBoundingClientRect();
|
||||
const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000);
|
||||
this.previewWidth = newWidth;
|
||||
};
|
||||
|
||||
private endPreviewResize = () => {
|
||||
this.isResizingPreview = false;
|
||||
document.removeEventListener('mousemove', this.handlePreviewResize);
|
||||
document.removeEventListener('mouseup', this.endPreviewResize);
|
||||
};
|
||||
|
||||
render() {
|
||||
const breadcrumbParts = this.currentPrefix
|
||||
? this.currentPrefix.split('/').filter(Boolean)
|
||||
: [];
|
||||
|
||||
return html`
|
||||
<div class="browser-container">
|
||||
<div class="toolbar">
|
||||
<div class="breadcrumb">
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.navigateToPrefix('')}
|
||||
>
|
||||
${this.bucketName}
|
||||
</span>
|
||||
${breadcrumbParts.map((part, index) => {
|
||||
const prefix = breadcrumbParts.slice(0, index + 1).join('/') + '/';
|
||||
return html`
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<span
|
||||
class="breadcrumb-item"
|
||||
@click=${() => this.navigateToPrefix(prefix)}
|
||||
>
|
||||
${part}
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
|
||||
${this.onChangeEvent ? html`
|
||||
<div class="stream-status">
|
||||
<span class="stream-dot ${this.isStreamConnected ? 'connected' : ''}"></span>
|
||||
${this.isStreamConnected ? 'Live' : 'Offline'}
|
||||
</div>
|
||||
` : ''}
|
||||
${this.recentChangeCount > 0
|
||||
? html`
|
||||
<div class="change-indicator pulse">
|
||||
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="view-toggle">
|
||||
<button
|
||||
class="view-btn ${this.viewType === 'columns' ? 'active' : ''}"
|
||||
@click=${() => this.setViewType('columns')}
|
||||
>
|
||||
Columns
|
||||
</button>
|
||||
<button
|
||||
class="view-btn ${this.viewType === 'keys' ? 'active' : ''}"
|
||||
@click=${() => this.setViewType('keys')}
|
||||
>
|
||||
List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content ${this.selectedKey ? 'has-preview' : ''}" style="--preview-width: ${this.previewWidth}px">
|
||||
<div class="main-view">
|
||||
${this.viewType === 'columns'
|
||||
? html`
|
||||
<dees-s3-columns
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></dees-s3-columns>
|
||||
`
|
||||
: html`
|
||||
<dees-s3-keys
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.currentPrefix=${this.currentPrefix}
|
||||
.refreshKey=${this.refreshKey}
|
||||
@key-selected=${this.handleKeySelected}
|
||||
@navigate=${this.handleNavigate}
|
||||
></dees-s3-keys>
|
||||
`}
|
||||
</div>
|
||||
|
||||
${this.selectedKey
|
||||
? html`
|
||||
<div
|
||||
class="resize-divider ${this.isResizingPreview ? 'active' : ''}"
|
||||
@mousedown=${this.startPreviewResize}
|
||||
></div>
|
||||
<div class="preview-panel">
|
||||
<dees-s3-preview
|
||||
.dataProvider=${this.dataProvider}
|
||||
.bucketName=${this.bucketName}
|
||||
.objectKey=${this.selectedKey}
|
||||
@object-deleted=${this.handleObjectDeleted}
|
||||
></dees-s3-preview>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1652
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.ts
Normal file
1652
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.ts
Normal file
File diff suppressed because it is too large
Load Diff
1094
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.ts
Normal file
1094
ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,540 @@
|
||||
import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
import type { IS3DataProvider } from './interfaces.js';
|
||||
import { formatSize, getFileName } from './utilities.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-s3-preview': DeesS3Preview;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-s3-preview')
|
||||
export class DeesS3Preview extends DeesElement {
|
||||
@property({ type: Object })
|
||||
public accessor dataProvider: IS3DataProvider | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
public accessor bucketName: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public accessor objectKey: string = '';
|
||||
|
||||
@state()
|
||||
private accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor saving: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor content: string = '';
|
||||
|
||||
@state()
|
||||
private accessor originalTextContent: string = '';
|
||||
|
||||
@state()
|
||||
private accessor hasChanges: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor editing: boolean = false;
|
||||
|
||||
@state()
|
||||
private accessor contentType: string = '';
|
||||
|
||||
@state()
|
||||
private accessor fileSize: number = 0;
|
||||
|
||||
@state()
|
||||
private accessor lastModified: string = '';
|
||||
|
||||
@state()
|
||||
private accessor error: string = '';
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
themeDefaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content dees-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-content.code-editor {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-content.code-editor dees-input-code {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
padding: 12px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#404040')};
|
||||
color: ${cssManager.bdTheme('#3f3f46', '#e0e0e0')};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.15)')};
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: #ef4444;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
border-color: #3b82f6;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.action-btn.primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
|
||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#555')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#aaa')};
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#18181b', '#fff')};
|
||||
}
|
||||
|
||||
.unsaved-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.unsaved-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fbbf24;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#a1a1aa', '#666')};
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: ${cssManager.bdTheme('#71717a', '#888')};
|
||||
}
|
||||
|
||||
.error-state {
|
||||
padding: 16px;
|
||||
color: #f87171;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('objectKey') || changedProperties.has('bucketName')) {
|
||||
if (this.objectKey) {
|
||||
this.loadObject();
|
||||
} else {
|
||||
this.content = '';
|
||||
this.contentType = '';
|
||||
this.error = '';
|
||||
this.originalTextContent = '';
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadObject() {
|
||||
if (!this.objectKey || !this.bucketName || !this.dataProvider) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
|
||||
try {
|
||||
const result = await this.dataProvider.getObject(this.bucketName, this.objectKey);
|
||||
if (!result) {
|
||||
this.error = 'Object not found';
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.content = result.content || '';
|
||||
this.contentType = result.contentType || '';
|
||||
this.fileSize = result.size || 0;
|
||||
this.lastModified = result.lastModified || '';
|
||||
|
||||
if (this.isText()) {
|
||||
this.originalTextContent = this.getTextContent();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading object:', err);
|
||||
this.error = 'Failed to load object';
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
private isImage(): boolean {
|
||||
return this.contentType.startsWith('image/');
|
||||
}
|
||||
|
||||
private isText(): boolean {
|
||||
return (
|
||||
this.contentType.startsWith('text/') ||
|
||||
this.contentType === 'application/json' ||
|
||||
this.contentType === 'application/xml' ||
|
||||
this.contentType === 'application/javascript'
|
||||
);
|
||||
}
|
||||
|
||||
private getTextContent(): string {
|
||||
try {
|
||||
const binaryString = atob(this.content);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
} catch {
|
||||
return 'Unable to decode content';
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownload() {
|
||||
try {
|
||||
const blob = new Blob([Uint8Array.from(atob(this.content), (c) => c.charCodeAt(0))], {
|
||||
type: this.contentType,
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = getFileName(this.objectKey);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('Error downloading:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDelete() {
|
||||
if (!this.dataProvider) return;
|
||||
if (!confirm(`Delete "${getFileName(this.objectKey)}"?`)) return;
|
||||
|
||||
try {
|
||||
await this.dataProvider.deleteObject(this.bucketName, this.objectKey);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('object-deleted', {
|
||||
detail: { key: this.objectKey },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error deleting object:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private getLanguage(): string {
|
||||
const ext = this.objectKey.split('.').pop()?.toLowerCase() || '';
|
||||
const languageMap: Record<string, string> = {
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
js: 'javascript',
|
||||
jsx: 'javascript',
|
||||
mjs: 'javascript',
|
||||
cjs: 'javascript',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
htm: 'html',
|
||||
css: 'css',
|
||||
scss: 'scss',
|
||||
sass: 'scss',
|
||||
less: 'less',
|
||||
md: 'markdown',
|
||||
markdown: 'markdown',
|
||||
xml: 'xml',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
py: 'python',
|
||||
rb: 'ruby',
|
||||
go: 'go',
|
||||
rs: 'rust',
|
||||
java: 'java',
|
||||
c: 'c',
|
||||
cpp: 'cpp',
|
||||
h: 'c',
|
||||
hpp: 'cpp',
|
||||
cs: 'csharp',
|
||||
php: 'php',
|
||||
sh: 'shell',
|
||||
bash: 'shell',
|
||||
zsh: 'shell',
|
||||
sql: 'sql',
|
||||
graphql: 'graphql',
|
||||
gql: 'graphql',
|
||||
dockerfile: 'dockerfile',
|
||||
txt: 'plaintext',
|
||||
};
|
||||
return languageMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
private handleContentChange(event: CustomEvent) {
|
||||
const newValue = event.detail as string;
|
||||
this.hasChanges = newValue !== this.originalTextContent;
|
||||
}
|
||||
|
||||
private handleEdit() {
|
||||
this.editing = true;
|
||||
}
|
||||
|
||||
private handleDiscard() {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
if (codeEditor) {
|
||||
codeEditor.value = this.originalTextContent;
|
||||
}
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
}
|
||||
|
||||
private async handleSave() {
|
||||
if (!this.hasChanges || this.saving || !this.dataProvider) return;
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
|
||||
const currentContent = codeEditor?.value ?? '';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(currentContent);
|
||||
const base64Content = btoa(String.fromCharCode(...bytes));
|
||||
|
||||
const success = await this.dataProvider.putObject(
|
||||
this.bucketName,
|
||||
this.objectKey,
|
||||
base64Content,
|
||||
this.contentType
|
||||
);
|
||||
|
||||
if (success) {
|
||||
this.originalTextContent = currentContent;
|
||||
this.hasChanges = false;
|
||||
this.editing = false;
|
||||
this.content = base64Content;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving object:', err);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.objectKey) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
<p>Select a file to preview</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="loading-state">Loading...</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="error-state">${this.error}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="preview-container">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">${getFileName(this.objectKey)}</div>
|
||||
<div class="preview-meta">
|
||||
<span class="meta-item">${this.contentType}</span>
|
||||
<span class="meta-item">${formatSize(this.fileSize)}</span>
|
||||
<span class="meta-item">${this.formatDate(this.lastModified)}</span>
|
||||
${this.hasChanges ? html`
|
||||
<span class="unsaved-indicator">
|
||||
<span class="unsaved-dot"></span>
|
||||
Unsaved changes
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-content ${this.editing ? 'code-editor' : ''}">
|
||||
${this.editing
|
||||
? html`
|
||||
<dees-input-code
|
||||
.value=${this.originalTextContent}
|
||||
.language=${this.getLanguage()}
|
||||
height="100%"
|
||||
@content-change=${(e: CustomEvent) => this.handleContentChange(e)}
|
||||
></dees-input-code>
|
||||
`
|
||||
: this.isText()
|
||||
? html`
|
||||
<dees-preview
|
||||
.textContent=${this.originalTextContent}
|
||||
.filename=${getFileName(this.objectKey)}
|
||||
.language=${this.getLanguage()}
|
||||
.showToolbar=${true}
|
||||
.showFilename=${false}
|
||||
></dees-preview>
|
||||
`
|
||||
: html`
|
||||
<dees-preview
|
||||
.base64=${this.content}
|
||||
.mimeType=${this.contentType}
|
||||
.filename=${getFileName(this.objectKey)}
|
||||
.showToolbar=${true}
|
||||
.showFilename=${false}
|
||||
></dees-preview>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="preview-actions">
|
||||
${this.editing
|
||||
? html`
|
||||
<button class="action-btn secondary" @click=${this.handleDiscard}>
|
||||
${this.hasChanges ? 'Discard' : 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click=${this.handleSave}
|
||||
?disabled=${this.saving || !this.hasChanges}
|
||||
>
|
||||
${this.saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${this.isText()
|
||||
? html`<button class="action-btn" @click=${this.handleEdit}>Edit</button>`
|
||||
: ''}
|
||||
<button class="action-btn" @click=${this.handleDownload}>Download</button>
|
||||
<button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './dees-s3-browser.js';
|
||||
export * from './dees-s3-columns.js';
|
||||
export * from './dees-s3-keys.js';
|
||||
export * from './dees-s3-preview.js';
|
||||
export * from './interfaces.js';
|
||||
export { formatSize, formatCount, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* S3 Data Provider interface - implement this to connect the S3 browser to your backend
|
||||
*/
|
||||
|
||||
export interface IS3Object {
|
||||
key: string;
|
||||
size?: number;
|
||||
lastModified?: string;
|
||||
isPrefix?: boolean;
|
||||
}
|
||||
|
||||
export interface IS3ChangeEvent {
|
||||
type: 'add' | 'modify' | 'delete';
|
||||
key: string;
|
||||
bucket: string;
|
||||
size?: number;
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export interface IS3DataProvider {
|
||||
listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IS3Object[]; prefixes: string[] }>;
|
||||
getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }>;
|
||||
putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean>;
|
||||
deleteObject(bucket: string, key: string): Promise<boolean>;
|
||||
deletePrefix(bucket: string, prefix: string): Promise<boolean>;
|
||||
getObjectUrl(bucket: string, key: string): Promise<string>;
|
||||
moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }>;
|
||||
movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }>;
|
||||
}
|
||||
|
||||
export interface IColumn {
|
||||
prefix: string;
|
||||
objects: IS3Object[];
|
||||
prefixes: string[];
|
||||
selectedItem: string | null;
|
||||
width: number;
|
||||
}
|
||||
120
ts_web/elements/00group-dataview/dees-s3-browser/utilities.ts
Normal file
120
ts_web/elements/00group-dataview/dees-s3-browser/utilities.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Shared utilities for S3 browser components
|
||||
*/
|
||||
|
||||
export interface IMoveValidation {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a byte size into a human-readable string
|
||||
*/
|
||||
export function formatSize(bytes?: number): string {
|
||||
if (bytes === undefined || bytes === null) return '-';
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a count into a compact human-readable string
|
||||
*/
|
||||
export function formatCount(count?: number): string {
|
||||
if (count === undefined || count === null) return '';
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file name from a path
|
||||
*/
|
||||
export function getFileName(path: string): string {
|
||||
const parts = path.replace(/\/$/, '').split('/');
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a move operation is allowed
|
||||
*/
|
||||
export function validateMove(sourceKey: string, destPrefix: string): IMoveValidation {
|
||||
if (sourceKey.endsWith('/')) {
|
||||
if (destPrefix.startsWith(sourceKey)) {
|
||||
return { valid: false, error: 'Cannot move a folder into itself' };
|
||||
}
|
||||
}
|
||||
|
||||
const sourceParent = getParentPrefix(sourceKey);
|
||||
if (sourceParent === destPrefix) {
|
||||
return { valid: false, error: 'Item is already in this location' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent prefix (directory) of a given key
|
||||
*/
|
||||
export function getParentPrefix(key: string): string {
|
||||
const trimmed = key.endsWith('/') ? key.slice(0, -1) : key;
|
||||
const lastSlash = trimmed.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? trimmed.substring(0, lastSlash + 1) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content type from file extension
|
||||
*/
|
||||
export function getContentType(ext: string): string {
|
||||
const contentTypes: Record<string, string> = {
|
||||
json: 'application/json',
|
||||
txt: 'text/plain',
|
||||
html: 'text/html',
|
||||
css: 'text/css',
|
||||
js: 'application/javascript',
|
||||
ts: 'text/typescript',
|
||||
md: 'text/markdown',
|
||||
xml: 'application/xml',
|
||||
yaml: 'text/yaml',
|
||||
yml: 'text/yaml',
|
||||
csv: 'text/csv',
|
||||
};
|
||||
return contentTypes[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default content for a new file based on extension
|
||||
*/
|
||||
export function getDefaultContent(ext: string): string {
|
||||
const defaults: Record<string, string> = {
|
||||
json: '{\n \n}',
|
||||
html: '<!DOCTYPE html>\n<html>\n<head>\n <title></title>\n</head>\n<body>\n \n</body>\n</html>',
|
||||
md: '# Title\n\n',
|
||||
txt: '',
|
||||
};
|
||||
return defaults[ext] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a prefix into cumulative path segments
|
||||
*/
|
||||
export function getPathSegments(prefix: string): string[] {
|
||||
if (!prefix) return [];
|
||||
const parts = prefix.split('/').filter(p => p);
|
||||
const segments: string[] = [];
|
||||
let cumulative = '';
|
||||
for (const part of parts) {
|
||||
cumulative += part + '/';
|
||||
segments.push(cumulative);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
css,
|
||||
unsafeCSS,
|
||||
cssManager,
|
||||
containerResponsive,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { TemplateResult } from '@design.estate/dees-element';
|
||||
|
||||
@@ -93,6 +94,7 @@ export interface IStatsTile {
|
||||
actions?: plugins.tsclass.website.IMenuItem[];
|
||||
}
|
||||
|
||||
@containerResponsive()
|
||||
@customElement('dees-statsgrid')
|
||||
export class DeesStatsGrid extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
@@ -801,6 +803,38 @@ export class DeesStatsGrid extends DeesElement {
|
||||
z-index: 1000;
|
||||
}
|
||||
`,
|
||||
// Container-responsive: when this statsgrid is narrow
|
||||
cssManager.cssForTablet(css`
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
`, this),
|
||||
cssManager.cssForPhablet(css`
|
||||
:host {
|
||||
--tile-padding: 12px;
|
||||
--value-font-size: 22px;
|
||||
--title-font-size: 12px;
|
||||
--grid-gap: 8px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.stats-tile {
|
||||
grid-column: span 1 !important;
|
||||
}
|
||||
`, this),
|
||||
cssManager.cssForPhone(css`
|
||||
:host {
|
||||
--tile-padding: 10px;
|
||||
--value-font-size: 20px;
|
||||
--header-spacing: 8px;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
`, this),
|
||||
];
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -544,7 +544,7 @@ export class DeesTable<T> extends DeesElement {
|
||||
if (!existing) {
|
||||
this.dataActions.unshift({
|
||||
name: 'Search',
|
||||
iconName: 'magnifyingGlass',
|
||||
iconName: 'lucide:Search',
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
console.log('open search');
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './dees-dataview-codebox/index.js';
|
||||
export * from './dees-dataview-statusobject/index.js';
|
||||
export * from './dees-table/index.js';
|
||||
export * from './dees-statsgrid/index.js';
|
||||
export * from './dees-s3-browser/index.js';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
type TemplateResult,
|
||||
DeesElement,
|
||||
type CSSResult,
|
||||
@@ -21,6 +22,10 @@ import { DeesInputMultitoggle } from '../../00group-input/dees-input-multitoggle
|
||||
import { DeesInputPhone } from '../../00group-input/dees-input-phone/dees-input-phone.js';
|
||||
import { DeesInputToggle } from '../../00group-input/dees-input-toggle/dees-input-toggle.js';
|
||||
import { DeesInputTypelist } from '../../00group-input/dees-input-typelist/dees-input-typelist.js';
|
||||
import { DeesInputTags } from '../../00group-input/dees-input-tags/dees-input-tags.js';
|
||||
import { DeesInputList } from '../../00group-input/dees-input-list/dees-input-list.js';
|
||||
import { DeesInputWysiwyg } from '../../00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
|
||||
import { DeesInputRichtext } from '../../00group-input/dees-input-richtext/component.js';
|
||||
import { DeesFormSubmit } from '../dees-form-submit/dees-form-submit.js';
|
||||
import { DeesTable } from '../../00group-dataview/dees-table/index.js';
|
||||
import { demoFunc } from './dees-form.demo.js';
|
||||
@@ -40,6 +45,10 @@ const FORM_INPUT_TYPES = [
|
||||
DeesInputText,
|
||||
DeesInputToggle,
|
||||
DeesInputTypelist,
|
||||
DeesInputTags,
|
||||
DeesInputList,
|
||||
DeesInputWysiwyg,
|
||||
DeesInputRichtext,
|
||||
DeesTable,
|
||||
];
|
||||
|
||||
@@ -57,6 +66,10 @@ export type TFormInputElement =
|
||||
| DeesInputText
|
||||
| DeesInputToggle
|
||||
| DeesInputTypelist
|
||||
| DeesInputTags
|
||||
| DeesInputList
|
||||
| DeesInputWysiwyg
|
||||
| DeesInputRichtext
|
||||
| DeesTable<any>;
|
||||
|
||||
declare global {
|
||||
@@ -81,13 +94,25 @@ export class DeesForm extends DeesElement {
|
||||
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
|
||||
accessor horizontalLayout: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
:host([horizontal-layout]) {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -54,37 +54,20 @@ export abstract class DeesInputBase<T = any> extends DeesElement {
|
||||
/* CSS Variables for consistent spacing */
|
||||
:host {
|
||||
--dees-input-spacing-unit: 8px;
|
||||
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
|
||||
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
|
||||
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
|
||||
}
|
||||
|
||||
/* Default vertical stacking mode (for forms) */
|
||||
/* Default block display with no margins - spacing is container-driven */
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0;
|
||||
margin-bottom: var(--dees-input-vertical-gap);
|
||||
}
|
||||
|
||||
/* Last child in container should have no bottom margin */
|
||||
:host(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Horizontal layout mode - activated by attribute */
|
||||
:host([layout-mode="horizontal"]) {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
margin-right: var(--dees-input-horizontal-gap);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:host([layout-mode="horizontal"]:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Auto mode - inherit from parent dees-form if present */
|
||||
|
||||
/* Label position variations */
|
||||
:host([label-position="left"]) .input-wrapper {
|
||||
display: grid;
|
||||
|
||||
@@ -31,6 +31,12 @@ export const demoFunc = () => html`
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
@@ -63,30 +69,32 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Country'}
|
||||
.options=${[
|
||||
{ option: 'United States', key: 'us' },
|
||||
{ option: 'Canada', key: 'ca' },
|
||||
{ option: 'Germany', key: 'de' },
|
||||
{ option: 'France', key: 'fr' },
|
||||
{ option: 'United Kingdom', key: 'uk' },
|
||||
{ option: 'Australia', key: 'au' },
|
||||
{ option: 'Japan', key: 'jp' },
|
||||
{ option: 'Brazil', key: 'br' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'United States', key: 'us' }}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Role'}
|
||||
.options=${[
|
||||
{ option: 'Administrator', key: 'admin' },
|
||||
{ option: 'Editor', key: 'editor' },
|
||||
{ option: 'Viewer', key: 'viewer' },
|
||||
{ option: 'Guest', key: 'guest' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
<div class="input-group">
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Country'}
|
||||
.options=${[
|
||||
{ option: 'United States', key: 'us' },
|
||||
{ option: 'Canada', key: 'ca' },
|
||||
{ option: 'Germany', key: 'de' },
|
||||
{ option: 'France', key: 'fr' },
|
||||
{ option: 'United Kingdom', key: 'uk' },
|
||||
{ option: 'Australia', key: 'au' },
|
||||
{ option: 'Japan', key: 'jp' },
|
||||
{ option: 'Brazil', key: 'br' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'United States', key: 'us' }}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Select Role'}
|
||||
.options=${[
|
||||
{ option: 'Administrator', key: 'admin' },
|
||||
{ option: 'Editor', key: 'editor' },
|
||||
{ option: 'Viewer', key: 'viewer' },
|
||||
{ option: 'Guest', key: 'guest' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -176,24 +184,26 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
|
||||
<dees-input-dropdown
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.options=${[
|
||||
{ option: 'Option A', key: 'a' },
|
||||
{ option: 'Option B', key: 'b' },
|
||||
{ option: 'Option C', key: 'c' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Disabled Dropdown'}
|
||||
.disabled=${true}
|
||||
.options=${[
|
||||
{ option: 'Cannot Select', key: 'disabled' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||
></dees-input-dropdown>
|
||||
<div class="input-group">
|
||||
<dees-input-dropdown
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.options=${[
|
||||
{ option: 'Option A', key: 'a' },
|
||||
{ option: 'Option B', key: 'b' },
|
||||
{ option: 'Option C', key: 'c' }
|
||||
]}
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
.label=${'Disabled Dropdown'}
|
||||
.disabled=${true}
|
||||
.options=${[
|
||||
{ option: 'Cannot Select', key: 'disabled' }
|
||||
]}
|
||||
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.payment-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -24,16 +30,18 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic IBAN Input'} .subtitle=${'International Bank Account Number with automatic formatting'}>
|
||||
<dees-input-iban
|
||||
.label=${'Bank Account IBAN'}
|
||||
.description=${'Enter your International Bank Account Number'}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Verified IBAN'}
|
||||
.description=${'This IBAN has been verified'}
|
||||
.value=${'DE89370400440532013000'}
|
||||
></dees-input-iban>
|
||||
<div class="input-group">
|
||||
<dees-input-iban
|
||||
.label=${'Bank Account IBAN'}
|
||||
.description=${'Enter your International Bank Account Number'}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Verified IBAN'}
|
||||
.description=${'This IBAN has been verified'}
|
||||
.value=${'DE89370400440532013000'}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Payment Information'} .subtitle=${'IBAN input with horizontal layout for payment forms'}>
|
||||
@@ -53,18 +61,20 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Required fields and disabled states'}>
|
||||
<dees-input-iban
|
||||
.label=${'Payment Account'}
|
||||
.description=${'Required for processing payments'}
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Locked IBAN'}
|
||||
.description=${'This IBAN cannot be changed'}
|
||||
.value=${'FR1420041010050500013M02606'}
|
||||
.disabled=${true}
|
||||
></dees-input-iban>
|
||||
<div class="input-group">
|
||||
<dees-input-iban
|
||||
.label=${'Payment Account'}
|
||||
.description=${'Required for processing payments'}
|
||||
.required=${true}
|
||||
></dees-input-iban>
|
||||
|
||||
<dees-input-iban
|
||||
.label=${'Locked IBAN'}
|
||||
.description=${'This IBAN cannot be changed'}
|
||||
.value=${'FR1420041010050500013M02606'}
|
||||
.disabled=${true}
|
||||
></dees-input-iban>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Bank Transfer Form'} .subtitle=${'Complete form example with IBAN validation'}>
|
||||
|
||||
@@ -13,6 +13,12 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -24,18 +30,20 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Phone Input'} .subtitle=${'Automatic formatting for phone numbers'}>
|
||||
<dees-input-phone
|
||||
.label=${'Phone Number'}
|
||||
.description=${'Enter your phone number with country code'}
|
||||
.value=${'5551234567'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Contact Phone'}
|
||||
.description=${'Required for account verification'}
|
||||
.required=${true}
|
||||
.placeholder=${'+1 (555) 000-0000'}
|
||||
></dees-input-phone>
|
||||
<div class="input-group">
|
||||
<dees-input-phone
|
||||
.label=${'Phone Number'}
|
||||
.description=${'Enter your phone number with country code'}
|
||||
.value=${'5551234567'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Contact Phone'}
|
||||
.description=${'Required for account verification'}
|
||||
.required=${true}
|
||||
.placeholder=${'+1 (555) 000-0000'}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Horizontal Layout'} .subtitle=${'Phone inputs arranged horizontally'}>
|
||||
@@ -55,17 +63,19 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'International Numbers'} .subtitle=${'Supports formatting for numbers with country codes'}>
|
||||
<dees-input-phone
|
||||
.label=${'International Contact'}
|
||||
.description=${'Automatically formats international numbers'}
|
||||
.value=${'441234567890'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Emergency Contact'}
|
||||
.value=${'911'}
|
||||
.disabled=${true}
|
||||
></dees-input-phone>
|
||||
<div class="input-group">
|
||||
<dees-input-phone
|
||||
.label=${'International Contact'}
|
||||
.description=${'Automatically formats international numbers'}
|
||||
.value=${'441234567890'}
|
||||
></dees-input-phone>
|
||||
|
||||
<dees-input-phone
|
||||
.label=${'Emergency Contact'}
|
||||
.value=${'911'}
|
||||
.disabled=${true}
|
||||
></dees-input-phone>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Form Integration'} .subtitle=${'Phone input as part of a contact form'}>
|
||||
|
||||
@@ -14,6 +14,12 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shopping-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
@@ -60,17 +66,19 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Quantity Selector'} .subtitle=${'Simple quantity input with increment/decrement buttons'}>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.description=${'Select the desired quantity'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Items in Cart'}
|
||||
.description=${'Adjust the quantity of items'}
|
||||
.value=${3}
|
||||
></dees-input-quantityselector>
|
||||
<div class="input-group">
|
||||
<dees-input-quantityselector
|
||||
.label=${'Quantity'}
|
||||
.description=${'Select the desired quantity'}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Items in Cart'}
|
||||
.description=${'Adjust the quantity of items'}
|
||||
.value=${3}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Shopping Cart'} .subtitle=${'Modern e-commerce product cards with interactive quantity selectors'} .runAfterRender=${async (elementArg: HTMLElement) => {
|
||||
@@ -169,19 +177,21 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different states for validation and restrictions'}>
|
||||
<dees-input-quantityselector
|
||||
.label=${'Number of Licenses'}
|
||||
.description=${'Select how many licenses you need'}
|
||||
.required=${true}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Fixed Quantity'}
|
||||
.description=${'This quantity cannot be changed'}
|
||||
.disabled=${true}
|
||||
.value=${5}
|
||||
></dees-input-quantityselector>
|
||||
<div class="input-group">
|
||||
<dees-input-quantityselector
|
||||
.label=${'Number of Licenses'}
|
||||
.description=${'Select how many licenses you need'}
|
||||
.required=${true}
|
||||
.value=${1}
|
||||
></dees-input-quantityselector>
|
||||
|
||||
<dees-input-quantityselector
|
||||
.label=${'Fixed Quantity'}
|
||||
.description=${'This quantity cannot be changed'}
|
||||
.disabled=${true}
|
||||
.value=${5}
|
||||
></dees-input-quantityselector>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Order Form'} .subtitle=${'Complete order form with quantity selection'}>
|
||||
|
||||
@@ -23,6 +23,12 @@ export const demoFunc = () => html`
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
@@ -60,20 +66,22 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
|
||||
<dees-input-radiogroup
|
||||
.label=${'Do you agree with the terms?'}
|
||||
.options=${['Yes', 'No', 'Maybe']}
|
||||
.direction=${'horizontal'}
|
||||
.selectedOption=${'Yes'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Experience Level'}
|
||||
.options=${['Beginner', 'Intermediate', 'Expert']}
|
||||
.direction=${'horizontal'}
|
||||
.selectedOption=${'Intermediate'}
|
||||
.description=${'Select your experience level with web development'}
|
||||
></dees-input-radiogroup>
|
||||
<div class="input-group">
|
||||
<dees-input-radiogroup
|
||||
.label=${'Do you agree with the terms?'}
|
||||
.options=${['Yes', 'No', 'Maybe']}
|
||||
.direction=${'horizontal'}
|
||||
.selectedOption=${'Yes'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Experience Level'}
|
||||
.options=${['Beginner', 'Intermediate', 'Expert']}
|
||||
.direction=${'horizontal'}
|
||||
.selectedOption=${'Intermediate'}
|
||||
.description=${'Select your experience level with web development'}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}>
|
||||
@@ -132,30 +140,32 @@ export const demoFunc = () => html`
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
|
||||
<dees-input-radiogroup
|
||||
.label=${'Theme Preference'}
|
||||
.options=${[
|
||||
{ option: 'Light Theme', key: 'light', payload: 'light' },
|
||||
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
|
||||
{ option: 'System Default', key: 'system', payload: 'auto' }
|
||||
]}
|
||||
.selectedOption=${'dark'}
|
||||
.description=${'Choose how the application should appear'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Notification Frequency'}
|
||||
.options=${['All Notifications', 'Important Only', 'None']}
|
||||
.selectedOption=${'Important Only'}
|
||||
.description=${'Control how often you receive notifications'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Language'}
|
||||
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
|
||||
.selectedOption=${'English'}
|
||||
.direction=${'horizontal'}
|
||||
></dees-input-radiogroup>
|
||||
<div class="input-group">
|
||||
<dees-input-radiogroup
|
||||
.label=${'Theme Preference'}
|
||||
.options=${[
|
||||
{ option: 'Light Theme', key: 'light', payload: 'light' },
|
||||
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
|
||||
{ option: 'System Default', key: 'system', payload: 'auto' }
|
||||
]}
|
||||
.selectedOption=${'dark'}
|
||||
.description=${'Choose how the application should appear'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Notification Frequency'}
|
||||
.options=${['All Notifications', 'Important Only', 'None']}
|
||||
.selectedOption=${'Important Only'}
|
||||
.description=${'Control how often you receive notifications'}
|
||||
></dees-input-radiogroup>
|
||||
|
||||
<dees-input-radiogroup
|
||||
.label=${'Language'}
|
||||
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
|
||||
.selectedOption=${'English'}
|
||||
.direction=${'horizontal'}
|
||||
></dees-input-radiogroup>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>
|
||||
|
||||
@@ -269,6 +269,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
|
||||
onUpdate: ({ editor }) => {
|
||||
this.value = editor.getHTML();
|
||||
this.updateWordCount();
|
||||
this.changeSubject.next(this.value);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('input', {
|
||||
detail: { value: this.value },
|
||||
|
||||
@@ -30,6 +30,12 @@ export const demoFunc = () => html`
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -83,25 +89,27 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Basic Text Inputs'} .subtitle=${'Standard text inputs with labels and descriptions'}>
|
||||
<dees-input-text
|
||||
.label=${'Username'}
|
||||
.value=${'johndoe'}
|
||||
.key=${'username'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Email Address'}
|
||||
.value=${'john@example.com'}
|
||||
.description=${'We will never share your email with anyone'}
|
||||
.key=${'email'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Password'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'secret123'}
|
||||
.key=${'password'}
|
||||
></dees-input-text>
|
||||
<div class="input-group">
|
||||
<dees-input-text
|
||||
.label=${'Username'}
|
||||
.value=${'johndoe'}
|
||||
.key=${'username'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Email Address'}
|
||||
.value=${'john@example.com'}
|
||||
.description=${'We will never share your email with anyone'}
|
||||
.key=${'email'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Password'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'secret123'}
|
||||
.key=${'password'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -172,31 +180,33 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Label Positions'} .subtitle=${'Different label positioning options for various layouts'}>
|
||||
<dees-input-text
|
||||
.label=${'Label on Top (Default)'}
|
||||
.value=${'Standard layout'}
|
||||
.labelPosition=${'top'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Label on Left'}
|
||||
.value=${'Inline label'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
|
||||
<div class="grid-layout">
|
||||
<div class="input-group">
|
||||
<dees-input-text
|
||||
.label=${'Label on Top (Default)'}
|
||||
.value=${'Standard layout'}
|
||||
.labelPosition=${'top'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Label on Left'}
|
||||
.value=${'Inline label'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
|
||||
<div class="grid-layout">
|
||||
<dees-input-text
|
||||
.label=${'City'}
|
||||
.value=${'New York'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'ZIP Code'}
|
||||
.value=${'10001'}
|
||||
<dees-input-text
|
||||
.label=${'ZIP Code'}
|
||||
.value=${'10001'}
|
||||
.labelPosition=${'left'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -234,24 +244,26 @@ export const demoFunc = () => html`
|
||||
}
|
||||
}}>
|
||||
<dees-panel .title=${'Validation & States'} .subtitle=${'Different validation states and input configurations'}>
|
||||
<dees-input-text
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.key=${'requiredField'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Disabled Field'}
|
||||
.value=${'Cannot edit this'}
|
||||
.disabled=${true}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Field with Error'}
|
||||
.value=${'invalid@'}
|
||||
.validationText=${'Please enter a valid email address'}
|
||||
.validationState=${'invalid'}
|
||||
></dees-input-text>
|
||||
<div class="input-group">
|
||||
<dees-input-text
|
||||
.label=${'Required Field'}
|
||||
.required=${true}
|
||||
.key=${'requiredField'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Disabled Field'}
|
||||
.value=${'Cannot edit this'}
|
||||
.disabled=${true}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'Field with Error'}
|
||||
.value=${'invalid@'}
|
||||
.validationText=${'Please enter a valid email address'}
|
||||
.validationState=${'invalid'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
@@ -279,19 +291,21 @@ export const demoFunc = () => html`
|
||||
});
|
||||
}}>
|
||||
<dees-panel .title=${'Advanced Features'} .subtitle=${'Password visibility toggle and other advanced features'}>
|
||||
<dees-input-text
|
||||
.label=${'Password with Toggle'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'mySecurePassword123'}
|
||||
.description=${'Click the eye icon to show/hide password'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'API Key'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'sk-1234567890abcdef'}
|
||||
.description=${'Keep this key secure and never share it'}
|
||||
></dees-input-text>
|
||||
<div class="input-group">
|
||||
<dees-input-text
|
||||
.label=${'Password with Toggle'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'mySecurePassword123'}
|
||||
.description=${'Click the eye icon to show/hide password'}
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
.label=${'API Key'}
|
||||
.isPasswordBool=${true}
|
||||
.value=${'sk-1234567890abcdef'}
|
||||
.description=${'Keep this key secure and never share it'}
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</dees-demowrapper>
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ export const demoFunc = () => html`
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.horizontal-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
@@ -39,27 +45,30 @@ export const demoFunc = () => html`
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'Basic Type List'} .subtitle=${'Add and remove items from a list'}>
|
||||
<dees-input-typelist
|
||||
.label=${'Tags'}
|
||||
.description=${'Add tags by typing and pressing Enter'}
|
||||
.value=${['javascript', 'typescript', 'web-components']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Team Members'}
|
||||
.description=${'Add email addresses of team members'}
|
||||
.value=${['alice@example.com', 'bob@example.com']}
|
||||
></dees-input-typelist>
|
||||
<div class="input-group">
|
||||
<dees-input-typelist
|
||||
.label=${'Tags'}
|
||||
.description=${'Add tags by typing and pressing Enter'}
|
||||
.value=${['javascript', 'typescript', 'web-components']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'Team Members'}
|
||||
.description=${'Add email addresses of team members'}
|
||||
.value=${['alice@example.com', 'bob@example.com']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Skills & Keywords'} .subtitle=${'Manage lists of skills and keywords'}>
|
||||
<dees-input-typelist
|
||||
.label=${'Your Skills'}
|
||||
.description=${'List your professional skills'}
|
||||
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<div class="input-group">
|
||||
<dees-input-typelist
|
||||
.label=${'Your Skills'}
|
||||
.description=${'List your professional skills'}
|
||||
.value=${['HTML', 'CSS', 'JavaScript', 'Node.js', 'React']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<div class="horizontal-group">
|
||||
<dees-input-typelist
|
||||
.label=${'Categories'}
|
||||
.layoutMode=${'horizontal'}
|
||||
@@ -72,22 +81,25 @@ export const demoFunc = () => html`
|
||||
.value=${['innovation', 'startup', 'growth']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different input states for validation'}>
|
||||
<dees-input-typelist
|
||||
.label=${'Project Dependencies'}
|
||||
.description=${'List all required npm packages'}
|
||||
.required=${true}
|
||||
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'System Tags'}
|
||||
.description=${'These tags are managed by the system'}
|
||||
.disabled=${true}
|
||||
.value=${['system', 'protected', 'readonly']}
|
||||
></dees-input-typelist>
|
||||
<div class="input-group">
|
||||
<dees-input-typelist
|
||||
.label=${'Project Dependencies'}
|
||||
.description=${'List all required npm packages'}
|
||||
.required=${true}
|
||||
.value=${['@design.estate/dees-element', '@design.estate/dees-domtools']}
|
||||
></dees-input-typelist>
|
||||
|
||||
<dees-input-typelist
|
||||
.label=${'System Tags'}
|
||||
.description=${'These tags are managed by the system'}
|
||||
.disabled=${true}
|
||||
.value=${['system', 'protected', 'readonly']}
|
||||
></dees-input-typelist>
|
||||
</div>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'Article Publishing Form'} .subtitle=${'Complete form with tag management'}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -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>
|
||||
|
||||
${this.duration > 0 ? html`
|
||||
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
|
||||
` : ''}
|
||||
|
||||
<div class="play-overlay">
|
||||
<div class="play-circle">
|
||||
<dees-icon icon="lucide:Play"></dees-icon>
|
||||
@@ -181,6 +177,17 @@ export class DeesTileAudio extends DeesTileBase {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
if (!this.label && !this.duration) return '';
|
||||
return html`
|
||||
<div class="tile-info-bar">
|
||||
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||
<span class="info-spacer"></span>
|
||||
${this.duration > 0 ? html`<span class="info-detail">${this.formatTime(this.duration)}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getTileClickDetail(): Record<string, unknown> {
|
||||
return {
|
||||
src: this.src,
|
||||
|
||||
@@ -145,10 +145,6 @@ export class DeesTileFolder extends DeesTileBase {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tile-badge-corner">
|
||||
${this.items.length} item${this.items.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
|
||||
${this.clickable ? html`
|
||||
<div class="tile-overlay">
|
||||
<dees-icon icon="lucide:FolderOpen"></dees-icon>
|
||||
@@ -158,6 +154,17 @@ export class DeesTileFolder extends DeesTileBase {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
if (!this.label && !this.items.length) return '';
|
||||
return html`
|
||||
<div class="tile-info-bar">
|
||||
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||
<span class="info-spacer"></span>
|
||||
<span class="info-detail">${this.items.length} item${this.items.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getTileClickDetail(): Record<string, unknown> {
|
||||
return {
|
||||
name: this.name,
|
||||
|
||||
@@ -55,14 +55,6 @@ export class DeesTileImage extends DeesTileBase {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tile-badge-topright.dimension-badge {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.tile-container.clickable:hover .tile-badge-topright.dimension-badge {
|
||||
opacity: 1;
|
||||
}
|
||||
`,
|
||||
] as any;
|
||||
|
||||
@@ -97,19 +89,6 @@ export class DeesTileImage extends DeesTileBase {
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.imageWidth > 0 && this.imageHeight > 0 ? html`
|
||||
<div class="tile-badge-topright dimension-badge">
|
||||
${this.imageWidth} × ${this.imageHeight}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.imageLoaded ? html`
|
||||
<div class="tile-info">
|
||||
<dees-icon icon="lucide:Image"></dees-icon>
|
||||
<span class="tile-info-text">${this.imageWidth} × ${this.imageHeight}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.clickable ? html`
|
||||
<div class="tile-overlay">
|
||||
<dees-icon icon="lucide:Eye"></dees-icon>
|
||||
@@ -119,6 +98,19 @@ export class DeesTileImage extends DeesTileBase {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
if (!this.label && !(this.imageWidth > 0)) return '';
|
||||
return html`
|
||||
<div class="tile-info-bar">
|
||||
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||
<span class="info-spacer"></span>
|
||||
${this.imageWidth > 0 && this.imageHeight > 0
|
||||
? html`<span class="info-detail">${this.imageWidth} × ${this.imageHeight}</span>`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getTileClickDetail(): Record<string, unknown> {
|
||||
return {
|
||||
src: this.src,
|
||||
|
||||
@@ -81,14 +81,6 @@ export class DeesTileNote extends DeesTileBase {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tile-badge-topright.note-language {
|
||||
background: ${cssManager.bdTheme('hsl(215 20% 92%)', 'hsl(215 20% 88%)')};
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 40%)')};
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.note-lines {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -132,10 +124,6 @@ export class DeesTileNote extends DeesTileBase {
|
||||
|
||||
return html`
|
||||
<div class="note-content">
|
||||
${this.language ? html`
|
||||
<div class="tile-badge-topright note-language">${this.language}</div>
|
||||
` : ''}
|
||||
|
||||
${this.title ? html`
|
||||
<div class="note-header">
|
||||
<div class="note-title">${this.title}</div>
|
||||
@@ -147,11 +135,6 @@ export class DeesTileNote extends DeesTileBase {
|
||||
${!this.isHovering ? html`<div class="note-fade"></div>` : ''}
|
||||
</div>
|
||||
|
||||
${this.isHovering && lines.length > 12 ? html`
|
||||
<div class="tile-badge-corner">
|
||||
Line ${this.getVisibleLineRange(lines.length)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.clickable ? html`
|
||||
@@ -163,6 +146,21 @@ export class DeesTileNote extends DeesTileBase {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
const lines = this.content.split('\n');
|
||||
if (!this.label && !this.language && !lines.length) return '';
|
||||
return html`
|
||||
<div class="tile-info-bar">
|
||||
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||
<span class="info-spacer"></span>
|
||||
${this.language ? html`<span class="info-detail">${this.language.toUpperCase()}</span>` : ''}
|
||||
${this.isHovering && lines.length > 12
|
||||
? html`<span class="info-detail">Line ${this.getVisibleLineRange(lines.length)}</span>`
|
||||
: html`<span class="info-detail">${lines.length} lines</span>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getTileClickDetail(): Record<string, unknown> {
|
||||
return {
|
||||
title: this.title,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { property, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element';
|
||||
import { property, state, html, customElement, type TemplateResult, type CSSResult } from '@design.estate/dees-element';
|
||||
import { DeesTileBase } from '../dees-tile-shared/DeesTileBase.js';
|
||||
import { tileBaseStyles } from '../dees-tile-shared/styles.js';
|
||||
import { PdfManager } from '../dees-pdf-shared/PdfManager.js';
|
||||
import { CanvasPool, type PooledCanvas } from '../dees-pdf-shared/CanvasPool.js';
|
||||
import { PerformanceMonitor, throttle } from '../dees-pdf-shared/utils.js';
|
||||
import { PerformanceMonitor, throttle, formatFileSize } from '../dees-pdf-shared/utils.js';
|
||||
import { tilePdfStyles } from './styles.js';
|
||||
import { demo as demoFunc } from './demo.js';
|
||||
|
||||
@@ -37,6 +37,9 @@ export class DeesTilePdf extends DeesTileBase {
|
||||
@property({ type: Boolean })
|
||||
accessor isA4Format: boolean = true;
|
||||
|
||||
@state()
|
||||
accessor fileSize: number = 0;
|
||||
|
||||
private renderPagesTask: Promise<void> | null = null;
|
||||
private renderPagesQueued: boolean = false;
|
||||
private pdfDocument: any;
|
||||
@@ -54,18 +57,6 @@ export class DeesTilePdf extends DeesTileBase {
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
${this.pageCount > 1 && this.isHovering ? html`
|
||||
<div class="tile-badge">
|
||||
Page ${this.currentPreviewPage} of ${this.pageCount}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.pageCount > 0 && !this.isHovering ? html`
|
||||
<div class="tile-badge-corner">
|
||||
${this.pageCount} page${this.pageCount > 1 ? 's' : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.clickable ? html`
|
||||
<div class="tile-overlay">
|
||||
<dees-icon icon="lucide:Eye"></dees-icon>
|
||||
@@ -75,6 +66,22 @@ export class DeesTilePdf extends DeesTileBase {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
if (!this.pageCount && !this.label) return '';
|
||||
return html`
|
||||
<div class="tile-info-bar">
|
||||
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||
<span class="info-spacer"></span>
|
||||
${this.pageCount > 1 && this.isHovering
|
||||
? html`<span class="info-detail">${this.currentPreviewPage}/${this.pageCount}</span>`
|
||||
: this.pageCount > 0
|
||||
? html`<span class="info-detail">${this.pageCount} pg</span>`
|
||||
: ''}
|
||||
${this.fileSize > 0 ? html`<span class="info-detail">${formatFileSize(this.fileSize)}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getTileClickDetail(): Record<string, unknown> {
|
||||
return {
|
||||
pdfUrl: this.pdfUrl,
|
||||
@@ -141,6 +148,13 @@ export class DeesTilePdf extends DeesTileBase {
|
||||
this.pdfDocument = await PdfManager.loadDocument(this.pdfUrl);
|
||||
this.pageCount = this.pdfDocument.numPages;
|
||||
this.currentPreviewPage = 1;
|
||||
|
||||
try {
|
||||
const downloadInfo = await this.pdfDocument.getDownloadInfo();
|
||||
this.fileSize = downloadInfo.length;
|
||||
} catch {
|
||||
// File size unavailable — not critical
|
||||
}
|
||||
this.loadedPdfUrl = this.pdfUrl;
|
||||
|
||||
this.loading = false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { html, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demo = () => {
|
||||
const samplePdfs = [
|
||||
@@ -29,7 +29,7 @@ export const demo = () => {
|
||||
<style>
|
||||
.demo-container {
|
||||
padding: 40px;
|
||||
background: #f5f5f5;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
@@ -40,6 +40,7 @@ export const demo = () => {
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
@@ -59,6 +60,7 @@ export const demo = () => {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ export const tilePdfStyles = css`
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 8px 8px 28px 8px;
|
||||
}
|
||||
|
||||
.preview-stack.non-a4 {
|
||||
padding: 12px;
|
||||
padding: 12px 12px 28px 12px;
|
||||
}
|
||||
|
||||
.preview-canvas {
|
||||
|
||||
@@ -59,9 +59,7 @@ export abstract class DeesTileBase extends DeesElement {
|
||||
|
||||
${!this.loading && !this.error ? this.renderTileContent() : ''}
|
||||
|
||||
${this.label ? html`
|
||||
<div class="tile-label">${this.label}</div>
|
||||
` : ''}
|
||||
${this.renderBottomBar()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -69,6 +67,11 @@ export abstract class DeesTileBase extends DeesElement {
|
||||
/** Subclasses implement this to render their specific content */
|
||||
protected abstract renderTileContent(): TemplateResult;
|
||||
|
||||
/** Subclasses override this to render a bottom info bar with metadata */
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
return '';
|
||||
}
|
||||
|
||||
public async connectedCallback(): Promise<void> {
|
||||
await super.connectedCallback();
|
||||
this.setupIntersectionObserver();
|
||||
|
||||
@@ -15,8 +15,9 @@ export const tileBaseStyles = [
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(215 20% 14%)')};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: box-shadow 0.2s ease;
|
||||
box-shadow: 0 1px 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.24)')};
|
||||
|
||||
}
|
||||
|
||||
.tile-container.clickable {
|
||||
@@ -24,7 +25,6 @@ export const tileBaseStyles = [
|
||||
}
|
||||
|
||||
.tile-container.clickable:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px ${cssManager.bdTheme('rgba(0, 0, 0, 0.12)', 'rgba(0, 0, 0, 0.3)')};
|
||||
}
|
||||
|
||||
@@ -71,90 +71,39 @@ export const tileBaseStyles = [
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tile-info {
|
||||
.tile-info-bar {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
padding: 6px 10px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.92)', 'hsl(215 20% 12% / 0.92)')};
|
||||
border-radius: 6px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 4px 8px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.95)', 'hsl(215 20% 12% / 0.95)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 45%)', 'hsl(215 16% 75%)')};
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tile-info dees-icon {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(213 93% 68%)')};
|
||||
}
|
||||
|
||||
.tile-info-text {
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tile-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
padding: 5px 8px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.7)', 'hsl(0 0% 100% / 0.9)')};
|
||||
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.tile-badge-corner {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
padding: 3px 8px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
|
||||
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
z-index: 25;
|
||||
font-variant-numeric: tabular-nums;
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tile-badge-topright {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 3px 8px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.6)', 'hsl(0 0% 100% / 0.85)')};
|
||||
color: ${cssManager.bdTheme('white', 'hsl(215 20% 12%)')};
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
.info-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Shift bottom badges up when label is present */
|
||||
.tile-container:has(.tile-label) .tile-badge-corner {
|
||||
bottom: 33px;
|
||||
.info-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tile-container:has(.tile-label) .tile-info {
|
||||
bottom: 33px;
|
||||
.info-detail {
|
||||
white-space: nowrap;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tile-loading,
|
||||
@@ -200,40 +149,12 @@ export const tileBaseStyles = [
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 6px 10px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100% / 0.95)', 'hsl(215 20% 12% / 0.95)')};
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215 16% 35%)', 'hsl(215 16% 75%)')};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
:host([size="small"]) .tile-container {
|
||||
width: 150px;
|
||||
|
||||
@@ -140,10 +140,6 @@ export class DeesTileVideo extends DeesTileBase {
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.duration > 0 ? html`
|
||||
<div class="tile-badge-corner">${this.formatTime(this.duration)}</div>
|
||||
` : ''}
|
||||
|
||||
${!this.isHovering ? html`
|
||||
<div class="play-overlay">
|
||||
<dees-icon icon="lucide:Play"></dees-icon>
|
||||
@@ -159,6 +155,17 @@ export class DeesTileVideo extends DeesTileBase {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderBottomBar(): TemplateResult | string {
|
||||
if (!this.label && !this.duration) return '';
|
||||
return html`
|
||||
<div class="tile-info-bar">
|
||||
${this.label ? html`<span class="info-label" title="${this.label}">${this.label}</span>` : ''}
|
||||
<span class="info-spacer"></span>
|
||||
${this.duration > 0 ? html`<span class="info-detail">${this.formatTime(this.duration)}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected getTileClickDetail(): Record<string, unknown> {
|
||||
return {
|
||||
src: this.src,
|
||||
|
||||
@@ -5,8 +5,6 @@ export * from './dees-video-viewer/index.js';
|
||||
export * from './dees-preview/index.js';
|
||||
|
||||
// PDF Components
|
||||
export * from './dees-pdf/index.js'; // @deprecated - Use dees-pdf-viewer or dees-tile-pdf instead
|
||||
export * from './dees-pdf-preview/index.js'; // @deprecated - Use dees-tile-pdf instead
|
||||
export * from './dees-pdf-shared/index.js';
|
||||
export * from './dees-pdf-viewer/index.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TemplateResult } from '@design.estate/dees-element';
|
||||
import type { IAppBarMenuItem } from './appbarmenuitem.js';
|
||||
import type { IMenuItem } from './tab.js';
|
||||
import type { IMenuItem, ITabAction } from './tab.js';
|
||||
import type { IMenuGroup } from './menugroup.js';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from './secondarymenu.js';
|
||||
|
||||
@@ -134,6 +134,8 @@ export type TDeesAppui = HTMLElement & {
|
||||
removeContentTab: (tabKey: string) => void;
|
||||
selectContentTab: (tabKey: string) => void;
|
||||
getSelectedContentTab: () => IMenuItem | undefined;
|
||||
setContentTabActionsLeft: (actions: ITabAction[]) => void;
|
||||
setContentTabActionsRight: (actions: ITabAction[]) => void;
|
||||
activityLog: IActivityLogAPI;
|
||||
setActivityLogVisible: (visible: boolean) => void;
|
||||
toggleActivityLog: () => void;
|
||||
|
||||
@@ -7,3 +7,11 @@ export interface IMenuItem {
|
||||
closeable?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface ITabAction {
|
||||
id: string;
|
||||
iconName: string;
|
||||
action: () => void | Promise<void>;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ export const CDN_VERSIONS = {
|
||||
xtermAddonFit: '0.8.0',
|
||||
xtermAddonSearch: '0.13.0',
|
||||
highlightJs: '11.11.1',
|
||||
apexcharts: '5.3.6',
|
||||
tiptap: '2.23.0',
|
||||
fontawesome: '7.1.0',
|
||||
apexcharts: '5.10.3',
|
||||
tiptap: '2.27.2',
|
||||
fontawesome: '7.2.0',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user