From 9103c57907a28e0a486d2ad5d60b772717a32cd0 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 11:57:42 -0400 Subject: [PATCH 01/12] =?UTF-8?q?ENG-2186=20Grammar=20=E2=80=BA=20Nodes=20?= =?UTF-8?q?drill-down=20and=20settings=20navigation=20primitive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the per-node-type tabs from the settings rail and drill into a node type from Grammar › Nodes instead: node list → node type → Index/Template sub-pages. Navigation state is a path owned by SettingsDialog, exposed through context. Builds on ENG-2189's settingsTabs registry rather than duplicating it, so tab ids and their legacy aliases stay in one place. A saved deep link carrying a node type uid, which used to select that node's own rail tab, now opens the node's page inside Grammar › Nodes. Also lands ENG-2188: NodeConfig's seven inner tabs become one vertical page grouped Identity / Recognition / Creation, reusing ENG-2189's SettingsGroup, with Index and Template as drill-down rows to their own pages. Co-Authored-By: Claude Opus 5 --- .../settings/DiscourseNodeConfigPanel.tsx | 28 +- .../settings/DiscourseNodeSuggestiveRules.tsx | 27 +- .../components/settings/GrammarNodesRoute.tsx | 74 ++++ .../src/components/settings/NodeConfig.tsx | 380 ++++++++---------- .../src/components/settings/NodeIndexPage.tsx | 26 ++ .../components/settings/NodeTemplatePage.tsx | 27 ++ .../roam/src/components/settings/Settings.tsx | 73 ++-- .../components/EphemeralBlocksPanel.tsx | 46 ++- .../components/SettingsDrillDownRow.tsx | 31 ++ .../navigation/SettingsNavContext.tsx | 53 +++ .../navigation/SettingsPageHeader.tsx | 76 ++++ .../settings/utils/settingsNavigation.ts | 89 ++++ apps/roam/src/styles/settingsStyles.css | 66 +++ 13 files changed, 680 insertions(+), 316 deletions(-) create mode 100644 apps/roam/src/components/settings/GrammarNodesRoute.tsx create mode 100644 apps/roam/src/components/settings/NodeIndexPage.tsx create mode 100644 apps/roam/src/components/settings/NodeTemplatePage.tsx create mode 100644 apps/roam/src/components/settings/components/SettingsDrillDownRow.tsx create mode 100644 apps/roam/src/components/settings/navigation/SettingsNavContext.tsx create mode 100644 apps/roam/src/components/settings/navigation/SettingsPageHeader.tsx create mode 100644 apps/roam/src/components/settings/utils/settingsNavigation.ts diff --git a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx index e543966c3..b1b3ae4fa 100644 --- a/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx @@ -8,9 +8,10 @@ import { Tooltip, } from "@blueprintjs/core"; import React, { useState } from "react"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import getDiscourseNodes, { + excludeDefaultNodes, +} from "~/utils/getDiscourseNodes"; import refreshConfigTree from "~/utils/refreshConfigTree"; -import type { CustomField } from "roamjs-components/components/ConfigPanels/types"; import posthog from "posthog-js"; import getDiscourseRelations, { type DiscourseRelation, @@ -24,20 +25,12 @@ import { } from "./utils/accessors"; import { GLOBAL_KEYS } from "./utils/settingKeys"; import { invalidateDiscourseNodeTypeCaches } from "~/utils/discourseNodeTypeCache"; +import { useSettingsNav } from "./navigation/SettingsNavContext"; -type DiscourseNodeConfigPanelProps = React.ComponentProps< - CustomField["options"]["component"] -> & { - isPopup?: boolean; - setSelectedTabId: (id: string) => void; -}; - -const DiscourseNodeConfigPanel: React.FC = ({ - isPopup, - setSelectedTabId, -}) => { +const DiscourseNodeConfigPanel: React.FC = () => { + const { push } = useSettingsNav(); const [nodes, setNodes] = useState(() => - getDiscourseNodes().filter((n) => n.backedBy === "user"), + getDiscourseNodes().filter(excludeDefaultNodes), ); const [label, setLabel] = useState(""); const [isCreating, setIsCreating] = useState(false); @@ -52,11 +45,8 @@ const DiscourseNodeConfigPanel: React.FC = ({ >([]); const [nodeTypeIdToDelete, setNodeTypeIdToDelete] = useState(""); const navigateToNode = (uid: string) => { - if (isPopup) { - setSelectedTabId(uid); - } else { - window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid } }); - } + push(uid); + posthog.capture("Settings: Node Type Opened", { nodeTypeUid: uid }); }; const createNodeType = async (): Promise => { diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index 67a546c92..bf20efe5b 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -1,7 +1,5 @@ import React, { useState, useMemo } from "react"; import { Button, Intent } from "@blueprintjs/core"; -import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; -import getSubTree from "roamjs-components/util/getSubTree"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; import extractRef from "roamjs-components/util/extractRef"; import { getAllDiscourseNodesSince } from "~/utils/getAllDiscourseNodesSince"; @@ -14,10 +12,8 @@ import { import { DISCOURSE_NODE_KEYS, SUGGESTIVE_RULES_KEYS, - TEMPLATE_SETTING_KEYS, } from "~/components/settings/utils/settingKeys"; import { RenderRoamBlock } from "~/utils/roamReactComponents"; -import { ROAM_DOCS, withDocsLink } from "./utils/docs"; const DiscourseNodeSuggestiveRules = ({ node, @@ -34,15 +30,6 @@ const DiscourseNodeSuggestiveRules = ({ [embeddingRef], ); - const templateUid = useMemo( - () => - getSubTree({ - parentUid: nodeUid, - key: "Template", - }).uid || "", - [nodeUid], - ); - const [isUpdating, setIsUpdating] = useState(false); const handleUpdateEmbeddings = async (): Promise => { @@ -68,19 +55,7 @@ const DiscourseNodeSuggestiveRules = ({ }; return ( -
- - +
= { + [nodeConfigSegmentIds.index]: "Index", + [nodeConfigSegmentIds.template]: "Template", +}; + +const GrammarNodesRoute = ({ + onloadArgs, +}: { + onloadArgs: OnloadArgs; +}): JSX.Element => { + const { segments, goToDepth } = useSettingsNav(); + const nodes = getDiscourseNodes().filter(excludeDefaultNodes); + + const [nodeTypeUid, subPage] = segments; + const node = nodeTypeUid + ? nodes.find((n) => n.type === nodeTypeUid) + : undefined; + + // A deleted node type or stale deep link resolves to nothing; return to the list. + const isStalePath = Boolean(nodeTypeUid) && !node; + useEffect(() => { + if (isStalePath) goToDepth(0); + }, [isStalePath, goToDepth]); + + const resolveLabel = (segment: string, segmentIndex: number): string => + segmentIndex === 0 + ? (nodes.find((n) => n.type === segment)?.text ?? segment) + : (SUB_PAGE_LABELS[segment] ?? segment); + + // Sub-pages fall through to the stylesheet's default dot colour. + const dotColor = subPage + ? undefined + : formatHexColor(node?.canvasSettings?.color ?? "") || undefined; + + return ( +
+ +
+ {!node ? ( + + ) : subPage === nodeConfigSegmentIds.index ? ( + + ) : subPage === nodeConfigSegmentIds.template ? ( + + ) : ( + + )} +
+
+ ); +}; + +export default GrammarNodesRoute; diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 92e68d5dd..4cc283cfd 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -1,13 +1,9 @@ import React, { useState, useCallback, useEffect, useRef } from "react"; import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; -import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; import { getSubTree } from "roamjs-components/util"; import Description from "~/components/settings/SettingsDescription"; import { Label, - Tabs, - Tab, - TabId, InputGroup, ControlGroup, Tooltip, @@ -18,8 +14,6 @@ import DiscourseNodeAttributes from "./DiscourseNodeAttributes"; import DiscourseNodeCanvasSettings, { formatHexColor, } from "./DiscourseNodeCanvasSettings"; -import DiscourseNodeIndex from "./DiscourseNodeIndex"; -import { OnloadArgs } from "roamjs-components/types"; import setInputSetting from "roamjs-components/util/setInputSetting"; import { getDiscourseNodeSetting, @@ -30,7 +24,6 @@ import { CANVAS_KEYS, DISCOURSE_NODE_KEYS, SPECIFICATION_KEYS, - TEMPLATE_SETTING_KEYS, } from "~/components/settings/utils/settingKeys"; import DiscourseNodeSuggestiveRules from "./DiscourseNodeSuggestiveRules"; import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors"; @@ -40,6 +33,10 @@ import { DiscourseNodeSelectPanel, } from "./components/BlockPropSettingPanels"; import { ROAM_DOCS, withDocsLink } from "./utils/docs"; +import { SettingsGroup } from "./components/SettingsHeadings"; +import SettingsDrillDownRow from "./components/SettingsDrillDownRow"; +import { useSettingsNav } from "./navigation/SettingsNavContext"; +import { nodeConfigSegmentIds } from "./utils/settingsNavigation"; export const getCleanTagText = (tag: string): string => { return tag.replace(/^#+/, "").trim().toUpperCase(); @@ -158,13 +155,7 @@ const generateTagPlaceholder = (node: DiscourseNode): string => { return `#${nodeTextPrefix}-candidate`; // Evidence = #evi-candidate }; -const NodeConfig = ({ - node, - onloadArgs, -}: { - node: DiscourseNode; - onloadArgs: OnloadArgs; -}) => { +const NodeConfig = ({ node }: { node: DiscourseNode }) => { const getUid = (key: string) => getSubTree({ parentUid: node.type, @@ -174,19 +165,17 @@ const NodeConfig = ({ const descriptionUid = getUid("Description"); const shortcutUid = getUid("Shortcut"); const tagUid = getUid("Tag"); - const templateUid = getUid("Template"); const overlayUid = getUid("Overlay"); const canvasUid = getUid("Canvas"); const graphOverviewUid = getUid("Graph Overview"); const specificationUid = getUid("Specification"); - const indexUid = getUid("Index"); const suggestiveRulesUid = getUid("Suggestive Rules"); const attributeNode = getSubTree({ parentUid: node.type, key: "Attributes", }); - const [selectedTabId, setSelectedTabId] = useState("general"); + const nav = useSettingsNav(); const [tagError, setTagError] = useState(""); const [formatError, setFormatError] = useState(""); const [shortcutError, setShortcutError] = useState(""); @@ -292,220 +281,169 @@ const NodeConfig = ({ ); return ( - <> - setSelectedTabId(id)} - selectedTabId={selectedTabId} - renderActiveTabPanelOnly={true} - > - - - -
- -
- -
- } +
+ + - - -
- } + + - + + + - - -
- } + description={withDocsLink( + `DEPRECATED - Use specification instead. The format ${node.text} pages should have.`, + ROAM_DOCS.grammarNodes, + )} + settingKeys={[DISCOURSE_NODE_KEYS.format]} + initialValue={node.format} + error={formatError} + onChange={setFormatValue} + order={3} + parentUid={node.type} + uid={formatUid} + /> + + nav.push(nodeConfigSegmentIds.index)} /> - + + + + - - - } + description={withDocsLink( + `The template that auto fills ${node.text} page when generated.`, + ROAM_DOCS.creatingNodes, + )} + buttonText="Edit template" + onClick={() => nav.push(nodeConfigSegmentIds.template)} /> - - >( - node.type, - [DISCOURSE_NODE_KEYS.attributes], - )} - /> - c.text)} - initialValue={ - getDiscourseNodeSetting(node.type, [ - DISCOURSE_NODE_KEYS.overlay, - ]) ?? "" - } - order={0} - parentUid={node.type} - uid={overlayUid} - /> - - } + + + + >( + node.type, + [DISCOURSE_NODE_KEYS.attributes], + )} /> - - - - + c.text)} + initialValue={ + getDiscourseNodeSetting(node.type, [ + DISCOURSE_NODE_KEYS.overlay, + ]) ?? "" } + order={0} + parentUid={node.type} + uid={overlayUid} /> - {isSyncEnabled() && ( - - - - } + + + + + + + + {isSyncEnabled() && ( + + - )} - - + + )} + ); }; diff --git a/apps/roam/src/components/settings/NodeIndexPage.tsx b/apps/roam/src/components/settings/NodeIndexPage.tsx new file mode 100644 index 000000000..c39558a82 --- /dev/null +++ b/apps/roam/src/components/settings/NodeIndexPage.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { getSubTree } from "roamjs-components/util"; +import { OnloadArgs } from "roamjs-components/types"; +import { DiscourseNode } from "~/utils/getDiscourseNodes"; +import DiscourseNodeIndex from "./DiscourseNodeIndex"; + +const NodeIndexPage = ({ + node, + onloadArgs, +}: { + node: DiscourseNode; + onloadArgs: OnloadArgs; +}): JSX.Element => { + const indexUid = getSubTree({ parentUid: node.type, key: "Index" }).uid; + return ( +
+ +
+ ); +}; + +export default NodeIndexPage; diff --git a/apps/roam/src/components/settings/NodeTemplatePage.tsx b/apps/roam/src/components/settings/NodeTemplatePage.tsx new file mode 100644 index 000000000..94947976d --- /dev/null +++ b/apps/roam/src/components/settings/NodeTemplatePage.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { getSubTree } from "roamjs-components/util"; +import { DiscourseNode } from "~/utils/getDiscourseNodes"; +import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; +import { TEMPLATE_SETTING_KEYS } from "~/components/settings/utils/settingKeys"; +import { ROAM_DOCS, withDocsLink } from "./utils/docs"; + +const NodeTemplatePage = ({ node }: { node: DiscourseNode }): JSX.Element => { + const templateUid = getSubTree({ parentUid: node.type, key: "Template" }).uid; + return ( +
+ +
+ ); +}; + +export default NodeTemplatePage; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index e2f0e7cba..fcba1a38e 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -1,4 +1,11 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; import { OnloadArgs } from "roamjs-components/types"; import { Classes, @@ -16,11 +23,6 @@ import discourseConfigRef from "~/utils/discourseConfigRef"; import DiscourseGraphExport from "./ExportSettings"; import QuerySettings from "./QuerySettings"; import AdminPanel from "./AdminPanel"; -import DiscourseNodeConfigPanel from "./DiscourseNodeConfigPanel"; -import getDiscourseNodes, { - excludeDefaultNodes, -} from "~/utils/getDiscourseNodes"; -import NodeConfig from "./NodeConfig"; import PreferencesGeneral from "./PreferencesGeneral"; import PreferencesStyling from "./PreferencesStyling"; import LeftSidebarSettings from "./LeftSidebarSettings"; @@ -32,7 +34,14 @@ import { getVersionWithDate } from "~/utils/getVersion"; import posthog from "posthog-js"; import { bulkReadSettings } from "./utils/accessors"; import { onSettingChange, settingKeys } from "./utils/settingsEmitter"; -import { SETTINGS_TAB_IDS, resolveSettingsTabId } from "./utils/settingsTabs"; +import { SETTINGS_TAB_IDS } from "./utils/settingsTabs"; +import { + resolveInitialSettingsPath, + settingsNavReducer, + tabIdOf, +} from "./utils/settingsNavigation"; +import { SettingsNavProvider } from "./navigation/SettingsNavContext"; +import GrammarNodesRoute from "./GrammarNodesRoute"; const SectionHeader = ({ children }: { children: React.ReactNode }) => (
@@ -77,10 +86,15 @@ export const SettingsDialog = ({ const relationsNode = grammarNode?.children.find( (node) => node.text === "relations", ); - const nodesNode = grammarNode?.children.find((node) => node.text === "nodes"); - const nodes = getDiscourseNodes().filter(excludeDefaultNodes); - const [activeTabId, setActiveTabId] = useState(() => - resolveSettingsTabId(selectedTabId), + const [path, dispatch] = useReducer( + settingsNavReducer, + selectedTabId, + resolveInitialSettingsPath, + ); + const activeTabId = tabIdOf(path); + const selectTab = useCallback( + (tabId: string) => dispatch({ type: "select-tab", tabId }), + [], ); // eslint-disable-next-line react-hooks/exhaustive-deps const settings = useMemo(() => bulkReadSettings(), [activeTabId]); @@ -98,15 +112,14 @@ export const SettingsDialog = ({ const { versionStamp } = getVersionWithDate(); const openAdminPanel = (): void => { setShowAdminPanel(true); - setActiveTabId(SETTINGS_TAB_IDS.admin); + selectTab(SETTINGS_TAB_IDS.admin); posthog.capture("Settings: Admin Panel Opened from Footer"); }; + const initialTabId = useRef(activeTabId).current; useEffect(() => { - posthog.capture("Settings: Dialog Opened", { - initialTabId: String(resolveSettingsTabId(selectedTabId)), - }); - }, [selectedTabId]); + posthog.capture("Settings: Dialog Opened", { initialTabId }); + }, [initialTabId]); useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { @@ -114,14 +127,14 @@ export const SettingsDialog = ({ e.stopPropagation(); e.preventDefault(); setShowAdminPanel(true); - setActiveTabId(SETTINGS_TAB_IDS.admin); + selectTab(SETTINGS_TAB_IDS.admin); posthog.capture("Settings: Admin Panel Opened via Shortcut"); } }; window.addEventListener("keydown", handleKeyPress); return () => window.removeEventListener("keydown", handleKeyPress); - }, []); + }, [selectTab]); return ( { - setActiveTabId(id); + selectTab(String(id)); posthog.capture("Settings: Tab Opened", { tabId: String(id), }); @@ -238,16 +251,10 @@ export const SettingsDialog = ({ + + + } /> } /> - Node types - {nodes.map((n) => ( - } - /> - ))} Advanced 0 ? dv : [{ text: " " }]; - void createBlock({ + const created = createBlock({ node: { text: TEMPLATE_BUFFER_TEXT, uid: newUid, children: seed }, parentUid: nodeType, order: "last", @@ -125,23 +125,44 @@ const DualWriteBlocksPanel = ({ return () => { cancelled = true; setBufferUid(null); - void deleteBlock(newUid); + // Deleting a uid whose createBlock is still in flight orphans the buffer block. + void created.then( + () => deleteBlock(newUid), + () => undefined, + ); }; }, [isNewStore, nodeType]); + const writeChanges = useCallback(() => { + if (!renderUid) return; + const tree = getFullTreeByParentUid(renderUid); + const serialized = serializeBlockTree(tree.children); + setDiscourseNodeSetting(nodeType, settingKeys, serialized); + if (isNewStore && renderUid !== uid) { + const legacyTree = getFullTreeByParentUid(uid); + mirrorBufferToLegacyChildren(tree.children, legacyTree.children, uid); + } + }, [renderUid, uid, isNewStore, nodeType, settingKeys]); + + // In a ref so unmount cleanup can flush without re-running on every identity change. + const writeChangesRef = useRef(writeChanges); + writeChangesRef.current = writeChanges; + + const flushPendingChanges = useCallback(() => { + if (!debounceRef.current) return; + window.clearTimeout(debounceRef.current); + debounceRef.current = 0; + writeChangesRef.current(); + }, []); + const handleChange = useCallback(() => { if (!renderUid) return; window.clearTimeout(debounceRef.current); debounceRef.current = window.setTimeout(() => { - const tree = getFullTreeByParentUid(renderUid); - const serialized = serializeBlockTree(tree.children); - setDiscourseNodeSetting(nodeType, settingKeys, serialized); - if (isNewStore && renderUid !== uid) { - const legacyTree = getFullTreeByParentUid(uid); - mirrorBufferToLegacyChildren(tree.children, legacyTree.children, uid); - } + debounceRef.current = 0; + writeChangesRef.current(); }, DEBOUNCE_MS); - }, [renderUid, uid, isNewStore, nodeType, settingKeys]); + }, [renderUid]); useEffect(() => { const el = containerRef.current; @@ -181,13 +202,14 @@ const DualWriteBlocksPanel = ({ return () => { cancelled = true; - window.clearTimeout(debounceRef.current); + // Navigating away lands right after a keystroke, and the buffer block is deleted next. + flushPendingChanges(); if (pullWatchArgsRef.current) { window.roamAlphaAPI.data.removePullWatch(...pullWatchArgsRef.current); pullWatchArgsRef.current = null; } }; - }, [renderUid, handleChange]); + }, [renderUid, handleChange, flushPendingChanges]); return ( <> diff --git a/apps/roam/src/components/settings/components/SettingsDrillDownRow.tsx b/apps/roam/src/components/settings/components/SettingsDrillDownRow.tsx new file mode 100644 index 000000000..1bb0b2c84 --- /dev/null +++ b/apps/roam/src/components/settings/components/SettingsDrillDownRow.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Button, Intent, Label } from "@blueprintjs/core"; +import Description from "~/components/settings/SettingsDescription"; + +const SettingsDrillDownRow = ({ + title, + description, + buttonText, + onClick, +}: { + title: string; + description: React.ReactNode; + buttonText: string; + onClick: () => void; +}): JSX.Element => ( + +); + +export default SettingsDrillDownRow; diff --git a/apps/roam/src/components/settings/navigation/SettingsNavContext.tsx b/apps/roam/src/components/settings/navigation/SettingsNavContext.tsx new file mode 100644 index 000000000..6bfc1f450 --- /dev/null +++ b/apps/roam/src/components/settings/navigation/SettingsNavContext.tsx @@ -0,0 +1,53 @@ +import React, { createContext, useContext, useMemo } from "react"; +import { + depthOf, + segmentsOf, + type SettingsNavAction, + type SettingsPath, +} from "../utils/settingsNavigation"; + +export type SettingsNavValue = { + path: SettingsPath; + depth: number; + segments: readonly string[]; + push: (segment: string) => void; + pop: () => void; + goToDepth: (depth: number) => void; +}; + +const SettingsNavContext = createContext(null); + +export const SettingsNavProvider = ({ + path, + dispatch, + children, +}: { + path: SettingsPath; + dispatch: React.Dispatch; + children: React.ReactNode; +}): JSX.Element => { + const value = useMemo( + () => ({ + path, + depth: depthOf(path), + segments: segmentsOf(path), + push: (segment) => dispatch({ type: "push", segment }), + pop: () => dispatch({ type: "pop" }), + goToDepth: (depth) => dispatch({ type: "truncate", depth }), + }), + [path, dispatch], + ); + return ( + + {children} + + ); +}; + +export const useSettingsNav = (): SettingsNavValue => { + const context = useContext(SettingsNavContext); + if (!context) { + throw new Error("useSettingsNav must be used within a SettingsNavProvider"); + } + return context; +}; diff --git a/apps/roam/src/components/settings/navigation/SettingsPageHeader.tsx b/apps/roam/src/components/settings/navigation/SettingsPageHeader.tsx new file mode 100644 index 000000000..9bdb4b3ee --- /dev/null +++ b/apps/roam/src/components/settings/navigation/SettingsPageHeader.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { Button } from "@blueprintjs/core"; +import { buildBreadcrumbTrail } from "../utils/settingsNavigation"; +import { useSettingsNav } from "./SettingsNavContext"; + +/** `ancestorLabels` are static crumbs above the tab itself: shown in the trail, not navigable. */ +const SettingsPageHeader = ({ + ancestorLabels, + rootLabel, + resolveLabel, + dotColor, +}: { + ancestorLabels: readonly string[]; + rootLabel: string; + resolveLabel: (segment: string, segmentIndex: number) => string; + dotColor?: string; +}): JSX.Element | null => { + const { path, depth, pop, goToDepth } = useSettingsNav(); + if (depth === 0) return null; + + // depth > 0, so the trail always has a root crumb plus at least one segment. + const trail = buildBreadcrumbTrail({ path, rootLabel, resolveLabel }); + const current = trail[trail.length - 1]; + const parent = trail[trail.length - 2]; + + return ( +
+
+
+
+ + {current.label} +
+
+ {ancestorLabels.map((label) => ( + {`${label} › `} + ))} + {trail.map((crumb) => ( + + {crumb.isCurrent ? ( + crumb.label + ) : ( + <> + goToDepth(crumb.depth)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") + goToDepth(crumb.depth); + }} + > + {crumb.label} + + {" › "} + + )} + + ))} +
+
+ ); +}; + +export default SettingsPageHeader; diff --git a/apps/roam/src/components/settings/utils/settingsNavigation.ts b/apps/roam/src/components/settings/utils/settingsNavigation.ts new file mode 100644 index 000000000..c15989ef0 --- /dev/null +++ b/apps/roam/src/components/settings/utils/settingsNavigation.ts @@ -0,0 +1,89 @@ +import type { TabId } from "@blueprintjs/core"; +import { SETTINGS_TAB_IDS, resolveSettingsTabId } from "./settingsTabs"; + +/** Settings dialog route: `["grammar-nodes", nodeTypeUid, "template"]`, tab first. */ +export type SettingsPath = readonly string[]; + +export type SettingsNavAction = + | { type: "select-tab"; tabId: string } + | { type: "push"; segment: string } + | { type: "pop" } + | { type: "truncate"; depth: number }; + +export const nodeConfigSegmentIds = { + index: "index", + template: "template", +} as const; + +export const rootPath = (tabId: string): SettingsPath => [tabId]; + +export const tabIdOf = (path: SettingsPath): string => path[0] ?? ""; + +/** Drill-down segments below the tab; 0 is the tab's own root page. */ +export const depthOf = (path: SettingsPath): number => + Math.max(0, path.length - 1); + +export const segmentsOf = (path: SettingsPath): readonly string[] => + path.slice(1); + +export const settingsNavReducer = ( + state: SettingsPath, + action: SettingsNavAction, +): SettingsPath => { + switch (action.type) { + case "select-tab": + return action.tabId === tabIdOf(state) && state.length === 1 + ? state + : rootPath(action.tabId); + case "push": + return [...state, action.segment]; + case "pop": + return state.length <= 1 ? state : state.slice(0, -1); + case "truncate": { + const length = Math.max(1, action.depth + 1); + return length >= state.length ? state : state.slice(0, length); + } + } +}; + +const KNOWN_TAB_IDS: readonly string[] = Object.values(SETTINGS_TAB_IDS); + +/** + * Resolves the tab a caller asked for, including the aliases in `settingsTabs`. An id that + * is not a tab at all is a node type uid from when every node type had its own rail tab; + * those links now open the node's page inside Grammar > Nodes. + */ +export const resolveInitialSettingsPath = ( + selectedTabId?: TabId, +): SettingsPath => { + const tabId = String(resolveSettingsTabId(selectedTabId)); + return KNOWN_TAB_IDS.includes(tabId) + ? rootPath(tabId) + : [SETTINGS_TAB_IDS.grammarNodes, tabId]; +}; + +export type SettingsCrumb = { + label: string; + depth: number; + isCurrent: boolean; +}; + +export const buildBreadcrumbTrail = ({ + path, + rootLabel, + resolveLabel, +}: { + path: SettingsPath; + rootLabel: string; + resolveLabel: (segment: string, segmentIndex: number) => string; +}): SettingsCrumb[] => { + const segments = segmentsOf(path); + return [ + { label: rootLabel, depth: 0, isCurrent: segments.length === 0 }, + ...segments.map((segment, index) => ({ + label: resolveLabel(segment, index), + depth: index + 1, + isCurrent: index === segments.length - 1, + })), + ]; +}; diff --git a/apps/roam/src/styles/settingsStyles.css b/apps/roam/src/styles/settingsStyles.css index 07b378ff0..1e7975238 100644 --- a/apps/roam/src/styles/settingsStyles.css +++ b/apps/roam/src/styles/settingsStyles.css @@ -35,4 +35,70 @@ display: flex; flex-direction: column; gap: 4px; + +:root { + /* Brand secondary, mirroring packages/tailwind-config. */ + --dg-secondary: #5f57c0; +} + +/* Hand-rolled, not Tailwind: apps/roam has no Tailwind build (tailwind.config.ts sets + `content: []`), so a utility class only applies if Roam's own stylesheet ships it. */ +.dg-settings-route { + display: flex; + flex-direction: column; + height: 100%; +} + +.dg-settings-route__body { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} + +.dg-settings-page-header { + display: block; + /* Shrinkable by default: without this the header squashes and overlaps the scrolling body. */ + flex-shrink: 0; + margin-bottom: 20px; +} + +.dg-settings-page-header__back { + display: block; + margin-bottom: 4px; +} + +.dg-settings-page-header__title { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 2px; + font-size: 24px; + font-weight: 600; + line-height: 1.3; +} + +.dg-settings-page-header__dot { + flex-shrink: 0; + width: 12px; + height: 12px; + border-radius: 9999px; + /* Node pages override this inline with the node's own colour. */ + background-color: var(--dg-secondary); +} + +.dg-settings-page-header__trail { + font-size: 14px; + line-height: 1.4; + opacity: 0.6; +} + +.dg-settings-page-header__crumb { + cursor: pointer; +} + +.dg-settings-node-page { + display: flex; + flex-direction: column; + gap: 28px; + padding: 2px; } From dd1462552211fdf495aea984abbdf0447c3dfcc9 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 12:18:46 -0400 Subject: [PATCH 02/12] Flush pending debounced writes when navigating away Back and the breadcrumb unmount an editor immediately, so BaseTextPanel and DiscourseNodeColorSetting now run their pending write during cleanup instead of clearing the timer. Both keep their existing validation guard, so an invalid tag, format or shortcut is still not persisted. Co-Authored-By: Claude Opus 5 --- apps/roam/src/components/settings/NodeConfig.tsx | 10 ++++++++++ .../components/BlockPropSettingPanels.tsx | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 4cc283cfd..24dea596a 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -78,11 +78,18 @@ const DiscourseNodeColorSetting = ({ [canvasUid, nodeType], ); + // Navigating away unmounts mid-debounce, so the pending colour is written rather than dropped. + const pendingColorRef = useRef(null); + const persistColorValueRef = useRef(persistColorValue); + persistColorValueRef.current = persistColorValue; useEffect(() => { return () => { if (!colorWriteTimeoutRef.current) return; window.clearTimeout(colorWriteTimeoutRef.current); + const pending = pendingColorRef.current; + pendingColorRef.current = null; + if (pending !== null) persistColorValueRef.current(pending); }; }, []); @@ -91,8 +98,10 @@ const DiscourseNodeColorSetting = ({ window.clearTimeout(colorWriteTimeoutRef.current); colorWriteTimeoutRef.current = null; } + pendingColorRef.current = colorValue; colorWriteTimeoutRef.current = window.setTimeout(() => { persistColorValue(colorValue); + pendingColorRef.current = null; colorWriteTimeoutRef.current = null; }, COLOR_WRITE_DEBOUNCE_MS); }; @@ -131,6 +140,7 @@ const DiscourseNodeColorSetting = ({ window.clearTimeout(colorWriteTimeoutRef.current); colorWriteTimeoutRef.current = null; } + pendingColorRef.current = null; setColor(""); persistColorValue(""); }} diff --git a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx index 82b2dd9a2..491a52177 100644 --- a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx +++ b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx @@ -148,8 +148,15 @@ const BaseTextPanel = ({ }); const syncToBlock = hasBlockSync ? rawSyncToBlock : undefined; + // Navigating away unmounts mid-debounce, so the pending write runs rather than being dropped. + const pendingWriteRef = useRef<(() => void) | null>(null); useEffect(() => { - return () => window.clearTimeout(debounceRef.current); + return () => { + window.clearTimeout(debounceRef.current); + const pending = pendingWriteRef.current; + pendingWriteRef.current = null; + pending?.(); + }; }, []); const handleChange = ( @@ -160,6 +167,12 @@ const BaseTextPanel = ({ onChange?.(newValue); window.clearTimeout(debounceRef.current); + pendingWriteRef.current = () => { + if (errorRef.current) return; + syncToBlock?.(newValue); + refreshConfigTree(); + setter(settingKeys, newValue); + }; debounceRef.current = window.setTimeout(() => { if (errorRef.current) return; syncToBlock?.(newValue); @@ -167,6 +180,7 @@ const BaseTextPanel = ({ if (errorRef.current) return; refreshConfigTree(); setter(settingKeys, newValue); + pendingWriteRef.current = null; }, 100); }, DEBOUNCE_MS); }; From 64f85006cfef97d0d92f6cde571f190cebd4fdc5 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 13:41:39 -0400 Subject: [PATCH 03/12] Show the template under Suggestive mode behind a collapsed toggle The embedding block ref is copied out of the template, so the editor is useful here. It is collapsed by default to keep the node page short, and Collapse unmounts its children, so the ephemeral buffer block is only created while open. Co-Authored-By: Claude Opus 5 --- .../settings/DiscourseNodeSuggestiveRules.tsx | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index bf20efe5b..87ce70a9a 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -1,5 +1,7 @@ import React, { useState, useMemo } from "react"; -import { Button, Intent } from "@blueprintjs/core"; +import { Button, Collapse, Intent } from "@blueprintjs/core"; +import DualWriteBlocksPanel from "./components/EphemeralBlocksPanel"; +import getSubTree from "roamjs-components/util/getSubTree"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; import extractRef from "roamjs-components/util/extractRef"; import { getAllDiscourseNodesSince } from "~/utils/getAllDiscourseNodesSince"; @@ -12,8 +14,10 @@ import { import { DISCOURSE_NODE_KEYS, SUGGESTIVE_RULES_KEYS, + TEMPLATE_SETTING_KEYS, } from "~/components/settings/utils/settingKeys"; import { RenderRoamBlock } from "~/utils/roamReactComponents"; +import { ROAM_DOCS, withDocsLink } from "./utils/docs"; const DiscourseNodeSuggestiveRules = ({ node, @@ -30,6 +34,16 @@ const DiscourseNodeSuggestiveRules = ({ [embeddingRef], ); + const templateUid = useMemo( + () => + getSubTree({ + parentUid: nodeUid, + key: "Template", + }).uid || "", + [nodeUid], + ); + const [isTemplateOpen, setIsTemplateOpen] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); const handleUpdateEmbeddings = async (): Promise => { @@ -56,6 +70,33 @@ const DiscourseNodeSuggestiveRules = ({ return (
+
+
+ Date: Mon, 31 Aug 2026 14:12:17 -0400 Subject: [PATCH 04/12] Use the toggle as the template's only header The collapse toggle and the panel's own Label both read Template. The panel now takes an optional title so the caller can own the header, and the collapsed body is indented to line up under the toggle. Co-Authored-By: Claude Opus 5 --- .../settings/DiscourseNodeSuggestiveRules.tsx | 27 ++++++++++++------- .../components/EphemeralBlocksPanel.tsx | 14 ++++++---- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index 87ce70a9a..f08566c8c 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -17,6 +17,7 @@ import { TEMPLATE_SETTING_KEYS, } from "~/components/settings/utils/settingKeys"; import { RenderRoamBlock } from "~/utils/roamReactComponents"; +import Description from "~/components/settings/SettingsDescription"; import { ROAM_DOCS, withDocsLink } from "./utils/docs"; const DiscourseNodeSuggestiveRules = ({ @@ -71,20 +72,28 @@ const DiscourseNodeSuggestiveRules = ({ return (
-
{/* Collapse unmounts its children, so the editor's ephemeral buffer block is only created while the template is actually open. */} -
+ {/* The toggle above is this setting's header, so the panel omits its own. */} +
& { + /** Omit when the caller already renders a header for this setting. */ + title?: string; uid: string; defaultValue?: InputTextNode[]; }; @@ -213,10 +215,12 @@ const DualWriteBlocksPanel = ({ return ( <> - + {title ? ( + + ) : null} -
- -
+ {enabled && ( +
+ +
+ )}
); }; diff --git a/apps/roam/src/components/settings/GrammarNodesRoute.tsx b/apps/roam/src/components/settings/GrammarNodesRoute.tsx index f37543cdf..eac2d8094 100644 --- a/apps/roam/src/components/settings/GrammarNodesRoute.tsx +++ b/apps/roam/src/components/settings/GrammarNodesRoute.tsx @@ -58,7 +58,9 @@ const GrammarNodesRoute = ({ />
{!node ? ( - +
+ +
) : subPage === nodeConfigSegmentIds.index ? ( ) : subPage === nodeConfigSegmentIds.template ? ( diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 24dea596a..d4a3b111a 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -293,6 +293,12 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { return (
+ nav.push(nodeConfigSegmentIds.index)} + /> { initialColor={node.canvasSettings?.color} tagValue={tagValue} /> - - - { parentUid={node.type} uid={formatUid} /> - - nav.push(nodeConfigSegmentIds.index)} - /> @@ -445,6 +422,31 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { /> + {/* Settings mid-migration live here until they either replace their + predecessor or are removed. */} + + + + {isSyncEnabled() && ( -
+
+ {/* Back sits inline with the title; the trail below already names where it goes. */}
-
Date: Fri, 4 Sep 2026 18:31:50 -0400 Subject: [PATCH 09/12] ENG-2186 Attributes last on the node page Review feedback: per-node attribute settings move to the very bottom, after the sync-gated Suggestive mode group. Co-Authored-By: Claude Fable 5.1 --- .../src/components/settings/NodeConfig.tsx | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index d4a3b111a..1bef47313 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -379,35 +379,6 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { /> - - >( - node.type, - [DISCOURSE_NODE_KEYS.attributes], - )} - /> - c.text)} - initialValue={ - getDiscourseNodeSetting(node.type, [ - DISCOURSE_NODE_KEYS.overlay, - ]) ?? "" - } - order={0} - parentUid={node.type} - uid={overlayUid} - /> - - { /> )} + + + >( + node.type, + [DISCOURSE_NODE_KEYS.attributes], + )} + /> + c.text)} + initialValue={ + getDiscourseNodeSetting(node.type, [ + DISCOURSE_NODE_KEYS.overlay, + ]) ?? "" + } + order={0} + parentUid={node.type} + uid={overlayUid} + /> +
); }; From 55cdeda68cb5b9676a75b7b191c0a763c87c1851 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 4 Sep 2026 18:43:19 -0400 Subject: [PATCH 10/12] ENG-2186 Open up the space between node page groups Review choice C: 44px between groups, against 24px of padding inside a row, so a section break cannot be mistaken for a row break. Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/styles/settingsStyles.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/roam/src/styles/settingsStyles.css b/apps/roam/src/styles/settingsStyles.css index 6bc909b5c..acde31d9d 100644 --- a/apps/roam/src/styles/settingsStyles.css +++ b/apps/roam/src/styles/settingsStyles.css @@ -94,6 +94,7 @@ .dg-settings-node-page { display: flex; flex-direction: column; - gap: 28px; + /* Wide enough that a group break is unmistakably more than a row break. */ + gap: 44px; padding: 2px; } From 656b9a60b248b797b1f6528a1179e58846060c19 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sat, 5 Sep 2026 18:53:09 -0400 Subject: [PATCH 11/12] ENG-2186 Restore the brace a merge dropped from settingsStyles.css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .dg-settings-group was left unclosed when the heading block and this branch's route CSS were merged, so with CSS nesting every later rule — :root, the route container, and the drill-down's scroll body — became a descendant rule of it and stopped matching. Grammar > Nodes lost its scroll and the brand variables. A test now parses the stylesheet and pins those rules at top level, since the bundler accepts the nested form without complaint. Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/styles/settingsStyles.css | 1 + .../utils/__tests__/settingsStyles.test.ts | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 apps/roam/src/utils/__tests__/settingsStyles.test.ts diff --git a/apps/roam/src/styles/settingsStyles.css b/apps/roam/src/styles/settingsStyles.css index acde31d9d..3472a2106 100644 --- a/apps/roam/src/styles/settingsStyles.css +++ b/apps/roam/src/styles/settingsStyles.css @@ -35,6 +35,7 @@ display: flex; flex-direction: column; gap: 4px; +} :root { /* Brand secondary, mirroring packages/tailwind-config. */ diff --git a/apps/roam/src/utils/__tests__/settingsStyles.test.ts b/apps/roam/src/utils/__tests__/settingsStyles.test.ts new file mode 100644 index 000000000..9fa58b12e --- /dev/null +++ b/apps/roam/src/utils/__tests__/settingsStyles.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +// A lost `}` does not fail the build: with CSS nesting, every rule after it +// silently becomes a descendant rule of the one left open and stops matching. +// That happened once to the scroll container of the settings drill-down, so the +// stylesheet's shape is pinned here. +const css = readFileSync( + resolve(__dirname, "../../styles/settingsStyles.css"), + "utf8", +); + +const stripComments = (s: string): string => s.replace(/\/\*[\s\S]*?\*\//g, ""); + +const topLevelSelectors = (s: string): string[] => { + const selectors: string[] = []; + let depth = 0; + let buf = ""; + for (const ch of stripComments(s)) { + if (ch === "{") { + if (depth === 0) selectors.push(buf.trim()); + depth += 1; + buf = ""; + } else if (ch === "}") { + depth -= 1; + buf = ""; + } else if (depth === 0) { + buf += ch; + } + } + expect(depth).toBe(0); + return selectors; +}; + +describe("settingsStyles.css", () => { + it("closes every rule it opens", () => { + const body = stripComments(css); + expect(body.split("{").length).toBe(body.split("}").length); + }); + + it("keeps the layout rules the settings dialog depends on at top level", () => { + const selectors = topLevelSelectors(css); + for (const required of [ + ":root", + ".dg-settings-route", + ".dg-settings-route__body", + ".dg-settings-heading", + ".dg-settings-group", + ]) { + expect(selectors).toContain(required); + } + }); +}); From fd1a59a48251d8ce6566eabd0b09943b7674aad0 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 7 Sep 2026 18:18:46 -0400 Subject: [PATCH 12/12] ENG-2186 Drop unit tests and trim comments to the decisions they record Co-Authored-By: Claude Fable 5.1 --- .../settings/DiscourseNodeSuggestiveRules.tsx | 4 +- .../components/settings/GrammarNodesRoute.tsx | 1 - .../src/components/settings/NodeConfig.tsx | 2 - .../components/BlockPropSettingPanels.tsx | 9 +- .../components/EphemeralBlocksPanel.tsx | 1 - .../navigation/SettingsPageHeader.tsx | 1 - .../__tests__/settingsNavigation.test.ts | 206 ------------------ .../settings/utils/settingsNavigation.ts | 7 +- .../components/settings/utils/settingsTabs.ts | 1 - apps/roam/src/styles/settingsStyles.css | 5 +- .../__tests__/pendingSettingWrites.test.ts | 106 --------- .../utils/__tests__/settingsStyles.test.ts | 54 ----- apps/roam/src/utils/pendingSettingWrites.ts | 16 +- apps/roam/src/utils/setBlockProps.ts | 10 +- apps/roam/vitest.config.mts | 1 - 15 files changed, 13 insertions(+), 411 deletions(-) delete mode 100644 apps/roam/src/components/settings/utils/__tests__/settingsNavigation.test.ts delete mode 100644 apps/roam/src/utils/__tests__/pendingSettingWrites.test.ts delete mode 100644 apps/roam/src/utils/__tests__/settingsStyles.test.ts diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index f08566c8c..5af703692 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -87,10 +87,8 @@ const DiscourseNodeSuggestiveRules = ({ )} />
- {/* Collapse unmounts its children, so the editor's ephemeral buffer block is only - created while the template is actually open. */} + {/* Collapse unmounts its children, so the buffer block only exists while open. */} - {/* The toggle above is this setting's header, so the panel omits its own. */}
n.type === segment)?.text ?? segment) : (SUB_PAGE_LABELS[segment] ?? segment); - // Sub-pages fall through to the stylesheet's default dot colour. const dotColor = subPage ? undefined : formatHexColor(node?.canvasSettings?.color ?? "") || undefined; diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 1bef47313..4f0672a86 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -393,8 +393,6 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { /> - {/* Settings mid-migration live here until they either replace their - predecessor or are removed. */}