clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

fix: restore inline title editing

Dami committed 711c0b6
- synchronize the header title with the Details JSON in both directions\n- preserve validation, draft persistence, and keyboard editing behavior\n- highlight the complete save split button when changes are pending
commit
711c0b669fd94d904540b0d2a1a8e4e5307ef666
parent
5f4adc6ba8fb340cf3fb0a0bf513a42241350e7e
author
Dami <dami.etoile@gmail.com>
6 files changed, +98 -8
Mapps/docs/docs/create/index.md2+
Mapps/docs/static/llms-full.txt2+
Mapps/editor/src/details-document.js10++++++
Mapps/editor/src/main.js47++++++++++++++++++++++++++++--
Mapps/editor/src/style.css36++++++++++++++++++++---
Mapps/editor/test/details-document.test.js9++++++
M apps/docs/docs/create/index.md +2 -0
M apps/docs/static/llms-full.txt +2 -0
M apps/editor/src/details-document.js +10 -0
M apps/editor/src/main.js +44 -3
@@ -3,6 +3,7 @@ import { exampleStateFor } from './examples.js'
33 import { refreshMatchingExampleMetadata } from './example-variants.js'44 import {55 DETAILS_MODEL_URI,6+ detailsStateWithTitle,67 parseDetailsDocument,78 serializeDetailsDocument,89 } from './details-document.js'
@@ -60,7 +61,7 @@ document.querySelector('#app').innerHTML = `
6061 <header class="app-header">6162 <div class="document-identity">6263 <a class="brand" href="/" aria-label="New diagram" title="New diagram"><img class="brand-mark" src="/icon.svg?v=4" alt=""></a>63- <span class="document-title" id="diagram-title">Untitled</span>64+ <input class="document-title" id="diagram-title" type="text" maxlength="200" placeholder="Untitled" aria-label="Diagram title" autocomplete="off" spellcheck="false">6465 </div>6566 <div class="header-meta">6667 <span class="render-status" data-state="idle" role="status">Ready</span>
@@ -289,7 +290,7 @@ document.querySelector('#app').innerHTML = `
289290 `290291 291292 const typePicker = document.querySelector('#diagram-type')292-const titleLabel = document.querySelector('#diagram-title')293+const titleInput = document.querySelector('#diagram-title')293294 const previewRawButton = document.querySelector('#preview-raw')294295 const previewTransparentButton = document.querySelector('#preview-transparent')295296 const previewThemeButton = document.querySelector('#preview-theme-toggle')
@@ -374,6 +375,10 @@ document.querySelector('#save').addEventListener('click', performPrimarySaveActi
374375 document.querySelector('#restore-saved').addEventListener('click', restoreSavedDiagram)375376 document.querySelector('#make-copy').addEventListener('click', makeDraftCopy)376377 document.querySelector('#share').addEventListener('click', openShareDialog)378+titleInput.addEventListener('focus', beginTitleEdit)379+titleInput.addEventListener('input', updateTitleFromHeader)380+titleInput.addEventListener('blur', finishTitleEdit)381+titleInput.addEventListener('keydown', handleTitleKeydown)377382 document.querySelectorAll('[data-primary-save]').forEach(button => {378383 button.addEventListener('click', () => {379384 primarySaveAction = button.dataset.primarySave
@@ -632,6 +637,42 @@ function scheduleDetailsUpdate() {
632637 }, 220)633638 }634639 640+let titleEditBaseline = ''641+642+function beginTitleEdit() {643+ if (!detailsValid) {644+ titleInput.blur()645+ ensureValidDetails()646+ return647+ }648+ titleEditBaseline = detailsState.meta.title649+}650+651+function updateTitleFromHeader() {652+ if (!detailsValid) return653+ clearTimeout(detailsCommitTimer)654+ setDetailsState(detailsStateWithTitle(detailsState, titleInput.value))655+ detailsCommitTimer = setTimeout(commitState, 220)656+}657+658+function finishTitleEdit() {659+ if (!detailsValid || titleInput.value === titleEditBaseline) return660+ clearTimeout(detailsCommitTimer)661+ commitState()662+}663+664+function handleTitleKeydown(event) {665+ if (event.key === 'Enter') {666+ event.preventDefault()667+ titleInput.blur()668+ } else if (event.key === 'Escape') {669+ event.preventDefault()670+ clearTimeout(detailsCommitTimer)671+ setDetailsState(detailsStateWithTitle(detailsState, titleEditBaseline))672+ titleInput.blur()673+ }674+}675+635676 function validateDetailsDocument(showError = false) {636677 try {637678 detailsState = parseDetailsDocument(detailsEditor.getValue())
@@ -1567,7 +1608,7 @@ function emptyRemoteState(overrides = {}) {
15671608 function updateDocumentMetadata() {15681609 const title = detailsState.meta.title.trim()15691610 const description = detailsState.meta.description.trim()1570- titleLabel.textContent = title || 'Untitled'1611+ if (titleInput.value !== detailsState.meta.title) titleInput.value = detailsState.meta.title15711612 document.title = documentTitle(title)15721613 document.querySelector('meta[name="description"]').content = description || 'Create diagrams from text and share them as a link.'15731614 }
M apps/editor/src/style.css +31 -5
@@ -155,7 +155,10 @@ button { cursor: pointer; }
155155 display: block;156156 width: min(38vw, 420px);157157 min-width: 90px;158- padding: 4px 7px;158+ padding: 3px 6px;159+ border: 1px solid transparent;160+ border-radius: 5px;161+ background: transparent;159162 color: var(--ink);160163 font-size: 22px;161164 font-weight: 440;
@@ -165,6 +168,16 @@ button { cursor: pointer; }
165168 white-space: nowrap;166169 }167170 171+.document-title:hover { border-color: var(--line); }172+173+.document-title:focus {174+ border-color: var(--muted);175+ background: var(--page);176+ outline: 0;177+}178+179+.document-title::placeholder { color: var(--muted); opacity: 1; }180+168181 .header-meta {169182 display: flex;170183 align-items: center;
@@ -268,10 +281,15 @@ button { cursor: pointer; }
268281 .save-split {269282 display: flex;270283 align-items: stretch;284+ border: 1px solid var(--line);285+ border-radius: 10px;286+ background: var(--surface);287+ overflow: hidden;271288 }272289 273290 .save-split #save {274- border-radius: 10px 0 0 10px;291+ border: 0;292+ border-radius: 0;275293 }276294 277295 .save-menu-toggle {
@@ -280,8 +298,9 @@ button { cursor: pointer; }
280298 height: 40px;281299 min-height: 40px;282300 padding: 0;283- border-left: 0;284- border-radius: 0 10px 10px 0;301+ border: 0;302+ border-left: 1px solid var(--line);303+ border-radius: 0;285304 font-size: 14px;286305 line-height: 1;287306 }
@@ -332,13 +351,20 @@ button { cursor: pointer; }
332351 .header-icon-action:hover { background: var(--soft); }333352 .header-icon-action:disabled { cursor: default; opacity: .58; }334353 335-#save[data-dirty="true"] {354+.save-split:has(#save[data-dirty="true"]) {336355 border-color: var(--unsaved);337356 box-shadow: inset 0 0 0 1px var(--unsaved);338357 }339358 340359 #save[data-dirty="true"]:disabled { opacity: .72; }341360 361+.save-split:has(:focus-visible) {362+ outline: 2px solid var(--accent-ink);363+ outline-offset: 2px;364+}365+366+.save-split :focus-visible { outline: 0; }367+342368 .header-action-icon {343369 width: 20px;344370 height: 20px;
M apps/editor/test/details-document.test.js +9 -0