Compare commits

...

5 Commits

Author SHA1 Message Date
8d6bd20321 v3.27.0
Some checks failed
Default (tags) / security (push) Failing after 14s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-01 20:25:05 +00:00
d7f3594dd4 feat(services): introduce DeesServiceLibLoader to lazy-load heavy client libraries from CDN and update components to use it 2026-01-01 20:25:05 +00:00
2a6457e192 v3.26.1
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-01 19:59:53 +00:00
979e1f7991 fix(dees-actionbar): animate actionbar hide using grid-template-rows and wait for animation before clearing state 2026-01-01 19:59:53 +00:00
bbb57f1b9f update 2026-01-01 18:33:05 +00:00
24 changed files with 518 additions and 63 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1,5 +1,23 @@
# Changelog # Changelog
## 2026-01-01 - 3.27.0 - feat(services)
introduce DeesServiceLibLoader to lazy-load heavy client libraries from CDN and update components to use it
- Add DeesServiceLibLoader singleton (ts_web/services/DeesServiceLibLoader.ts) to lazily load and cache libraries via jsDelivr ESM: xterm, xterm-addon-fit, highlight.js, ApexCharts, and Tiptap.
- Inject xterm CSS dynamically to avoid shipping xterm styles in the initial bundle.
- Expose helper methods preloadAll() and isLoaded(), and typed bundle interfaces (IXtermBundle, IXtermFitAddonBundle, ITiptapBundle).
- Update components to use runtime-loaded modules: dees-chart-area, dees-dataview-codebox, dees-input-richtext, wysiwyg code block, dees-workspace-terminal, terminal-tab-manager, dees-workspace-terminal-preview.
- TerminalTabManager now requires setXtermModules(...) before creating tabs and will throw if not initialized; workspace terminal now initializes and passes the loaded modules.
- Replace direct runtime imports of heavy libs with typed imports and runtime-loaded bundles to reduce initial bundle size and improve load performance.
## 2026-01-01 - 3.26.1 - fix(dees-actionbar)
animate actionbar hide using grid-template-rows and wait for animation before clearing state
- Switch host layout from block/max-height to grid using grid-template-rows for open/close transitions
- Add min-height: 0 to .actionbar-item to prevent flex children overflow and collapsing
- Introduce async hideCurrentBar() that removes 'visible', sets isVisible=false, waits 220ms then clears currentBar and currentResolve
- processQueue() now calls hideCurrentBar() asynchronously instead of clearing state immediately
## 2026-01-01 - 3.26.0 - feat(workspace) ## 2026-01-01 - 3.26.0 - feat(workspace)
add external file change detection, conflict resolution UI, and diff editor add external file change detection, conflict resolution UI, and diff editor

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.26.0", "version": "3.27.0",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",

View File

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

View File

@@ -11,7 +11,8 @@ import { demoFunc } from './demo.js';
import { chartAreaStyles } from './styles.js'; import { chartAreaStyles } from './styles.js';
import { renderChartArea } from './template.js'; import { renderChartArea } from './template.js';
import ApexCharts from 'apexcharts'; import type ApexCharts from 'apexcharts';
import { DeesServiceLibLoader } from '../../../services/index.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -151,6 +152,9 @@ export class DeesChartArea extends DeesElement {
public async firstUpdated() { public async firstUpdated() {
await this.domtoolsPromise; await this.domtoolsPromise;
// Load ApexCharts from CDN
const ApexChartsLib = await DeesServiceLibLoader.getInstance().loadApexCharts();
// Wait for next animation frame to ensure layout is complete // Wait for next animation frame to ensure layout is complete
await new Promise(resolve => requestAnimationFrame(resolve)); await new Promise(resolve => requestAnimationFrame(resolve));
@@ -353,7 +357,7 @@ export class DeesChartArea extends DeesElement {
}; };
try { try {
this.chart = new ApexCharts(this.shadowRoot.querySelector('.chartContainer'), options); this.chart = new ApexChartsLib(this.shadowRoot.querySelector('.chartContainer'), options);
await this.chart.render(); await this.chart.render();
// Give the chart a moment to fully initialize before resizing // Give the chart a moment to fully initialize before resizing

View File

@@ -10,12 +10,13 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { cssGeistFontFamily, cssMonoFontFamily } from '../../00fonts.js'; import { cssGeistFontFamily, cssMonoFontFamily } from '../../00fonts.js';
import hlight from 'highlight.js'; import type { HLJSApi } from 'highlight.js';
import * as smartstring from '@push.rocks/smartstring'; import * as smartstring from '@push.rocks/smartstring';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import { DeesServiceLibLoader } from '../../../services/index.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -229,6 +230,7 @@ export class DeesDataviewCodebox extends DeesElement {
} }
private codeToDisplayStore = ''; private codeToDisplayStore = '';
private highlightJs: HLJSApi | null = null;
public async updated(_changedProperties) { public async updated(_changedProperties) {
super.updated(_changedProperties); super.updated(_changedProperties);
@@ -250,11 +252,17 @@ export class DeesDataviewCodebox extends DeesElement {
this.codeToDisplay = this.codeToDisplayStore; this.codeToDisplay = this.codeToDisplayStore;
} }
await domtools.plugins.smartdelay.delayFor(0); await domtools.plugins.smartdelay.delayFor(0);
// Load highlight.js from CDN if not already loaded
if (!this.highlightJs) {
this.highlightJs = await DeesServiceLibLoader.getInstance().loadHighlightJs();
}
const localCodeNode = this.shadowRoot.querySelector('code'); const localCodeNode = this.shadowRoot.querySelector('code');
const html = hlight.highlight(this.codeToDisplayStore, { const highlightedHtml = this.highlightJs.highlight(this.codeToDisplayStore, {
language: this.progLang, language: this.progLang,
ignoreIllegals: true, ignoreIllegals: true,
}); });
localCodeNode.innerHTML = html.value; localCodeNode.innerHTML = highlightedHtml.value;
} }
} }

View File

@@ -14,12 +14,8 @@ import {
query, query,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { Editor } from '@tiptap/core'; import type { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit'; import { DeesServiceLibLoader, type ITiptapBundle } from '../../../services/index.js';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Typography from '@tiptap/extension-typography';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -63,6 +59,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
private editorElement: HTMLElement; private editorElement: HTMLElement;
private linkInputElement: HTMLInputElement; private linkInputElement: HTMLInputElement;
private tiptapBundle: ITiptapBundle | null = null;
public editor: Editor; public editor: Editor;
@@ -233,13 +230,19 @@ export class DeesInputRichtext extends DeesInputBase<string> {
public async firstUpdated() { public async firstUpdated() {
await this.updateComplete; await this.updateComplete;
// Load Tiptap from CDN
this.tiptapBundle = await DeesServiceLibLoader.getInstance().loadTiptap();
this.editorElement = this.shadowRoot.querySelector('.editor-content'); this.editorElement = this.shadowRoot.querySelector('.editor-content');
this.linkInputElement = this.shadowRoot.querySelector('.link-input input'); this.linkInputElement = this.shadowRoot.querySelector('.link-input input');
this.initializeEditor(); this.initializeEditor();
} }
private initializeEditor(): void { private initializeEditor(): void {
if (this.disabled) return; if (this.disabled || !this.tiptapBundle) return;
const { Editor, StarterKit, Underline, TextAlign, Link, Typography } = this.tiptapBundle;
this.editor = new Editor({ this.editor = new Editor({
element: this.editorElement, element: this.editorElement,
@@ -249,7 +252,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
levels: [1, 2, 3], levels: [1, 2, 3],
}, },
}), }),
Underline, Underline.configure({}),
TextAlign.configure({ TextAlign.configure({
types: ['heading', 'paragraph'], types: ['heading', 'paragraph'],
}), }),
@@ -259,7 +262,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
class: 'editor-link', class: 'editor-link',
}, },
}), }),
Typography, Typography.configure({}),
], ],
content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''), content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''),
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {

View File

@@ -2,9 +2,10 @@ import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js'; import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element'; import { cssManager } from '@design.estate/dees-element';
import { WysiwygSelection } from '../../wysiwyg.selection.js'; import { WysiwygSelection } from '../../wysiwyg.selection.js';
import hlight from 'highlight.js'; import type { HLJSApi } from 'highlight.js';
import { cssGeistFontFamily, cssMonoFontFamily } from '../../../../00fonts.js'; import { cssGeistFontFamily, cssMonoFontFamily } from '../../../../00fonts.js';
import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js'; import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
import { DeesServiceLibLoader } from '../../../../../services/index.js';
/** /**
* CodeBlockHandler with improved architecture * CodeBlockHandler with improved architecture
@@ -20,6 +21,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
type = 'code'; type = 'code';
private highlightTimer: any = null; private highlightTimer: any = null;
private highlightJs: HLJSApi | null = null;
render(block: IBlock, isSelected: boolean): string { render(block: IBlock, isSelected: boolean): string {
const language = block.metadata?.language || 'typescript'; const language = block.metadata?.language || 'typescript';
@@ -306,10 +308,15 @@ export class CodeBlockHandler extends BaseBlockHandler {
return linesBeforeCursor.length - 1; // 0-indexed return linesBeforeCursor.length - 1; // 0-indexed
} }
private applyHighlighting(element: HTMLElement, block: IBlock): void { private async applyHighlighting(element: HTMLElement, block: IBlock): Promise<void> {
const editor = element.querySelector('.code-editor') as HTMLElement; const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return; if (!editor) return;
// Load highlight.js from CDN if not already loaded
if (!this.highlightJs) {
this.highlightJs = await DeesServiceLibLoader.getInstance().loadHighlightJs();
}
// Store cursor position // Store cursor position
const cursorPos = this.getCursorPosition(element); const cursorPos = this.getCursorPosition(element);
@@ -319,9 +326,9 @@ export class CodeBlockHandler extends BaseBlockHandler {
// Apply highlighting // Apply highlighting
try { try {
const result = hlight.highlight(content, { const result = this.highlightJs.highlight(content, {
language: language, language: language,
ignoreIllegals: true ignoreIllegals: true,
}); });
// Only update if we have valid highlighted content // Only update if we have valid highlighted content

View File

@@ -7,9 +7,10 @@ import {
css, css,
cssManager, cssManager,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { Terminal } from 'xterm'; import type { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import type { FitAddon } from 'xterm-addon-fit';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
import { DeesServiceLibLoader } from '../../../services/index.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -305,8 +306,15 @@ export class DeesWorkspaceTerminalPreview extends DeesElement {
const domtoolsInstance = await this.domtoolsPromise; const domtoolsInstance = await this.domtoolsPromise;
const isBright = domtoolsInstance.themeManager.goBrightBoolean; const isBright = domtoolsInstance.themeManager.goBrightBoolean;
// Create xterm terminal in read-only mode // Load xterm from CDN
this.terminal = new Terminal({ const libLoader = DeesServiceLibLoader.getInstance();
const [xtermBundle, fitAddonBundle] = await Promise.all([
libLoader.loadXterm(),
libLoader.loadXtermFitAddon(),
]);
// Create xterm terminal in read-only mode using CDN-loaded module
this.terminal = new xtermBundle.Terminal({
convertEol: true, convertEol: true,
cursorBlink: false, cursorBlink: false,
disableStdin: true, disableStdin: true,
@@ -323,7 +331,7 @@ export class DeesWorkspaceTerminalPreview extends DeesElement {
} }
}); });
this.fitAddon = new FitAddon(); this.fitAddon = new fitAddonBundle.FitAddon();
this.terminal.loadAddon(this.fitAddon); this.terminal.loadAddon(this.fitAddon);
this.terminal.open(container); this.terminal.open(container);
this.fitAddon.fit(); this.fitAddon.fit();

View File

@@ -10,18 +10,20 @@ import {
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { Terminal } from 'xterm'; import type { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
import type { IExecutionEnvironment } from '../../00group-runtime/index.js'; import type { IExecutionEnvironment } from '../../00group-runtime/index.js';
import { WebContainerEnvironment } from '../../00group-runtime/index.js'; import { WebContainerEnvironment } from '../../00group-runtime/index.js';
import '../../dees-icon/dees-icon.js'; import '../../dees-icon/dees-icon.js';
import '../../dees-actionbar/dees-actionbar.js';
import type { DeesActionbar } from '../../dees-actionbar/dees-actionbar.js';
import { TerminalTabManager } from './terminal-tab-manager.js'; import { TerminalTabManager } from './terminal-tab-manager.js';
import type { import type {
ITerminalTab, ITerminalTab,
ICreateTerminalTabOptions, ICreateTerminalTabOptions,
TTerminalTabType, TTerminalTabType,
} from './interfaces.js'; } from './interfaces.js';
import { DeesServiceLibLoader } from '../../../services/index.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -79,6 +81,9 @@ export class DeesWorkspaceTerminal extends DeesElement {
private terminalThemeSubscription: any = null; private terminalThemeSubscription: any = null;
private isBright: boolean = false; private isBright: boolean = false;
// Actionbar reference for terminal-context notifications
private terminalActionbar: DeesActionbar | null = null;
/** /**
* Promise that resolves when the environment is ready. * Promise that resolves when the environment is ready.
* @deprecated Use executionEnvironment directly * @deprecated Use executionEnvironment directly
@@ -120,17 +125,21 @@ export class DeesWorkspaceTerminal extends DeesElement {
.terminal-content { .terminal-content {
flex: 1; flex: 1;
position: relative; display: flex;
flex-direction: column;
overflow: hidden; overflow: hidden;
background: ${cssManager.bdTheme('#ffffff', '#000000')}; background: ${cssManager.bdTheme('#ffffff', '#000000')};
} }
#active-terminal-container { #active-terminal-container {
position: absolute; flex: 1;
top: 20px; position: relative;
left: 20px; min-height: 0;
right: 20px; margin: 20px;
bottom: 20px; }
.terminal-content dees-actionbar {
flex-shrink: 0;
} }
/* Tab bar on the right side */ /* Tab bar on the right side */
@@ -426,6 +435,7 @@ export class DeesWorkspaceTerminal extends DeesElement {
<span>No terminal open</span> <span>No terminal open</span>
</div> </div>
`} `}
<dees-actionbar></dees-actionbar>
</div> </div>
<!-- Vertical tab bar on the right --> <!-- Vertical tab bar on the right -->
@@ -485,17 +495,31 @@ export class DeesWorkspaceTerminal extends DeesElement {
} }
); );
// Load xterm from CDN
const libLoader = DeesServiceLibLoader.getInstance();
const [xtermBundle, fitAddonBundle] = await Promise.all([
libLoader.loadXterm(),
libLoader.loadXtermFitAddon(),
]);
// Initialize tab manager with loaded modules
this.tabManager.setXtermModules(xtermBundle, fitAddonBundle);
// Create default shell tab // Create default shell tab
await this.createShellTab(); await this.createShellTab();
} }
async connectedCallback(): Promise<void> { async connectedCallback(): Promise<void> {
await super.connectedCallback(); await super.connectedCallback();
this.resizeObserver.observe(this); // ResizeObserver is set up in attachTerminalToContainer when the container exists
} }
async disconnectedCallback(): Promise<void> { async disconnectedCallback(): Promise<void> {
this.resizeObserver.unobserve(this); // Unobserve the terminal container
const container = this.shadowRoot?.getElementById('active-terminal-container');
if (container) {
this.resizeObserver.unobserve(container);
}
if (this.terminalThemeSubscription) { if (this.terminalThemeSubscription) {
this.terminalThemeSubscription.unsubscribe(); this.terminalThemeSubscription.unsubscribe();
this.terminalThemeSubscription = null; this.terminalThemeSubscription = null;
@@ -558,6 +582,10 @@ export class DeesWorkspaceTerminal extends DeesElement {
const container = this.shadowRoot?.getElementById('active-terminal-container'); const container = this.shadowRoot?.getElementById('active-terminal-container');
if (!container) return; if (!container) return;
// Observe container for resize (handles actionbar appearing/disappearing)
// ResizeObserver.observe() is idempotent - safe to call multiple times
this.resizeObserver.observe(container);
// Clear container // Clear container
container.innerHTML = ''; container.innerHTML = '';
@@ -656,6 +684,36 @@ export class DeesWorkspaceTerminal extends DeesElement {
detail: { tabId, exitCode }, detail: { tabId, exitCode },
}) })
); );
// Show actionbar to offer closing the tab (only if tab is closeable)
if (tab.closeable) {
this.showExitedTabActionbar(tabId, tab.label, exitCode);
}
}
/**
* Show actionbar offering to close an exited tab
*/
private async showExitedTabActionbar(tabId: string, tabLabel: string, exitCode: number): Promise<void> {
const isSuccess = exitCode === 0;
const result = await this.showActionbar({
message: isSuccess
? `"${tabLabel}" completed. Close tab?`
: `"${tabLabel}" exited (code ${exitCode}). Close tab?`,
type: isSuccess ? 'info' : 'warning',
icon: isSuccess ? 'lucide:checkCircle' : 'lucide:alertTriangle',
actions: [
{ id: 'close', label: 'Close Tab', primary: true },
{ id: 'keep', label: 'Keep Open' },
],
timeout: { duration: 10000, defaultActionId: 'close' },
dismissible: true,
});
// Close tab if user clicked "Close Tab" or timeout triggered auto-close
if (result.actionId === 'close') {
this.closeTab(tabId);
}
} }
// ========== Public API ========== // ========== Public API ==========
@@ -816,6 +874,19 @@ export class DeesWorkspaceTerminal extends DeesElement {
return true; return true;
} }
/**
* Show an actionbar notification in the terminal panel context.
* Use this for terminal-related decisions (e.g., retry failed process, kill process, etc.)
*/
public async showActionbar(
options: Parameters<DeesActionbar['show']>[0]
): Promise<ReturnType<DeesActionbar['show']>> {
if (!this.terminalActionbar) {
this.terminalActionbar = this.shadowRoot?.querySelector('dees-actionbar') as DeesActionbar;
}
return this.terminalActionbar?.show(options);
}
// ========== Utility Methods ========== // ========== Utility Methods ==========
public async waitForPrompt(term: Terminal, prompt: string): Promise<void> { public async waitForPrompt(term: Terminal, prompt: string): Promise<void> {

View File

@@ -1,6 +1,7 @@
import { Terminal } from 'xterm'; import type { Terminal, ITerminalOptions } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import type { FitAddon } from 'xterm-addon-fit';
import type { ITerminalTab, ICreateTerminalTabOptions, TTerminalTabType } from './interfaces.js'; import type { ITerminalTab, ICreateTerminalTabOptions, TTerminalTabType } from './interfaces.js';
import type { IXtermBundle, IXtermFitAddonBundle } from '../../../services/index.js';
/** /**
* Manages terminal tabs lifecycle and state * Manages terminal tabs lifecycle and state
@@ -8,6 +9,17 @@ import type { ITerminalTab, ICreateTerminalTabOptions, TTerminalTabType } from '
export class TerminalTabManager { export class TerminalTabManager {
private tabs: Map<string, ITerminalTab> = new Map(); private tabs: Map<string, ITerminalTab> = new Map();
private tabCounter: number = 0; private tabCounter: number = 0;
private xtermBundle: IXtermBundle | null = null;
private xtermFitAddonBundle: IXtermFitAddonBundle | null = null;
/**
* Initialize the manager with loaded xterm modules.
* Must be called before creating tabs.
*/
public setXtermModules(xtermBundle: IXtermBundle, fitAddonBundle: IXtermFitAddonBundle): void {
this.xtermBundle = xtermBundle;
this.xtermFitAddonBundle = fitAddonBundle;
}
/** /**
* Generate unique tab ID * Generate unique tab ID
@@ -96,11 +108,15 @@ export class TerminalTabManager {
* Create a new tab instance * Create a new tab instance
*/ */
createTab(options: ICreateTerminalTabOptions, isBright: boolean): ITerminalTab { createTab(options: ICreateTerminalTabOptions, isBright: boolean): ITerminalTab {
if (!this.xtermBundle || !this.xtermFitAddonBundle) {
throw new Error('TerminalTabManager: xterm modules not initialized. Call setXtermModules() first.');
}
const id = this.generateTabId(); const id = this.generateTabId();
const type = options.type; const type = options.type;
// Create xterm.js Terminal instance // Create xterm.js Terminal instance using CDN-loaded module
const terminal = new Terminal({ const terminal = new this.xtermBundle.Terminal({
convertEol: true, convertEol: true,
cursorBlink: true, cursorBlink: true,
theme: this.getTerminalTheme(isBright), theme: this.getTerminalTheme(isBright),
@@ -109,8 +125,8 @@ export class TerminalTabManager {
lineHeight: 1.2, lineHeight: 1.2,
}); });
// Create FitAddon // Create FitAddon using CDN-loaded module
const fitAddon = new FitAddon(); const fitAddon = new this.xtermFitAddonBundle.FitAddon();
terminal.loadAddon(fitAddon); terminal.loadAddon(fitAddon);
const tab: ITerminalTab = { const tab: ITerminalTab = {

View File

@@ -957,6 +957,7 @@ testSmartPromise();
></dees-workspace-monaco> ></dees-workspace-monaco>
`} `}
</div> </div>
<dees-actionbar></dees-actionbar>
</div> </div>
<!-- Horizontal resize handle for terminal --> <!-- Horizontal resize handle for terminal -->
@@ -1021,9 +1022,6 @@ testSmartPromise();
.executionEnvironment=${this.executionEnvironment} .executionEnvironment=${this.executionEnvironment}
@run-process=${this.handleRunProcess} @run-process=${this.handleRunProcess}
></dees-workspace-bottombar> ></dees-workspace-bottombar>
<!-- Action Bar for notifications -->
<dees-actionbar></dees-actionbar>
</div> </div>
`; `;
} }
@@ -1056,9 +1054,6 @@ testSmartPromise();
this.currentFileTreeWidth = this.fileTreeWidth; this.currentFileTreeWidth = this.fileTreeWidth;
this.currentTerminalHeight = this.terminalHeight; this.currentTerminalHeight = this.terminalHeight;
// Get actionbar reference for file change notifications
this.actionbarElement = this.shadowRoot?.querySelector('dees-actionbar') as DeesActionbar;
if (this.executionEnvironment) { if (this.executionEnvironment) {
await this.initializeWorkspace(); await this.initializeWorkspace();
} }
@@ -1068,6 +1063,11 @@ testSmartPromise();
if (changedProperties.has('executionEnvironment') && this.executionEnvironment) { if (changedProperties.has('executionEnvironment') && this.executionEnvironment) {
await this.initializeWorkspace(); await this.initializeWorkspace();
} }
// Capture actionbar reference when it becomes available (after initialization completes)
if (!this.actionbarElement) {
this.actionbarElement = this.shadowRoot?.querySelector('.editor-panel dees-actionbar') as DeesActionbar;
}
} }
private async initializeWorkspace() { private async initializeWorkspace() {

View File

@@ -128,19 +128,19 @@ export class DeesActionbar extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
display: block; display: grid;
overflow: hidden; grid-template-rows: 0fr;
height: 0; transition: grid-template-rows 0.2s ease-out;
transition: height 0.2s ease-out;
} }
:host(.visible) { :host(.visible) {
height: auto; grid-template-rows: 1fr;
} }
.actionbar-item { .actionbar-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0;
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 12%)')}; background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 12%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 20%)')}; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 88%)', 'hsl(0 0% 20%)')};
overflow: hidden; overflow: hidden;
@@ -416,12 +416,27 @@ export class DeesActionbar extends DeesElement {
// ========== Private Methods ========== // ========== Private Methods ==========
private processQueue(): void { /**
if (this.queue.length === 0) { * Hide the current actionbar with animation.
* Removes visible class first to trigger CSS transition, then clears content after animation.
*/
private async hideCurrentBar(): Promise<void> {
// Remove visible class to start close animation
this.classList.remove('visible');
this.isVisible = false;
// Wait for animation to complete (200ms transition + buffer)
await new Promise(resolve => setTimeout(resolve, 220));
// Now safe to clear content
this.currentBar = null; this.currentBar = null;
this.currentResolve = null; this.currentResolve = null;
this.isVisible = false; }
this.classList.remove('visible');
private processQueue(): void {
if (this.queue.length === 0) {
// Hide with animation - don't await, let it run async
this.hideCurrentBar();
return; return;
} }

View File

@@ -0,0 +1,285 @@
import { CDN_BASE, CDN_VERSIONS } from './versions.js';
// Type imports (no runtime overhead)
import type { Terminal, ITerminalOptions } from 'xterm';
import type { FitAddon } from 'xterm-addon-fit';
import type { HLJSApi } from 'highlight.js';
import type ApexChartsType from 'apexcharts';
import type { Editor, EditorOptions } from '@tiptap/core';
import type { StarterKitOptions } from '@tiptap/starter-kit';
import type { UnderlineOptions } from '@tiptap/extension-underline';
import type { TextAlignOptions } from '@tiptap/extension-text-align';
import type { LinkOptions } from '@tiptap/extension-link';
/**
* Bundle type for xterm and its addons
*/
export interface IXtermBundle {
Terminal: typeof Terminal;
}
/**
* Bundle type for xterm-addon-fit
*/
export interface IXtermFitAddonBundle {
FitAddon: typeof FitAddon;
}
/**
* Bundle type for Tiptap editor and extensions
*/
export interface ITiptapBundle {
Editor: typeof Editor;
StarterKit: { configure: (options?: Partial<StarterKitOptions>) => any };
Underline: { configure: (options?: Partial<UnderlineOptions>) => any };
TextAlign: { configure: (options?: Partial<TextAlignOptions>) => any };
Link: { configure: (options?: Partial<LinkOptions>) => any };
Typography: { configure: (options?: any) => any };
}
/**
* Singleton service for lazy-loading heavy libraries from CDN.
*
* This reduces initial bundle size by loading libraries only when needed.
* Libraries are cached after first load to avoid duplicate fetches.
*
* @example
* ```typescript
* const libLoader = DeesServiceLibLoader.getInstance();
* const xterm = await libLoader.loadXterm();
* const terminal = new xterm.Terminal({ ... });
* ```
*/
export class DeesServiceLibLoader {
private static instance: DeesServiceLibLoader;
// Cached library references
private xtermLib: IXtermBundle | null = null;
private xtermFitAddonLib: IXtermFitAddonBundle | null = null;
private highlightJsLib: HLJSApi | null = null;
private apexChartsLib: typeof ApexChartsType | null = null;
private tiptapLib: ITiptapBundle | null = null;
// Loading promises to prevent duplicate concurrent loads
private xtermLoadingPromise: Promise<IXtermBundle> | null = null;
private xtermFitAddonLoadingPromise: Promise<IXtermFitAddonBundle> | null = null;
private highlightJsLoadingPromise: Promise<HLJSApi> | null = null;
private apexChartsLoadingPromise: Promise<typeof ApexChartsType> | null = null;
private tiptapLoadingPromise: Promise<ITiptapBundle> | null = null;
private constructor() {}
/**
* Get the singleton instance of DeesServiceLibLoader
*/
public static getInstance(): DeesServiceLibLoader {
if (!DeesServiceLibLoader.instance) {
DeesServiceLibLoader.instance = new DeesServiceLibLoader();
}
return DeesServiceLibLoader.instance;
}
/**
* Load xterm terminal emulator from CDN
* @returns Promise resolving to xterm module with Terminal class
*/
public async loadXterm(): Promise<IXtermBundle> {
if (this.xtermLib) {
return this.xtermLib;
}
if (this.xtermLoadingPromise) {
return this.xtermLoadingPromise;
}
this.xtermLoadingPromise = (async () => {
const url = `${CDN_BASE}/xterm@${CDN_VERSIONS.xterm}/+esm`;
const module = await import(/* @vite-ignore */ url);
// Also load and inject xterm CSS
await this.injectXtermStyles();
this.xtermLib = {
Terminal: module.Terminal,
};
return this.xtermLib;
})();
return this.xtermLoadingPromise;
}
/**
* Load xterm-addon-fit from CDN
* @returns Promise resolving to FitAddon class
*/
public async loadXtermFitAddon(): Promise<IXtermFitAddonBundle> {
if (this.xtermFitAddonLib) {
return this.xtermFitAddonLib;
}
if (this.xtermFitAddonLoadingPromise) {
return this.xtermFitAddonLoadingPromise;
}
this.xtermFitAddonLoadingPromise = (async () => {
const url = `${CDN_BASE}/xterm-addon-fit@${CDN_VERSIONS.xtermAddonFit}/+esm`;
const module = await import(/* @vite-ignore */ url);
this.xtermFitAddonLib = {
FitAddon: module.FitAddon,
};
return this.xtermFitAddonLib;
})();
return this.xtermFitAddonLoadingPromise;
}
/**
* Inject xterm CSS styles into the document head
*/
private async injectXtermStyles(): Promise<void> {
const styleId = 'xterm-cdn-styles';
if (document.getElementById(styleId)) {
return; // Already injected
}
const cssUrl = `${CDN_BASE}/xterm@${CDN_VERSIONS.xterm}/css/xterm.css`;
const response = await fetch(cssUrl);
const cssText = await response.text();
const style = document.createElement('style');
style.id = styleId;
style.textContent = cssText;
document.head.appendChild(style);
}
/**
* Load highlight.js syntax highlighter from CDN
* @returns Promise resolving to highlight.js API
*/
public async loadHighlightJs(): Promise<HLJSApi> {
if (this.highlightJsLib) {
return this.highlightJsLib;
}
if (this.highlightJsLoadingPromise) {
return this.highlightJsLoadingPromise;
}
this.highlightJsLoadingPromise = (async () => {
const url = `${CDN_BASE}/highlight.js@${CDN_VERSIONS.highlightJs}/+esm`;
const module = await import(/* @vite-ignore */ url);
this.highlightJsLib = module.default;
return this.highlightJsLib;
})();
return this.highlightJsLoadingPromise;
}
/**
* Load ApexCharts charting library from CDN
* @returns Promise resolving to ApexCharts constructor
*/
public async loadApexCharts(): Promise<typeof ApexChartsType> {
if (this.apexChartsLib) {
return this.apexChartsLib;
}
if (this.apexChartsLoadingPromise) {
return this.apexChartsLoadingPromise;
}
this.apexChartsLoadingPromise = (async () => {
const url = `${CDN_BASE}/apexcharts@${CDN_VERSIONS.apexcharts}/+esm`;
const module = await import(/* @vite-ignore */ url);
this.apexChartsLib = module.default;
return this.apexChartsLib;
})();
return this.apexChartsLoadingPromise;
}
/**
* Load Tiptap rich text editor and extensions from CDN
* @returns Promise resolving to Tiptap bundle with Editor and extensions
*/
public async loadTiptap(): Promise<ITiptapBundle> {
if (this.tiptapLib) {
return this.tiptapLib;
}
if (this.tiptapLoadingPromise) {
return this.tiptapLoadingPromise;
}
this.tiptapLoadingPromise = (async () => {
const version = CDN_VERSIONS.tiptap;
// Load all Tiptap modules in parallel
const [
coreModule,
starterKitModule,
underlineModule,
textAlignModule,
linkModule,
typographyModule,
] = await Promise.all([
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/core@${version}/+esm`),
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/starter-kit@${version}/+esm`),
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-underline@${version}/+esm`),
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-text-align@${version}/+esm`),
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-link@${version}/+esm`),
import(/* @vite-ignore */ `${CDN_BASE}/@tiptap/extension-typography@${version}/+esm`),
]);
this.tiptapLib = {
Editor: coreModule.Editor,
StarterKit: starterKitModule.default || starterKitModule.StarterKit,
Underline: underlineModule.default || underlineModule.Underline,
TextAlign: textAlignModule.default || textAlignModule.TextAlign,
Link: linkModule.default || linkModule.Link,
Typography: typographyModule.default || typographyModule.Typography,
};
return this.tiptapLib;
})();
return this.tiptapLoadingPromise;
}
/**
* Preload multiple libraries in parallel
* Useful for warming the cache before components are rendered
*/
public async preloadAll(): Promise<void> {
await Promise.all([
this.loadXterm(),
this.loadXtermFitAddon(),
this.loadHighlightJs(),
this.loadApexCharts(),
this.loadTiptap(),
]);
}
/**
* Check if a specific library is already loaded
*/
public isLoaded(library: 'xterm' | 'xtermFitAddon' | 'highlightJs' | 'apexCharts' | 'tiptap'): boolean {
switch (library) {
case 'xterm':
return this.xtermLib !== null;
case 'xtermFitAddon':
return this.xtermFitAddonLib !== null;
case 'highlightJs':
return this.highlightJsLib !== null;
case 'apexCharts':
return this.apexChartsLib !== null;
case 'tiptap':
return this.tiptapLib !== null;
default:
return false;
}
}
}

3
ts_web/services/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { DeesServiceLibLoader } from './DeesServiceLibLoader.js';
export type { IXtermBundle, IXtermFitAddonBundle, ITiptapBundle } from './DeesServiceLibLoader.js';
export { CDN_BASE, CDN_VERSIONS } from './versions.js';

View File

@@ -0,0 +1,17 @@
/**
* CDN versions for lazy-loaded libraries.
* Keep these in sync with package.json for type compatibility.
*/
export const CDN_VERSIONS = {
xterm: '5.3.0',
xtermAddonFit: '0.8.0',
highlightJs: '11.11.1',
apexcharts: '5.3.6',
tiptap: '2.23.0',
fontawesome: '7.1.0',
} as const;
/**
* Base CDN URL for jsdelivr ESM imports
*/
export const CDN_BASE = 'https://cdn.jsdelivr.net/npm';