From 504ac87be2e7163dabe449a32b448e166e7de17a Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 28 Jun 2026 15:04:12 -0600 Subject: [PATCH 1/8] Fix weekly note navigation buttons --- src/features/weekly-notes.ts | 42 ++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/features/weekly-notes.ts b/src/features/weekly-notes.ts index 0d077f2..a61f05b 100644 --- a/src/features/weekly-notes.ts +++ b/src/features/weekly-notes.ts @@ -45,6 +45,7 @@ const DAYS = [ const DATE_REGEX = new RegExp(`{(${DAYS.join("|")}):(.*?)}`, "g"); const FORMAT_DEFAULT_VALUE = "{monday:MM/dd yyyy} - {sunday:MM/dd yyyy}"; const CONFIG = `roam/js/${ID}`; +const ROAM_TITLE_CONTAINER_CLASS = "rm-title-display-container"; const formatCache = { current: "" }; const getFormat = (tree?: TreeNode[]) => @@ -442,26 +443,49 @@ export const toggleFeature = ( title ); setTimeout(() => { - const header = document.querySelector( + const header = document.querySelector( ".roam-article h1.rm-title-display" - ) as HTMLHeadingElement; - const headerContainer = header.parentElement; + ); + const headerContainer = header?.closest( + `.${ROAM_TITLE_CONTAINER_CLASS}` + ); + const insertionPoint = headerContainer || header; + if (!insertionPoint) return; + const buttonContainer = document.createElement("div"); buttonContainer.style.display = "flex"; buttonContainer.style.justifyContent = "space-between"; buttonContainer.style.marginBottom = "32px"; buttonContainer.id = "roamjs-weekly-mode-nav"; - headerContainer?.appendChild(buttonContainer); + insertionPoint.insertAdjacentElement("afterend", buttonContainer); - const makeButton = (pagename: string, label: string) => { + const makeButton = ( + pagename: string, + label: string, + direction: "left" | "right" + ) => { const button = document.createElement("button"); - button.className = "bp3-button"; + button.className = "bp3-button bp3-minimal bp3-outlined"; + button.type = "button"; button.onclick = () => navigateToPage(pagename); - button.innerText = label; + + const icon = document.createElement("span"); + icon.className = `bp3-icon-standard bp3-icon-arrow-${direction}`; + icon.setAttribute("aria-hidden", "true"); + + const text = document.createElement("span"); + text.innerText = label; + + if (direction === "left") { + button.append(icon, text); + } else { + button.append(text, icon); + } + buttonContainer.appendChild(button); }; - makeButton(prevTitle, "Last Week"); - makeButton(nextTitle, "Next Week"); + makeButton(prevTitle, "Last Week", "left"); + makeButton(nextTitle, "Next Week", "right"); }); } } From de2413adac2130aca8e01c08bc241da06b084985 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 28 Jun 2026 15:14:10 -0600 Subject: [PATCH 2/8] Render weekly note nav from title observer --- src/features/weekly-notes.ts | 154 ++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/src/features/weekly-notes.ts b/src/features/weekly-notes.ts index a61f05b..465380e 100644 --- a/src/features/weekly-notes.ts +++ b/src/features/weekly-notes.ts @@ -413,6 +413,92 @@ export const toggleFeature = ( return { dateArray: [], formats, valid: false }; }; + const getWeeklyNavigationTitles = (title: string) => { + const { dateArray, valid, formats } = getFormatDateData(title); + if (!valid) return null; + + const formattedDateArray = dateArray + .map((d, i) => ({ + cur: dateFnsFormat(d, formats[i]), + prev: dateFnsFormat(subWeeks(d, 1), formats[i]), + next: dateFnsFormat(addWeeks(d, 1), formats[i]), + })) + .filter( + (info): info is { cur: string; prev: string; next: string } => + !!info.cur && !!info.prev && !!info.next + ); + const prevTitle = formattedDateArray.reduce( + (acc, info) => acc.replace(info.cur, info.prev), + title + ); + const nextTitle = formattedDateArray.reduce( + (acc, info) => acc.replace(info.cur, info.next), + title + ); + + return { prevTitle, nextTitle }; + }; + + const renderWeeklyNoteNav = ( + header: HTMLHeadingElement, + title = getPageTitleValueByHtmlElement(header), + expectedTitle = "" + ) => { + if (!header.closest(".roam-article")) return false; + if (expectedTitle && title !== expectedTitle) return false; + + document.getElementById("roamjs-weekly-mode-nav")?.remove?.(); + + const weeklyNavigationTitles = getWeeklyNavigationTitles(title); + if (!weeklyNavigationTitles) return false; + + const headerContainer = header.closest(`.${ROAM_TITLE_CONTAINER_CLASS}`); + const insertionPoint = headerContainer || header; + const buttonContainer = document.createElement("div"); + buttonContainer.style.display = "flex"; + buttonContainer.style.justifyContent = "space-between"; + buttonContainer.style.marginBottom = "32px"; + buttonContainer.id = "roamjs-weekly-mode-nav"; + insertionPoint.insertAdjacentElement("afterend", buttonContainer); + + const makeButton = ( + pagename: string, + label: string, + direction: "left" | "right" + ) => { + const button = document.createElement("button"); + button.className = "bp3-button bp3-minimal bp3-outlined"; + button.type = "button"; + button.onclick = () => navigateToPage(pagename); + + const icon = document.createElement("span"); + icon.className = `bp3-icon-standard bp3-icon-arrow-${direction}`; + icon.setAttribute("aria-hidden", "true"); + + const text = document.createElement("span"); + text.innerText = label; + + if (direction === "left") { + button.append(icon, text); + } else { + button.append(text, icon); + } + + buttonContainer.appendChild(button); + }; + makeButton(weeklyNavigationTitles.prevTitle, "Last Week", "left"); + makeButton(weeklyNavigationTitles.nextTitle, "Next Week", "right"); + + return true; + }; + + const renderCurrentWeeklyNoteNav = (expectedTitle = "") => { + const header = document.querySelector( + ".roam-article h1.rm-title-display" + ); + if (header) renderWeeklyNoteNav(header, undefined, expectedTitle); + }; + const hashListener = (newUrl: string) => { document.getElementById("roamjs-weekly-mode-nav")?.remove?.(); const urlUid = newUrl.match(/\/page\/(.*)$/)?.[1]; @@ -422,71 +508,8 @@ export const toggleFeature = ( formatCache.current = ""; return; } - const { dateArray, valid, formats } = getFormatDateData(title); - if (valid) { - const formattedDateArray = dateArray - .map((d, i) => ({ - cur: dateFnsFormat(d, formats[i]), - prev: dateFnsFormat(subWeeks(d, 1), formats[i]), - next: dateFnsFormat(addWeeks(d, 1), formats[i]), - })) - .filter( - (info): info is { cur: string; prev: string; next: string } => - !!info.cur && !!info.prev && !!info.next - ); - const prevTitle = formattedDateArray.reduce( - (acc, info) => acc.replace(info.cur, info.prev), - title - ); - const nextTitle = formattedDateArray.reduce( - (acc, info) => acc.replace(info.cur, info.next), - title - ); - setTimeout(() => { - const header = document.querySelector( - ".roam-article h1.rm-title-display" - ); - const headerContainer = header?.closest( - `.${ROAM_TITLE_CONTAINER_CLASS}` - ); - const insertionPoint = headerContainer || header; - if (!insertionPoint) return; - - const buttonContainer = document.createElement("div"); - buttonContainer.style.display = "flex"; - buttonContainer.style.justifyContent = "space-between"; - buttonContainer.style.marginBottom = "32px"; - buttonContainer.id = "roamjs-weekly-mode-nav"; - insertionPoint.insertAdjacentElement("afterend", buttonContainer); - - const makeButton = ( - pagename: string, - label: string, - direction: "left" | "right" - ) => { - const button = document.createElement("button"); - button.className = "bp3-button bp3-minimal bp3-outlined"; - button.type = "button"; - button.onclick = () => navigateToPage(pagename); - - const icon = document.createElement("span"); - icon.className = `bp3-icon-standard bp3-icon-arrow-${direction}`; - icon.setAttribute("aria-hidden", "true"); - - const text = document.createElement("span"); - text.innerText = label; - - if (direction === "left") { - button.append(icon, text); - } else { - button.append(text, icon); - } - - buttonContainer.appendChild(button); - }; - makeButton(prevTitle, "Last Week", "left"); - makeButton(nextTitle, "Next Week", "right"); - }); + if (getWeeklyNavigationTitles(title)) { + setTimeout(() => renderCurrentWeeklyNoteNav(title)); } } }; @@ -510,6 +533,7 @@ export const toggleFeature = ( callback: (header: HTMLElement) => { const title = getPageTitleValueByHtmlElement(header); const { valid } = getFormatDateData(title); + renderWeeklyNoteNav(header as HTMLHeadingElement, title); if (valid) { header.onmousedown = (e) => { if (!e.shiftKey) { From 1ed5920d4b64feeadf2e7851dddb5197d845d680 Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 28 Jun 2026 15:18:34 -0600 Subject: [PATCH 3/8] Render weekly nav buttons with React --- src/features/weekly-notes.ts | 68 ++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/src/features/weekly-notes.ts b/src/features/weekly-notes.ts index 465380e..e5b84a3 100644 --- a/src/features/weekly-notes.ts +++ b/src/features/weekly-notes.ts @@ -1,8 +1,11 @@ +import { Button } from "@blueprintjs/core"; import setDay from "date-fns/setDay"; import _dateFnsFormat from "date-fns/format"; import _parse from "date-fns/parse"; import addWeeks from "date-fns/addWeeks"; import subWeeks from "date-fns/subWeeks"; +import React from "react"; +import ReactDOM from "react-dom"; import { createConfigObserver } from "roamjs-components/components/ConfigPage"; import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; @@ -46,6 +49,7 @@ const DATE_REGEX = new RegExp(`{(${DAYS.join("|")}):(.*?)}`, "g"); const FORMAT_DEFAULT_VALUE = "{monday:MM/dd yyyy} - {sunday:MM/dd yyyy}"; const CONFIG = `roam/js/${ID}`; const ROAM_TITLE_CONTAINER_CLASS = "rm-title-display-container"; +const WEEKLY_NOTE_NAV_ID = "roamjs-weekly-mode-nav"; const formatCache = { current: "" }; const getFormat = (tree?: TreeNode[]) => @@ -439,6 +443,14 @@ export const toggleFeature = ( return { prevTitle, nextTitle }; }; + const removeWeeklyNoteNav = () => { + const nav = document.getElementById(WEEKLY_NOTE_NAV_ID); + if (nav) { + ReactDOM.unmountComponentAtNode(nav); + nav.remove(); + } + }; + const renderWeeklyNoteNav = ( header: HTMLHeadingElement, title = getPageTitleValueByHtmlElement(header), @@ -447,7 +459,7 @@ export const toggleFeature = ( if (!header.closest(".roam-article")) return false; if (expectedTitle && title !== expectedTitle) return false; - document.getElementById("roamjs-weekly-mode-nav")?.remove?.(); + removeWeeklyNoteNav(); const weeklyNavigationTitles = getWeeklyNavigationTitles(title); if (!weeklyNavigationTitles) return false; @@ -458,36 +470,30 @@ export const toggleFeature = ( buttonContainer.style.display = "flex"; buttonContainer.style.justifyContent = "space-between"; buttonContainer.style.marginBottom = "32px"; - buttonContainer.id = "roamjs-weekly-mode-nav"; + buttonContainer.id = WEEKLY_NOTE_NAV_ID; insertionPoint.insertAdjacentElement("afterend", buttonContainer); - const makeButton = ( - pagename: string, - label: string, - direction: "left" | "right" - ) => { - const button = document.createElement("button"); - button.className = "bp3-button bp3-minimal bp3-outlined"; - button.type = "button"; - button.onclick = () => navigateToPage(pagename); - - const icon = document.createElement("span"); - icon.className = `bp3-icon-standard bp3-icon-arrow-${direction}`; - icon.setAttribute("aria-hidden", "true"); - - const text = document.createElement("span"); - text.innerText = label; - - if (direction === "left") { - button.append(icon, text); - } else { - button.append(text, icon); - } - - buttonContainer.appendChild(button); - }; - makeButton(weeklyNavigationTitles.prevTitle, "Last Week", "left"); - makeButton(weeklyNavigationTitles.nextTitle, "Next Week", "right"); + ReactDOM.render( + React.createElement( + React.Fragment, + null, + React.createElement(Button, { + icon: "arrow-left", + minimal: true, + onClick: () => navigateToPage(weeklyNavigationTitles.prevTitle), + outlined: true, + text: "Last Week", + }), + React.createElement(Button, { + minimal: true, + onClick: () => navigateToPage(weeklyNavigationTitles.nextTitle), + outlined: true, + rightIcon: "arrow-right", + text: "Next Week", + }) + ), + buttonContainer + ); return true; }; @@ -500,7 +506,7 @@ export const toggleFeature = ( }; const hashListener = (newUrl: string) => { - document.getElementById("roamjs-weekly-mode-nav")?.remove?.(); + removeWeeklyNoteNav(); const urlUid = newUrl.match(/\/page\/(.*)$/)?.[1]; if (urlUid) { const title = getPageTitleByPageUid(urlUid); @@ -519,6 +525,7 @@ export const toggleFeature = ( unloads.add(() => window.removeEventListener("hashchange", wrappedListener) ); + unloads.add(removeWeeklyNoteNav); const autoLoad = getFullTreeByParentUid( getPageUidByPageTitle(CONFIG) @@ -546,6 +553,7 @@ export const toggleFeature = ( }; } }, + removeCallback: removeWeeklyNoteNav, }); unloads.add(() => h1Observer.disconnect()); } else { From 88c9d65c0cc967080d8727e2c2c13f78dd478f5c Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 28 Jun 2026 15:23:38 -0600 Subject: [PATCH 4/8] Extract weekly nav React component --- src/features/WeeklyNoteNav.tsx | 33 +++++++++++++++++++++++++++++++ src/features/weekly-notes.ts | 36 ++++++++++------------------------ 2 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 src/features/WeeklyNoteNav.tsx diff --git a/src/features/WeeklyNoteNav.tsx b/src/features/WeeklyNoteNav.tsx new file mode 100644 index 0000000..73d4ea1 --- /dev/null +++ b/src/features/WeeklyNoteNav.tsx @@ -0,0 +1,33 @@ +import { Button } from "@blueprintjs/core"; +import React from "react"; + +type WeeklyNoteNavProps = { + nextTitle: string; + onNavigate: (pageName: string) => void; + prevTitle: string; +}; + +const WeeklyNoteNav = ({ + nextTitle, + onNavigate, + prevTitle, +}: WeeklyNoteNavProps) => ( +
+
+); + +export default WeeklyNoteNav; diff --git a/src/features/weekly-notes.ts b/src/features/weekly-notes.ts index e5b84a3..042b344 100644 --- a/src/features/weekly-notes.ts +++ b/src/features/weekly-notes.ts @@ -1,4 +1,3 @@ -import { Button } from "@blueprintjs/core"; import setDay from "date-fns/setDay"; import _dateFnsFormat from "date-fns/format"; import _parse from "date-fns/parse"; @@ -34,6 +33,7 @@ import { Field, UnionField, } from "roamjs-components/components/ConfigPanels/types"; +import WeeklyNoteNav from "./WeeklyNoteNav"; const ID = "weekly-notes"; const DAYS = [ @@ -466,33 +466,17 @@ export const toggleFeature = ( const headerContainer = header.closest(`.${ROAM_TITLE_CONTAINER_CLASS}`); const insertionPoint = headerContainer || header; - const buttonContainer = document.createElement("div"); - buttonContainer.style.display = "flex"; - buttonContainer.style.justifyContent = "space-between"; - buttonContainer.style.marginBottom = "32px"; - buttonContainer.id = WEEKLY_NOTE_NAV_ID; - insertionPoint.insertAdjacentElement("afterend", buttonContainer); + const navContainer = document.createElement("div"); + navContainer.id = WEEKLY_NOTE_NAV_ID; + insertionPoint.insertAdjacentElement("afterend", navContainer); ReactDOM.render( - React.createElement( - React.Fragment, - null, - React.createElement(Button, { - icon: "arrow-left", - minimal: true, - onClick: () => navigateToPage(weeklyNavigationTitles.prevTitle), - outlined: true, - text: "Last Week", - }), - React.createElement(Button, { - minimal: true, - onClick: () => navigateToPage(weeklyNavigationTitles.nextTitle), - outlined: true, - rightIcon: "arrow-right", - text: "Next Week", - }) - ), - buttonContainer + React.createElement(WeeklyNoteNav, { + nextTitle: weeklyNavigationTitles.nextTitle, + onNavigate: navigateToPage, + prevTitle: weeklyNavigationTitles.prevTitle, + }), + navContainer ); return true; From e8711c38f7fcc5ae2c00c7fcec721e2c50ab5cfb Mon Sep 17 00:00:00 2001 From: Michael Gartner Date: Sun, 28 Jun 2026 15:25:50 -0600 Subject: [PATCH 5/8] Document agent UI conventions --- Agents.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Agents.md diff --git a/Agents.md b/Agents.md new file mode 100644 index 0000000..8755d38 --- /dev/null +++ b/Agents.md @@ -0,0 +1,17 @@ +# Agent Conventions + +## UI Mounted Into Roam DOM + +- Prefer real React components for UI, even when Roam owns the surrounding DOM. +- When an observer or Roam DOM hook needs to insert UI, create only an empty mount element imperatively, then render a React component into it. +- Unmount React with `ReactDOM.unmountComponentAtNode` before removing an imperatively inserted mount element. + +## Styling + +- Use Tailwind utility classes for layout, spacing, and sizing instead of inline `style` mutations. +- Keep imperative DOM code focused on locating insertion points and creating mount anchors, not presentation. + +## Blueprint Controls + +- Use Blueprint React components and props instead of manually building Blueprint class names. +- For buttons, prefer `