Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- client/app.css +14 -4
- client/src/doc.js +32 -6
- server/collab.js +20 -10
- server/playground.js +1 -8
client/app.css
CHANGED
|
@@ -461,13 +461,23 @@ textarea { resize: vertical; }
|
|
| 461 |
border: 1px solid var(--border); border-radius: 12px; font-size: 13px;
|
| 462 |
}
|
| 463 |
#slug-legend .legend-title { color: var(--text-3); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
|
| 464 |
-
#slug-legend
|
| 465 |
-
#slug-legend
|
| 466 |
-
#slug-legend
|
| 467 |
-
#slug-legend
|
|
|
|
|
|
|
| 468 |
#slug-legend code {
|
| 469 |
background: rgba(0, 0, 0, 0.05); padding: 1px 7px; border-radius: 5px; cursor: pointer;
|
| 470 |
font-size: 12px;
|
| 471 |
}
|
| 472 |
#slug-legend code:hover { background: rgba(29, 92, 69, 0.12); }
|
| 473 |
#slug-legend code.copied { background: var(--accent-soft); color: var(--accent); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
border: 1px solid var(--border); border-radius: 12px; font-size: 13px;
|
| 462 |
}
|
| 463 |
#slug-legend .legend-title { color: var(--text-3); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
|
| 464 |
+
#slug-legend table { border-collapse: collapse; width: 100%; }
|
| 465 |
+
#slug-legend th { text-align: left; font-size: 11.5px; color: var(--text-3); font-weight: 600; padding: 4px 12px 4px 0; border-bottom: 1px solid var(--line); }
|
| 466 |
+
#slug-legend td { padding: 5px 12px 5px 0; border-bottom: 1px solid var(--line); }
|
| 467 |
+
#slug-legend tr:last-child td { border-bottom: none; }
|
| 468 |
+
#slug-legend td a { color: var(--text); text-decoration: none; }
|
| 469 |
+
#slug-legend td a:hover { text-decoration: underline; }
|
| 470 |
#slug-legend code {
|
| 471 |
background: rgba(0, 0, 0, 0.05); padding: 1px 7px; border-radius: 5px; cursor: pointer;
|
| 472 |
font-size: 12px;
|
| 473 |
}
|
| 474 |
#slug-legend code:hover { background: rgba(29, 92, 69, 0.12); }
|
| 475 |
#slug-legend code.copied { background: var(--accent-soft); color: var(--accent); }
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
/* compact structure page: no phantom card height, hint above the field */
|
| 479 |
+
.structure-mode #editor { min-height: 0; padding: 22px 28px; }
|
| 480 |
+
#structure-hint {
|
| 481 |
+
margin-bottom: 12px; padding: 10px 14px; font-size: 12.5px; color: var(--text-2);
|
| 482 |
+
background: rgba(29, 92, 69, 0.05); border: 1px solid var(--line); border-radius: 10px;
|
| 483 |
+
}
|
client/src/doc.js
CHANGED
|
@@ -203,6 +203,17 @@ async function initPage() {
|
|
| 203 |
})
|
| 204 |
}
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
renderMargin(editor)
|
| 207 |
setTimeout(() => updateTitle(editor), 600)
|
| 208 |
}
|
|
@@ -440,27 +451,37 @@ function renderSlugLegend(s) {
|
|
| 440 |
return
|
| 441 |
}
|
| 442 |
const box = existing || el('div', { id: 'slug-legend' })
|
| 443 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
for (const [slug, title] of Object.entries(s.titles || {})) {
|
| 445 |
if (slug === '_structure') continue
|
| 446 |
-
const
|
|
|
|
| 447 |
const a = el('a', { href: slug === 'home' ? `/d/${docId}` : `/d/${docId}/${slug}` }, title || slug)
|
| 448 |
a.addEventListener('click', e => {
|
| 449 |
if (e.metaKey || e.ctrlKey || e.button !== 0) return
|
| 450 |
e.preventDefault()
|
| 451 |
openPage(slug)
|
| 452 |
})
|
| 453 |
-
|
| 454 |
-
|
| 455 |
const code = el('code', { title: 'Click to copy' }, slug)
|
| 456 |
code.addEventListener('click', () => {
|
| 457 |
navigator.clipboard?.writeText(slug)
|
| 458 |
code.classList.add('copied')
|
| 459 |
setTimeout(() => code.classList.remove('copied'), 800)
|
| 460 |
})
|
| 461 |
-
|
| 462 |
-
|
|
|
|
| 463 |
}
|
|
|
|
|
|
|
| 464 |
if (!existing) document.getElementById('editor').after(box)
|
| 465 |
}
|
| 466 |
|
|
@@ -572,6 +593,11 @@ async function loadStructure() {
|
|
| 572 |
}
|
| 573 |
|
| 574 |
function updateTitle(editor) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
let title = 'Untitled'
|
| 576 |
editor.state.doc.descendants(node => {
|
| 577 |
if (title === 'Untitled' && node.isTextblock && node.textContent.trim()) {
|
|
|
|
| 203 |
})
|
| 204 |
}
|
| 205 |
|
| 206 |
+
document.getElementById('layout').classList.toggle('structure-mode', pageSlug === '_structure')
|
| 207 |
+
document.getElementById('structure-hint')?.remove()
|
| 208 |
+
if (pageSlug === '_structure') {
|
| 209 |
+
const hint = el(
|
| 210 |
+
'div',
|
| 211 |
+
{ id: 'structure-hint' },
|
| 212 |
+
'This YAML is the page tree: one "- slug" line per page, indent two spaces to nest. Pages missing here show under "unfiled" — nothing is ever deleted by editing this. Changes can also be proposed as suggestions.'
|
| 213 |
+
)
|
| 214 |
+
document.getElementById('editor').before(hint)
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
renderMargin(editor)
|
| 218 |
setTimeout(() => updateTitle(editor), 600)
|
| 219 |
}
|
|
|
|
| 451 |
return
|
| 452 |
}
|
| 453 |
const box = existing || el('div', { id: 'slug-legend' })
|
| 454 |
+
const table = el('table', {})
|
| 455 |
+
const thead = el('thead', {})
|
| 456 |
+
const hr = el('tr', {})
|
| 457 |
+
hr.append(el('th', {}, 'Page'), el('th', {}, 'Slug (click to copy)'))
|
| 458 |
+
thead.appendChild(hr)
|
| 459 |
+
table.appendChild(thead)
|
| 460 |
+
const tbody = el('tbody', {})
|
| 461 |
for (const [slug, title] of Object.entries(s.titles || {})) {
|
| 462 |
if (slug === '_structure') continue
|
| 463 |
+
const tr = el('tr', {})
|
| 464 |
+
const tdTitle = el('td', {})
|
| 465 |
const a = el('a', { href: slug === 'home' ? `/d/${docId}` : `/d/${docId}/${slug}` }, title || slug)
|
| 466 |
a.addEventListener('click', e => {
|
| 467 |
if (e.metaKey || e.ctrlKey || e.button !== 0) return
|
| 468 |
e.preventDefault()
|
| 469 |
openPage(slug)
|
| 470 |
})
|
| 471 |
+
tdTitle.appendChild(a)
|
| 472 |
+
const tdSlug = el('td', {})
|
| 473 |
const code = el('code', { title: 'Click to copy' }, slug)
|
| 474 |
code.addEventListener('click', () => {
|
| 475 |
navigator.clipboard?.writeText(slug)
|
| 476 |
code.classList.add('copied')
|
| 477 |
setTimeout(() => code.classList.remove('copied'), 800)
|
| 478 |
})
|
| 479 |
+
tdSlug.appendChild(code)
|
| 480 |
+
tr.append(tdTitle, tdSlug)
|
| 481 |
+
tbody.appendChild(tr)
|
| 482 |
}
|
| 483 |
+
table.appendChild(tbody)
|
| 484 |
+
box.replaceChildren(el('div', { class: 'legend-title' }, 'Pages in this project'), table)
|
| 485 |
if (!existing) document.getElementById('editor').after(box)
|
| 486 |
}
|
| 487 |
|
|
|
|
| 593 |
}
|
| 594 |
|
| 595 |
function updateTitle(editor) {
|
| 596 |
+
if (pageSlug === '_structure') {
|
| 597 |
+
document.getElementById('doc-title').textContent = 'Structure'
|
| 598 |
+
document.title = 'Structure'
|
| 599 |
+
return
|
| 600 |
+
}
|
| 601 |
let title = 'Untitled'
|
| 602 |
editor.state.doc.descendants(node => {
|
| 603 |
if (title === 'Untitled' && node.isTextblock && node.textContent.trim()) {
|
server/collab.js
CHANGED
|
@@ -77,6 +77,24 @@ export const hocuspocus = new Hocuspocus({
|
|
| 77 |
try {
|
| 78 |
migrateLegacySuggestionAnchors(document)
|
| 79 |
} catch {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
}, 50)
|
| 81 |
}
|
| 82 |
},
|
|
@@ -791,21 +809,13 @@ export async function ensureStructurePage(projectId) {
|
|
| 791 |
await withDoc(name, document => {
|
| 792 |
const fragment = document.getXmlFragment(FIELD)
|
| 793 |
if (fragment.length > 0) return
|
| 794 |
-
|
| 795 |
-
h.setAttribute('level', 1)
|
| 796 |
-
const ht = new Y.XmlText()
|
| 797 |
-
ht.insert(0, 'Structure')
|
| 798 |
-
h.insert(0, [ht])
|
| 799 |
-
const intro = new Y.XmlElement('paragraph')
|
| 800 |
-
const it = new Y.XmlText()
|
| 801 |
-
it.insert(0, 'This page defines the sidebar tree: one "- slug" line per page, indent 2 spaces to nest. Pages missing here appear under "unfiled". Edit it directly or discuss changes as suggestions.')
|
| 802 |
-
intro.insert(0, [it])
|
| 803 |
const code = new Y.XmlElement('codeBlock')
|
| 804 |
code.setAttribute('language', 'yaml')
|
| 805 |
const ct = new Y.XmlText()
|
| 806 |
ct.insert(0, defaultStructureYaml(slugs))
|
| 807 |
code.insert(0, [ct])
|
| 808 |
-
fragment.insert(0, [
|
| 809 |
})
|
| 810 |
store.upsertPageMeta(projectId, '_structure', { title: 'Structure', createdAt: Date.now() })
|
| 811 |
ensuredStructure.add(projectId)
|
|
|
|
| 77 |
try {
|
| 78 |
migrateLegacySuggestionAnchors(document)
|
| 79 |
} catch {}
|
| 80 |
+
// structure pages are just the yaml field now — drop old heading/intro
|
| 81 |
+
try {
|
| 82 |
+
if (String(documentName).endsWith('::_structure') && !document.isDestroyed) {
|
| 83 |
+
const fragment = document.getXmlFragment(FIELD)
|
| 84 |
+
let hasCode = false
|
| 85 |
+
for (let i = 0; i < fragment.length; i++) {
|
| 86 |
+
if (fragment.get(i) instanceof Y.XmlElement && fragment.get(i).nodeName === 'codeBlock') hasCode = true
|
| 87 |
+
}
|
| 88 |
+
if (hasCode) {
|
| 89 |
+
document.transact(() => {
|
| 90 |
+
for (let i = fragment.length - 1; i >= 0; i--) {
|
| 91 |
+
const el2 = fragment.get(i)
|
| 92 |
+
if (!(el2 instanceof Y.XmlElement) || el2.nodeName !== 'codeBlock') fragment.delete(i, 1)
|
| 93 |
+
}
|
| 94 |
+
})
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
} catch {}
|
| 98 |
}, 50)
|
| 99 |
}
|
| 100 |
},
|
|
|
|
| 809 |
await withDoc(name, document => {
|
| 810 |
const fragment = document.getXmlFragment(FIELD)
|
| 811 |
if (fragment.length > 0) return
|
| 812 |
+
// just the yaml field — the explanation lives in the page chrome
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
const code = new Y.XmlElement('codeBlock')
|
| 814 |
code.setAttribute('language', 'yaml')
|
| 815 |
const ct = new Y.XmlText()
|
| 816 |
ct.insert(0, defaultStructureYaml(slugs))
|
| 817 |
code.insert(0, [ct])
|
| 818 |
+
fragment.insert(0, [code])
|
| 819 |
})
|
| 820 |
store.upsertPageMeta(projectId, '_structure', { title: 'Structure', createdAt: Date.now() })
|
| 821 |
ensuredStructure.add(projectId)
|
server/playground.js
CHANGED
|
@@ -134,14 +134,7 @@ export async function seedPlayground(username, existingId = null) {
|
|
| 134 |
await seedPage(id, home)
|
| 135 |
await seedPage(docNameFor(id, 'notes'), NOTES_MD)
|
| 136 |
store.upsertPageMeta(id, 'notes', { title: 'Notes', createdAt: Date.now() })
|
| 137 |
-
await seedPage(docNameFor(id, '_structure'), `
|
| 138 |
-
|
| 139 |
-
This page defines the sidebar tree: one "- slug" line per page, indent 2 spaces to nest. Pages missing here appear under "unfiled". Edit it directly or discuss changes as suggestions.
|
| 140 |
-
|
| 141 |
-
\`\`\`yaml
|
| 142 |
-
- home
|
| 143 |
-
- notes
|
| 144 |
-
\`\`\``)
|
| 145 |
store.upsertPageMeta(id, '_structure', { title: 'Structure', createdAt: Date.now() })
|
| 146 |
|
| 147 |
// block indices in HOME_MD (after image insertion): find dynamically
|
|
|
|
| 134 |
await seedPage(id, home)
|
| 135 |
await seedPage(docNameFor(id, 'notes'), NOTES_MD)
|
| 136 |
store.upsertPageMeta(id, 'notes', { title: 'Notes', createdAt: Date.now() })
|
| 137 |
+
await seedPage(docNameFor(id, '_structure'), '```yaml\n- home\n- notes\n```')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
store.upsertPageMeta(id, '_structure', { title: 'Structure', createdAt: Date.now() })
|
| 139 |
|
| 140 |
// block indices in HOME_MD (after image insertion): find dynamically
|