From 65278f399fee362f00018a5eb5efc1265ac1ab52 Mon Sep 17 00:00:00 2001 From: mdroidian Date: Tue, 13 Oct 2026 10:31:39 -0600 Subject: [PATCH 1/6] Fix Presentation 2 chevrons and horizontal block layout --- CHANGELOG.md | 7 ++ e2e/roam-v2-layout.mjs | 186 +++++++++++++++++++++++++++++++ src/components/Presentation2.tsx | 17 ++- src/index.ts | 41 ++++++- 4 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 e2e/roam-v2-layout.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index aee4fa6..e7a6491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Fixed + +- Scale and align collapsible bullet chevrons with Presentation 2 text. +- Keep native horizontal block views visible and within Presentation 2 slides. + ## 1.2.0 - 2026-09-13 ### Fixed diff --git a/e2e/roam-v2-layout.mjs b/e2e/roam-v2-layout.mjs new file mode 100644 index 0000000..73a08cd --- /dev/null +++ b/e2e/roam-v2-layout.mjs @@ -0,0 +1,186 @@ +import assert from "node:assert/strict"; + +export default { + runtimeNames: ["presentation"], + async run({ page, outDir }) { + const fixture = await page.evaluate(async () => { + const api = window.roamAlphaAPI; + const uid = api.util.generateUID(); + await api.createPage({ + page: { uid, title: `Presentation layout regression ${Date.now()}` }, + }); + window.__presentationLayoutFixture = uid; + const create = async (parent, string, order, extra = {}) => { + const uid = api.util.generateUID(); + await api.createBlock({ + location: { "parent-uid": parent, order }, + block: { uid, string, ...extra }, + }); + return uid; + }; + const collapsible = await create( + uid, + "{{presentation2: {collapsible}}}", + 0, + ); + const slide = await create(collapsible, "Collapsible bullets", 0); + for (let i = 0; i < 2; i++) { + const parent = await create(slide, `Parent ${i + 1}`, i); + await create(parent, `Detail ${i + 1}`, 0); + } + const horizontal = await create(uid, "{{presentation2}}", 1); + const content = await create(horizontal, "Horizontal block view", 0); + const columns = await create(content, "Three columns", 0, { + "block-view-type": "horizontal-outline", + }); + for (let i = 0; i < 3; i++) { + const column = await create( + columns, + ["Discover", "Build", "Review"][i], + i, + ); + await create( + column, + [ + "Understand the problem", + "Make the smallest change", + "Verify the result", + ][i], + 0, + ); + } + const rootHorizontal = await create( + horizontal, + "Horizontal slide root", + 1, + { "block-view-type": "horizontal-outline" }, + ); + for (let i = 0; i < 3; i++) + await create(rootHorizontal, `Column ${i + 1}`, i); + await api.ui.mainWindow.openPage({ page: { uid } }); + return { uid }; + }); + const close = async () => { + await page.locator("#roamjs-presentation-container > button").click(); + await page + .locator("#roamjs-presentation-container") + .waitFor({ state: "detached" }); + }; + const checks = []; + const originalViewport = page.viewportSize(); + try { + for (const viewport of [ + { width: 1440, height: 1000 }, + { width: 1024, height: 768 }, + ]) { + await page.setViewportSize(viewport); + await page.locator("[data-roamjs-presentation2]").first().click(); + const toggle = page + .getByRole("button", { name: "Expand bullet" }) + .first(); + await toggle.waitFor(); + const alignment = await toggle.evaluate((button) => { + const svg = button.querySelector("svg").getBoundingClientRect(); + const text = button.parentElement.querySelector(".rm-block__input"); + const lineHeight = parseFloat(getComputedStyle(text).lineHeight); + const top = text.getBoundingClientRect().top; + return { + iconHeight: svg.height, + lineHeight, + centerDelta: Math.abs( + svg.y + svg.height / 2 - top - lineHeight / 2, + ), + }; + }); + assert( + alignment.iconHeight >= 16, + "Chevron must be legible at slide scale", + ); + assert( + alignment.centerDelta < 8, + "Chevron must align with the first text line", + ); + await page.screenshot({ + path: `${outDir}/chevrons-${viewport.width}.png`, + }); + await toggle.click(); + await page + .locator("#roamjs-reveal-root") + .getByText("Detail 1", { exact: true }) + .waitFor(); + await page + .getByRole("button", { name: "Collapse bullet" }) + .first() + .click(); + assert.equal( + await page + .locator("#roamjs-reveal-root") + .getByText("Detail 1", { exact: true }) + .count(), + 0, + ); + await close(); + await page.locator("[data-roamjs-presentation2]").nth(1).focus(); + await page.locator("[data-roamjs-presentation2]").nth(1).click(); + for (let slide = 0; slide < 2; slide++) { + if (slide) + await page.locator("#roamjs-reveal-root .navigate-right").click(); + const columns = page.locator( + "section.present .rm-level-horizontal-outline__child", + ); + await columns.first().waitFor(); + await page.waitForTimeout(1200); + const layout = await columns.evaluateAll((elements) => + elements.map((element) => { + const rect = element.getBoundingClientRect(); + return { + x: rect.x, + y: rect.y, + width: rect.width, + right: rect.right, + textSize: parseFloat( + getComputedStyle(element.querySelector(".rm-block__input")) + .fontSize, + ), + }; + }), + ); + assert.equal(layout.length, 3); + assert( + layout.every( + (column) => column.width > 100 && column.textSize >= 20, + ), + ); + assert( + Math.max(...layout.map((column) => column.y)) - + Math.min(...layout.map((column) => column.y)) < + 2, + ); + assert(layout[0].x < layout[1].x && layout[1].x < layout[2].x); + assert( + layout[2].right <= viewport.width + 2, + JSON.stringify({ viewport, slide, layout }), + ); + await page.screenshot({ + path: `${outDir}/horizontal-${slide}-${viewport.width}.png`, + }); + checks.push({ viewport, slide, alignment, layout }); + } + await close(); + } + return { fixture, checks }; + } finally { + if (await page.locator("#roamjs-presentation-container").count()) + await close(); + if (originalViewport) await page.setViewportSize(originalViewport); + } + }, + async cleanup({ page }) { + await page.evaluate(async () => { + const uid = window.__presentationLayoutFixture; + if (uid) await window.roamAlphaAPI.deletePage({ page: { uid } }); + delete window.__presentationLayoutFixture; + }); + return { fixtureDeleted: true }; + }, +}; diff --git a/src/components/Presentation2.tsx b/src/components/Presentation2.tsx index f48b06c..bc3cd61 100644 --- a/src/components/Presentation2.tsx +++ b/src/components/Presentation2.tsx @@ -167,18 +167,17 @@ const NativeCollapsibleNode = ({ node }: { node: TreeNode }) => { }`} > {!!children.length && ( - + + )} diff --git a/src/index.ts b/src/index.ts index 6d42cf3..a103f7d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -326,19 +326,52 @@ code[class*="language-"],pre[class*="language-"]{color:black;text-shadow:0 1px w } [data-roamjs-native-renderer] .roamjs-native-collapsible { - padding-left: 32px; + padding-left: 1em; } [data-roamjs-native-renderer] .roamjs-native-collapsible-toggle { + align-items: center; color: inherit; - left: -32px; + display: flex; + font-size: inherit; + height: 1.5em; + justify-content: center; + left: -1em; + min-height: 0; + min-width: 0; + padding: 0; position: absolute; - top: 4px; + top: 0; + width: 1em; z-index: 1; } +[data-roamjs-native-renderer] .roamjs-native-collapsible-toggle svg { + fill: none; + height: 0.5em; + margin: 0; + stroke: currentColor; + stroke-width: 2; + width: 0.5em; +} + [data-roamjs-native-renderer] .roamjs-native-collapsible-children { - margin-left: 32px; + margin-left: 1em; +} + +/* Roam's horizontal gutters are measured against the graph viewport. Keep + them inside the slide so Reveal's fitting pass does not chase overflow. */ +[data-roamjs-native-renderer] .rm-level-horizontal-outline__children { + gap: 0.5em; + margin: 0 !important; + max-width: 100%; + padding: 0 !important; +} + +[data-roamjs-native-renderer] .rm-level-horizontal-outline__child { + flex: 1 1 0; + max-width: none; + min-width: 0; } [data-roamjs-native-renderer] .roamjs-native-source-container img, From c70a16fdcaed23f1e9bb78bd2aeeeb3c59a44bb4 Mon Sep 17 00:00:00 2001 From: mdroidian Date: Tue, 13 Oct 2026 10:36:20 -0600 Subject: [PATCH 2/6] Close Roam settings before running layout regression checks --- e2e/roam-v2-layout.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/e2e/roam-v2-layout.mjs b/e2e/roam-v2-layout.mjs index 73a08cd..c2d0368 100644 --- a/e2e/roam-v2-layout.mjs +++ b/e2e/roam-v2-layout.mjs @@ -3,6 +3,11 @@ import assert from "node:assert/strict"; export default { runtimeNames: ["presentation"], async run({ page, outDir }) { + const settingsClose = page.locator(".rm-settings-close-button"); + if (await settingsClose.isVisible()) { + await settingsClose.click(); + await settingsClose.waitFor({ state: "hidden" }); + } const fixture = await page.evaluate(async () => { const api = window.roamAlphaAPI; const uid = api.util.generateUID(); From 861d98f7040eed89119a4ed817c22328649892a9 Mon Sep 17 00:00:00 2001 From: mdroidian Date: Tue, 13 Oct 2026 11:15:42 -0600 Subject: [PATCH 3/6] Preserve horizontal block views with collapsible Presentation 2 bullets --- CHANGELOG.md | 2 +- e2e/roam-v2-layout.mjs | 112 +++++++++++++++++++++++++++++++ src/components/Presentation2.tsx | 16 ++++- src/index.ts | 10 +++ 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a6491..965cd21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Fixed - Scale and align collapsible bullet chevrons with Presentation 2 text. -- Keep native horizontal block views visible and within Presentation 2 slides. +- Keep native horizontal block views visible and within Presentation 2 slides, including when collapsible bullets are enabled. ## 1.2.0 - 2026-09-13 diff --git a/e2e/roam-v2-layout.mjs b/e2e/roam-v2-layout.mjs index c2d0368..455ccbc 100644 --- a/e2e/roam-v2-layout.mjs +++ b/e2e/roam-v2-layout.mjs @@ -62,6 +62,40 @@ export default { ); for (let i = 0; i < 3; i++) await create(rootHorizontal, `Column ${i + 1}`, i); + const collapsibleHorizontal = await create( + uid, + "{{presentation2: {theme:black} {collapsible}}}", + 2, + ); + const collapsibleContent = await create( + collapsibleHorizontal, + "Collapsible horizontal block view", + 0, + ); + const collapsibleColumns = await create( + collapsibleContent, + "Expand these three columns", + 0, + { "block-view-type": "horizontal-outline" }, + ); + for (let i = 0; i < 3; i++) { + const column = await create( + collapsibleColumns, + ["Discover", "Build", "Review"][i], + i, + ); + await create(column, `Column detail ${i + 1}`, 0); + } + const collapsibleRoot = await create( + collapsibleHorizontal, + "Collapsible horizontal slide root", + 1, + { "block-view-type": "horizontal-outline" }, + ); + for (let i = 0; i < 3; i++) { + const column = await create(collapsibleRoot, `Root column ${i + 1}`, i); + await create(column, `Root detail ${i + 1}`, 0); + } await api.ui.mainWindow.openPage({ page: { uid } }); return { uid }; }); @@ -172,6 +206,84 @@ export default { checks.push({ viewport, slide, alignment, layout }); } await close(); + await page.locator("[data-roamjs-presentation2]").nth(2).focus(); + await page.locator("[data-roamjs-presentation2]").nth(2).click(); + for (let slide = 0; slide < 2; slide++) { + if (slide) + await page.locator("#roamjs-reveal-root .navigate-right").click(); + await page.waitForTimeout(1200); + const active = page.locator("section.present"); + if (!slide) { + await active + .getByRole("button", { name: "Expand bullet" }) + .first() + .click(); + } + const columns = active.locator( + slide + ? ".roamjs-native-slide-tree > .roamjs-native-collapsible-node" + : ".roamjs-native-slide-tree > .roamjs-native-collapsible-node > .roamjs-native-collapsible-children > .roamjs-native-collapsible-node", + ); + assert.equal(await columns.count(), 3); + for (let i = 0; i < 3; i++) { + await columns + .nth(i) + .getByRole("button", { name: "Expand bullet" }) + .click(); + } + await page.waitForTimeout(300); + const layout = await columns.evaluateAll((elements) => + elements.map((element) => { + const rect = element.getBoundingClientRect(); + return { + x: rect.x, + y: rect.y, + width: rect.width, + right: rect.right, + }; + }), + ); + await page.screenshot({ + path: `${outDir}/collapsible-horizontal-${slide}-${viewport.width}.png`, + }); + assert( + Math.max(...layout.map((column) => column.y)) - + Math.min(...layout.map((column) => column.y)) < + 2, + `Collapsible children must remain horizontal: ${JSON.stringify(layout)}`, + ); + assert(layout[0].x < layout[1].x && layout[1].x < layout[2].x); + assert(layout[2].right <= viewport.width + 2); + const detail = slide ? "Root detail 2" : "Column detail 2"; + await columns + .nth(1) + .getByRole("button", { name: "Collapse bullet" }) + .click(); + assert.equal( + await active.getByText(detail, { exact: true }).count(), + 0, + ); + assert.equal( + await active + .getByText(slide ? "Root detail 1" : "Column detail 1", { + exact: true, + }) + .count(), + 1, + ); + checks.push({ viewport, slide, collapsibleHorizontal: true, layout }); + if (!slide) { + await active + .getByRole("button", { name: "Collapse bullet" }) + .first() + .click(); + assert.equal( + await active.getByText("Discover", { exact: true }).count(), + 0, + ); + } + } + await close(); } return { fixture, checks }; } finally { diff --git a/src/components/Presentation2.tsx b/src/components/Presentation2.tsx index bc3cd61..62481b2 100644 --- a/src/components/Presentation2.tsx +++ b/src/components/Presentation2.tsx @@ -182,7 +182,13 @@ const NativeCollapsibleNode = ({ node }: { node: TreeNode }) => { {expanded && ( -
+
{children.map((child) => ( ))} @@ -264,7 +270,13 @@ const NativeSlideTree = ({ (node) => !hiddenUidSet.has(node.uid) && !HIDE_REGEX.test(node.text), ); return ( -
+
{visibleNodes.map((node) => ( ))} diff --git a/src/index.ts b/src/index.ts index a103f7d..9607810 100644 --- a/src/index.ts +++ b/src/index.ts @@ -359,6 +359,16 @@ code[class*="language-"],pre[class*="language-"]{color:black;text-shadow:0 1px w margin-left: 1em; } +[data-roamjs-native-renderer] .roamjs-native-collapsible-horizontal { + display: flex; + gap: 1em; +} + +[data-roamjs-native-renderer] .roamjs-native-collapsible-horizontal > .roamjs-native-collapsible-node { + flex: 1 1 0; + min-width: 0; +} + /* Roam's horizontal gutters are measured against the graph viewport. Keep them inside the slide so Reveal's fitting pass does not chase overflow. */ [data-roamjs-native-renderer] .rm-level-horizontal-outline__children { From 8836625725169ea53e1ce8475ce02b71d667d16f Mon Sep 17 00:00:00 2001 From: mdroidian Date: Tue, 13 Oct 2026 11:32:46 -0600 Subject: [PATCH 4/6] Bump presentation minor version to 1.3.0 --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 965cd21..65ca358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 1.3.0 - 2026-10-13 ### Fixed diff --git a/package-lock.json b/package-lock.json index 52e9b89..ddf2315 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "presentation", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "presentation", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "reveal.js": "^4.3.0", diff --git a/package.json b/package.json index 8407b1c..8eff3fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "presentation", - "version": "1.2.0", + "version": "1.3.0", "description": "Build slideshow presentations from within Roam!", "main": "./build/main.js", "scripts": { From 50ae7fb6831dc9131e53b46ce19f68a2d845542a Mon Sep 17 00:00:00 2001 From: mdroidian Date: Tue, 13 Oct 2026 11:55:03 -0600 Subject: [PATCH 5/6] Use patch version 1.2.1 for presentation fixes --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ca358..25c946f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 1.3.0 - 2026-10-13 +## 1.2.1 - 2026-10-13 ### Fixed diff --git a/package-lock.json b/package-lock.json index ddf2315..2a9091e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "presentation", - "version": "1.3.0", + "version": "1.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "presentation", - "version": "1.3.0", + "version": "1.2.1", "license": "MIT", "dependencies": { "reveal.js": "^4.3.0", diff --git a/package.json b/package.json index 8eff3fa..96d0bf8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "presentation", - "version": "1.3.0", + "version": "1.2.1", "description": "Build slideshow presentations from within Roam!", "main": "./build/main.js", "scripts": { From bad40dbc10684e7128dff4f7df84fd18e97aaf3d Mon Sep 17 00:00:00 2001 From: mdroidian Date: Tue, 13 Oct 2026 12:27:58 -0600 Subject: [PATCH 6/6] Hide block separators in horizontal Presentation 2 layouts --- CHANGELOG.md | 1 + src/index.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c946f..fadb294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Scale and align collapsible bullet chevrons with Presentation 2 text. - Keep native horizontal block views visible and within Presentation 2 slides, including when collapsible bullets are enabled. +- Remove blank separator space from horizontal Presentation 2 blocks. ## 1.2.0 - 2026-09-13 diff --git a/src/index.ts b/src/index.ts index 9607810..bf5e882 100644 --- a/src/index.ts +++ b/src/index.ts @@ -384,6 +384,10 @@ code[class*="language-"],pre[class*="language-"]{color:black;text-shadow:0 1px w min-width: 0; } +[data-roamjs-native-renderer] :is(.rm-level-horizontal-outline, .roamjs-native-collapsible-horizontal) .rm-block-separator { + display: none; +} + [data-roamjs-native-renderer] .roamjs-native-source-container img, [data-roamjs-native-renderer] .roamjs-native-source-container video, [data-roamjs-native-renderer] .roamjs-native-source-container svg {