Compare commits
5 commits
704e4cdfd1
...
2942467cfd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2942467cfd | ||
|
|
54842f71c6 | ||
|
|
2f1a1ef284 | ||
|
|
b915f2e766 | ||
|
|
fd0d96b349 |
12 changed files with 93 additions and 19 deletions
|
|
@ -254,6 +254,23 @@ dialog.modal::backdrop {
|
||||||
padding-left: calc(1ch + var(--space-xs));
|
padding-left: calc(1ch + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-tree-name-button {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--color-link);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree-name-button:hover,
|
||||||
|
.file-tree-name-button:focus-visible {
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.file-tree-toggle[aria-expanded="true"] .chevron {
|
.file-tree-toggle[aria-expanded="true"] .chevron {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
@ -583,8 +600,7 @@ button.danger-outline:hover {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
margin-left: auto;
|
padding-left: var(--space-s);
|
||||||
padding-left: var(--space-m);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 80ms ease;
|
transition: opacity 80ms ease;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,23 @@
|
||||||
* in tokens.css; this file scopes the editor container's chrome to
|
* in tokens.css; this file scopes the editor container's chrome to
|
||||||
* match the rest of the app. */
|
* match the rest of the app. */
|
||||||
|
|
||||||
|
/* Pre-hide opt-in textareas to avoid the textarea-visible-then-hidden
|
||||||
|
* flicker on page load. editor.js un-hides them again if the bundle
|
||||||
|
* fails to load or a per-textarea mount throws. The <noscript> rule
|
||||||
|
* in _editor_assets.html un-hides for JS-disabled users. */
|
||||||
|
textarea[data-editor-language] { display: none; }
|
||||||
|
|
||||||
|
/* Pre-reserves the slot cm6 will mount into. The min-height formula
|
||||||
|
* matches the height EditorView.theme applies inside editor-entry.js,
|
||||||
|
* keyed off the textarea's `rows` attribute via the inline --editor-rows
|
||||||
|
* custom property. Same expression on both sides = zero layout shift. */
|
||||||
|
.editor-mount {
|
||||||
|
min-height: calc(var(--editor-rows, 8) * 1.84rem + 1.125rem);
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
border: var(--line);
|
border: var(--line);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
min-height: 8em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor.cm-focused {
|
.cm-editor.cm-focused {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,23 @@
|
||||||
// a named alias for the files-editor modal.
|
// a named alias for the files-editor modal.
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// editor.css pre-hides every textarea[data-editor-language] so the
|
||||||
|
// page never paints the raw textarea before cm6 takes over. Both
|
||||||
|
// failure paths below restore the textarea by clearing the CSS rule
|
||||||
|
// with an inline display:revert.
|
||||||
|
function unhideTextarea(ta) {
|
||||||
|
ta.style.display = "revert";
|
||||||
|
}
|
||||||
|
|
||||||
if (!window.__editor || typeof window.__editor.mount !== "function") {
|
if (!window.__editor || typeof window.__editor.mount !== "function") {
|
||||||
return; // bundle didn't load — graceful no-JS fallback
|
// Bundle didn't load — un-hide every editor textarea so the form
|
||||||
|
// is still usable. Mirrors the <noscript> override for the
|
||||||
|
// JS-disabled case.
|
||||||
|
for (const ta of document.querySelectorAll("textarea[data-editor-language]")) {
|
||||||
|
unhideTextarea(ta);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let vocabPromise = null;
|
let vocabPromise = null;
|
||||||
|
|
@ -37,7 +52,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const vocab = (lang === "srccfg") ? await loadSrccfgVocab() : null;
|
const vocab = (lang === "srccfg") ? await loadSrccfgVocab() : null;
|
||||||
const controller = window.__editor.mount(textarea, { language: lang, vocab });
|
const rows = parseInt(textarea.getAttribute("rows") || "0", 10) || 0;
|
||||||
|
const controller = window.__editor.mount(textarea, { language: lang, vocab, rows });
|
||||||
|
|
||||||
// Submit-time copy bridge
|
// Submit-time copy bridge
|
||||||
const form = textarea.closest("form");
|
const form = textarea.closest("form");
|
||||||
|
|
@ -72,7 +88,10 @@
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
for (const ta of document.querySelectorAll("textarea[data-editor-language]")) {
|
for (const ta of document.querySelectorAll("textarea[data-editor-language]")) {
|
||||||
mountOne(ta).catch(err => console.error("[editor] mount failed", err));
|
mountOne(ta).catch(err => {
|
||||||
|
console.error("[editor] mount failed", err);
|
||||||
|
unhideTextarea(ta); // restore the form-usable raw textarea
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
// overlay is type='files' and the user can edit). The script binds:
|
// overlay is type='files' and the user can edit). The script binds:
|
||||||
//
|
//
|
||||||
// * Per-row hover actions: + new file, + new folder, ⬇ zip, ✕ on
|
// * Per-row hover actions: + new file, + new folder, ⬇ zip, ✕ on
|
||||||
// folders; edit, ✕ on files (download is a regular <a>).
|
// folders; ⬇ (download), ✕ on files. Clicking the filename opens
|
||||||
|
// the editor (binary fallback for non-editable files).
|
||||||
// * Drag-and-drop: dragging from the OS uploads (one POST per file,
|
// * Drag-and-drop: dragging from the OS uploads (one POST per file,
|
||||||
// queue with concurrency 3); dragging a row inside the tree moves
|
// queue with concurrency 3); dragging a row inside the tree moves
|
||||||
// (rename/move via /files/move).
|
// (rename/move via /files/move).
|
||||||
|
|
@ -977,7 +978,9 @@
|
||||||
// ---------- click delegation: action buttons ----------------------------
|
// ---------- click delegation: action buttons ----------------------------
|
||||||
|
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
const action = event.target?.closest?.(".files-row-action[data-action]");
|
const action = event.target?.closest?.(
|
||||||
|
".files-row-action[data-action], .file-tree-name-button[data-action]"
|
||||||
|
);
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
if (!manager.contains(action)) return;
|
if (!manager.contains(action)) return;
|
||||||
const op = action.dataset.action;
|
const op = action.dataset.action;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
||||||
6700b694fe25837f52e77c780d88f3eb5aef2a1591dc461c26efa3fa9724290b editor.bundle.js
|
910031cfc346106af240df71b9ef8069f1b38f1a4c63128392c2aa074e7e57b2 editor.bundle.js
|
||||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 editor.bundle.css
|
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 editor.bundle.css
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
{# Editor assets — include on any page that mounts a <textarea data-editor-language>. #}
|
{# Editor assets — included from {% block extra_head %} on any page that
|
||||||
|
mounts a <textarea data-editor-language>. Loading from <head> means the
|
||||||
|
CSS pre-hide rule in editor.css applies before first paint (no textarea
|
||||||
|
flicker), and the defer'd scripts download in parallel with HTML parse
|
||||||
|
and execute before DOMContentLoaded. #}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/editor.bundle.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/editor.bundle.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/editor.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/editor.css') }}">
|
||||||
|
<noscript><style>textarea[data-editor-language] { display: revert; }</style></noscript>
|
||||||
<script src="{{ url_for('static', filename='vendor/editor.bundle.js') }}" nonce="{{ g.csp_nonce }}" defer></script>
|
<script src="{{ url_for('static', filename='vendor/editor.bundle.js') }}" nonce="{{ g.csp_nonce }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/editor.js') }}" nonce="{{ g.csp_nonce }}" defer></script>
|
<script src="{{ url_for('static', filename='js/editor.js') }}" nonce="{{ g.csp_nonce }}" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@
|
||||||
<span>{{ entry.name }}</span>
|
<span>{{ entry.name }}</span>
|
||||||
<span class="file-tree-badge file-tree-badge-warn">broken link</span>
|
<span class="file-tree-badge file-tree-badge-warn">broken link</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if download_supported %}
|
{% if files_overlay %}
|
||||||
|
<button type="button" class="file-tree-name-button" data-action="edit" data-target-path="{{ entry.rel }}" data-editable="{{ '1' if entry.editable else '0' }}" title="Open in editor">{{ entry.name }}</button>
|
||||||
|
{% elif download_supported %}
|
||||||
<a href="{{ files_base_url }}/files/download?path={{ entry.rel|urlencode }}">{{ entry.name }}</a>
|
<a href="{{ files_base_url }}/files/download?path={{ entry.rel|urlencode }}">{{ entry.name }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span>{{ entry.name }}</span>
|
<span>{{ entry.name }}</span>
|
||||||
|
|
@ -34,7 +36,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if files_overlay and not entry.broken %}
|
{% if files_overlay and not entry.broken %}
|
||||||
<span class="files-row-actions" aria-label="File actions">
|
<span class="files-row-actions" aria-label="File actions">
|
||||||
<button type="button" class="files-row-action" data-action="edit" data-target-path="{{ entry.rel }}" data-editable="{{ '1' if entry.editable else '0' }}" title="Edit">edit</button>
|
|
||||||
<a class="files-row-action" href="{{ files_base_url }}/files/download?path={{ entry.rel|urlencode }}" title="Download">⬇</a>
|
<a class="files-row-action" href="{{ files_base_url }}/files/download?path={{ entry.rel|urlencode }}" title="Download">⬇</a>
|
||||||
<button type="button" class="files-row-action files-row-danger" data-action="delete" data-target-path="{{ entry.rel }}" data-row-kind="file" data-row-name="{{ entry.name }}" title="Delete">✕</button>
|
<button type="button" class="files-row-action files-row-danger" data-action="delete" data-target-path="{{ entry.rel }}" data-row-kind="file" data-row-name="{{ entry.name }}" title="Delete">✕</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/layout.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/layout.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/logs.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/logs.css') }}">
|
||||||
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
{% block title %}Blueprint {{ blueprint.name }} | left4me{% endblock %}
|
{% block title %}Blueprint {{ blueprint.name }} | left4me{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}{% include "_editor_assets.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="page-heading">
|
<div class="page-heading">
|
||||||
|
|
@ -49,7 +51,7 @@
|
||||||
<pre class="config-preview" aria-label="Auto-loaded overlay configs">{% for o in exposed %}exec {{ o.name }}.cfg
|
<pre class="config-preview" aria-label="Auto-loaded overlay configs">{% for o in exposed %}exec {{ o.name }}.cfg
|
||||||
{% endfor %}</pre>
|
{% endfor %}</pre>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<textarea name="config" rows="8" spellcheck="false" data-editor-language="srccfg">{{ config_lines | join('\n') }}</textarea>
|
<div class="editor-mount" style="--editor-rows: 8"><textarea name="config" rows="8" spellcheck="false" data-editor-language="srccfg">{{ config_lines | join('\n') }}</textarea></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Save blueprint</button>
|
<button type="submit">Save blueprint</button>
|
||||||
|
|
@ -92,5 +94,4 @@
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
<script src="{{ url_for('static', filename='js/blueprint-overlay-picker.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/blueprint-overlay-picker.js') }}" defer></script>
|
||||||
{% include "_editor_assets.html" %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
{% block title %}Overlay {{ overlay.name }} | left4me{% endblock %}
|
{% block title %}Overlay {{ overlay.name }} | left4me{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}{% include "_editor_assets.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set can_edit = g.user.admin or (overlay.type in ['workshop', 'script', 'files'] and overlay.user_id == g.user.id) %}
|
{% set can_edit = g.user.admin or (overlay.type in ['workshop', 'script', 'files'] and overlay.user_id == g.user.id) %}
|
||||||
{% set is_files_overlay = overlay.type == 'files' %}
|
{% set is_files_overlay = overlay.type == 'files' %}
|
||||||
|
|
@ -22,7 +24,7 @@
|
||||||
<form method="post" action="/overlays/{{ overlay.id }}/script" class="stack">
|
<form method="post" action="/overlays/{{ overlay.id }}/script" class="stack">
|
||||||
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||||||
<label>Bash script
|
<label>Bash script
|
||||||
<textarea name="script" rows="20" spellcheck="false" data-editor-language="bash">{{ overlay.script or "" }}</textarea>
|
<div class="editor-mount" style="--editor-rows: 20"><textarea name="script" rows="20" spellcheck="false" data-editor-language="bash">{{ overlay.script or "" }}</textarea></div>
|
||||||
</label>
|
</label>
|
||||||
<p class="muted">Runs sandboxed against the overlay directory mounted at <code>/overlay</code>.</p>
|
<p class="muted">Runs sandboxed against the overlay directory mounted at <code>/overlay</code>.</p>
|
||||||
{% if not latest_build_is_running %}
|
{% if not latest_build_is_running %}
|
||||||
|
|
@ -184,7 +186,7 @@
|
||||||
</label>
|
</label>
|
||||||
<label class="files-editor-field">
|
<label class="files-editor-field">
|
||||||
<span class="files-field-label">Content</span>
|
<span class="files-field-label">Content</span>
|
||||||
<textarea class="files-editor-content" rows="14" spellcheck="false" data-editor-language="auto"></textarea>
|
<div class="editor-mount" style="--editor-rows: 14"><textarea class="files-editor-content" rows="14" spellcheck="false" data-editor-language="auto"></textarea></div>
|
||||||
</label>
|
</label>
|
||||||
<div class="files-editor-meta muted">
|
<div class="files-editor-meta muted">
|
||||||
<span class="files-editor-byte-count">UTF-8 · 0 bytes</span>
|
<span class="files-editor-byte-count">UTF-8 · 0 bytes</span>
|
||||||
|
|
@ -282,5 +284,4 @@
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/files-overlay.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/files-overlay.js') }}" defer></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "_editor_assets.html" %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function pickThemeForMatchMedia(mm) {
|
||||||
return mm.matches ? editorDarkTheme : editorLightTheme;
|
return mm.matches ? editorDarkTheme : editorLightTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mount(textarea, { language = "plain", vocab = null } = {}) {
|
function mount(textarea, { language = "plain", vocab = null, rows = 0 } = {}) {
|
||||||
const langCompartment = new Compartment();
|
const langCompartment = new Compartment();
|
||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
const autocompleteCompartment = new Compartment();
|
const autocompleteCompartment = new Compartment();
|
||||||
|
|
@ -29,7 +29,21 @@ function mount(textarea, { language = "plain", vocab = null } = {}) {
|
||||||
const lang = pickLanguage(language);
|
const lang = pickLanguage(language);
|
||||||
const mm = window.matchMedia("(prefers-color-scheme: dark)");
|
const mm = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
|
// Fix the editor's rendered height to match the textarea's `rows`
|
||||||
|
// attribute via a small EditorView.theme — keeps the wrapper's
|
||||||
|
// pre-reserved space (editor.css `.editor-mount`) and the actual
|
||||||
|
// cm6 height in lockstep, eliminating the mount-time layout shift.
|
||||||
|
// Empirical per-row metric on this build: 1.84rem content + 1.125rem
|
||||||
|
// chrome (8px top + 8px bottom padding + 1px×2 border).
|
||||||
|
const heightThemes = rows > 0 ? [
|
||||||
|
EditorView.theme({
|
||||||
|
"&": { height: `calc(${rows} * 1.84rem + 1.125rem)` },
|
||||||
|
".cm-scroller": { overflow: "auto" },
|
||||||
|
}),
|
||||||
|
] : [];
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
...heightThemes,
|
||||||
history(),
|
history(),
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue