diff --git a/apps/server/internal/httpserver/static/comments.js b/apps/server/internal/httpserver/static/comments.js index b548c5c..325e1fe 100644 --- a/apps/server/internal/httpserver/static/comments.js +++ b/apps/server/internal/httpserver/static/comments.js @@ -7,7 +7,11 @@ const article = document.querySelector('[data-document-path]'); const workspaceShell = article?.closest('.workspace-shell--document'); const commentsAsideList = document.querySelector('[data-comments-aside-list]'); + const commentsAsideListMobile = document.querySelector('[data-comments-aside-list-mobile]'); const stripTrack = document.querySelector('[data-comments-strip-track]'); + const commentsAside = document.querySelector('[data-comments-aside]'); + const commentsSheetToggle = document.querySelector('[data-toggle-comments]'); + const commentsSheetClose = document.querySelector('[data-close-comments]'); const documentId = documentPath ? 'doc:' + documentPath.replace(/\.md$/, '') : null; const commentToggle = document.querySelector('[data-comment-toggle]'); const commentVisibilityKey = documentId ? `cairnquire:document-comments:${documentId}` : null; @@ -165,60 +169,69 @@ updateStripMarkers(); } - function renderCommentsAside() { - if (!commentsAsideList) return; - commentsAsideList.innerHTML = ''; + function buildAsideItemFor(el) { + const comments = []; + el.querySelectorAll('.comment-item').forEach(item => { + const meta = item.querySelector('.comment-meta'); + const bodyEl = item.querySelector('.comment-body'); + comments.push({ + authorName: meta?.querySelector('strong')?.textContent || 'Anonymous', + createdAt: meta?.querySelector('time')?.textContent || '', + content: bodyEl?.textContent || '', + }); + }); + if (!comments.length) return null; + + const targetText = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120); + const truncated = targetText.length > 120 ? targetText + '…' : targetText; + + const asideItem = document.createElement('div'); + asideItem.className = 'comments-aside-item'; + asideItem.dataset.anchorHash = el.dataset.anchorHash; + + const targetEl = document.createElement('div'); + targetEl.className = 'comments-aside-item__target'; + targetEl.textContent = truncated || 'Commented section'; + targetEl.addEventListener('click', () => { + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + asideItem.appendChild(targetEl); + + comments.forEach(c => { + const commentEl = document.createElement('div'); + commentEl.className = 'comments-aside-item__comment'; + commentEl.innerHTML = ` +
${escapeHtml(c.authorName)}
+
${escapeHtml(c.content)}
+ `; + asideItem.appendChild(commentEl); + }); + + return asideItem; + } + + function renderCommentsInto(container) { + if (!container) return; + container.innerHTML = ''; const body = document.querySelector('.markdown-body'); if (!body) return; const commentables = body.querySelectorAll('.commentable[data-has-comments="true"]'); if (!commentables.length) { - commentsAsideList.innerHTML = '

No comments yet.

'; + container.innerHTML = '

No comments yet.

'; return; } commentables.forEach(el => { - const comments = []; - el.querySelectorAll('.comment-item').forEach(item => { - const meta = item.querySelector('.comment-meta'); - const bodyEl = item.querySelector('.comment-body'); - comments.push({ - authorName: meta?.querySelector('strong')?.textContent || 'Anonymous', - createdAt: meta?.querySelector('time')?.textContent || '', - content: bodyEl?.textContent || '', - }); - }); - if (!comments.length) return; - - const targetText = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120); - const truncated = targetText.length > 120 ? targetText + '…' : targetText; - - const asideItem = document.createElement('div'); - asideItem.className = 'comments-aside-item'; - asideItem.dataset.anchorHash = el.dataset.anchorHash; - - const targetEl = document.createElement('div'); - targetEl.className = 'comments-aside-item__target'; - targetEl.textContent = truncated || 'Commented section'; - targetEl.addEventListener('click', () => { - el.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }); - asideItem.appendChild(targetEl); - - comments.forEach(c => { - const commentEl = document.createElement('div'); - commentEl.className = 'comments-aside-item__comment'; - commentEl.innerHTML = ` -
${escapeHtml(c.authorName)}
-
${escapeHtml(c.content)}
- `; - asideItem.appendChild(commentEl); - }); - - commentsAsideList.appendChild(asideItem); + const item = buildAsideItemFor(el); + if (item) container.appendChild(item); }); + } + function renderCommentsAside() { + renderCommentsInto(commentsAsideList); + renderCommentsInto(commentsAsideListMobile); } function updateStripMarkers() { @@ -326,6 +339,45 @@ return d.toLocaleString(); } + function commentsSheetOpen() { + return commentsAside?.classList.contains('is-open'); + } + + function setCommentsSheetOpen(open) { + if (!commentsAside) return; + commentsAside.classList.toggle('is-open', open); + if (commentsSheetToggle) { + commentsSheetToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); + commentsSheetToggle.setAttribute('aria-label', open ? 'Hide comments' : 'Show comments'); + } + if (open) { + document.body.classList.add('drawer-open'); + renderCommentsAside(); + } else { + if (!pickerOpen() && !metaOpen()) { + document.body.classList.remove('drawer-open'); + } + } + } + + function pickerOpen() { + return document.querySelector('[data-picker-drawer]')?.classList.contains('is-open'); + } + + function metaOpen() { + return document.querySelector('[data-meta-drawer]')?.classList.contains('is-open'); + } + + if (commentsSheetToggle) { + commentsSheetToggle.addEventListener('click', () => { + setCommentsSheetOpen(!commentsSheetOpen()); + }); + } + + if (commentsSheetClose) { + commentsSheetClose.addEventListener('click', () => setCommentsSheetOpen(false)); + } + function init() { const body = document.querySelector('.markdown-body'); if (!body) return; diff --git a/apps/server/internal/httpserver/static/mobile.js b/apps/server/internal/httpserver/static/mobile.js new file mode 100644 index 0000000..157c2cd --- /dev/null +++ b/apps/server/internal/httpserver/static/mobile.js @@ -0,0 +1,77 @@ +(function () { + 'use strict'; + + const backdrop = document.querySelector('[data-mobile-drawer-backdrop]'); + const pickerDrawer = document.querySelector('[data-picker-drawer]'); + const metaDrawer = document.querySelector('[data-meta-drawer]'); + const commentsAside = document.querySelector('[data-comments-aside]'); + const pickerToggle = document.querySelector('[data-toggle-picker]'); + const metaToggle = document.querySelector('[data-toggle-meta]'); + const pickerClose = document.querySelector('[data-close-picker]'); + const metaClose = document.querySelector('[data-close-meta]'); + + function anyDrawerOpen() { + return Boolean( + pickerDrawer?.classList.contains('is-open') || + metaDrawer?.classList.contains('is-open') || + commentsAside?.classList.contains('is-open') + ); + } + + function updateBackdrop() { + document.body.classList.toggle('drawer-open', anyDrawerOpen()); + } + + function setPickerOpen(open) { + if (!pickerDrawer) return; + pickerDrawer.classList.toggle('is-open', open); + if (pickerToggle) pickerToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); + updateBackdrop(); + } + + function setMetaOpen(open) { + if (!metaDrawer) return; + metaDrawer.classList.toggle('is-open', open); + if (metaToggle) metaToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); + updateBackdrop(); + } + + function closeAllDrawers() { + setPickerOpen(false); + setMetaOpen(false); + if (commentsAside) { + commentsAside.classList.remove('is-open'); + const toggle = document.querySelector('[data-toggle-comments]'); + if (toggle) toggle.setAttribute('aria-expanded', 'false'); + } + updateBackdrop(); + } + + if (pickerToggle) { + pickerToggle.addEventListener('click', () => { + const willOpen = !pickerDrawer?.classList.contains('is-open'); + closeAllDrawers(); + setPickerOpen(willOpen); + }); + } + + if (metaToggle) { + metaToggle.addEventListener('click', () => { + const willOpen = !metaDrawer?.classList.contains('is-open'); + closeAllDrawers(); + setMetaOpen(willOpen); + }); + } + + if (pickerClose) pickerClose.addEventListener('click', () => setPickerOpen(false)); + if (metaClose) metaClose.addEventListener('click', () => setMetaOpen(false)); + + if (backdrop) { + backdrop.addEventListener('click', closeAllDrawers); + } + + // Close drawers on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeAllDrawers(); + }); +})(); diff --git a/apps/server/internal/httpserver/templates/base.gohtml b/apps/server/internal/httpserver/templates/base.gohtml index 8888fbe..6f6268e 100644 --- a/apps/server/internal/httpserver/templates/base.gohtml +++ b/apps/server/internal/httpserver/templates/base.gohtml @@ -111,6 +111,7 @@ + diff --git a/docker-compose.yml b/docker-compose.yml index 4997c27..4189d21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,11 @@ services: context: . dockerfile: Dockerfile ports: - - "$PORT:$PORT" + - "${PORT:-8080}:${PORT:-8080}" depends_on: - mailpit environment: + PORT: "${PORT:-8080}" CAIRNQUIRE_DATABASE_PATH: "/workspace/data/db.sqlite" CAIRNQUIRE_CONTENT_SOURCE_DIR: "/workspace/content" CAIRNQUIRE_CONTENT_STORE_DIR: "/workspace/data/files" @@ -20,7 +21,7 @@ services: - cairnquire_data:/workspace/data - ./content:/workspace/content healthcheck: - test: ["CMD", "curl", "--fail", "http://127.0.0.1:${PORT}/healthz"] + test: ["CMD", "curl", "--fail", "http://127.0.0.1:${PORT:-8080}/healthz"] interval: 10s timeout: 3s retries: 5