fix(editor-v2): fix cm6 to rows-derived height, eliminate layout shift
CLS verified zero (0.00000) on /blueprints/1 and /overlays/1 via
PerformanceObserver({type: 'layout-shift', buffered: true}) on a
real browser session — previously CLS=0.00859 from a 253 px shift
when cm6 mounted into a display:none slot.
Mechanism:
- editor-entry.js: mount() accepts `rows`. When provided, prepends
an EditorView.theme that pins
.cm-editor { height: calc(rows * 1.84rem + 1.125rem) }
and sets .cm-scroller overflow:auto. cm6 renders at a fixed,
predictable height; long content scrolls internally (same UX the
raw <textarea rows="N"> used to give).
- editor.js: reads textarea.rows attribute and passes it to mount().
- editor.css: new .editor-mount wrapper uses the same calc on
min-height keyed off an inline --editor-rows CSS custom property,
so the slot is pre-reserved BEFORE cm6 mounts. Wrapper and cm6
match exactly (browser-measured 254 / 254 px for rows=8 and
607 / 607 px for rows=20).
- Templates: each editor textarea wrapped in
<div class="editor-mount" style="--editor-rows: N">. Single source
of truth on N (only the rows attribute + the inline custom prop
vary per call site).
Per-row metric 1.84 rem derived empirically: 253 px for rows=8 minus
1.125 rem chrome = 235 px content, ÷ 8 ≈ 29.4 px = 1.84 rem.
Fast suite + e2e suite still green (3 + 2 pass, 0 fail).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2f1a1ef284
commit
54842f71c6
7 changed files with 30 additions and 8 deletions
|
|
@ -9,10 +9,17 @@
|
||||||
* in _editor_assets.html un-hides for JS-disabled users. */
|
* in _editor_assets.html un-hides for JS-disabled users. */
|
||||||
textarea[data-editor-language] { display: none; }
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -52,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");
|
||||||
|
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -51,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>
|
||||||
|
|
|
||||||
|
|
@ -24,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 %}
|
||||||
|
|
@ -186,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>
|
||||||
|
|
|
||||||
|
|
@ -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