Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/frontend/src/CustomEmbeddableRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
Dashboard,
StateIndicator,
ControlButton,
HtmlEditor,
Editor,
Terminal,
} from './pad';
Expand All @@ -28,7 +27,11 @@ export const renderCustomEmbeddable = (

switch (path) {
case 'html':
content = <HtmlEditor element={element} appState={appState} excalidrawAPI={excalidrawAPI} />;
content = <Editor
element={element}
language="html"
excalidrawAPI={excalidrawAPI}
/>;
title = "HTML Editor";
break;
case 'editor':
Expand Down
193 changes: 189 additions & 4 deletions src/frontend/src/pad/editors/Editor.scss
Original file line number Diff line number Diff line change
@@ -1,18 +1,105 @@
.editor {
&__wrapper {
display: flex;
flex-direction: column;
position: relative; /* For absolute positioning of toolbar */
height: 100%;
width: 100%;
overflow: hidden; /* Prevent overflow issues */
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;

&--split {
.monaco-editor {
width: 50% !important; /* Force Monaco editor to resize */
}

.editor__container {
width: 50% !important; /* Force container to resize */
}

.editor__toolbar {
right: 50%; /* Limit toolbar width to match the editor in split view */
}
}
}

&__toolbar {
display: flex;
justify-content: right;
justify-content: flex-end; /* Default justify to the right when no HTML controls */
padding: 8px;
position: absolute; /* Position at bottom */
bottom: 0;
left: 0;
right: 0;
z-index: 10; /* Keep toolbar above other elements */
// background-color: #191919; /* Match editor background */
border-top: 2px solid #3c3c3c;
}

&__toolbar-right {
display: flex;
align-items: center;
gap: 8px; /* Add spacing between toolbar items */

.excalidraw-tooltip-wrapper {
height: 100%;
display: flex;
align-items: center;
}
}

&__html-controls {
display: flex;
align-items: center;
gap: 8px;
margin-right: auto; /* Push to the left side */

.html-editor__label {
font-size: 12px;
color: #e0e0e0;
display: flex;
align-items: center;
gap: 4px;
}

.html-editor__button {
padding: 6px 12px;
background: #5294f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;

&:hover {
background: #4285e7;
}
}
}

&__format-button {
background-color: #252526;
color: #cccccc;
border: 1px solid #3c3c3c;
border-radius: 7px;
padding: 4px 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&:hover {
background-color: #2a2d2e;
color: #ffffff;
}

&:focus {
outline: none;
border-color: #007fd4;
}
}

&__language-selector {
display: flex;
margin-right: 10px;
}

Expand All @@ -35,8 +122,106 @@
color: #cccccc;
}
}

&__searchable-language-container {
display: flex;
align-items: center;
background-color: #252526;
border: 1px solid #3c3c3c;
border-radius: 7px;
max-width: 150px;
position: relative;

&:focus-within {
border-color: #007fd4;
}
}

&__searchable-language-input {
background-color: transparent;
color: #cccccc;
border: none;
padding: 4px 8px;
font-size: 12px;
width: 100%;

&:focus {
outline: none;
}

&::placeholder {
color: #cccccc;
opacity: 0.8;
}
}

&__searchable-language-toggle {
background: none;
border: none;
color: #cccccc;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
transition: transform 0.2s ease;

&:hover {
color: #ffffff;
}

&:focus {
outline: none;
}
}

&__searchable-language-dropdown {
position: absolute;
bottom: 100%; /* Position above instead of below */
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background-color: #252526;
border: 1px solid #3c3c3c;
border-radius: 7px;
margin-bottom: 4px; /* Margin at bottom instead of top */
z-index: 20; /* Higher z-index to ensure it's above the toolbar */
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.3); /* Shadow adjusted for upward direction */
}

&__searchable-language-option {
padding: 4px 8px;
font-size: 12px;
color: #cccccc;
cursor: pointer;

&:hover {
background-color: #2a2d2e;
}

&--highlighted {
background-color: #04395e;
}

&--selected {
color: #ffffff;
font-weight: 500;
}
}

&__searchable-language-no-results {
padding: 4px 8px;
font-size: 12px;
color: #cccccc;
font-style: italic;
text-align: center;
}

&__container {
flex: 1;
height: 100%;
width: 100%;
padding-bottom: 60px; /* Make room for the toolbar */
box-sizing: border-box;
}
}
105 changes: 96 additions & 9 deletions src/frontend/src/pad/editors/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
import React, { useRef, useState, useEffect, useCallback } from 'react';
import MonacoEditor from '@monaco-editor/react';
import LanguageSelector from './LanguageSelector';
import { Tooltip, updateTooltipPosition, getTooltipDiv } from '@atyrode/excalidraw';
import SearchableLanguageSelector from './SearchableLanguageSelector';
import { useHtmlEditor, HtmlEditorControls, defaultHtml, HtmlEditorSplitView } from './HtmlEditor';
import './Editor.scss';

// Custom tooltip wrapper that positions the tooltip at the top
const TopTooltip: React.FC<{label: string, children: React.ReactNode}> = ({ label, children }) => {
const handlePointerEnter = (event: React.PointerEvent<HTMLDivElement>) => {
const tooltip = getTooltipDiv();
tooltip.classList.add("excalidraw-tooltip--visible");
tooltip.textContent = label;

const itemRect = event.currentTarget.getBoundingClientRect();
updateTooltipPosition(tooltip, itemRect, "top");
};

const handlePointerLeave = () => {
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
};

return (
<div
className="excalidraw-tooltip-wrapper"
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
{children}
</div>
);
};

interface EditorProps {
defaultValue?: string;
language?: string;
Expand All @@ -21,7 +49,7 @@ interface EditorProps {

const Editor: React.FC<EditorProps> = ({
defaultValue = '',
language = 'javascript',
language = 'plaintext',
theme = 'vs-dark',
height = '100%',
options = {
Expand Down Expand Up @@ -259,25 +287,84 @@ const Editor: React.FC<EditorProps> = ({
}
};

// Format document function
const formatDocument = () => {
if (editorRef.current) {
// Trigger Monaco's format document action
editorRef.current.getAction('editor.action.formatDocument')?.run();
}
};

// Check if the language is HTML
const isHtml = currentLanguage === 'html';

// Always initialize HTML editor hooks, but pass isActive flag
const htmlEditor = useHtmlEditor(
element,
editorRef,
excalidrawAPI,
isHtml
);

// Determine if we should show the split view
const showSplitView = isHtml && !htmlEditor.createNew && htmlEditor.showPreview;

return (
<div className="editor__wrapper">
<div className={`editor__wrapper ${showSplitView ? 'editor__wrapper--split' : ''}`}>
<MonacoEditor
height={height}
language={currentLanguage}
defaultValue={defaultValue}
defaultValue={defaultValue || (isHtml ? defaultHtml : '')}
theme={theme}
options={options}
onMount={handleEditorDidMount}
onChange={handleEditorChange}
className={className}
/>

{/* Render the HTML preview in split view mode */}
{showSplitView && (
<HtmlEditorSplitView
editorContent={contentRef.current || ''}
previewContent={htmlEditor.previewContent}
showPreview={htmlEditor.showPreview}
/>
)}
{showLanguageSelector && (
<div className="editor__toolbar">
<LanguageSelector
value={currentLanguage}
onChange={handleLanguageChange}
className="editor__language-selector"
/>
{/* Show HTML-specific controls when language is HTML */}
{isHtml && (
<div className="editor__html-controls">
<HtmlEditorControls
createNew={htmlEditor.createNew}
setCreateNew={htmlEditor.setCreateNew}
applyHtml={htmlEditor.applyHtml}
showPreview={htmlEditor.showPreview}
togglePreview={htmlEditor.togglePreview}
/>
</div>
)}

{/* Group format button and language selector together on the right */}
<div className="editor__toolbar-right">
<TopTooltip label="Format" children={
<button
className="editor__format-button"
onClick={formatDocument}
aria-label="Format Document"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4H14M4 8H12M6 12H10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
} />

<SearchableLanguageSelector
value={currentLanguage}
onChange={handleLanguageChange}
className="editor__language-selector"
/>
</div>
</div>
)}
</div>
Expand Down
Loading