Compare commits
2 Commits
aac84a0a7f
...
939a7fd191
| Author | SHA1 | Date | |
|---|---|---|---|
| 939a7fd191 | |||
| d12319392f |
@@ -0,0 +1,178 @@
|
||||
# Monaco Editor v1
|
||||
|
||||
## 1. Doel
|
||||
Monaco voegt nu waarde toe omdat de huidige textarea-editor functioneel is, maar beperkt blijft voor code- en configbestanden. Voor deze app is de winst vooral: betere leesbaarheid, syntax highlighting, line numbers, current-line focus en een meer betrouwbare editervaring voor technische bestanden.
|
||||
|
||||
Dit past naast de bestaande text editor modal als een vervanging van de edit-UI, niet als een nieuwe backendflow. De bestaande backend read/save-flow bestaat al en moet ongewijzigd worden hergebruikt:
|
||||
- initial read via de bestaande edit/view-flow
|
||||
- save via het bestaande save-endpoint
|
||||
- bestaande conflict- en io_error-semantiek blijven leidend
|
||||
|
||||
Het doel is een "VS Code light"-achtige ervaring voor 1 bestand tegelijk, zonder IDE-verbreding.
|
||||
|
||||
## 2. Scope
|
||||
In scope voor v1:
|
||||
- Monaco Editor in een modal
|
||||
- F4 opent Monaco voor ondersteunde tekst/codebestanden
|
||||
- syntax highlighting
|
||||
- line numbers
|
||||
- current line highlight
|
||||
- save via bestaande save-flow
|
||||
- dirty state behouden
|
||||
- conflict handling behouden
|
||||
- light/dark theme-integratie
|
||||
|
||||
Niet in scope voor v1:
|
||||
- LSP
|
||||
- autocomplete op IDE-niveau
|
||||
- multi-tab editor
|
||||
- diff editor
|
||||
- command palette
|
||||
- file tree in de modal
|
||||
- symbol outline
|
||||
- search panel met geavanceerde editorfeatures
|
||||
- plugin- of extensionmodel
|
||||
|
||||
## 3. Integratierichting
|
||||
Aanbevolen route: moderne browser-side Monaco-integratie met de officiële ESM-distributie van `monaco-editor`, gebundeld als statische frontend-assets voor deze app.
|
||||
|
||||
Aanbevolen technische richting:
|
||||
- Monaco alleen lazy loaden wanneer de edit modal voor het eerst opent
|
||||
- editor-instance per modal-open lifecycle aanmaken
|
||||
- bij sluiten altijd `editor.dispose()` en eventuele model cleanup uitvoeren
|
||||
- per geopend bestand 1 model tegelijk gebruiken
|
||||
- bestaande modal DOM behouden, maar de textarea vervangen door een editor host-container
|
||||
|
||||
Waarom deze route:
|
||||
- sluit aan op de huidige lichte frontend zonder volledige frameworkmigratie
|
||||
- voorkomt dat Monaco initieel alle schermen vertraagt
|
||||
- houdt lifecycle beheersbaar
|
||||
|
||||
Belangrijke lifecycle-regels:
|
||||
- editor pas initialiseren nadat modal zichtbaar is en afmetingen stabiel zijn
|
||||
- bij heropenen van een ander bestand geen oude editor hergebruiken zonder expliciete reset
|
||||
- model en listeners bij sluiten expliciet opruimen
|
||||
- resize van modal/viewport koppelen aan `editor.layout()` zolang modal open is
|
||||
|
||||
## 4. Taalondersteuning
|
||||
Minimale extensie/bestandsnaammapping voor v1:
|
||||
- `js`, `mjs`, `cjs` -> `javascript`
|
||||
- `ts`, `tsx` -> `typescript`
|
||||
- `json` -> `json`
|
||||
- `css` -> `css`
|
||||
- `html`, `htm` -> `html`
|
||||
- `xml` -> `xml`
|
||||
- `yml`, `yaml` -> `yaml`
|
||||
- `py` -> `python`
|
||||
- `sh`, `bash`, `zsh`, `fish` -> `shell`
|
||||
- `md`, `markdown` -> `markdown`
|
||||
- `txt`, `log`, `ini`, `cfg`, `conf` -> `plaintext`
|
||||
- `Dockerfile`, `Containerfile` -> `dockerfile`
|
||||
|
||||
V1-verwachting per taal:
|
||||
- syntax highlighting voor alle bovenstaande talen
|
||||
- bracket matching / basic Monaco editor behavior waar Monaco dat standaard biedt
|
||||
- geen extra taalservices of projectintelligentie buiten wat Monaco standaard client-side levert
|
||||
|
||||
Pragmatische keuze:
|
||||
- v1 focust op highlighting en nette editing
|
||||
- rijkere taalfeatures zijn een bijeffect van Monaco waar beschikbaar, maar geen contract
|
||||
|
||||
## 5. Theme-integratie
|
||||
Monaco moet aansluiten op de bestaande light/dark mode.
|
||||
|
||||
Aanbevolen v1-richting:
|
||||
- dark mode -> Monaco `vs-dark`
|
||||
- light mode -> Monaco `vs`
|
||||
- alleen een custom Monaco theme toevoegen als kleurafwijking zichtbaar storend is
|
||||
|
||||
Dit houdt regressierisico laag. De rest van de UI blijft leidend; Monaco hoeft in v1 niet pixel-perfect het app-thema te kopieren, zolang het visueel coherent is.
|
||||
|
||||
## 6. Modal-UX
|
||||
Aanbevolen editor-modal:
|
||||
- groot, bijna viewport-vullend
|
||||
- duidelijk groter dan de huidige eenvoudige edit-modal
|
||||
- behoud van titelbalk met bestandsnaam/pad
|
||||
- behoud van `Save`, `Cancel` en `X`
|
||||
|
||||
Gedrag:
|
||||
- `Save` gebruikt bestaande save-flow
|
||||
- `Cancel` sluit alleen zonder prompt als niet dirty
|
||||
- `X` volgt hetzelfde gedrag als `Cancel`
|
||||
- `Escape`:
|
||||
- zonder wijzigingen -> sluit
|
||||
- met wijzigingen -> bestaande discard-waarschuwing behouden
|
||||
|
||||
Keyboard/focus:
|
||||
- terwijl Monaco open is, mogen paneelshortcuts niet ingrijpen
|
||||
- editor krijgt focus direct na openen
|
||||
- modal blijft de enige actieve interactiezone totdat hij sluit
|
||||
|
||||
## 7. Regressiebehoud
|
||||
Moet functioneel behouden blijven:
|
||||
- bestaande F4 edit-semantiek
|
||||
- bestaande backend save/conflict-flow
|
||||
- bestaande optimistic locking / expected_modified gedrag
|
||||
- bestaande dirty-state logica
|
||||
- bestaande text/video/pdf viewers
|
||||
- bestaande browse- en paneelworkflow buiten de modal
|
||||
|
||||
Niet doen in v1:
|
||||
- backend save-contract wijzigen
|
||||
- nieuwe backend editorfeatures toevoegen
|
||||
- bestaande foutcodes of response-shapes wijzigen
|
||||
|
||||
## 8. Performance en risico
|
||||
Belangrijkste risico's:
|
||||
- Monaco vergroot frontend bundle/load
|
||||
- editor initialisatie is zwaarder dan textarea
|
||||
- foutieve dispose kan memory leaks geven
|
||||
- taalbundels kunnen groter zijn dan nodig
|
||||
|
||||
Laag-risico aanpak:
|
||||
- lazy load alleen bij eerste edit-open
|
||||
- alleen de noodzakelijke Monaco assets shippen
|
||||
- geen extra taalplugins of worker-uitbreidingen toevoegen buiten wat nodig is
|
||||
- editor instance altijd disposer'en bij sluiten
|
||||
|
||||
Reële tradeoff:
|
||||
- Monaco is zwaarder dan strikt nodig voor een kleine file manager
|
||||
- maar de UX-winst voor code/config editing is groot genoeg als de integratie sober blijft
|
||||
|
||||
## 9. Teststrategie
|
||||
UI smoke/regressietests:
|
||||
- Monaco host-container aanwezig in edit modal
|
||||
- F4 wiring blijft aanwezig
|
||||
- `Save`, `Cancel`, `X` blijven aanwezig
|
||||
- bestaande edit modalflow blijft openen/sluiten
|
||||
- light/dark theme blijft editor-opening niet breken
|
||||
|
||||
Handmatige validatie:
|
||||
- openen van ondersteund codebestand via F4
|
||||
- syntax highlighting voor representatieve typen:
|
||||
- js
|
||||
- json
|
||||
- yaml
|
||||
- py
|
||||
- markdown
|
||||
- Dockerfile
|
||||
- save success
|
||||
- save conflict
|
||||
- cancel/escape met dirty state
|
||||
- herhaald open/close van editor zonder UI-vertraging of kapotte focus
|
||||
- switch tussen light/dark terwijl editor open of dicht is
|
||||
|
||||
## 10. Aanbeveling
|
||||
Aanbevolen v1-richting met laag regressierisico:
|
||||
- vervang de huidige textarea-editor UI door een Monaco-modal
|
||||
- hergebruik ongewijzigd de bestaande backend read/save-flow
|
||||
- beperk v1 tot syntax highlighting, line numbers, current line highlight en nette modal-UX
|
||||
- gebruik Monaco lazy-loaded en dispose correct bij sluiten
|
||||
- gebruik standaard `vs` / `vs-dark` thema's tenzij er een duidelijke mismatch blijkt
|
||||
|
||||
Niet proberen in v1:
|
||||
- IDE-features najagen
|
||||
- extra backendsemantiek toevoegen
|
||||
- editorflow verbreden naar meerdere bestanden of workspaceconcepten
|
||||
|
||||
De juiste lat voor v1 is: betere editor-ergonomie zonder de app architectonisch zwaarder te maken dan nodig.
|
||||
Binary file not shown.
@@ -18,6 +18,7 @@ TEXT_CONTENT_TYPES = {
|
||||
".yaml": "text/yaml",
|
||||
".json": "application/json",
|
||||
".js": "text/javascript",
|
||||
".py": "text/x-python",
|
||||
".css": "text/css",
|
||||
".html": "text/html",
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -67,6 +67,19 @@ class EditApiGoldenTest(unittest.TestCase):
|
||||
self.assertFalse(body["truncated"])
|
||||
self.assertIn("modified", body)
|
||||
|
||||
def test_edit_view_python_success(self) -> None:
|
||||
file_path = self.root / "script.py"
|
||||
file_path.write_text("print('hello')\n", encoding="utf-8")
|
||||
|
||||
response = self._request("GET", "/api/files/view", params={"path": "storage1/script.py", "for_edit": "true"})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = response.json()
|
||||
self.assertEqual(body["path"], "storage1/script.py")
|
||||
self.assertEqual(body["name"], "script.py")
|
||||
self.assertEqual(body["content_type"], "text/x-python")
|
||||
self.assertEqual(body["content"], "print('hello')\n")
|
||||
|
||||
def test_save_success(self) -> None:
|
||||
file_path = self.root / "notes.txt"
|
||||
file_path.write_text("hello", encoding="utf-8")
|
||||
|
||||
@@ -76,7 +76,7 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('id="settings-logs-list"', body)
|
||||
self.assertIn('id="viewer-content"', body)
|
||||
self.assertIn('id="editor-modal"', body)
|
||||
self.assertIn('id="editor-content"', body)
|
||||
self.assertIn('id="editor-host"', body)
|
||||
self.assertIn('id="editor-save-btn"', body)
|
||||
self.assertIn('id="editor-cancel-btn"', body)
|
||||
self.assertIn('id="move-popup"', body)
|
||||
@@ -156,8 +156,13 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('function openRenamePopup()', app_js)
|
||||
self.assertIn('document.getElementById("rename-btn").onclick = openRenamePopup;', app_js)
|
||||
self.assertIn('return triggerActionButton("rename-btn");', app_js)
|
||||
self.assertIn('".py"', app_js)
|
||||
self.assertIn('function openVideoViewer()', app_js)
|
||||
self.assertIn('function openPdfViewer()', app_js)
|
||||
self.assertIn('async function loadMonacoModule()', app_js)
|
||||
self.assertIn('async function ensureMonacoEditor(path, content)', app_js)
|
||||
self.assertIn('function disposeMonacoEditor()', app_js)
|
||||
self.assertIn('https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm', app_js)
|
||||
self.assertIn('document.getElementById("pdf-modal")', app_js)
|
||||
self.assertIn("`/api/files/pdf?", app_js)
|
||||
self.assertIn('if (isPdfSelection(selected)) {', app_js)
|
||||
@@ -186,6 +191,8 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
||||
self.assertIn('.entry-media-icon.pdf', style_css)
|
||||
self.assertIn('.entry-media-svg', style_css)
|
||||
self.assertIn('.entry-media-icon.file', style_css)
|
||||
self.assertIn('.editor-card', style_css)
|
||||
self.assertIn('.editor-host', style_css)
|
||||
self.assertNotIn('.select-marker', style_css)
|
||||
|
||||
app_js_url = app.url_path_for("ui", path="/app.js")
|
||||
|
||||
+138
-11
@@ -32,6 +32,13 @@ let editorState = {
|
||||
originalContent: "",
|
||||
modified: null,
|
||||
};
|
||||
let monacoState = {
|
||||
module: null,
|
||||
loadPromise: null,
|
||||
editor: null,
|
||||
model: null,
|
||||
resizeHandler: null,
|
||||
};
|
||||
let moveState = {
|
||||
source: null,
|
||||
destination: "",
|
||||
@@ -81,6 +88,9 @@ function applyTheme(theme) {
|
||||
button.setAttribute("aria-label", `Switch to ${nextTheme === "dark" ? "light" : "dark"} mode`);
|
||||
button.setAttribute("title", `Switch to ${nextTheme === "dark" ? "light" : "dark"} mode`);
|
||||
}
|
||||
if (monacoState.module) {
|
||||
monacoState.module.editor.setTheme(nextTheme === "light" ? "vs" : "vs-dark");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
@@ -143,7 +153,7 @@ function editorElements() {
|
||||
fileName: document.getElementById("editor-file-name"),
|
||||
filePath: document.getElementById("editor-file-path"),
|
||||
error: document.getElementById("editor-error"),
|
||||
content: document.getElementById("editor-content"),
|
||||
host: document.getElementById("editor-host"),
|
||||
closeButton: document.getElementById("editor-close-btn"),
|
||||
saveButton: document.getElementById("editor-save-btn"),
|
||||
cancelButton: document.getElementById("editor-cancel-btn"),
|
||||
@@ -552,7 +562,48 @@ function isEditableSelection(item) {
|
||||
if (lower === "dockerfile" || lower === "containerfile") {
|
||||
return true;
|
||||
}
|
||||
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
|
||||
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".py", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
|
||||
}
|
||||
|
||||
function monacoLanguageForName(name) {
|
||||
const lower = (name || "").toLowerCase();
|
||||
if (lower === "dockerfile" || lower === "containerfile") {
|
||||
return "dockerfile";
|
||||
}
|
||||
if ([".js", ".mjs", ".cjs"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "javascript";
|
||||
}
|
||||
if ([".ts", ".tsx"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "typescript";
|
||||
}
|
||||
if (lower.endsWith(".json")) {
|
||||
return "json";
|
||||
}
|
||||
if (lower.endsWith(".css")) {
|
||||
return "css";
|
||||
}
|
||||
if ([".html", ".htm"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "html";
|
||||
}
|
||||
if (lower.endsWith(".xml")) {
|
||||
return "xml";
|
||||
}
|
||||
if ([".yml", ".yaml"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "yaml";
|
||||
}
|
||||
if (lower.endsWith(".py")) {
|
||||
return "python";
|
||||
}
|
||||
if ([".sh", ".bash", ".zsh", ".fish"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "shell";
|
||||
}
|
||||
if ([".md", ".markdown"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "markdown";
|
||||
}
|
||||
if ([".txt", ".log", ".ini", ".cfg", ".conf"].some((suffix) => lower.endsWith(suffix))) {
|
||||
return "plaintext";
|
||||
}
|
||||
return "plaintext";
|
||||
}
|
||||
|
||||
function isVideoSelection(item) {
|
||||
@@ -1264,6 +1315,82 @@ function isEditorOpen() {
|
||||
return !editorElements().overlay.classList.contains("hidden");
|
||||
}
|
||||
|
||||
async function loadMonacoModule() {
|
||||
if (monacoState.module) {
|
||||
return monacoState.module;
|
||||
}
|
||||
if (!monacoState.loadPromise) {
|
||||
monacoState.loadPromise = import("https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm")
|
||||
.then((module) => {
|
||||
monacoState.module = module;
|
||||
module.editor.setTheme(document.documentElement.dataset.theme === "light" ? "vs" : "vs-dark");
|
||||
return module;
|
||||
});
|
||||
}
|
||||
return monacoState.loadPromise;
|
||||
}
|
||||
|
||||
function disposeMonacoEditor() {
|
||||
if (typeof monacoState.resizeHandler === "function") {
|
||||
window.removeEventListener("resize", monacoState.resizeHandler);
|
||||
monacoState.resizeHandler = null;
|
||||
}
|
||||
if (monacoState.editor) {
|
||||
monacoState.editor.dispose();
|
||||
monacoState.editor = null;
|
||||
}
|
||||
if (monacoState.model) {
|
||||
monacoState.model.dispose();
|
||||
monacoState.model = null;
|
||||
}
|
||||
}
|
||||
|
||||
function nextAnimationFrame() {
|
||||
return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
|
||||
}
|
||||
|
||||
async function ensureMonacoEditor(path, content) {
|
||||
const editor = editorElements();
|
||||
const monaco = await loadMonacoModule();
|
||||
await nextAnimationFrame();
|
||||
await nextAnimationFrame();
|
||||
disposeMonacoEditor();
|
||||
editor.host.textContent = "";
|
||||
const model = monaco.editor.createModel(content, monacoLanguageForName(baseName(path)));
|
||||
const instance = monaco.editor.create(editor.host, {
|
||||
model,
|
||||
theme: document.documentElement.dataset.theme === "light" ? "vs" : "vs-dark",
|
||||
language: monacoLanguageForName(baseName(path)),
|
||||
automaticLayout: false,
|
||||
lineNumbers: "on",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
renderLineHighlight: "line",
|
||||
wordWrap: "on",
|
||||
fontSize: 13,
|
||||
roundedSelection: false,
|
||||
readOnly: false,
|
||||
});
|
||||
const resizeHandler = () => {
|
||||
if (monacoState.editor) {
|
||||
monacoState.editor.layout();
|
||||
}
|
||||
};
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
monacoState.model = model;
|
||||
monacoState.editor = instance;
|
||||
monacoState.resizeHandler = resizeHandler;
|
||||
instance.layout();
|
||||
instance.focus();
|
||||
}
|
||||
|
||||
function currentEditorContent() {
|
||||
if (monacoState.editor) {
|
||||
return monacoState.editor.getValue();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function escapeRegExp(text) {
|
||||
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
||||
}
|
||||
@@ -1854,7 +1981,7 @@ async function openSettings(tab = "general") {
|
||||
}
|
||||
|
||||
function editorIsDirty() {
|
||||
return editorElements().content.value !== editorState.originalContent;
|
||||
return currentEditorContent() !== editorState.originalContent;
|
||||
}
|
||||
|
||||
function resetEditorState() {
|
||||
@@ -1874,9 +2001,10 @@ function attemptCloseEditor() {
|
||||
|
||||
function closeEditor() {
|
||||
const editor = editorElements();
|
||||
disposeMonacoEditor();
|
||||
editor.overlay.classList.add("hidden");
|
||||
editor.error.textContent = "";
|
||||
editor.content.value = "";
|
||||
editor.host.textContent = "";
|
||||
resetEditorState();
|
||||
}
|
||||
|
||||
@@ -1962,21 +2090,20 @@ async function openEditor() {
|
||||
editor.fileName.textContent = selected.name;
|
||||
editor.filePath.textContent = selected.path;
|
||||
editor.error.textContent = "";
|
||||
editor.content.value = "";
|
||||
editor.content.disabled = true;
|
||||
editor.host.textContent = "Loading editor...";
|
||||
editor.saveButton.disabled = true;
|
||||
try {
|
||||
const data = await apiRequest("GET", `/api/files/view?${new URLSearchParams({ path: selected.path, for_edit: "true" }).toString()}`);
|
||||
editor.fileName.textContent = data.name;
|
||||
editor.filePath.textContent = data.path;
|
||||
editor.content.value = data.content;
|
||||
editor.content.disabled = false;
|
||||
await ensureMonacoEditor(data.path, data.content);
|
||||
editor.saveButton.disabled = false;
|
||||
editorState.path = data.path;
|
||||
editorState.originalContent = data.content;
|
||||
editorState.modified = data.modified;
|
||||
editor.content.focus();
|
||||
} catch (err) {
|
||||
disposeMonacoEditor();
|
||||
editor.host.textContent = "";
|
||||
editor.error.textContent = err.message;
|
||||
}
|
||||
}
|
||||
@@ -2007,10 +2134,10 @@ async function saveEditor() {
|
||||
try {
|
||||
const response = await apiRequest("POST", "/api/files/save", {
|
||||
path: editorState.path,
|
||||
content: editor.content.value,
|
||||
content: currentEditorContent(),
|
||||
expected_modified: editorState.modified,
|
||||
});
|
||||
editorState.originalContent = editor.content.value;
|
||||
editorState.originalContent = currentEditorContent();
|
||||
editorState.modified = response.modified;
|
||||
setStatus(`Saved ${response.path}`);
|
||||
closeEditor();
|
||||
|
||||
@@ -226,13 +226,13 @@
|
||||
</div>
|
||||
|
||||
<div id="editor-modal" class="popup-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="editor-title">
|
||||
<div class="popup-card viewer-card">
|
||||
<div class="popup-card viewer-card editor-card">
|
||||
<button id="editor-close-btn" class="viewer-close" type="button" aria-label="Close editor">X</button>
|
||||
<h3 id="editor-title">Edit</h3>
|
||||
<div id="editor-file-name" class="popup-meta"></div>
|
||||
<div id="editor-file-path" class="popup-meta"></div>
|
||||
<div id="editor-error" class="error"></div>
|
||||
<textarea id="editor-content" class="editor-content" spellcheck="false"></textarea>
|
||||
<div id="editor-host" class="editor-host" aria-label="Code editor"></div>
|
||||
<div class="popup-actions">
|
||||
<button id="editor-save-btn" type="button">Save</button>
|
||||
<button id="editor-cancel-btn" type="button">Cancel</button>
|
||||
|
||||
+28
-6
@@ -587,6 +587,19 @@ button:disabled {
|
||||
margin: 4px 0 8px 0;
|
||||
}
|
||||
|
||||
#editor-file-name {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#editor-file-path {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.popup-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
@@ -625,6 +638,11 @@ button:disabled {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-card {
|
||||
width: min(1180px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 24px);
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 180px);
|
||||
@@ -802,7 +820,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.viewer-content,
|
||||
.editor-content {
|
||||
.editor-host {
|
||||
margin: 6px 0 0 0;
|
||||
padding: 11px;
|
||||
overflow: auto;
|
||||
@@ -821,13 +839,17 @@ button:disabled {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
.editor-host {
|
||||
margin-bottom: 8px;
|
||||
min-height: 280px;
|
||||
max-height: calc(100vh - 220px);
|
||||
min-height: 520px;
|
||||
height: calc(100vh - 220px);
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-host .monaco-editor {
|
||||
border-radius: calc(var(--radius-sm) - 1px);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
Reference in New Issue
Block a user