From 60d4280dd62636764be7132a86a07f3dc258ba9b Mon Sep 17 00:00:00 2001 From: mdroidian Date: Sun, 13 Sep 2026 13:01:33 -0600 Subject: [PATCH 1/2] Fix presentation styling with Roam Studio Craft --- CHANGELOG.md | 6 ++ README.md | 14 ++++ e2e/roam-theme-compat.mjs | 139 ++++++++++++++++++++++++++++++++++++++ src/index.ts | 67 ++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 e2e/roam-theme-compat.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ba94b9..5d0760c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- Keep presentation text, links, tables, native headings, code, and embeds readable when graph themes such as Roam Studio Craft are enabled. + ## 1.0.1 - 2026-08-16 ### Added diff --git a/README.md b/README.md index 7422297..3ebdbc5 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,20 @@ Apart from these themes, you could also use `roam/css` to style the presentation - `navigate-left` - The go left button - `navigate-right` - The go right button +### Graph theme compatibility + +Presentation content uses the selected Reveal theme for its text and links, even +when a graph theme such as Roam Studio Craft is enabled. Native tables, headings, +and code retain presentation-sized text; embedded content keeps readable contrast. The graph theme remains active outside +the presentation. + +To run the live theme regression checks, enable Roam Studio with Craft in a test +graph, build the extension, then use `roamjs-load-extension` with +`--test-module e2e/roam-theme-compat.mjs`. The check exercises both renderers with +black and white Reveal themes, compares body styles with Studio styles temporarily +disabled, and checks collapsible content, tables, headings, code, and native block/page embeds. It removes +its temporary fixture page afterward. + ## Notes To add Speaker notes, add `{notes:true}` to the button text. The last bullet of each child will be used as the speaker notes of the slide. To combine with theme above, the button text would look like this: diff --git a/e2e/roam-theme-compat.mjs b/e2e/roam-theme-compat.mjs new file mode 100644 index 0000000..d39ffc6 --- /dev/null +++ b/e2e/roam-theme-compat.mjs @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import fs from 'node:fs/promises'; + +// Run with roamjs-load-extension against a graph with Roam Studio/Craft enabled. +// Fixtures are isolated to one temporary page and removed even on failure. +let fixtureUid; +let supportUid; +export default { + runtimeNames: ['presentation'], + async run({ page, outDir }) { + await page.keyboard.press('Escape'); + assert(await page.locator('#roamstudio-css-theme').count(), 'Enable Roam Studio before running this test'); + const fixture = await page.evaluate(async () => { + const api = window.roamAlphaAPI; + const uid = api.util.generateUID(); + await api.createPage({ page: { uid, title: `Presentation theme regression ${Date.now()}` } }); + const add = async (parent, order, string, extra = {}) => { + const uid = api.util.generateUID(); + await api.createBlock({ location: { 'parent-uid': parent, order }, block: { uid, string, ...extra } }); + return uid; + }; + const supportUid = api.util.generateUID(); + const supportTitle = `Presentation embed source ${Date.now()}`; + await api.createPage({page:{uid:supportUid,title:supportTitle}}); + const source = await add(supportUid,0,'Embedded body text'); + await add(source,0,'Nested child'); + const roots = []; + for (const renderer of ['presentation', 'presentation2']) { + for (const theme of ['black', 'white']) { + const root = await add(uid, roots.length, `{{${renderer}:{theme:${theme}}}}`); + const slide = await add(root, 0, 'Theme compatibility'); + await add(slide, 0, 'Readable body text'); + await add(slide, 1, '[[Daily Notes]] and [external link](https://roamresearch.com)'); + await add(slide, 2, '`inline code` and ^^highlighted text^^'); + await add(slide, 3, '{{TODO}} Task text'); + const nested = await add(root, 1, 'Expand {collapsible}'); + const parent = await add(nested, 0, 'Parent'); + await add(parent, 0, 'Revealed child'); + const table = await add(root, 2, 'Table'); + const tableBlock = await add(table, 0, '{{table}}'); + const col = await add(tableBlock, 0, 'Header'); + await add(col, 0, 'Cell text'); + const code = await add(root, 3, 'Code and headings'); + await add(code, 0, 'Heading one', {heading:1}); + await add(code, 1, 'Heading two', {heading:2}); + await add(code, 2, 'Heading three', {heading:3}); + await add(code, 3, '```javascript\nconst answer = 42;\n```'); + if(renderer==='presentation2') { + const embed = await add(root,4,'Block embed'); + await add(embed,0,'{{embed: (('+source+'))}}'); + const pageEmbed = await add(root,5,'Page embed'); + await add(pageEmbed,0,'{{embed: [['+supportTitle+']]}}'); + } + roots.push({renderer,theme,root,slide}); + } + } + await api.ui.mainWindow.openPage({page:{uid}}); + return {uid,roots,supportUid}; + }); + fixtureUid = fixture.uid; + supportUid = fixture.supportUid; + const checks = []; + for (const {renderer,theme,root} of fixture.roots) { + await page.evaluate(uid => window.roamAlphaAPI.ui.mainWindow.openBlock({block:{uid}}), root); + const button = page.locator(renderer==='presentation2'?'[data-roamjs-presentation2]':'[data-roamjs-presentation]').first(); + await button.click(); + await page.locator('#roamjs-reveal-root.ready').waitFor(); + await page.waitForTimeout(500); + const deck = page.locator('#roamjs-reveal-root'); + const measure = () => page.locator('#roamjs-reveal-root section.present').evaluate(section => { + const body = [...section.querySelectorAll('.rm-block__input,li')].find(e=>e.textContent.trim()==='Readable body text'); + const link = section.querySelector('.rm-page-ref'); + return {body:body && {color:getComputedStyle(body).color,size:parseFloat(getComputedStyle(body).fontSize)},link:link && getComputedStyle(link).color,slide:getComputedStyle(section).color}; + }); + const themed = await measure(); + assert(themed.body, 'Body text rendered'); + assert.equal(themed.body.color,themed.slide, 'Body inherits the Reveal theme color'); + assert(themed.body.size>=30, 'Body text retains presentation size'); + assert.notEqual(themed.link,'rgb(26, 26, 26)','Link does not use Craft light-theme color'); + const screenshot = path.join(outDir,`${renderer}-${theme}.png`); + await page.screenshot({path:screenshot}); + // Disable only Studio's styles briefly to compare the computed cascade. + const disabled = await page.evaluate(() => [...document.querySelectorAll('style[id^="roamstudio-"]')].map(s=>{const old=s.disabled;s.disabled=true;return {id:s.id,disabled:old};})); + let baseline; + try { baseline=await measure(); } + finally { await page.evaluate(states=>states.forEach(({id,disabled})=>{document.getElementById(id).disabled=disabled;}),disabled); } + assert.deepEqual(themed.body,baseline.body,'Graph theme does not change body typography'); + await deck.locator('.navigate-right').click(); + await page.waitForTimeout(400); + const toggle = page.locator(renderer==='presentation2'?'.present .roamjs-native-collapsible-toggle':'.present .roamjs-collapsible-caret').first(); + assert(!await deck.locator('section.present').getByText('Revealed child',{exact:true}).isVisible()); + await toggle.click(); + await deck.locator('section.present').getByText('Revealed child',{exact:true}).waitFor(); + await deck.locator('.navigate-right').click(); + await page.waitForTimeout(400); + const cell = deck.locator('section.present td').filter({hasText:'Cell text'}).first(); + await cell.waitFor(); + const cellStyle = await cell.evaluate(e=>({color:getComputedStyle(e).color,size:parseFloat(getComputedStyle(e).fontSize)})); + assert.equal(cellStyle.color,themed.slide,'Table text uses presentation color'); + assert(cellStyle.size>=25,'Table text retains presentation size'); + await deck.locator('.navigate-right').click(); + await page.waitForTimeout(500); + if(renderer==='presentation2') { + const fonts = await deck.locator('section.present [class*="rm-heading-level"] .rm-block__input').evaluateAll(es=>es.map(e=>parseFloat(getComputedStyle(e).fontSize))); + assert(fonts[0]>fonts[1] && fonts[1]>fonts[2] && fonts[2]>=30,'Native heading hierarchy remains presentation-sized'); + const line = deck.locator('section.present .cm-line').first(); + assert(await line.evaluate(e=>parseFloat(getComputedStyle(e).fontSize))>=30,'Native code retains presentation size'); + await page.screenshot({path:path.join(outDir,`${renderer}-${theme}-code.png`)}); + } + if(renderer==='presentation2') { + for(const type of ['block','page']) { + await deck.locator('.navigate-right').click(); + await page.waitForTimeout(500); + const embed = deck.locator('section.present .rm-embed-container').first(); + await embed.waitFor(); + if(type==='page') await embed.locator('button.rm-closed').click(); + assert.equal(await embed.evaluate(e=>getComputedStyle(e).backgroundColor),'rgba(0, 0, 0, 0)','Embedded card background follows the slide'); + await page.screenshot({path:path.join(outDir,`${renderer}-${theme}-${type}-embed.png`)}); + await fs.writeFile(path.join(outDir,`${renderer}-${theme}-${type}-embed.html`),await embed.innerHTML()); + const text=embed.locator('.rm-block__input').filter({hasText:'Embedded body text'}).first(); + assert.equal(await text.evaluate(e=>getComputedStyle(e).color),themed.slide,'Embedded text retains contrast'); + await page.screenshot({path:path.join(outDir,`${renderer}-${theme}-${type}-embed.png`)}); + } + } + await page.keyboard.press('Escape'); + await page.locator('#roamjs-presentation-container').waitFor({state:'detached'}); + checks.push({renderer,theme,themed,baseline,cellStyle,screenshot,collapsible:true}); + } + assert(await page.locator('#roamstudio-css-theme').evaluate(e=>!e.disabled),'Roam Studio stays enabled'); + return {checks}; + }, + async cleanup({page}) { + await page.keyboard.press('Escape'); + if(fixtureUid) await page.evaluate(uid=>window.roamAlphaAPI.deletePage({page:{uid}}),fixtureUid); + if(supportUid) await page.evaluate(uid=>window.roamAlphaAPI.deletePage({page:{uid}}),supportUid); + await page.goto(page.url().split('/page/')[0]); + } +}; diff --git a/src/index.ts b/src/index.ts index 5e379b1..6d42cf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,6 +104,73 @@ code[class*="language-"],pre[class*="language-"]{color:black;text-shadow:0 1px w pointer-events: none; } +/* Keep graph themes from replacing the presentation's typography. */ +#roamjs-presentation-container .slides :is(.roam-block-container, .rm-block-main, .rm-block__input) { + color: inherit; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + line-height: inherit; +} +#roamjs-presentation-container .slides .rm-block__input { + min-height: 0 !important; +} +#roamjs-presentation-container .slides :is(.rm-page-ref, .rm-alias) { + color: var(--r-link-color); + background: transparent; + font-size: inherit; + font-family: inherit; + border: 0; +} +#roamjs-presentation-container .slides :is(.rm-page-ref__brackets, .rm-block-ref, .rm-strikethrough) { + color: inherit; +} +#roamjs-presentation-container .slides .rm-highlight { + color: #202224; +} +#roamjs-presentation-container .slides :not(pre) > code { + font-size: 0.9em; + line-height: inherit; +} + +#roamjs-presentation-container .slides .rm-code-block .cm-line { + font-family: monospace; + font-size: inherit; + line-height: 1.4; +} + +#roamjs-presentation-container .slides :is(.rm-table, .rm-table__cell, tr, td, th, .rm-attr-ref, blockquote) { + color: inherit; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +#roamjs-presentation-container .slides .rm-heading-level-1 > .rm-block-main .rm-block__input { + font-size: 1.4em !important; +} +#roamjs-presentation-container .slides .rm-heading-level-2 > .rm-block-main .rm-block__input { + font-size: 1.2em !important; +} +#roamjs-presentation-container .slides .rm-heading-level-3 > .rm-block-main .rm-block__input { + font-size: 1.1em !important; +} +#roamjs-presentation-container .slides :is(.rm-bullet, .rm-bullet__inner) { + font-size: inherit; +} +#roamjs-presentation-container .slides .rm-block--document > .rm-block-main > .rm-block__controls { + visibility: hidden; +} + +#roamjs-presentation-container .slides :is(.rm-embed-container, .rm-embed-settings, .rm-query, .rm-query-title, .rm-search-query-header) { + background-color: transparent; + color: inherit; +} + +#roamjs-presentation-container .slides :is(.rm-page__title, .rm-page__title a) { + color: var(--r-link-color); + font-size: inherit; +} + [data-roamjs-native-renderer] .roamjs-native-block-string * { color: inherit; font-family: Inter, sans-serif; From 0c4bdd2c086831f346bb464bff32746a1f7cb3f1 Mon Sep 17 00:00:00 2001 From: mdroidian Date: Sun, 13 Sep 2026 13:49:06 -0600 Subject: [PATCH 2/2] Prepare 1.2.0 and remove compatibility test and README changes --- CHANGELOG.md | 2 +- README.md | 14 ---- e2e/roam-theme-compat.mjs | 139 -------------------------------------- package-lock.json | 4 +- package.json | 2 +- 5 files changed, 4 insertions(+), 157 deletions(-) delete mode 100644 e2e/roam-theme-compat.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d0760c..aee4fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 1.2.0 - 2026-09-13 ### Fixed diff --git a/README.md b/README.md index 3ebdbc5..7422297 100644 --- a/README.md +++ b/README.md @@ -59,20 +59,6 @@ Apart from these themes, you could also use `roam/css` to style the presentation - `navigate-left` - The go left button - `navigate-right` - The go right button -### Graph theme compatibility - -Presentation content uses the selected Reveal theme for its text and links, even -when a graph theme such as Roam Studio Craft is enabled. Native tables, headings, -and code retain presentation-sized text; embedded content keeps readable contrast. The graph theme remains active outside -the presentation. - -To run the live theme regression checks, enable Roam Studio with Craft in a test -graph, build the extension, then use `roamjs-load-extension` with -`--test-module e2e/roam-theme-compat.mjs`. The check exercises both renderers with -black and white Reveal themes, compares body styles with Studio styles temporarily -disabled, and checks collapsible content, tables, headings, code, and native block/page embeds. It removes -its temporary fixture page afterward. - ## Notes To add Speaker notes, add `{notes:true}` to the button text. The last bullet of each child will be used as the speaker notes of the slide. To combine with theme above, the button text would look like this: diff --git a/e2e/roam-theme-compat.mjs b/e2e/roam-theme-compat.mjs deleted file mode 100644 index d39ffc6..0000000 --- a/e2e/roam-theme-compat.mjs +++ /dev/null @@ -1,139 +0,0 @@ -import assert from 'node:assert/strict'; -import path from 'node:path'; -import fs from 'node:fs/promises'; - -// Run with roamjs-load-extension against a graph with Roam Studio/Craft enabled. -// Fixtures are isolated to one temporary page and removed even on failure. -let fixtureUid; -let supportUid; -export default { - runtimeNames: ['presentation'], - async run({ page, outDir }) { - await page.keyboard.press('Escape'); - assert(await page.locator('#roamstudio-css-theme').count(), 'Enable Roam Studio before running this test'); - const fixture = await page.evaluate(async () => { - const api = window.roamAlphaAPI; - const uid = api.util.generateUID(); - await api.createPage({ page: { uid, title: `Presentation theme regression ${Date.now()}` } }); - const add = async (parent, order, string, extra = {}) => { - const uid = api.util.generateUID(); - await api.createBlock({ location: { 'parent-uid': parent, order }, block: { uid, string, ...extra } }); - return uid; - }; - const supportUid = api.util.generateUID(); - const supportTitle = `Presentation embed source ${Date.now()}`; - await api.createPage({page:{uid:supportUid,title:supportTitle}}); - const source = await add(supportUid,0,'Embedded body text'); - await add(source,0,'Nested child'); - const roots = []; - for (const renderer of ['presentation', 'presentation2']) { - for (const theme of ['black', 'white']) { - const root = await add(uid, roots.length, `{{${renderer}:{theme:${theme}}}}`); - const slide = await add(root, 0, 'Theme compatibility'); - await add(slide, 0, 'Readable body text'); - await add(slide, 1, '[[Daily Notes]] and [external link](https://roamresearch.com)'); - await add(slide, 2, '`inline code` and ^^highlighted text^^'); - await add(slide, 3, '{{TODO}} Task text'); - const nested = await add(root, 1, 'Expand {collapsible}'); - const parent = await add(nested, 0, 'Parent'); - await add(parent, 0, 'Revealed child'); - const table = await add(root, 2, 'Table'); - const tableBlock = await add(table, 0, '{{table}}'); - const col = await add(tableBlock, 0, 'Header'); - await add(col, 0, 'Cell text'); - const code = await add(root, 3, 'Code and headings'); - await add(code, 0, 'Heading one', {heading:1}); - await add(code, 1, 'Heading two', {heading:2}); - await add(code, 2, 'Heading three', {heading:3}); - await add(code, 3, '```javascript\nconst answer = 42;\n```'); - if(renderer==='presentation2') { - const embed = await add(root,4,'Block embed'); - await add(embed,0,'{{embed: (('+source+'))}}'); - const pageEmbed = await add(root,5,'Page embed'); - await add(pageEmbed,0,'{{embed: [['+supportTitle+']]}}'); - } - roots.push({renderer,theme,root,slide}); - } - } - await api.ui.mainWindow.openPage({page:{uid}}); - return {uid,roots,supportUid}; - }); - fixtureUid = fixture.uid; - supportUid = fixture.supportUid; - const checks = []; - for (const {renderer,theme,root} of fixture.roots) { - await page.evaluate(uid => window.roamAlphaAPI.ui.mainWindow.openBlock({block:{uid}}), root); - const button = page.locator(renderer==='presentation2'?'[data-roamjs-presentation2]':'[data-roamjs-presentation]').first(); - await button.click(); - await page.locator('#roamjs-reveal-root.ready').waitFor(); - await page.waitForTimeout(500); - const deck = page.locator('#roamjs-reveal-root'); - const measure = () => page.locator('#roamjs-reveal-root section.present').evaluate(section => { - const body = [...section.querySelectorAll('.rm-block__input,li')].find(e=>e.textContent.trim()==='Readable body text'); - const link = section.querySelector('.rm-page-ref'); - return {body:body && {color:getComputedStyle(body).color,size:parseFloat(getComputedStyle(body).fontSize)},link:link && getComputedStyle(link).color,slide:getComputedStyle(section).color}; - }); - const themed = await measure(); - assert(themed.body, 'Body text rendered'); - assert.equal(themed.body.color,themed.slide, 'Body inherits the Reveal theme color'); - assert(themed.body.size>=30, 'Body text retains presentation size'); - assert.notEqual(themed.link,'rgb(26, 26, 26)','Link does not use Craft light-theme color'); - const screenshot = path.join(outDir,`${renderer}-${theme}.png`); - await page.screenshot({path:screenshot}); - // Disable only Studio's styles briefly to compare the computed cascade. - const disabled = await page.evaluate(() => [...document.querySelectorAll('style[id^="roamstudio-"]')].map(s=>{const old=s.disabled;s.disabled=true;return {id:s.id,disabled:old};})); - let baseline; - try { baseline=await measure(); } - finally { await page.evaluate(states=>states.forEach(({id,disabled})=>{document.getElementById(id).disabled=disabled;}),disabled); } - assert.deepEqual(themed.body,baseline.body,'Graph theme does not change body typography'); - await deck.locator('.navigate-right').click(); - await page.waitForTimeout(400); - const toggle = page.locator(renderer==='presentation2'?'.present .roamjs-native-collapsible-toggle':'.present .roamjs-collapsible-caret').first(); - assert(!await deck.locator('section.present').getByText('Revealed child',{exact:true}).isVisible()); - await toggle.click(); - await deck.locator('section.present').getByText('Revealed child',{exact:true}).waitFor(); - await deck.locator('.navigate-right').click(); - await page.waitForTimeout(400); - const cell = deck.locator('section.present td').filter({hasText:'Cell text'}).first(); - await cell.waitFor(); - const cellStyle = await cell.evaluate(e=>({color:getComputedStyle(e).color,size:parseFloat(getComputedStyle(e).fontSize)})); - assert.equal(cellStyle.color,themed.slide,'Table text uses presentation color'); - assert(cellStyle.size>=25,'Table text retains presentation size'); - await deck.locator('.navigate-right').click(); - await page.waitForTimeout(500); - if(renderer==='presentation2') { - const fonts = await deck.locator('section.present [class*="rm-heading-level"] .rm-block__input').evaluateAll(es=>es.map(e=>parseFloat(getComputedStyle(e).fontSize))); - assert(fonts[0]>fonts[1] && fonts[1]>fonts[2] && fonts[2]>=30,'Native heading hierarchy remains presentation-sized'); - const line = deck.locator('section.present .cm-line').first(); - assert(await line.evaluate(e=>parseFloat(getComputedStyle(e).fontSize))>=30,'Native code retains presentation size'); - await page.screenshot({path:path.join(outDir,`${renderer}-${theme}-code.png`)}); - } - if(renderer==='presentation2') { - for(const type of ['block','page']) { - await deck.locator('.navigate-right').click(); - await page.waitForTimeout(500); - const embed = deck.locator('section.present .rm-embed-container').first(); - await embed.waitFor(); - if(type==='page') await embed.locator('button.rm-closed').click(); - assert.equal(await embed.evaluate(e=>getComputedStyle(e).backgroundColor),'rgba(0, 0, 0, 0)','Embedded card background follows the slide'); - await page.screenshot({path:path.join(outDir,`${renderer}-${theme}-${type}-embed.png`)}); - await fs.writeFile(path.join(outDir,`${renderer}-${theme}-${type}-embed.html`),await embed.innerHTML()); - const text=embed.locator('.rm-block__input').filter({hasText:'Embedded body text'}).first(); - assert.equal(await text.evaluate(e=>getComputedStyle(e).color),themed.slide,'Embedded text retains contrast'); - await page.screenshot({path:path.join(outDir,`${renderer}-${theme}-${type}-embed.png`)}); - } - } - await page.keyboard.press('Escape'); - await page.locator('#roamjs-presentation-container').waitFor({state:'detached'}); - checks.push({renderer,theme,themed,baseline,cellStyle,screenshot,collapsible:true}); - } - assert(await page.locator('#roamstudio-css-theme').evaluate(e=>!e.disabled),'Roam Studio stays enabled'); - return {checks}; - }, - async cleanup({page}) { - await page.keyboard.press('Escape'); - if(fixtureUid) await page.evaluate(uid=>window.roamAlphaAPI.deletePage({page:{uid}}),fixtureUid); - if(supportUid) await page.evaluate(uid=>window.roamAlphaAPI.deletePage({page:{uid}}),supportUid); - await page.goto(page.url().split('/page/')[0]); - } -}; diff --git a/package-lock.json b/package-lock.json index 457b9cb..52e9b89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "presentation", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "presentation", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "reveal.js": "^4.3.0", diff --git a/package.json b/package.json index 4202ef5..8407b1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "presentation", - "version": "1.1.0", + "version": "1.2.0", "description": "Build slideshow presentations from within Roam!", "main": "./build/main.js", "scripts": {