Compare commits

...

10 Commits

Author SHA1 Message Date
fc2661fb4c v3.23.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-31 18:24:10 +00:00
deb50dfde2 feat(workspace): add resizable file tree and terminal panes with draggable handles and public layout APIs 2025-12-31 18:24:10 +00:00
7ac0ac8b0a v3.22.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-31 17:03:27 +00:00
9fa48e511c feat(workspace): add resizable markdown editor/preview split with draggable handle and markdown outlet styling/demo 2025-12-31 17:03:27 +00:00
11c88f9749 v3.21.0
Some checks failed
Default (tags) / security (push) Failing after 13s
Default (tags) / test (push) Failing after 15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-31 14:01:42 +00:00
d0bd4027bb feat(terminal): add dynamic bright/dark theming for terminal components and terminal preview 2025-12-31 14:01:42 +00:00
62de004350 v3.20.1
Some checks failed
Default (tags) / security (push) Failing after 17s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-31 12:45:19 +00:00
cfe3490bcf fix(dees-workspace): fix demo wrapper and workspace layout; always render terminal preview 2025-12-31 12:45:19 +00:00
826689ec0e v3.20.0
Some checks failed
Default (tags) / security (push) Failing after 18s
Default (tags) / test (push) Failing after 16s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-31 12:37:14 +00:00
15bca09086 feat(workspace): rename editor components to workspace group and move terminal & TypeScript intellisense into workspace 2025-12-31 12:37:14 +00:00
40 changed files with 1898 additions and 354 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,5 +1,52 @@
# Changelog # Changelog
## 2025-12-31 - 3.23.0 - feat(workspace)
add resizable file tree and terminal panes with draggable handles and public layout APIs
- Introduce reactive state for currentFileTreeWidth, currentTerminalHeight, isDraggingFileTree and isDraggingTerminal
- Add mouse event handlers (mousedown/move/up) to drag-resize the file tree and terminal with min/max clamping
- Dispatch window resize event after resizing to notify Monaco/editor of layout changes
- Clean up resize event listeners in disconnectedCallback
- Initialize current sizes from component properties in firstUpdated
- Expose public layout methods: setFileTreeWidth, setTerminalHeight, resetLayout
## 2025-12-31 - 3.22.0 - feat(workspace)
add resizable markdown editor/preview split with draggable handle and markdown outlet styling/demo
- Introduce a flexible split layout for the workspace markdown editor with a draggable resize handle (splitRatio, minPanelSize, dragging state, mouse handlers and cleanup).
- Enhance dees-workspace-markdown: switch from grid to flex, add resize handle UI, prevent pointer selection while dragging, notify Monaco on resize.
- Add comprehensive styling and demo content for dees-workspace-markdownoutlet (dark/light themed markdown styles, syntax highlighting classes, and demo scenarios).
- Fix typescript-intellisense monaco model update: only set model value when content actually changed to avoid cursor resets.
- Add markdown outlet demo helper and numerous screenshot/image assets (.playwright-mcp) for demos/documentation.
## 2025-12-31 - 3.21.0 - feat(terminal)
add dynamic bright/dark theming for terminal components and terminal preview
- Add bright/dark theme PNG assets under .playwright-mcp for previews.
- Replace hardcoded terminal background/colors with cssManager.bdTheme in workspace terminal and preview styles.
- Introduce getTerminalTheme helper to compute xterm theme for bright/dark modes.
- Subscribe to themeManager.themeObservable and apply updates to xterm (terminal.options.theme) so terminals update live on theme change.
- Remove hardcoded background property/CSS var and unused background accessor from workspace terminal.
- Ensure proper cleanup: unsubscribe theme subscriptions and dispose terminals in disconnectedCallback.
## 2025-12-31 - 3.20.1 - fix(dees-workspace)
fix demo wrapper and workspace layout; always render terminal preview
- Import @design.estate/dees-wcctools/demotools and wrap demo content in <dees-demowrapper>
- Create an absolute-positioned container so the dees-workspace fills 100% height/width
- Always render dees-workspace-terminal-preview (use empty command when none) to avoid conditional rendering issues
- Set a fixed height (200px) for the terminal preview in the initializing state
- Add Playwright demo asset .playwright-mcp/dees-workspace-demo-4k.png
## 2025-12-31 - 3.20.0 - feat(workspace)
rename editor components to workspace group and move terminal & TypeScript intellisense into workspace
- Renamed components and modules from 00group-editor/dees-editor-* to 00group-workspace/dees-workspace-* (e.g. dees-editor-monaco -> dees-workspace-monaco).
- Moved terminal implementation from dees-terminal to dees-workspace-terminal and updated related imports/usages.
- Moved TypeScript intellisense manager into 00group-workspace and updated paths.
- Updated ts_web elements index to export 00group-workspace instead of 00group-editor and adjusted internal imports accordingly.
- Updated scripts/update-monaco-version.cjs to write MONACO_VERSION into the new workspace path and updated log tags.
## 2025-12-31 - 3.19.1 - fix(intellisense) ## 2025-12-31 - 3.19.1 - fix(intellisense)
Debounce TypeScript/JavaScript IntelliSense processing and cache missing packages to reduce work and noisy logs Debounce TypeScript/JavaScript IntelliSense processing and cache missing packages to reduce work and noisy logs

View File

@@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "3.19.1", "version": "3.23.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

@@ -11,7 +11,7 @@ function resolveMonacoPackageJson() {
}); });
return resolvedPath; return resolvedPath;
} catch (error) { } catch (error) {
console.error('[dees-editor] Unable to resolve monaco-editor/package.json'); console.error('[dees-workspace] Unable to resolve monaco-editor/package.json');
throw error; throw error;
} }
} }
@@ -20,25 +20,25 @@ function getMonacoVersion() {
const monacoPackagePath = resolveMonacoPackageJson(); const monacoPackagePath = resolveMonacoPackageJson();
const monacoPackage = require(monacoPackagePath); const monacoPackage = require(monacoPackagePath);
if (!monacoPackage.version) { if (!monacoPackage.version) {
throw new Error('[dees-editor] monaco-editor/package.json does not expose a version field'); throw new Error('[dees-workspace] monaco-editor/package.json does not expose a version field');
} }
return monacoPackage.version; return monacoPackage.version;
} }
function writeVersionModule(version) { function writeVersionModule(version) {
const targetDir = path.join(projectRoot, 'ts_web', 'elements', '00group-editor', 'dees-editor-monaco'); const targetDir = path.join(projectRoot, 'ts_web', 'elements', '00group-workspace', 'dees-workspace-monaco');
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
const targetFile = path.join(targetDir, 'version.ts'); const targetFile = path.join(targetDir, 'version.ts');
const fileContent = `// Auto-generated by scripts/update-monaco-version.cjs\nexport const MONACO_VERSION = '${version}';\n`; const fileContent = `// Auto-generated by scripts/update-monaco-version.cjs\nexport const MONACO_VERSION = '${version}';\n`;
fs.writeFileSync(targetFile, fileContent, 'utf8'); fs.writeFileSync(targetFile, fileContent, 'utf8');
console.log(`[dees-editor] Wrote ${path.relative(projectRoot, targetFile)} with monaco-editor@${version}`); console.log(`[dees-workspace] Wrote ${path.relative(projectRoot, targetFile)} with monaco-editor@${version}`);
} }
try { try {
const version = getMonacoVersion(); const version = getMonacoVersion();
writeVersionModule(version); writeVersionModule(version);
} catch (error) { } catch (error) {
console.error('[dees-editor] Failed to update Monaco version module.'); console.error('[dees-workspace] Failed to update Monaco version module.');
console.error(error instanceof Error ? error.message : error); console.error(error instanceof Error ? error.message : error);
process.exitCode = 1; process.exitCode = 1;
} }

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@design.estate/dees-catalog', name: '@design.estate/dees-catalog',
version: '3.19.1', version: '3.23.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

@@ -1 +0,0 @@
export * from './dees-editor-filetree.js';

View File

@@ -1,102 +0,0 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
domtools
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
import { DeesEditorMonaco } from '../dees-editor-monaco/dees-editor-monaco.js';
const deferred = domtools.plugins.smartpromise.defer();
declare global {
interface HTMLElementTagNameMap {
'dees-editormarkdown': DeesEditorMarkdown;
}
}
@customElement('dees-editormarkdown')
export class DeesEditorMarkdown extends DeesElement {
public static demo = () => html`<dees-editormarkdown></dees-editormarkdown>`;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
.gridcontainer {
position: absolute;
height: 100%;
width: 100%;
display: grid;
grid-template-columns: 60% 40%;
}
.editorContainer {
position: relative;
}
.outletContainer {
background: #111;
color: #fff;
font-family: 'Roboto Slab';
padding: 20px;
overflow-y: scroll;
}
`,
];
public render() {
return html`
<div class="gridcontainer">
<div class="editorContainer">
<dees-editor-monaco
.language=${'markdown'}
.content=${`# a test content
This is test content that is of longer form an hopefully starts to wrap when I need it. And yes, it does perfectly. nice.
Test | Hello
--- | ---
Yeah | So good
This is real asset I think. Why would we want to leave that on the table? Can you tell my that?
Why are we here?
Do you know?
> note:
There is something going on.
\`\`\`typescript
const hello = 'yes'
\`\`\`
`}
wordWrap="bounded"
></dees-editor-monaco>
</div>
<div class="outletContainer">
<dees-editormarkdownoutlet></dees-editormarkdownoutlet>
</div>
</div>
`;
}
public async firstUpdated(_changedPropertiesArg) {
await super.firstUpdated(_changedPropertiesArg);
const editor = this.shadowRoot.querySelector('dees-editor-monaco') as DeesEditorMonaco;
// lets care about wiring the markdown stuff.
const markdownOutlet = this.shadowRoot.querySelector('dees-editormarkdownoutlet');
const smartmarkdownInstance = new domtools.plugins.smartmarkdown.SmartMarkdown();
const mdParsedResult = await smartmarkdownInstance.getMdParsedResultFromMarkdown('loading...')
editor.contentSubject.subscribe(async contentArg => {
await mdParsedResult.updateFromMarkdownString(contentArg)
const html = mdParsedResult.html;
markdownOutlet.updateHtmlText(html);
})
}
}

View File

@@ -1 +0,0 @@
export * from './dees-editor-markdown.js';

View File

@@ -1,42 +0,0 @@
import { customElement, DeesElement, html, type TemplateResult } from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'dees-editormarkdownoutlet': DeesEditorMarkdownOutlet;
}
}
@customElement('dees-editormarkdownoutlet')
export class DeesEditorMarkdownOutlet extends DeesElement {
// DEMO
public static demo = () => html`<dees-editormarkdownoutlet></dees-editormarkdownoutlet>`;
// INSTANCE
private outlet: HTMLElement;
public render(): TemplateResult {
return html`
<div class="outlet markdown-body">
<h1>Hi there</h1>
</div>
`;
}
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
const styleElement = document.createElement('style');
const cssText = await (
await fetch('https://unpkg.com/github-markdown-css@5.1.0/github-markdown-dark.css')
).text();
styleElement.textContent = cssText;
this.shadowRoot.append(styleElement);
}
public async updateHtmlText(htmlTextArg: string) {
await this.updateComplete;
if (!this.outlet) {
this.outlet = this.shadowRoot.querySelector('.outlet');
}
this.outlet.innerHTML = htmlTextArg;
}
}

View File

@@ -1 +0,0 @@
export * from './dees-editor-markdownoutlet.js';

View File

@@ -1 +0,0 @@
export * from './dees-editor-monaco.js';

View File

@@ -1,6 +0,0 @@
// Editor Components
export * from './dees-editor-monaco/index.js';
export * from './dees-editor-filetree/index.js';
export * from './dees-editor-workspace/index.js';
export * from './dees-editor-markdown/index.js';
export * from './dees-editor-markdownoutlet/index.js';

View File

@@ -12,8 +12,8 @@ import { themeDefaultStyles } from '../../00theme.js';
import { DeesModal } from '../../dees-modal/dees-modal.js'; import { DeesModal } from '../../dees-modal/dees-modal.js';
import '../../dees-icon/dees-icon.js'; import '../../dees-icon/dees-icon.js';
import '../../dees-label/dees-label.js'; import '../../dees-label/dees-label.js';
import '../../00group-editor/dees-editor-monaco/dees-editor-monaco.js'; import '../../00group-workspace/dees-workspace-monaco/dees-workspace-monaco.js';
import { DeesEditorMonaco } from '../../00group-editor/dees-editor-monaco/dees-editor-monaco.js'; import { DeesWorkspaceMonaco } from '../../00group-workspace/dees-workspace-monaco/dees-workspace-monaco.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -77,7 +77,7 @@ export class DeesInputCode extends DeesInputBase<string> {
@state() @state()
accessor copySuccess: boolean = false; accessor copySuccess: boolean = false;
private editorElement: DeesEditorMonaco | null = null; private editorElement: DeesWorkspaceMonaco | null = null;
public static styles = [ public static styles = [
themeDefaultStyles, themeDefaultStyles,
@@ -207,7 +207,7 @@ export class DeesInputCode extends DeesInputBase<string> {
position: relative; position: relative;
} }
dees-editor-monaco { dees-workspace-monaco {
display: block; display: block;
} }
@@ -295,12 +295,12 @@ export class DeesInputCode extends DeesInputBase<string> {
</div> </div>
</div> </div>
<div class="editor-wrapper"> <div class="editor-wrapper">
<dees-editor-monaco <dees-workspace-monaco
.content=${this.value} .content=${this.value}
.language=${this.language} .language=${this.language}
.wordWrap=${this.wordWrap} .wordWrap=${this.wordWrap}
@content-change=${this.handleContentChange} @content-change=${this.handleContentChange}
></dees-editor-monaco> ></dees-workspace-monaco>
</div> </div>
</div> </div>
</div> </div>
@@ -308,7 +308,7 @@ export class DeesInputCode extends DeesInputBase<string> {
} }
async firstUpdated() { async firstUpdated() {
this.editorElement = this.shadowRoot?.querySelector('dees-editor-monaco') as DeesEditorMonaco; this.editorElement = this.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
if (this.editorElement) { if (this.editorElement) {
// Subscribe to content changes from the editor // Subscribe to content changes from the editor
this.editorElement.contentSubject.subscribe((newContent: string) => { this.editorElement.contentSubject.subscribe((newContent: string) => {
@@ -386,7 +386,7 @@ export class DeesInputCode extends DeesInputBase<string> {
public async openFullscreen() { public async openFullscreen() {
const currentValue = this.value; const currentValue = this.value;
let modalEditorElement: DeesEditorMonaco | null = null; let modalEditorElement: DeesWorkspaceMonaco | null = null;
// Modal-specific state // Modal-specific state
let modalLanguage = this.language; let modalLanguage = this.language;
@@ -579,11 +579,11 @@ export class DeesInputCode extends DeesInputBase<string> {
</div> </div>
</div> </div>
<div class="modal-editor-wrapper"> <div class="modal-editor-wrapper">
<dees-editor-monaco <dees-workspace-monaco
.content=${currentValue} .content=${currentValue}
.language=${modalLanguage} .language=${modalLanguage}
.wordWrap=${modalWordWrap} .wordWrap=${modalWordWrap}
></dees-editor-monaco> ></dees-workspace-monaco>
</div> </div>
`, `,
menuOptions: [ menuOptions: [
@@ -597,7 +597,7 @@ export class DeesInputCode extends DeesInputBase<string> {
name: 'Save & Close', name: 'Save & Close',
action: async (modalRef) => { action: async (modalRef) => {
// Get the editor content from the modal // Get the editor content from the modal
modalEditorElement = modalRef.shadowRoot?.querySelector('dees-editor-monaco') as DeesEditorMonaco; modalEditorElement = modalRef.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
if (modalEditorElement) { if (modalEditorElement) {
const editor = await modalEditorElement.editorDeferred.promise; const editor = await modalEditorElement.editorDeferred.promise;
const newValue = editor.getValue(); const newValue = editor.getValue();
@@ -611,7 +611,7 @@ export class DeesInputCode extends DeesInputBase<string> {
// Wait for modal to render // Wait for modal to render
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
modalEditorElement = modal.shadowRoot?.querySelector('dees-editor-monaco') as DeesEditorMonaco; modalEditorElement = modal.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
// Wire up toolbar event handlers // Wire up toolbar event handlers
const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar'); const toolbar = modal.shadowRoot?.querySelector('.modal-toolbar');

View File

@@ -14,7 +14,7 @@ import {
domtools, domtools,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import '../../dees-icon/dees-icon.js'; import '../../dees-icon/dees-icon.js';
import type { DeesTerminal } from '../../dees-terminal/dees-terminal.js'; import type { DeesWorkspaceTerminal } from '../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js';
import { themeDefaultStyles } from '../../00theme.js'; import { themeDefaultStyles } from '../../00theme.js';
declare global { declare global {
@@ -393,7 +393,7 @@ export class DeesSimpleAppDash extends DeesElement {
} }
} }
public currentTerminal: DeesTerminal; public currentTerminal: DeesWorkspaceTerminal;
public async launchTerminal() { public async launchTerminal() {
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
if (this.currentTerminal) { if (this.currentTerminal) {
@@ -401,10 +401,10 @@ export class DeesSimpleAppDash extends DeesElement {
await this.closeTerminal(); await this.closeTerminal();
return; return;
} }
const maincontainer = this.shadowRoot.querySelector('.maincontainer'); const maincontainer = this.shadowRoot.querySelector('.maincontainer');
const { DeesTerminal } = await import('../../dees-terminal/dees-terminal.js'); const { DeesWorkspaceTerminal } = await import('../../00group-workspace/dees-workspace-terminal/dees-workspace-terminal.js');
const terminal = new DeesTerminal(); const terminal = new DeesWorkspaceTerminal();
terminal.setupCommand = this.terminalSetupCommand; terminal.setupCommand = this.terminalSetupCommand;
this.currentTerminal = terminal; this.currentTerminal = terminal;
maincontainer.appendChild(terminal); maincontainer.appendChild(terminal);
@@ -417,7 +417,6 @@ export class DeesSimpleAppDash extends DeesElement {
terminal.style.opacity = '0'; terminal.style.opacity = '0';
terminal.style.transform = 'translateY(8px) scale(0.99)'; terminal.style.transform = 'translateY(8px) scale(0.99)';
terminal.style.transition = 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)'; terminal.style.transition = 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)';
terminal.background = 'hsl(220 13% 8%)';
terminal.style.boxShadow = '0 25px 50px -12px rgb(0 0 0 / 0.5), 0 0 0 1px rgb(255 255 255 / 0.05)'; terminal.style.boxShadow = '0 25px 50px -12px rgb(0 0 0 / 0.5), 0 0 0 1px rgb(255 255 255 / 0.05)';
terminal.style.maxWidth = `calc(${maincontainer.clientWidth}px -240px)`; terminal.style.maxWidth = `calc(${maincontainer.clientWidth}px -240px)`;
terminal.style.maxHeight = `calc(${maincontainer.clientHeight}px - 24px)`; terminal.style.maxHeight = `calc(${maincontainer.clientHeight}px - 24px)`;

View File

@@ -20,7 +20,7 @@ import { DeesInputText } from '../../00group-input/dees-input-text/dees-input-te
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dees-editor-filetree': DeesEditorFiletree; 'dees-workspace-filetree': DeesWorkspaceFiletree;
} }
} }
@@ -30,11 +30,11 @@ interface ITreeNode extends IFileEntry {
level: number; level: number;
} }
@customElement('dees-editor-filetree') @customElement('dees-workspace-filetree')
export class DeesEditorFiletree extends DeesElement { export class DeesWorkspaceFiletree extends DeesElement {
public static demo = () => html` public static demo = () => html`
<div style="width: 300px; height: 400px; position: relative;"> <div style="width: 300px; height: 400px; position: relative;">
<dees-editor-filetree></dees-editor-filetree> <dees-workspace-filetree></dees-workspace-filetree>
</div> </div>
`; `;

View File

@@ -0,0 +1 @@
export * from './dees-workspace-filetree.js';

View File

@@ -0,0 +1,255 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
domtools,
state,
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
import { DeesWorkspaceMonaco } from '../dees-workspace-monaco/dees-workspace-monaco.js';
const deferred = domtools.plugins.smartpromise.defer();
declare global {
interface HTMLElementTagNameMap {
'dees-workspace-markdown': DeesWorkspaceMarkdown;
}
}
@customElement('dees-workspace-markdown')
export class DeesWorkspaceMarkdown extends DeesElement {
public static demo = () => html`<dees-workspace-markdown></dees-workspace-markdown>`;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
width: 100%;
height: 100%;
}
.splitContainer {
position: absolute;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
}
.editorContainer {
position: relative;
height: 100%;
min-width: 100px;
overflow: hidden;
}
.resizeHandle {
width: 6px;
height: 100%;
background: ${cssManager.bdTheme('#e5e7eb', '#30363d')};
cursor: col-resize;
flex-shrink: 0;
transition: background 0.15s ease;
position: relative;
}
.resizeHandle:hover,
.resizeHandle.dragging {
background: ${cssManager.bdTheme('#3b82f6', '#58a6ff')};
}
.resizeHandle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 32px;
background: ${cssManager.bdTheme('#9ca3af', '#6e7681')};
border-radius: 1px;
opacity: 0.6;
}
.resizeHandle:hover::after,
.resizeHandle.dragging::after {
background: ${cssManager.bdTheme('#ffffff', '#ffffff')};
opacity: 1;
}
.outletContainer {
position: relative;
height: 100%;
min-width: 100px;
background: ${cssManager.bdTheme('#ffffff', '#0d1117')};
color: ${cssManager.bdTheme('#24292f', '#e6edf3')};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
padding: 20px;
overflow-y: auto;
box-sizing: border-box;
}
/* Prevent text selection while dragging */
.splitContainer.dragging {
user-select: none;
}
.splitContainer.dragging .editorContainer,
.splitContainer.dragging .outletContainer {
pointer-events: none;
}
`,
];
/**
* Initial split ratio for the editor (left) panel.
* Value from 0 to 100 representing percentage width.
* Default is 50 (50/50 split).
*/
@property({ type: Number })
accessor splitRatio: number = 50;
/**
* Minimum width percentage for either panel.
*/
@property({ type: Number })
accessor minPanelSize: number = 10;
@state()
accessor currentSplitRatio: number = 50;
@state()
accessor isDragging: boolean = false;
private resizeHandleElement: HTMLElement;
private containerElement: HTMLElement;
public render() {
return html`
<div class="splitContainer ${this.isDragging ? 'dragging' : ''}">
<div class="editorContainer" style="width: ${this.currentSplitRatio}%">
<dees-workspace-monaco
.language=${'markdown'}
.content=${`# a test content
This is test content that is of longer form an hopefully starts to wrap when I need it. And yes, it does perfectly. nice.
Test | Hello
--- | ---
Yeah | So good
This is real asset I think. Why would we want to leave that on the table? Can you tell my that?
Why are we here?
Do you know?
> note:
There is something going on.
\`\`\`typescript
const hello = 'yes'
\`\`\`
`}
wordWrap="bounded"
></dees-workspace-monaco>
</div>
<div
class="resizeHandle ${this.isDragging ? 'dragging' : ''}"
@mousedown=${this.handleMouseDown}
></div>
<div class="outletContainer" style="width: ${100 - this.currentSplitRatio}%">
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</div>
</div>
`;
}
public async firstUpdated(_changedPropertiesArg) {
await super.firstUpdated(_changedPropertiesArg);
// Initialize current ratio from property
this.currentSplitRatio = this.splitRatio;
// Cache elements
this.containerElement = this.shadowRoot.querySelector('.splitContainer');
this.resizeHandleElement = this.shadowRoot.querySelector('.resizeHandle');
const editor = this.shadowRoot.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
// Wire up markdown rendering
const markdownOutlet = this.shadowRoot.querySelector('dees-workspace-markdownoutlet');
const smartmarkdownInstance = new domtools.plugins.smartmarkdown.SmartMarkdown();
const mdParsedResult = await smartmarkdownInstance.getMdParsedResultFromMarkdown('loading...')
editor.contentSubject.subscribe(async contentArg => {
await mdParsedResult.updateFromMarkdownString(contentArg)
const html = mdParsedResult.html;
markdownOutlet.updateHtmlText(html);
});
}
private handleMouseDown = (e: MouseEvent) => {
e.preventDefault();
this.isDragging = true;
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
};
private handleMouseMove = (e: MouseEvent) => {
if (!this.isDragging || !this.containerElement) return;
const containerRect = this.containerElement.getBoundingClientRect();
const containerWidth = containerRect.width;
const mouseX = e.clientX - containerRect.left;
// Calculate percentage, accounting for the resize handle width (6px)
let newRatio = (mouseX / containerWidth) * 100;
// Clamp to min/max
newRatio = Math.max(this.minPanelSize, Math.min(100 - this.minPanelSize, newRatio));
this.currentSplitRatio = newRatio;
};
private handleMouseUp = () => {
this.isDragging = false;
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
// Trigger resize on monaco editor
const editor = this.shadowRoot.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
if (editor) {
// Monaco needs to be notified of size changes
window.dispatchEvent(new Event('resize'));
}
};
async disconnectedCallback() {
await super.disconnectedCallback();
// Clean up event listeners
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
/**
* Programmatically set the split ratio
*/
public setSplitRatio(ratio: number) {
this.currentSplitRatio = Math.max(this.minPanelSize, Math.min(100 - this.minPanelSize, ratio));
}
/**
* Reset to initial split ratio
*/
public resetSplitRatio() {
this.currentSplitRatio = this.splitRatio;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-workspace-markdown.js';

View File

@@ -0,0 +1,289 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import '../../dees-panel/dees-panel.js';
import type { DeesWorkspaceMarkdownoutlet } from './dees-workspace-markdownoutlet.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 900px;
margin: 0 auto;
}
`}
</style>
<div class="demo-container">
<!-- Demo 1: Headings -->
<dees-panel
.title=${'Headings'}
.subtitle=${'All heading levels from H1 to H6'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<h1>Heading Level 1</h1>
<h2>Heading Level 2</h2>
<h3>Heading Level 3</h3>
<h4>Heading Level 4</h4>
<h5>Heading Level 5</h5>
<h6>Heading Level 6</h6>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
<!-- Demo 2: Text Formatting -->
<dees-panel
.title=${'Text Formatting'}
.subtitle=${'Bold, italic, links, code, and keyboard input'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<p>This is a paragraph with <strong>bold text</strong> and <em>italic text</em>.</p>
<p>You can also use <strong><em>bold italic</em></strong> for emphasis.</p>
<p>Here's a <a href="#">link example</a> and some <code>inline code</code>.</p>
<p>Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to copy text.</p>
<hr>
<p>Above is a horizontal rule separator.</p>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
<!-- Demo 3: Lists -->
<dees-panel
.title=${'Lists'}
.subtitle=${'Ordered, unordered, and task lists'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<h3>Unordered List</h3>
<ul>
<li>First item</li>
<li>Second item
<ul>
<li>Nested item 1</li>
<li>Nested item 2</li>
</ul>
</li>
<li>Third item</li>
</ul>
<h3>Ordered List</h3>
<ol>
<li>Step one</li>
<li>Step two</li>
<li>Step three</li>
</ol>
<h3>Task List</h3>
<ul>
<li class="task-list-item"><input type="checkbox" checked disabled> Completed task</li>
<li class="task-list-item"><input type="checkbox" disabled> Pending task</li>
<li class="task-list-item"><input type="checkbox" disabled> Another task</li>
</ul>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
<!-- Demo 4: Code Blocks -->
<dees-panel
.title=${'Code Blocks'}
.subtitle=${'Syntax highlighted code examples'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<p>Inline code: <code>const greeting = "Hello, World!";</code></p>
<h3>TypeScript Example</h3>
<pre><code><span class="hljs-keyword">import</span> { html, css } <span class="hljs-keyword">from</span> <span class="hljs-string">'@design.estate/dees-element'</span>;
<span class="hljs-keyword">interface</span> <span class="hljs-title">IUser</span> {
<span class="hljs-attr">name</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">email</span>: <span class="hljs-built_in">string</span>;
<span class="hljs-attr">age</span>?: <span class="hljs-built_in">number</span>;
}
<span class="hljs-keyword">const</span> <span class="hljs-title">getUser</span> = <span class="hljs-keyword">async</span> (<span class="hljs-attr">id</span>: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;IUser&gt; =&gt; {
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(\`/api/users/\${id}\`);
<span class="hljs-keyword">return</span> response.json();
};</code></pre>
<h3>JSON Example</h3>
<pre><code>{
<span class="hljs-attr">"name"</span>: <span class="hljs-string">"dees-catalog"</span>,
<span class="hljs-attr">"version"</span>: <span class="hljs-string">"3.0.0"</span>,
<span class="hljs-attr">"dependencies"</span>: {
<span class="hljs-attr">"@design.estate/dees-element"</span>: <span class="hljs-string">"^2.0.0"</span>
}
}</code></pre>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
<!-- Demo 5: Tables -->
<dees-panel
.title=${'Tables'}
.subtitle=${'Styled tables with alternating rows'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<table>
<thead>
<tr>
<th>Feature</th>
<th>Status</th>
<th>Priority</th>
</tr>
</thead>
<tbody>
<tr>
<td>Dark mode support</td>
<td>Completed</td>
<td>High</td>
</tr>
<tr>
<td>Responsive design</td>
<td>In Progress</td>
<td>Medium</td>
</tr>
<tr>
<td>Accessibility</td>
<td>Planned</td>
<td>High</td>
</tr>
<tr>
<td>Documentation</td>
<td>In Progress</td>
<td>Low</td>
</tr>
</tbody>
</table>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
<!-- Demo 6: Blockquotes & Alerts -->
<dees-panel
.title=${'Blockquotes & Alerts'}
.subtitle=${'Quotes and GitHub-style alerts'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<blockquote>
<p>This is a standard blockquote. It's great for highlighting important information or quotes from other sources.</p>
</blockquote>
<div class="markdown-alert markdown-alert-note">
<p><strong>Note:</strong> This is an informational note to draw attention to important details.</p>
</div>
<div class="markdown-alert markdown-alert-tip">
<p><strong>Tip:</strong> Here's a helpful tip to improve your workflow.</p>
</div>
<div class="markdown-alert markdown-alert-important">
<p><strong>Important:</strong> This information is crucial for understanding the topic.</p>
</div>
<div class="markdown-alert markdown-alert-warning">
<p><strong>Warning:</strong> Be careful when performing this action.</p>
</div>
<div class="markdown-alert markdown-alert-caution">
<p><strong>Caution:</strong> This action may have unintended consequences.</p>
</div>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
<!-- Demo 7: Full Document -->
<dees-panel
.title=${'Full Document'}
.subtitle=${'A complete markdown document example'}
.runAfterRender=${async (panelEl: HTMLElement) => {
const outlet = panelEl.querySelector('dees-workspace-markdownoutlet') as DeesWorkspaceMarkdownoutlet;
await outlet?.updateHtmlText(`
<h1>Getting Started Guide</h1>
<p>Welcome to the <strong>dees-catalog</strong> component library. This guide will help you get up and running quickly.</p>
<h2>Installation</h2>
<p>Install the package using your preferred package manager:</p>
<pre><code><span class="hljs-comment"># Using pnpm (recommended)</span>
pnpm add @design.estate/dees-catalog
<span class="hljs-comment"># Using npm</span>
npm install @design.estate/dees-catalog</code></pre>
<h2>Basic Usage</h2>
<p>Import and use components in your TypeScript files:</p>
<pre><code><span class="hljs-keyword">import</span> { DeesButton } <span class="hljs-keyword">from</span> <span class="hljs-string">'@design.estate/dees-catalog'</span>;
<span class="hljs-comment">// Use in your templates</span>
html\`&lt;dees-button&gt;Click me&lt;/dees-button&gt;\`</code></pre>
<div class="markdown-alert markdown-alert-tip">
<p><strong>Tip:</strong> Check the demo showcase for live examples of all components.</p>
</div>
<h2>Available Components</h2>
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>dees-button</code></td>
<td>Primary button component</td>
</tr>
<tr>
<td><code>dees-input-text</code></td>
<td>Text input field</td>
</tr>
<tr>
<td><code>dees-table</code></td>
<td>Data table with sorting</td>
</tr>
<tr>
<td><code>dees-modal</code></td>
<td>Modal dialog</td>
</tr>
</tbody>
</table>
<h2>Next Steps</h2>
<ul>
<li>Explore the <a href="#">component documentation</a></li>
<li>Check out the <a href="#">theming guide</a></li>
<li>Join our <a href="#">community Discord</a></li>
</ul>
<blockquote>
<p>"The best component library is one that gets out of your way." — Design Systems Team</p>
</blockquote>
`);
}}
>
<dees-workspace-markdownoutlet></dees-workspace-markdownoutlet>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@@ -0,0 +1,406 @@
import {
customElement,
DeesElement,
html,
type TemplateResult,
css,
cssManager,
} from '@design.estate/dees-element';
import { themeDefaultStyles } from '../../00theme.js';
import { demoFunc } from './dees-workspace-markdownoutlet.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-workspace-markdownoutlet': DeesWorkspaceMarkdownoutlet;
}
}
@customElement('dees-workspace-markdownoutlet')
export class DeesWorkspaceMarkdownoutlet extends DeesElement {
// DEMO
public static demo = demoFunc;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
color: ${cssManager.bdTheme('#24292f', '#e6edf3')};
}
.outlet {
word-wrap: break-word;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
color: ${cssManager.bdTheme('#1f2328', '#f0f6fc')};
}
h1 {
font-size: 2em;
padding-bottom: 0.3em;
border-bottom: 1px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
}
h2 {
font-size: 1.5em;
padding-bottom: 0.3em;
border-bottom: 1px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1em;
}
h5 {
font-size: 0.875em;
}
h6 {
font-size: 0.85em;
color: ${cssManager.bdTheme('#656d76', '#8b949e')};
}
/* Paragraphs and text */
p {
margin-top: 0;
margin-bottom: 16px;
}
/* Links */
a {
color: ${cssManager.bdTheme('#0969da', '#58a6ff')};
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Strong and emphasis */
strong {
font-weight: 600;
color: ${cssManager.bdTheme('#1f2328', '#f0f6fc')};
}
em {
font-style: italic;
}
/* Lists */
ul, ol {
margin-top: 0;
margin-bottom: 16px;
padding-left: 2em;
}
li {
margin-top: 4px;
}
li + li {
margin-top: 4px;
}
ul ul, ul ol, ol ul, ol ol {
margin-top: 0;
margin-bottom: 0;
}
/* Blockquotes */
blockquote {
margin: 0 0 16px 0;
padding: 0 1em;
color: ${cssManager.bdTheme('#656d76', '#8b949e')};
border-left: 4px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
}
blockquote > :first-child {
margin-top: 0;
}
blockquote > :last-child {
margin-bottom: 0;
}
/* Inline code */
code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
background-color: ${cssManager.bdTheme('rgba(175, 184, 193, 0.2)', 'rgba(110, 118, 129, 0.4)')};
border-radius: 6px;
white-space: break-spaces;
}
/* Code blocks */
pre {
margin-top: 0;
margin-bottom: 16px;
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
background-color: ${cssManager.bdTheme('#f6f8fa', '#161b22')};
border-radius: 6px;
word-wrap: normal;
}
pre code {
display: block;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
font-size: 100%;
white-space: pre;
}
/* Tables */
table {
width: max-content;
max-width: 100%;
margin-top: 0;
margin-bottom: 16px;
border-spacing: 0;
border-collapse: collapse;
overflow: auto;
display: block;
}
table th {
font-weight: 600;
padding: 6px 13px;
border: 1px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
background-color: ${cssManager.bdTheme('#f6f8fa', '#161b22')};
color: ${cssManager.bdTheme('#1f2328', '#e6edf3')};
}
table td {
padding: 6px 13px;
border: 1px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
color: ${cssManager.bdTheme('#1f2328', '#e6edf3')};
}
table tr {
background-color: ${cssManager.bdTheme('#ffffff', '#0d1117')};
border-top: 1px solid ${cssManager.bdTheme('#d1d9e0', '#21262d')};
}
table tr:nth-child(2n) {
background-color: ${cssManager.bdTheme('#f6f8fa', '#161b22')};
}
/* Horizontal rules */
hr {
height: 4px;
padding: 0;
margin: 24px 0;
background-color: ${cssManager.bdTheme('#d1d9e0', '#30363d')};
border: 0;
border-radius: 2px;
}
/* Images */
img {
max-width: 100%;
box-sizing: border-box;
border-radius: 6px;
}
/* Task lists */
.task-list-item {
list-style-type: none;
}
.task-list-item input {
margin: 0 0.2em 0.25em -1.4em;
vertical-align: middle;
}
/* Definition lists */
dl {
padding: 0;
}
dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
/* Keyboard input */
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
line-height: 10px;
color: ${cssManager.bdTheme('#1f2328', '#e6edf3')};
vertical-align: middle;
background-color: ${cssManager.bdTheme('#f6f8fa', '#161b22')};
border: 1px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
border-radius: 6px;
box-shadow: inset 0 -1px 0 ${cssManager.bdTheme('#d1d9e0', '#30363d')};
}
/* Footnotes */
.footnotes {
font-size: 12px;
color: ${cssManager.bdTheme('#656d76', '#8b949e')};
border-top: 1px solid ${cssManager.bdTheme('#d1d9e0', '#30363d')};
padding-top: 16px;
margin-top: 32px;
}
/* Alerts/Admonitions */
.markdown-alert {
padding: 8px 16px;
margin-bottom: 16px;
border-left: 4px solid;
border-radius: 6px;
}
.markdown-alert-note {
border-color: ${cssManager.bdTheme('#0969da', '#58a6ff')};
background-color: ${cssManager.bdTheme('rgba(9, 105, 218, 0.1)', 'rgba(56, 139, 253, 0.1)')};
}
.markdown-alert-warning {
border-color: ${cssManager.bdTheme('#bf8700', '#d29922')};
background-color: ${cssManager.bdTheme('rgba(191, 135, 0, 0.1)', 'rgba(187, 128, 9, 0.1)')};
}
.markdown-alert-important {
border-color: ${cssManager.bdTheme('#8250df', '#a371f7')};
background-color: ${cssManager.bdTheme('rgba(130, 80, 223, 0.1)', 'rgba(163, 113, 247, 0.1)')};
}
.markdown-alert-caution {
border-color: ${cssManager.bdTheme('#cf222e', '#f85149')};
background-color: ${cssManager.bdTheme('rgba(207, 34, 46, 0.1)', 'rgba(248, 81, 73, 0.1)')};
}
.markdown-alert-tip {
border-color: ${cssManager.bdTheme('#1a7f37', '#3fb950')};
background-color: ${cssManager.bdTheme('rgba(26, 127, 55, 0.1)', 'rgba(46, 160, 67, 0.1)')};
}
/* Syntax highlighting for code blocks */
.hljs-comment,
.hljs-quote {
color: ${cssManager.bdTheme('#6a737d', '#8b949e')};
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-addition {
color: ${cssManager.bdTheme('#d73a49', '#ff7b72')};
}
.hljs-number,
.hljs-string,
.hljs-meta .hljs-meta-string,
.hljs-literal,
.hljs-doctag,
.hljs-regexp {
color: ${cssManager.bdTheme('#032f62', '#a5d6ff')};
}
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: ${cssManager.bdTheme('#6f42c1', '#d2a8ff')};
}
.hljs-attribute,
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-class .hljs-title,
.hljs-type {
color: ${cssManager.bdTheme('#005cc5', '#79c0ff')};
}
.hljs-symbol,
.hljs-bullet,
.hljs-subst,
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-link {
color: ${cssManager.bdTheme('#e36209', '#ffa657')};
}
.hljs-built_in,
.hljs-deletion {
color: ${cssManager.bdTheme('#b31d28', '#ffa198')};
}
.hljs-formula {
background-color: ${cssManager.bdTheme('#f6f8fa', '#161b22')};
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
`,
];
// INSTANCE
private outlet: HTMLElement;
public render(): TemplateResult {
return html`
<div class="outlet">
<h1>Markdown Preview</h1>
<p>Content will appear here when markdown is parsed.</p>
</div>
`;
}
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
await super.firstUpdated(_changedProperties);
this.outlet = this.shadowRoot.querySelector('.outlet');
}
public async updateHtmlText(htmlTextArg: string) {
await this.updateComplete;
if (!this.outlet) {
this.outlet = this.shadowRoot.querySelector('.outlet');
}
this.outlet.innerHTML = htmlTextArg;
}
}

View File

@@ -0,0 +1 @@
export * from './dees-workspace-markdownoutlet.js';

View File

@@ -15,14 +15,14 @@ import type * as monaco from 'monaco-editor';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dees-editor-monaco': DeesEditorMonaco; 'dees-workspace-monaco': DeesWorkspaceMonaco;
} }
} }
@customElement('dees-editor-monaco') @customElement('dees-workspace-monaco')
export class DeesEditorMonaco extends DeesElement { export class DeesWorkspaceMonaco extends DeesElement {
// DEMO // DEMO
public static demo = () => html`<dees-editor-monaco></dees-editor-monaco>`; public static demo = () => html`<dees-workspace-monaco></dees-workspace-monaco>`;
// STATIC // STATIC
public static monacoDeferred: ReturnType<typeof domtools.plugins.smartpromise.defer>; public static monacoDeferred: ReturnType<typeof domtools.plugins.smartpromise.defer>;
@@ -98,17 +98,17 @@ export class DeesEditorMonaco extends DeesElement {
const container = this.shadowRoot.getElementById('container'); const container = this.shadowRoot.getElementById('container');
const monacoCdnBase = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}`; const monacoCdnBase = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}`;
if (!DeesEditorMonaco.monacoDeferred) { if (!DeesWorkspaceMonaco.monacoDeferred) {
DeesEditorMonaco.monacoDeferred = domtools.plugins.smartpromise.defer(); DeesWorkspaceMonaco.monacoDeferred = domtools.plugins.smartpromise.defer();
const scriptUrl = `${monacoCdnBase}/min/vs/loader.js`; const scriptUrl = `${monacoCdnBase}/min/vs/loader.js`;
const script = document.createElement('script'); const script = document.createElement('script');
script.src = scriptUrl; script.src = scriptUrl;
script.onload = () => { script.onload = () => {
DeesEditorMonaco.monacoDeferred.resolve(); DeesWorkspaceMonaco.monacoDeferred.resolve();
}; };
document.head.appendChild(script); document.head.appendChild(script);
} }
await DeesEditorMonaco.monacoDeferred.promise; await DeesWorkspaceMonaco.monacoDeferred.promise;
(window as any).require.config({ (window as any).require.config({
paths: { vs: `${monacoCdnBase}/min/vs` }, paths: { vs: `${monacoCdnBase}/min/vs` },

View File

@@ -0,0 +1 @@
export * from './dees-workspace-monaco.js';

View File

@@ -0,0 +1,395 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
} from '@design.estate/dees-element';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { themeDefaultStyles } from '../../00theme.js';
declare global {
interface HTMLElementTagNameMap {
'dees-workspace-terminal-preview': DeesWorkspaceTerminalPreview;
}
}
/**
* A read-only terminal preview component using xterm.js for rendering.
* Used during workspace initialization to show onInit command progress.
*/
@customElement('dees-workspace-terminal-preview')
export class DeesWorkspaceTerminalPreview extends DeesElement {
public static demo = () => html`
<dees-workspace-terminal-preview
.command=${'pnpm install'}
.lines=${[
'Packages: +42',
'Progress: resolved 142, reused 140, downloaded 2, added 42, done',
'',
'dependencies:',
'+ @push.rocks/smartpromise 4.2.3',
'+ typescript 5.3.3',
'',
'Done in 2.3s'
]}
></dees-workspace-terminal-preview>
`;
/**
* The command being displayed (shown in header)
*/
@property({ type: String })
accessor command: string = '';
/**
* Output lines to display
*/
@property({ type: Array })
accessor lines: string[] = [];
private terminal: Terminal | null = null;
private fitAddon: FitAddon | null = null;
private lastLineCount: number = 0;
private resizeObserver: ResizeObserver | null = null;
private terminalThemeSubscription: any = null;
public static styles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
height: 200px;
}
.terminal-preview {
height: 100%;
border-radius: 8px;
overflow: hidden;
background: ${cssManager.bdTheme('#ffffff', '#000000')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
display: flex;
flex-direction: column;
}
.terminal-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 10%)')};
font-size: 12px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 20%)')};
flex-shrink: 0;
}
.terminal-header-icon {
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
}
.terminal-header-command {
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 80%)')};
font-weight: 500;
}
.terminal-container {
flex: 1;
position: relative;
padding: 8px;
}
#xterm-container {
position: absolute;
top: 8px;
left: 8px;
right: 8px;
bottom: 8px;
}
/* xterm.js styles */
.xterm {
font-feature-settings: 'liga' 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
background: ${cssManager.bdTheme('#ffffff', '#000000')};
color: ${cssManager.bdTheme('#333333', '#ffffff')};
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
background-color: ${cssManager.bdTheme('#ffffff', '#000000')};
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
cursor: default;
}
.xterm.xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}
/* Custom scrollbar for xterm viewport */
.xterm .xterm-viewport::-webkit-scrollbar {
width: 8px;
}
.xterm .xterm-viewport::-webkit-scrollbar-track {
background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 8%)')};
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(0 0% 25%)')};
border-radius: 4px;
}
.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 35%)')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="terminal-preview">
<div class="terminal-header">
<span class="terminal-header-icon">$</span>
<span class="terminal-header-command">${this.command || 'Waiting...'}</span>
</div>
<div class="terminal-container">
<div id="xterm-container"></div>
</div>
</div>
`;
}
/**
* Get terminal theme colors based on bright/dark mode
*/
private getTerminalTheme(isBright: boolean) {
return isBright
? {
background: '#ffffff',
foreground: '#333333',
cursor: '#333333',
cursorAccent: '#ffffff',
selectionBackground: 'rgba(0, 0, 0, 0.2)',
}
: {
background: '#000000',
foreground: '#cccccc',
cursor: '#cccccc',
cursorAccent: '#000000',
selectionBackground: 'rgba(255, 255, 255, 0.2)',
};
}
public async firstUpdated(
_changedProperties: Map<string | number | symbol, unknown>
): Promise<void> {
super.firstUpdated(_changedProperties);
const container = this.shadowRoot?.getElementById('xterm-container');
if (!container) return;
// Get current theme from domtools
const domtoolsInstance = await this.domtoolsPromise;
const isBright = domtoolsInstance.themeManager.goBrightBoolean;
// Create xterm terminal in read-only mode
this.terminal = new Terminal({
convertEol: true,
cursorBlink: false,
disableStdin: true,
fontSize: 12,
fontFamily: "'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace",
theme: this.getTerminalTheme(isBright),
scrollback: 1000,
});
// Subscribe to theme changes
this.terminalThemeSubscription = domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
if (this.terminal) {
this.terminal.options.theme = this.getTerminalTheme(goBright);
}
});
this.fitAddon = new FitAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.open(container);
this.fitAddon.fit();
// Set up resize observer to refit terminal
this.resizeObserver = new ResizeObserver(() => {
if (this.fitAddon) {
this.fitAddon.fit();
}
});
this.resizeObserver.observe(container);
// Write any existing lines
this.writeNewLines();
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('lines')) {
this.writeNewLines();
}
}
private writeNewLines() {
if (!this.terminal) return;
// Write only new lines since last update
const newLines = this.lines.slice(this.lastLineCount);
for (const line of newLines) {
this.terminal.writeln(line);
}
this.lastLineCount = this.lines.length;
}
public async disconnectedCallback(): Promise<void> {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
if (this.terminalThemeSubscription) {
this.terminalThemeSubscription.unsubscribe();
this.terminalThemeSubscription = null;
}
if (this.terminal) {
this.terminal.dispose();
this.terminal = null;
}
await super.disconnectedCallback();
}
/**
* Add a new line to the output
*/
public addLine(line: string) {
this.lines = [...this.lines, line];
}
/**
* Clear all output lines
*/
public clear() {
this.lines = [];
this.lastLineCount = 0;
if (this.terminal) {
this.terminal.clear();
}
}
}

View File

@@ -0,0 +1 @@
export * from './dees-workspace-terminal-preview.js';

View File

@@ -11,21 +11,21 @@ import * as domtools from '@design.estate/dees-domtools';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; 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';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dees-terminal': DeesTerminal; 'dees-workspace-terminal': DeesWorkspaceTerminal;
} }
} }
@customElement('dees-terminal') @customElement('dees-workspace-terminal')
export class DeesTerminal extends DeesElement { export class DeesWorkspaceTerminal extends DeesElement {
public static demo = () => { public static demo = () => {
const env = new WebContainerEnvironment(); const env = new WebContainerEnvironment();
return html`<dees-terminal .executionEnvironment=${env}></dees-terminal>`; return html`<dees-workspace-terminal .executionEnvironment=${env}></dees-workspace-terminal>`;
}; };
// INSTANCE // INSTANCE
@@ -47,9 +47,6 @@ export class DeesTerminal extends DeesElement {
@property() @property()
accessor environmentVariables: { [key: string]: string } = {}; accessor environmentVariables: { [key: string]: string } = {};
@property()
accessor background: string = '#000000';
/** /**
* Promise that resolves when the environment is ready. * Promise that resolves when the environment is ready.
* @deprecated Use executionEnvironment directly * @deprecated Use executionEnvironment directly
@@ -57,6 +54,9 @@ export class DeesTerminal extends DeesElement {
private environmentDeferred = new domtools.plugins.smartpromise.Deferred<IExecutionEnvironment>(); private environmentDeferred = new domtools.plugins.smartpromise.Deferred<IExecutionEnvironment>();
public environmentPromise = this.environmentDeferred.promise; public environmentPromise = this.environmentDeferred.promise;
// Theme subscription for dynamic theme updates
private terminalThemeSubscription: any = null;
constructor() { constructor() {
super(); super();
this.resizeObserver = new ResizeObserver((entries) => { this.resizeObserver = new ResizeObserver((entries) => {
@@ -72,10 +72,9 @@ export class DeesTerminal extends DeesElement {
themeDefaultStyles, themeDefaultStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
:host { :host {
padding: 20px; padding: 20px;
background: var(--dees-terminal-background, #000000); background: ${cssManager.bdTheme('#ffffff', '#000000')};
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -170,8 +169,8 @@ export class DeesTerminal extends DeesElement {
.xterm .composition-view { .xterm .composition-view {
/* TODO: Composition position got messed up somewhere */ /* TODO: Composition position got messed up somewhere */
background: var(--dees-terminal-background, #000000); background: ${cssManager.bdTheme('#ffffff', '#000000')};
color: #fff; color: ${cssManager.bdTheme('#333333', '#ffffff')};
display: none; display: none;
position: absolute; position: absolute;
white-space: nowrap; white-space: nowrap;
@@ -184,7 +183,7 @@ export class DeesTerminal extends DeesElement {
.xterm .xterm-viewport { .xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */ /* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: var(--dees-terminal-background, #000000); background-color: ${cssManager.bdTheme('#ffffff', '#000000')};
overflow-y: scroll; overflow-y: scroll;
cursor: default; cursor: default;
position: absolute; position: absolute;
@@ -275,25 +274,51 @@ export class DeesTerminal extends DeesElement {
private fitAddon: FitAddon; private fitAddon: FitAddon;
private terminal: Terminal | null = null; private terminal: Terminal | null = null;
/**
* Get terminal theme colors based on bright/dark mode
*/
private getTerminalTheme(isBright: boolean) {
return isBright
? {
background: '#ffffff',
foreground: '#333333',
cursor: '#333333',
cursorAccent: '#ffffff',
selectionBackground: 'rgba(0, 0, 0, 0.2)',
}
: {
background: '#000000',
foreground: '#cccccc',
cursor: '#cccccc',
cursorAccent: '#000000',
selectionBackground: 'rgba(255, 255, 255, 0.2)',
};
}
public async firstUpdated( public async firstUpdated(
_changedProperties: Map<string | number | symbol, unknown> _changedProperties: Map<string | number | symbol, unknown>
): Promise<void> { ): Promise<void> {
const domtools = await this.domtoolsPromise; const domtoolsInstance = await this.domtoolsPromise;
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
// Sync CSS variable with background property // Get current theme
this.style.setProperty('--dees-terminal-background', this.background); const isBright = domtoolsInstance.themeManager.goBrightBoolean;
const container = this.shadowRoot.getElementById('container'); const container = this.shadowRoot.getElementById('container');
const term = new Terminal({ const term = new Terminal({
convertEol: true, convertEol: true,
cursorBlink: true, cursorBlink: true,
theme: { theme: this.getTerminalTheme(isBright),
background: this.background,
},
}); });
this.terminal = term; this.terminal = term;
// Subscribe to theme changes
this.terminalThemeSubscription = domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
if (this.terminal) {
this.terminal.options.theme = this.getTerminalTheme(goBright);
}
});
this.fitAddon = new FitAddon(); this.fitAddon = new FitAddon();
term.loadAddon(this.fitAddon); term.loadAddon(this.fitAddon);
@@ -383,6 +408,14 @@ export class DeesTerminal extends DeesElement {
async disconnectedCallback(): Promise<void> { async disconnectedCallback(): Promise<void> {
this.resizeObserver.unobserve(this); this.resizeObserver.unobserve(this);
if (this.terminalThemeSubscription) {
this.terminalThemeSubscription.unsubscribe();
this.terminalThemeSubscription = null;
}
if (this.terminal) {
this.terminal.dispose();
this.terminal = null;
}
await super.disconnectedCallback(); await super.disconnectedCallback();
} }

View File

@@ -0,0 +1 @@
export * from './dees-workspace-terminal.js';

View File

@@ -13,18 +13,20 @@ import { themeDefaultStyles } from '../../00theme.js';
import type { IExecutionEnvironment, IFileWatcher } from '../../00group-runtime/index.js'; import type { IExecutionEnvironment, IFileWatcher } from '../../00group-runtime/index.js';
import { WebContainerEnvironment } from '../../00group-runtime/index.js'; import { WebContainerEnvironment } from '../../00group-runtime/index.js';
import type { FileSystemTree } from '@webcontainer/api'; import type { FileSystemTree } from '@webcontainer/api';
import '../dees-editor-monaco/dees-editor-monaco.js'; import '../dees-workspace-monaco/dees-workspace-monaco.js';
import '../dees-editor-filetree/dees-editor-filetree.js'; import '../dees-workspace-filetree/dees-workspace-filetree.js';
import { DeesEditorFiletree } from '../dees-editor-filetree/dees-editor-filetree.js'; import { DeesWorkspaceFiletree } from '../dees-workspace-filetree/dees-workspace-filetree.js';
import '../../dees-terminal/dees-terminal.js'; import '../dees-workspace-terminal/dees-workspace-terminal.js';
import '../dees-workspace-terminal-preview/dees-workspace-terminal-preview.js';
import '../../dees-icon/dees-icon.js'; import '../../dees-icon/dees-icon.js';
import { DeesEditorMonaco } from '../dees-editor-monaco/dees-editor-monaco.js'; import { DeesWorkspaceMonaco } from '../dees-workspace-monaco/dees-workspace-monaco.js';
import { TypeScriptIntelliSenseManager } from './typescript-intellisense.js'; import { TypeScriptIntelliSenseManager } from './typescript-intellisense.js';
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
import '@design.estate/dees-wcctools/demotools';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dees-editor-workspace': DeesEditorWorkspace; 'dees-workspace': DeesWorkspace;
} }
} }
@@ -35,8 +37,8 @@ interface IOpenFile {
modified: boolean; modified: boolean;
} }
@customElement('dees-editor-workspace') @customElement('dees-workspace')
export class DeesEditorWorkspace extends DeesElement { export class DeesWorkspace extends DeesElement {
public static demo = () => { public static demo = () => {
const env = new WebContainerEnvironment(); const env = new WebContainerEnvironment();
@@ -173,13 +175,19 @@ testSmartPromise();
await env.mount(fileTree); await env.mount(fileTree);
})(); })();
// Create container element for proper 100% height like dees-appui-base
const containerElement = document.createElement('div');
containerElement.style.cssText = 'position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden;';
const workspaceElement = document.createElement('dees-workspace') as DeesWorkspace;
workspaceElement.executionEnvironment = env;
workspaceElement.initializationPromise = mountPromise;
containerElement.appendChild(workspaceElement);
return html` return html`
<div style="width: 100%; height: 600px; position: relative;"> <dees-demowrapper>
<dees-editor-workspace ${containerElement}
.executionEnvironment=${env} </dees-demowrapper>
.initializationPromise=${mountPromise}
></dees-editor-workspace>
</div>
`; `;
}; };
@@ -232,7 +240,7 @@ testSmartPromise();
resource: { path: string }; resource: { path: string };
}> = []; }> = [];
private editorElement: DeesEditorMonaco | null = null; private editorElement: DeesWorkspaceMonaco | null = null;
private initializationStarted: boolean = false; private initializationStarted: boolean = false;
private intelliSenseManager: TypeScriptIntelliSenseManager | null = null; private intelliSenseManager: TypeScriptIntelliSenseManager | null = null;
private intelliSenseInitialized: boolean = false; private intelliSenseInitialized: boolean = false;
@@ -247,6 +255,26 @@ testSmartPromise();
accessor autoSave: boolean = false; accessor autoSave: boolean = false;
private autoSaveInterval: ReturnType<typeof setInterval> | null = null; private autoSaveInterval: ReturnType<typeof setInterval> | null = null;
// Init terminal preview state
@state()
accessor initCommand: string = '';
@state()
accessor initOutput: string[] = [];
// Resize state
@state()
accessor currentFileTreeWidth: number = 250;
@state()
accessor currentTerminalHeight: number = 200;
@state()
accessor isDraggingFileTree: boolean = false;
@state()
accessor isDraggingTerminal: boolean = false;
// Keyboard shortcut handler (bound for proper cleanup) // Keyboard shortcut handler (bound for proper cleanup)
private keydownHandler = (e: KeyboardEvent) => { private keydownHandler = (e: KeyboardEvent) => {
// Cmd+S (Mac) or Ctrl+S (Windows/Linux) - Save // Cmd+S (Mac) or Ctrl+S (Windows/Linux) - Save
@@ -262,6 +290,71 @@ testSmartPromise();
} }
}; };
// ========== Filetree Resize Handlers ==========
private handleFileTreeMouseDown = (e: MouseEvent) => {
e.preventDefault();
this.isDraggingFileTree = true;
document.addEventListener('mousemove', this.handleFileTreeMouseMove);
document.addEventListener('mouseup', this.handleFileTreeMouseUp);
};
private handleFileTreeMouseMove = (e: MouseEvent) => {
if (!this.isDraggingFileTree) return;
const containerRect = this.getBoundingClientRect();
const mouseX = e.clientX - containerRect.left;
// Clamp to min/max (150px min, 50% of container max)
const minWidth = 150;
const maxWidth = containerRect.width * 0.5;
const newWidth = Math.max(minWidth, Math.min(maxWidth, mouseX));
this.currentFileTreeWidth = newWidth;
};
private handleFileTreeMouseUp = () => {
this.isDraggingFileTree = false;
document.removeEventListener('mousemove', this.handleFileTreeMouseMove);
document.removeEventListener('mouseup', this.handleFileTreeMouseUp);
// Notify Monaco editor of size change
window.dispatchEvent(new Event('resize'));
};
// ========== Terminal Resize Handlers ==========
private handleTerminalMouseDown = (e: MouseEvent) => {
e.preventDefault();
this.isDraggingTerminal = true;
document.addEventListener('mousemove', this.handleTerminalMouseMove);
document.addEventListener('mouseup', this.handleTerminalMouseUp);
};
private handleTerminalMouseMove = (e: MouseEvent) => {
if (!this.isDraggingTerminal) return;
const containerRect = this.getBoundingClientRect();
const mouseY = e.clientY - containerRect.top;
// Calculate terminal height from bottom
const terminalHeight = containerRect.height - mouseY;
// Clamp to min/max (100px min, 70% of container max)
const minHeight = 100;
const maxHeight = containerRect.height * 0.7;
const newHeight = Math.max(minHeight, Math.min(maxHeight, terminalHeight));
this.currentTerminalHeight = newHeight;
};
private handleTerminalMouseUp = () => {
this.isDraggingTerminal = false;
document.removeEventListener('mousemove', this.handleTerminalMouseMove);
document.removeEventListener('mouseup', this.handleTerminalMouseUp);
// Notify Monaco editor of size change
window.dispatchEvent(new Event('resize'));
};
public static styles = [ public static styles = [
themeDefaultStyles, themeDefaultStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
@@ -278,67 +371,45 @@ testSmartPromise();
} }
.workspace-container { .workspace-container {
display: grid; display: flex;
flex-direction: row;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.workspace-container.with-filetree.with-terminal { .editor-area {
grid-template-columns: auto 1fr; display: flex;
grid-template-rows: 1fr auto; flex-direction: column;
grid-template-areas: flex: 1;
"filetree editor" min-width: 0;
"filetree terminal"; overflow: hidden;
}
.workspace-container.with-filetree:not(.with-terminal) {
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
grid-template-areas: "filetree editor";
}
.workspace-container:not(.with-filetree).with-terminal {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
grid-template-areas:
"editor"
"terminal";
}
.workspace-container:not(.with-filetree):not(.with-terminal) {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: "editor";
} }
.filetree-panel { .filetree-panel {
grid-area: filetree;
position: relative; position: relative;
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
overflow: hidden; overflow: hidden;
transition: width 0.2s ease; flex-shrink: 0;
} }
.filetree-panel.collapsed { .filetree-panel.collapsed {
width: 0 !important; width: 0 !important;
border-right: none;
} }
.editor-panel { .editor-panel {
grid-area: editor;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
flex: 1;
min-width: 200px;
} }
.terminal-panel { .terminal-panel {
grid-area: terminal;
position: relative; position: relative;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')}; border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')};
overflow: hidden; overflow: hidden;
transition: height 0.2s ease; flex-shrink: 0;
} }
.terminal-panel.collapsed { .terminal-panel.collapsed {
@@ -630,6 +701,11 @@ testSmartPromise();
opacity: 0.5; opacity: 0.5;
} }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.initializing { .initializing {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -641,7 +717,18 @@ testSmartPromise();
gap: 12px; gap: 12px;
} }
dees-editor-filetree { .initializing dees-icon {
animation: spin 1s linear infinite;
}
.initializing dees-workspace-terminal-preview {
margin-top: 24px;
width: 80%;
max-width: 600px;
height: 200px;
}
dees-workspace-filetree {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -649,7 +736,7 @@ testSmartPromise();
bottom: 0; bottom: 0;
} }
dees-editor-monaco { dees-workspace-monaco {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -657,21 +744,103 @@ testSmartPromise();
bottom: 0; bottom: 0;
} }
dees-terminal { dees-workspace-terminal {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
/* Resize handles */
.resize-handle-vertical {
width: 6px;
cursor: col-resize;
background: transparent;
transition: background 0.15s ease;
position: relative;
flex-shrink: 0;
z-index: 10;
}
.resize-handle-vertical:hover,
.resize-handle-vertical.dragging {
background: ${cssManager.bdTheme('#3b82f6', '#58a6ff')};
}
.resize-handle-vertical::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 32px;
background: ${cssManager.bdTheme('#9ca3af', '#6e7681')};
border-radius: 1px;
opacity: 0;
transition: opacity 0.15s ease;
}
.resize-handle-vertical:hover::after,
.resize-handle-vertical.dragging::after {
opacity: 1;
background: ${cssManager.bdTheme('#ffffff', '#ffffff')};
}
.resize-handle-horizontal {
height: 6px;
cursor: row-resize;
background: transparent;
transition: background 0.15s ease;
position: relative;
flex-shrink: 0;
z-index: 10;
}
.resize-handle-horizontal:hover,
.resize-handle-horizontal.dragging {
background: ${cssManager.bdTheme('#3b82f6', '#58a6ff')};
}
.resize-handle-horizontal::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 2px;
background: ${cssManager.bdTheme('#9ca3af', '#6e7681')};
border-radius: 1px;
opacity: 0;
transition: opacity 0.15s ease;
}
.resize-handle-horizontal:hover::after,
.resize-handle-horizontal.dragging::after {
opacity: 1;
background: ${cssManager.bdTheme('#ffffff', '#ffffff')};
}
/* Prevent text selection while dragging */
.workspace-container.dragging {
user-select: none;
}
.workspace-container.dragging .filetree-panel,
.workspace-container.dragging .editor-panel,
.workspace-container.dragging .terminal-panel {
pointer-events: none;
}
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const containerClasses = [ const containerClasses = [
'workspace-container', 'workspace-container',
this.showFileTree && !this.isFileTreeCollapsed ? 'with-filetree' : '', (this.isDraggingFileTree || this.isDraggingTerminal) ? 'dragging' : '',
this.showTerminal ? 'with-terminal' : '',
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
if (this.isInitializing) { if (this.isInitializing) {
@@ -679,107 +848,130 @@ testSmartPromise();
<div class="initializing"> <div class="initializing">
<dees-icon .icon=${'lucide:loader2'} iconSize="32"></dees-icon> <dees-icon .icon=${'lucide:loader2'} iconSize="32"></dees-icon>
<span>Initializing workspace...</span> <span>Initializing workspace...</span>
<dees-workspace-terminal-preview
.command=${this.initCommand || ''}
.lines=${this.initOutput}
></dees-workspace-terminal-preview>
</div> </div>
`; `;
} }
return html` return html`
<div class="${containerClasses}"> <div class="${containerClasses}">
<!-- Filetree panel (full height) -->
${this.showFileTree ? html` ${this.showFileTree ? html`
<div <div
class="filetree-panel ${this.isFileTreeCollapsed ? 'collapsed' : ''}" class="filetree-panel ${this.isFileTreeCollapsed ? 'collapsed' : ''}"
style="width: ${this.isFileTreeCollapsed ? 0 : this.fileTreeWidth}px" style="width: ${this.isFileTreeCollapsed ? 0 : this.currentFileTreeWidth}px"
> >
<dees-editor-filetree <dees-workspace-filetree
.executionEnvironment=${this.executionEnvironment} .executionEnvironment=${this.executionEnvironment}
.selectedPath=${this.activeFilePath} .selectedPath=${this.activeFilePath}
@file-select=${this.handleFileSelect} @file-select=${this.handleFileSelect}
></dees-editor-filetree> ></dees-workspace-filetree>
</div> </div>
${!this.isFileTreeCollapsed ? html`
<div
class="resize-handle-vertical ${this.isDraggingFileTree ? 'dragging' : ''}"
@mousedown=${this.handleFileTreeMouseDown}
></div>
` : ''}
` : ''} ` : ''}
<div class="editor-panel"> <!-- Editor + Terminal area -->
<div class="tabs-bar"> <div class="editor-area">
<div class="tabs-container"> <div class="editor-panel">
${this.openFiles.map(file => html` <div class="tabs-bar">
<div <div class="tabs-container">
class="tab ${file.path === this.activeFilePath ? 'active' : ''}" ${this.openFiles.map(file => html`
@click=${() => this.activateFile(file.path)} <div
> class="tab ${file.path === this.activeFilePath ? 'active' : ''}"
${file.modified ? html`<span class="tab-modified"></span>` : ''} @click=${() => this.activateFile(file.path)}
<span class="tab-name">${file.name}</span> >
<span class="tab-close" @click=${(e: Event) => this.closeFile(e, file.path)}> ${file.modified ? html`<span class="tab-modified"></span>` : ''}
<dees-icon .icon=${'lucide:x'} iconSize="12"></dees-icon> <span class="tab-name">${file.name}</span>
</span> <span class="tab-close" @click=${(e: Event) => this.closeFile(e, file.path)}>
</div> <dees-icon .icon=${'lucide:x'} iconSize="12"></dees-icon>
`)} </span>
</div> </div>
<div class="editor-menu-button" @click=${this.showEditorMenu} title="Editor options"> `)}
<dees-icon .icon=${'lucide:moreVertical'} iconSize="16"></dees-icon>
</div>
</div>
<div class="editor-content">
${this.openFiles.length === 0 ? html`
<div class="empty-state">
<dees-icon .icon=${'lucide:fileCode'} iconSize="48"></dees-icon>
<span>Select a file to edit</span>
</div> </div>
` : html` <div class="editor-menu-button" @click=${this.showEditorMenu} title="Editor options">
<dees-editor-monaco <dees-icon .icon=${'lucide:moreVertical'} iconSize="16"></dees-icon>
.filePath=${this.activeFilePath} </div>
.content=${this.getActiveFileContent()} </div>
.language=${this.getLanguageFromPath(this.activeFilePath)} <div class="editor-content">
@content-change=${this.handleContentChange} ${this.openFiles.length === 0 ? html`
></dees-editor-monaco> <div class="empty-state">
`} <dees-icon .icon=${'lucide:fileCode'} iconSize="48"></dees-icon>
<span>Select a file to edit</span>
</div>
` : html`
<dees-workspace-monaco
.filePath=${this.activeFilePath}
.content=${this.getActiveFileContent()}
.language=${this.getLanguageFromPath(this.activeFilePath)}
@content-change=${this.handleContentChange}
></dees-workspace-monaco>
`}
</div>
</div> </div>
<!-- Horizontal resize handle for terminal -->
${this.showTerminal && !this.isTerminalCollapsed ? html`
<div
class="resize-handle-horizontal ${this.isDraggingTerminal ? 'dragging' : ''}"
@mousedown=${this.handleTerminalMouseDown}
></div>
` : ''}
<!-- Terminal panel -->
${this.showTerminal ? html`
<div
class="terminal-panel ${this.isTerminalCollapsed ? 'collapsed' : ''}"
style="height: ${this.isTerminalCollapsed ? 32 : this.currentTerminalHeight}px"
>
<div class="panel-header">
<div class="panel-tabs">
<div
class="panel-tab ${this.activeBottomPanel === 'terminal' ? 'active' : ''}"
@click=${() => this.activeBottomPanel = 'terminal'}
>
<dees-icon .icon=${'lucide:terminal'} iconSize="14"></dees-icon>
Terminal
</div>
<div
class="panel-tab ${this.activeBottomPanel === 'problems' ? 'active' : ''}"
@click=${() => this.activeBottomPanel = 'problems'}
>
<dees-icon .icon=${'lucide:circleAlert'} iconSize="14"></dees-icon>
Problems
${this.diagnosticMarkers.length > 0 ? html`
<span class="panel-tab-badge ${this.getErrorCount() === 0 ? 'warning' : ''}">${this.diagnosticMarkers.length}</span>
` : ''}
</div>
</div>
<div class="panel-header-actions">
<div class="panel-action" @click=${this.toggleTerminal}>
<dees-icon
.icon=${this.isTerminalCollapsed ? 'lucide:chevronUp' : 'lucide:chevronDown'}
iconSize="14"
></dees-icon>
</div>
</div>
</div>
<div class="terminal-content" style="display: ${this.activeBottomPanel === 'terminal' ? 'block' : 'none'}">
<dees-workspace-terminal
.executionEnvironment=${this.executionEnvironment}
.setupCommand=${''}
></dees-workspace-terminal>
</div>
<div class="problems-content" style="display: ${this.activeBottomPanel === 'problems' ? 'block' : 'none'}">
${this.renderProblemsPanel()}
</div>
</div>
` : ''}
</div> </div>
${this.showTerminal ? html`
<div
class="terminal-panel ${this.isTerminalCollapsed ? 'collapsed' : ''}"
style="height: ${this.isTerminalCollapsed ? 32 : this.terminalHeight}px"
>
<div class="panel-header">
<div class="panel-tabs">
<div
class="panel-tab ${this.activeBottomPanel === 'terminal' ? 'active' : ''}"
@click=${() => this.activeBottomPanel = 'terminal'}
>
<dees-icon .icon=${'lucide:terminal'} iconSize="14"></dees-icon>
Terminal
</div>
<div
class="panel-tab ${this.activeBottomPanel === 'problems' ? 'active' : ''}"
@click=${() => this.activeBottomPanel = 'problems'}
>
<dees-icon .icon=${'lucide:circleAlert'} iconSize="14"></dees-icon>
Problems
${this.diagnosticMarkers.length > 0 ? html`
<span class="panel-tab-badge ${this.getErrorCount() === 0 ? 'warning' : ''}">${this.diagnosticMarkers.length}</span>
` : ''}
</div>
</div>
<div class="panel-header-actions">
<div class="panel-action" @click=${this.toggleTerminal}>
<dees-icon
.icon=${this.isTerminalCollapsed ? 'lucide:chevronUp' : 'lucide:chevronDown'}
iconSize="14"
></dees-icon>
</div>
</div>
</div>
<div class="terminal-content" style="display: ${this.activeBottomPanel === 'terminal' ? 'block' : 'none'}">
<dees-terminal
.executionEnvironment=${this.executionEnvironment}
.setupCommand=${''}
></dees-terminal>
</div>
<div class="problems-content" style="display: ${this.activeBottomPanel === 'problems' ? 'block' : 'none'}">
${this.renderProblemsPanel()}
</div>
</div>
` : ''}
</div> </div>
`; `;
} }
@@ -792,6 +984,13 @@ testSmartPromise();
async disconnectedCallback() { async disconnectedCallback() {
await super.disconnectedCallback(); await super.disconnectedCallback();
document.removeEventListener('keydown', this.keydownHandler); document.removeEventListener('keydown', this.keydownHandler);
// Clean up resize event listeners
document.removeEventListener('mousemove', this.handleFileTreeMouseMove);
document.removeEventListener('mouseup', this.handleFileTreeMouseUp);
document.removeEventListener('mousemove', this.handleTerminalMouseMove);
document.removeEventListener('mouseup', this.handleTerminalMouseUp);
if (this.autoSaveInterval) { if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval); clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null; this.autoSaveInterval = null;
@@ -800,6 +999,10 @@ testSmartPromise();
} }
public async firstUpdated() { public async firstUpdated() {
// Initialize current sizes from properties
this.currentFileTreeWidth = this.fileTreeWidth;
this.currentTerminalHeight = this.terminalHeight;
if (this.executionEnvironment) { if (this.executionEnvironment) {
await this.initializeWorkspace(); await this.initializeWorkspace();
} }
@@ -856,12 +1059,36 @@ testSmartPromise();
if (onInit && typeof onInit === 'string') { if (onInit && typeof onInit === 'string') {
console.log('Executing onInit command:', onInit); console.log('Executing onInit command:', onInit);
// Set command for terminal preview display
this.initCommand = onInit;
this.initOutput = [];
// Parse command and args // Parse command and args
const [cmd, ...args] = onInit.split(' '); const [cmd, ...args] = onInit.split(' ');
const process = await this.executionEnvironment.spawn(cmd, args); const process = await this.executionEnvironment.spawn(cmd, args);
// Capture stdout/stderr for terminal preview
process.output.pipeTo(
new WritableStream({
write: (data) => {
// Split data into lines and append
const lines = data.split('\n');
for (const line of lines) {
if (line) {
this.initOutput = [...this.initOutput, line];
}
}
},
})
);
// Wait for completion // Wait for completion
const exitCode = await process.exit; const exitCode = await process.exit;
console.log('onInit command completed with exit code:', exitCode); console.log('onInit command completed with exit code:', exitCode);
// Add completion message
this.initOutput = [...this.initOutput, '', `Command completed with exit code: ${exitCode}`];
} }
} }
} catch (error) { } catch (error) {
@@ -1212,7 +1439,7 @@ testSmartPromise();
// Wait for editor to be ready, then navigate to the line // Wait for editor to be ready, then navigate to the line
await this.updateComplete; await this.updateComplete;
const editorElement = this.shadowRoot?.querySelector('dees-editor-monaco') as DeesEditorMonaco; const editorElement = this.shadowRoot?.querySelector('dees-workspace-monaco') as DeesWorkspaceMonaco;
if (editorElement) { if (editorElement) {
const editor = await editorElement.editorDeferred.promise; const editor = await editorElement.editorDeferred.promise;
editor.revealLineInCenter(marker.startLineNumber); editor.revealLineInCenter(marker.startLineNumber);
@@ -1256,4 +1483,35 @@ testSmartPromise();
resource: { path: m.resource.path }, resource: { path: m.resource.path },
})); }));
} }
// ========== Public Layout Methods ==========
/**
* Programmatically set the file tree width
*/
public setFileTreeWidth(width: number): void {
const minWidth = 150;
const maxWidth = this.getBoundingClientRect().width * 0.5;
this.currentFileTreeWidth = Math.max(minWidth, Math.min(maxWidth, width));
window.dispatchEvent(new Event('resize'));
}
/**
* Programmatically set the terminal height
*/
public setTerminalHeight(height: number): void {
const minHeight = 100;
const maxHeight = this.getBoundingClientRect().height * 0.7;
this.currentTerminalHeight = Math.max(minHeight, Math.min(maxHeight, height));
window.dispatchEvent(new Event('resize'));
}
/**
* Reset layout to initial property values
*/
public resetLayout(): void {
this.currentFileTreeWidth = this.fileTreeWidth;
this.currentTerminalHeight = this.terminalHeight;
window.dispatchEvent(new Event('resize'));
}
} }

View File

@@ -1,2 +1,2 @@
export * from './dees-editor-workspace.js'; export * from './dees-workspace.js';
export * from './typescript-intellisense.js'; export * from './typescript-intellisense.js';

View File

@@ -378,7 +378,10 @@ export class TypeScriptIntelliSenseManager {
const existingModel = this.monacoInstance.editor.getModel(uri); const existingModel = this.monacoInstance.editor.getModel(uri);
if (existingModel) { if (existingModel) {
existingModel.setValue(content); // Only update if content actually changed (prevents cursor reset)
if (existingModel.getValue() !== content) {
existingModel.setValue(content);
}
} else { } else {
const language = this.getLanguageFromPath(path); const language = this.getLanguageFromPath(path);
this.monacoInstance.editor.createModel(content, language, uri); this.monacoInstance.editor.createModel(content, language, uri);

View File

@@ -0,0 +1,8 @@
// Workspace Components
export * from './dees-workspace/index.js';
export * from './dees-workspace-monaco/index.js';
export * from './dees-workspace-filetree/index.js';
export * from './dees-workspace-terminal/index.js';
export * from './dees-workspace-terminal-preview/index.js';
export * from './dees-workspace-markdown/index.js';
export * from './dees-workspace-markdownoutlet/index.js';

View File

@@ -1 +0,0 @@
export * from './dees-terminal.js';

View File

@@ -6,7 +6,7 @@ export * from './00group-appui/index.js';
export * from './00group-button/index.js'; export * from './00group-button/index.js';
export * from './00group-chart/index.js'; export * from './00group-chart/index.js';
export * from './00group-dataview/index.js'; export * from './00group-dataview/index.js';
export * from './00group-editor/index.js'; export * from './00group-workspace/index.js';
export * from './00group-form/index.js'; export * from './00group-form/index.js';
export * from './00group-input/index.js'; export * from './00group-input/index.js';
export * from './00group-pdf/index.js'; export * from './00group-pdf/index.js';
@@ -34,7 +34,7 @@ export * from './dees-spinner/index.js';
export * from './dees-statsgrid/index.js'; export * from './dees-statsgrid/index.js';
export * from './dees-stepper/index.js'; export * from './dees-stepper/index.js';
export * from './dees-table/index.js'; export * from './dees-table/index.js';
export * from './dees-terminal/index.js'; // dees-terminal is now part of 00group-workspace as dees-workspace-terminal
export * from './dees-toast/index.js'; export * from './dees-toast/index.js';
export * from './dees-updater/index.js'; export * from './dees-updater/index.js';
export * from './dees-windowcontrols/index.js'; export * from './dees-windowcontrols/index.js';