Compare commits

..

No commits in common. "2942467cfda344778e2e3a519f1fdee6b7f55acf" and "704e4cdfd187c70b277c01f82cc532de9b99ee1e" have entirely different histories.

12 changed files with 19 additions and 93 deletions

View file

@ -254,23 +254,6 @@ dialog.modal::backdrop {
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 {
transform: rotate(90deg);
}
@ -600,7 +583,8 @@ button.danger-outline:hover {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding-left: var(--space-s);
margin-left: auto;
padding-left: var(--space-m);
opacity: 0;
pointer-events: none;
transition: opacity 80ms ease;

View file

@ -3,23 +3,10 @@
* in tokens.css; this file scopes the editor container's chrome to
* 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 {
border: var(--line);
border-radius: var(--radius-s);
min-height: 8em;
}
.cm-editor.cm-focused {

View file

@ -4,23 +4,8 @@
// a named alias for the files-editor modal.
(function () {
"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") {
// 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;
return; // bundle didn't load — graceful no-JS fallback
}
let vocabPromise = null;
@ -52,8 +37,7 @@
}
const vocab = (lang === "srccfg") ? await loadSrccfgVocab() : null;
const rows = parseInt(textarea.getAttribute("rows") || "0", 10) || 0;
const controller = window.__editor.mount(textarea, { language: lang, vocab, rows });
const controller = window.__editor.mount(textarea, { language: lang, vocab });
// Submit-time copy bridge
const form = textarea.closest("form");
@ -88,10 +72,7 @@
function init() {
for (const ta of document.querySelectorAll("textarea[data-editor-language]")) {
mountOne(ta).catch(err => {
console.error("[editor] mount failed", err);
unhideTextarea(ta); // restore the form-usable raw textarea
});
mountOne(ta).catch(err => console.error("[editor] mount failed", err));
}
}

View file

@ -3,8 +3,7 @@
// overlay is type='files' and the user can edit). The script binds:
//
// * Per-row hover actions: + new file, + new folder, ⬇ zip, ✕ on
// folders; ⬇ (download), ✕ on files. Clicking the filename opens
// the editor (binary fallback for non-editable files).
// folders; edit, ✕ on files (download is a regular <a>).
// * Drag-and-drop: dragging from the OS uploads (one POST per file,
// queue with concurrency 3); dragging a row inside the tree moves
// (rename/move via /files/move).
@ -978,9 +977,7 @@
// ---------- click delegation: action buttons ----------------------------
document.addEventListener("click", (event) => {
const action = event.target?.closest?.(
".files-row-action[data-action], .file-tree-name-button[data-action]"
);
const action = event.target?.closest?.(".files-row-action[data-action]");
if (!action) return;
if (!manager.contains(action)) return;
const op = action.dataset.action;

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
910031cfc346106af240df71b9ef8069f1b38f1a4c63128392c2aa074e7e57b2 editor.bundle.js
6700b694fe25837f52e77c780d88f3eb5aef2a1591dc461c26efa3fa9724290b editor.bundle.js
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 editor.bundle.css

View file

@ -1,10 +1,5 @@
{# 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. #}
{# Editor assets — include on any page that mounts a <textarea data-editor-language>. #}
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/editor.bundle.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='js/editor.js') }}" nonce="{{ g.csp_nonce }}" defer></script>

View file

@ -24,9 +24,7 @@
<span>{{ entry.name }}</span>
<span class="file-tree-badge file-tree-badge-warn">broken link</span>
{% else %}
{% 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 %}
{% if download_supported %}
<a href="{{ files_base_url }}/files/download?path={{ entry.rel|urlencode }}">{{ entry.name }}</a>
{% else %}
<span>{{ entry.name }}</span>
@ -36,6 +34,7 @@
{% endif %}
{% if files_overlay and not entry.broken %}
<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>
<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>

View file

@ -9,7 +9,6 @@
<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/logs.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<header class="site-header">

View file

@ -2,8 +2,6 @@
{% block title %}Blueprint {{ blueprint.name }} | left4me{% endblock %}
{% block extra_head %}{% include "_editor_assets.html" %}{% endblock %}
{% block content %}
<section class="panel">
<div class="page-heading">
@ -51,7 +49,7 @@
<pre class="config-preview" aria-label="Auto-loaded overlay configs">{% for o in exposed %}exec {{ o.name }}.cfg
{% endfor %}</pre>
{% endif %}
<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>
<textarea name="config" rows="8" spellcheck="false" data-editor-language="srccfg">{{ config_lines | join('\n') }}</textarea>
</div>
</label>
<button type="submit">Save blueprint</button>
@ -94,4 +92,5 @@
</div>
</dialog>
<script src="{{ url_for('static', filename='js/blueprint-overlay-picker.js') }}" defer></script>
{% include "_editor_assets.html" %}
{% endblock %}

View file

@ -2,8 +2,6 @@
{% block title %}Overlay {{ overlay.name }} | left4me{% endblock %}
{% block extra_head %}{% include "_editor_assets.html" %}{% endblock %}
{% block content %}
{% 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' %}
@ -24,7 +22,7 @@
<form method="post" action="/overlays/{{ overlay.id }}/script" class="stack">
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
<label>Bash script
<div class="editor-mount" style="--editor-rows: 20"><textarea name="script" rows="20" spellcheck="false" data-editor-language="bash">{{ overlay.script or "" }}</textarea></div>
<textarea name="script" rows="20" spellcheck="false" data-editor-language="bash">{{ overlay.script or "" }}</textarea>
</label>
<p class="muted">Runs sandboxed against the overlay directory mounted at <code>/overlay</code>.</p>
{% if not latest_build_is_running %}
@ -186,7 +184,7 @@
</label>
<label class="files-editor-field">
<span class="files-field-label">Content</span>
<div class="editor-mount" style="--editor-rows: 14"><textarea class="files-editor-content" rows="14" spellcheck="false" data-editor-language="auto"></textarea></div>
<textarea class="files-editor-content" rows="14" spellcheck="false" data-editor-language="auto"></textarea>
</label>
<div class="files-editor-meta muted">
<span class="files-editor-byte-count">UTF-8 · 0 bytes</span>
@ -284,4 +282,5 @@
<script src="{{ url_for('static', filename='js/files-overlay.js') }}" defer></script>
{% endif %}
{% include "_editor_assets.html" %}
{% endblock %}

View file

@ -21,7 +21,7 @@ function pickThemeForMatchMedia(mm) {
return mm.matches ? editorDarkTheme : editorLightTheme;
}
function mount(textarea, { language = "plain", vocab = null, rows = 0 } = {}) {
function mount(textarea, { language = "plain", vocab = null } = {}) {
const langCompartment = new Compartment();
const themeCompartment = new Compartment();
const autocompleteCompartment = new Compartment();
@ -29,21 +29,7 @@ function mount(textarea, { language = "plain", vocab = null, rows = 0 } = {}) {
const lang = pickLanguage(language);
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 = [
...heightThemes,
history(),
lineNumbers(),
highlightActiveLine(),