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));
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
|
@ -583,8 +600,7 @@ button.danger-outline:hover {
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
margin-left: auto;
|
||||
padding-left: var(--space-m);
|
||||
padding-left: var(--space-s);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 80ms ease;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,23 @@
|
|||
* 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 {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,23 @@
|
|||
// 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") {
|
||||
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;
|
||||
|
|
@ -37,7 +52,8 @@
|
|||
}
|
||||
|
||||
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
|
||||
const form = textarea.closest("form");
|
||||
|
|
@ -72,7 +88,10 @@
|
|||
|
||||
function init() {
|
||||
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:
|
||||
//
|
||||
// * 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,
|
||||
// queue with concurrency 3); dragging a row inside the tree moves
|
||||
// (rename/move via /files/move).
|
||||
|
|
@ -977,7 +978,9 @@
|
|||
// ---------- click delegation: action buttons ----------------------------
|
||||
|
||||
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 (!manager.contains(action)) return;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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='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>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@
|
|||
<span>{{ entry.name }}</span>
|
||||
<span class="file-tree-badge file-tree-badge-warn">broken link</span>
|
||||
{% 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>
|
||||
{% else %}
|
||||
<span>{{ entry.name }}</span>
|
||||
|
|
@ -34,7 +36,6 @@
|
|||
{% 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>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<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">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
{% block title %}Blueprint {{ blueprint.name }} | left4me{% endblock %}
|
||||
|
||||
{% block extra_head %}{% include "_editor_assets.html" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<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
|
||||
{% endfor %}</pre>
|
||||
{% 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>
|
||||
</label>
|
||||
<button type="submit">Save blueprint</button>
|
||||
|
|
@ -92,5 +94,4 @@
|
|||
</div>
|
||||
</dialog>
|
||||
<script src="{{ url_for('static', filename='js/blueprint-overlay-picker.js') }}" defer></script>
|
||||
{% include "_editor_assets.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
{% 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' %}
|
||||
|
|
@ -22,7 +24,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
|
||||
<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>
|
||||
<p class="muted">Runs sandboxed against the overlay directory mounted at <code>/overlay</code>.</p>
|
||||
{% if not latest_build_is_running %}
|
||||
|
|
@ -184,7 +186,7 @@
|
|||
</label>
|
||||
<label class="files-editor-field">
|
||||
<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>
|
||||
<div class="files-editor-meta muted">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% include "_editor_assets.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function pickThemeForMatchMedia(mm) {
|
|||
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 themeCompartment = new Compartment();
|
||||
const autocompleteCompartment = new Compartment();
|
||||
|
|
@ -29,7 +29,21 @@ function mount(textarea, { language = "plain", vocab = null } = {}) {
|
|||
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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue