Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d6bd20321 | |||
| d7f3594dd4 | |||
| 2a6457e192 | |||
| 979e1f7991 | |||
| bbb57f1b9f |
BIN
.playwright-mcp/both-actionbars-test.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
.playwright-mcp/editor-actionbar-test.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
.playwright-mcp/editor-actionbar-visible.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
.playwright-mcp/editor-actionbar-working.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
.playwright-mcp/terminal-actionbar-resize-issue.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
.playwright-mcp/terminal-resize-fix-verification.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
.playwright-mcp/terminal-with-actionbar-fix.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
.playwright-mcp/workspace-actionbar-layout.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
.playwright-mcp/workspace-file-open.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
18
changelog.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -150,7 +151,10 @@ 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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -18,8 +19,9 @@ import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
|
|||||||
*/
|
*/
|
||||||
export class CodeBlockHandler extends BaseBlockHandler {
|
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,28 +308,33 @@ 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);
|
||||||
|
|
||||||
// Get plain text content
|
// Get plain text content
|
||||||
const content = editor.textContent || '';
|
const content = editor.textContent || '';
|
||||||
const language = block.metadata?.language || 'typescript';
|
const language = block.metadata?.language || 'typescript';
|
||||||
|
|
||||||
// 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
|
||||||
if (result.value) {
|
if (result.value) {
|
||||||
editor.innerHTML = result.value;
|
editor.innerHTML = result.value;
|
||||||
|
|
||||||
// Restore cursor position if editor is focused
|
// Restore cursor position if editor is focused
|
||||||
if (document.activeElement === editor && cursorPos !== null) {
|
if (document.activeElement === editor && cursorPos !== null) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.currentResolve = null;
|
||||||
|
}
|
||||||
|
|
||||||
private processQueue(): void {
|
private processQueue(): void {
|
||||||
if (this.queue.length === 0) {
|
if (this.queue.length === 0) {
|
||||||
this.currentBar = null;
|
// Hide with animation - don't await, let it run async
|
||||||
this.currentResolve = null;
|
this.hideCurrentBar();
|
||||||
this.isVisible = false;
|
|
||||||
this.classList.remove('visible');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
285
ts_web/services/DeesServiceLibLoader.ts
Normal 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
@@ -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';
|
||||||
17
ts_web/services/versions.ts
Normal 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';
|
||||||