From d59d2993b7a29be3dec0a38a3cede2706fffa7e2 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 8 Sep 2026 13:05:13 -0400 Subject: [PATCH 01/13] ENG-2188 Node type settings are spread across seven inner tabs; put them on one page A node type's settings become one vertical page: Identity (Index first, then Description, Tag, Color, Format), Creation, Canvas, Legacy (Specification), the sync-gated Suggestive mode, and Attributes last. Index and Template are drill-down rows on the ENG-2186 primitive, each with its own sub-page. Specification's toggle reads "Enabled" (its lowercase block text stays the storage key via blockKey) and the query builder renders only while it is on. Format loses its DEPRECATED note: Specification is the one in migration. Co-Authored-By: Claude Fable 5.1 --- .../settings/DiscourseNodeSpecification.tsx | 27 +- .../settings/DiscourseNodeSuggestiveRules.tsx | 49 ++- .../components/settings/GrammarNodesRoute.tsx | 34 +- .../src/components/settings/NodeConfig.tsx | 382 ++++++++---------- .../src/components/settings/NodeIndexPage.tsx | 26 ++ .../components/settings/NodeTemplatePage.tsx | 27 ++ .../components/SettingsDrillDownRow.tsx | 31 ++ .../settings/utils/settingsNavigation.ts | 5 + apps/roam/src/styles/settingsStyles.css | 7 + 9 files changed, 331 insertions(+), 257 deletions(-) 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 diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index ea32cd0e9..e387f6ec3 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -155,7 +155,8 @@ const NodeSpecification = ({ -
- -
+ {enabled && ( +
+ +
+ )} ); }; diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index 67a546c92..5af703692 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -1,5 +1,5 @@ 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"; @@ -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 = ({ @@ -42,6 +43,7 @@ const DiscourseNodeSuggestiveRules = ({ }).uid || "", [nodeUid], ); + const [isTemplateOpen, setIsTemplateOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); @@ -68,18 +70,39 @@ const DiscourseNodeSuggestiveRules = ({ }; return ( -
- +
+
+
+
+ {/* Collapse unmounts its children, so the buffer block only exists while open. */} + +
+ +
+
+
= { + [nodeConfigSegmentIds.index]: "Index", + [nodeConfigSegmentIds.template]: "Template", +}; + const GrammarNodesRoute = ({ onloadArgs, }: { @@ -19,7 +27,7 @@ const GrammarNodesRoute = ({ const { segments, goToDepth } = useSettingsNav(); const nodes = getDiscourseNodes().filter(excludeDefaultNodes); - const [nodeTypeUid] = segments; + const [nodeTypeUid, subPage] = segments; const node = nodeTypeUid ? nodes.find((n) => n.type === nodeTypeUid) : undefined; @@ -30,8 +38,14 @@ const GrammarNodesRoute = ({ if (isStalePath) goToDepth(0); }, [isStalePath, goToDepth]); - const resolveLabel = (segment: string): string => - nodes.find((n) => n.type === segment)?.text ?? segment; + const resolveLabel = (segment: string, segmentIndex: number): string => + segmentIndex === 0 + ? (nodes.find((n) => n.type === segment)?.text ?? segment) + : (SUB_PAGE_LABELS[segment] ?? segment); + + const dotColor = subPage + ? undefined + : formatHexColor(node?.canvasSettings?.color ?? "") || undefined; return (
@@ -39,17 +53,19 @@ const GrammarNodesRoute = ({ ancestorLabels={NODES_ANCESTOR_LABELS} rootLabel="Nodes" resolveLabel={resolveLabel} - dotColor={ - formatHexColor(node?.canvasSettings?.color ?? "") || undefined - } + dotColor={dotColor} />
- {node ? ( - - ) : ( + {!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 b4114b63b..4f0672a86 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(); @@ -168,13 +165,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, @@ -184,19 +175,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(""); @@ -302,220 +291,169 @@ const NodeConfig = ({ ); return ( - <> - setSelectedTabId(id)} - selectedTabId={selectedTabId} - renderActiveTabPanelOnly={true} - > - - - -
- -
- -
- } - /> - + + - -
- } + description={`The saved list of all ${node.text} pages \u2014 which pages appear and which columns show.`} + buttonText={`See all ${node.text} nodes`} + onClick={() => nav.push(nodeConfigSegmentIds.index)} + /> + + + - - - - - } + description={withDocsLink( + `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} + /> + + + + - - - - } + 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} - /> - - } + + + + + + + + + + + + {isSyncEnabled() && ( + + + + )} + + + >( + node.type, + [DISCOURSE_NODE_KEYS.attributes], + )} /> - - - - + c.text)} + initialValue={ + getDiscourseNodeSetting(node.type, [ + DISCOURSE_NODE_KEYS.overlay, + ]) ?? "" } + order={0} + parentUid={node.type} + uid={overlayUid} /> - {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/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/utils/settingsNavigation.ts b/apps/roam/src/components/settings/utils/settingsNavigation.ts index 2d27b08da..bcef6a444 100644 --- a/apps/roam/src/components/settings/utils/settingsNavigation.ts +++ b/apps/roam/src/components/settings/utils/settingsNavigation.ts @@ -14,6 +14,11 @@ export type SettingsNavAction = | { 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] ?? ""; diff --git a/apps/roam/src/styles/settingsStyles.css b/apps/roam/src/styles/settingsStyles.css index 9ad138a46..806bce34d 100644 --- a/apps/roam/src/styles/settingsStyles.css +++ b/apps/roam/src/styles/settingsStyles.css @@ -89,3 +89,10 @@ .dg-settings-page-header__crumb { cursor: pointer; } + +.dg-settings-node-page { + display: flex; + flex-direction: column; + gap: 44px; + padding: 2px; +} From b7d499d3739eaded14fd7473feba8d81e93fbd13 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 11:27:14 -0400 Subject: [PATCH 02/13] ENG-2185 Move export settings out of Settings into the Export panel The 7 export controls were used by exactly one surface but lived in Settings, so changing an option meant leaving the Export flow, opening Settings, and coming back. Move them into a collapsed "Export options" disclosure at the bottom of the Export dialog's Export tab, and remove the Advanced > Export tab that ENG-2186 parked in Settings for this change to take out. The controls, their storage keys, and the read path in getExportSettings are unchanged, so saved values persist and export behaviour is untouched. The advancedExport tab id goes with it, and the saved "discourse-graph-export" deep link now resolves to the default tab instead of a tab that no longer renders. Co-Authored-By: Claude Opus 5 --- apps/roam/src/components/Export.tsx | 66 +++++++++++++++---- .../ExportSettings.tsx => ExportOptions.tsx} | 10 +-- .../roam/src/components/settings/Settings.tsx | 9 --- .../components/settings/utils/settingsTabs.ts | 4 +- .../roam/guides/sharing-discourse-graph.md | 2 +- 5 files changed, 63 insertions(+), 28 deletions(-) rename apps/roam/src/components/{settings/ExportSettings.tsx => ExportOptions.tsx} (94%) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 0f1c350fb..4941b4dd6 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -15,8 +15,9 @@ import { RadioGroup, Radio, FormGroup, + Collapse, } from "@blueprintjs/core"; -import React, { useState, useEffect, useMemo, FormEvent } from "react"; +import React, { useState, useEffect, useMemo, useRef, FormEvent } from "react"; import MenuItemSelect from "roamjs-components/components/MenuItemSelect"; import { saveAs } from "file-saver"; import { Result } from "roamjs-components/types/query-builder"; @@ -91,7 +92,12 @@ import { type NodeUidWithType, } from "~/utils/publishNodesToGroups"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; -import { isNodeSharingEnabled } from "~/components/settings/utils/accessors"; +import { + bulkReadSettings, + isNodeSharingEnabled, +} from "~/components/settings/utils/accessors"; +import refreshConfigTree from "~/utils/refreshConfigTree"; +import ExportOptions from "./ExportOptions"; const ExportProgress = ({ id }: { id: string }) => { const [progress, setProgress] = useState(0); @@ -225,6 +231,22 @@ const ExportDialog: ExportDialogComponent = ({ if (initialPanel) setSelectedTabId(INITIAL_PANEL_TO_TAB_ID[initialPanel]); }, [initialPanel, sharingEnabled]); const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); + const [exportOptionsOpen, setExportOptionsOpen] = useState(false); + const exportOptionsOpened = useRef(false); + const exportGlobalSettings = useMemo( + () => bulkReadSettings().globalSettings, + [], + ); + + // The export option panels write legacy config blocks alongside block props, so + // the cached config tree has to be refreshed the way SettingsDialog does. Gated + // on the section having been opened because refreshConfigTree re-reads every + // node page and re-registers the datalog translators, which is too heavy to run + // on every close of a dialog that is opened for each export. + const closeDialog = (): void => { + if (exportOptionsOpened.current) refreshConfigTree(); + onClose(); + }; const [gitHubAccessToken, setGitHubAccessToken] = useState( getSetting("oauth-github", null), ); @@ -756,7 +778,7 @@ const ExportDialog: ExportDialogComponent = ({ }); } finally { setLoading(false); - onClose(); + closeDialog(); } }; @@ -796,7 +818,7 @@ const ExportDialog: ExportDialogComponent = ({ fileCount: files.length, }); } - onClose(); + closeDialog(); } catch (e) { setError("Failed to export files."); posthog.capture("Export Dialog: Export Failed", { @@ -888,7 +910,7 @@ const ExportDialog: ExportDialogComponent = ({ : "success", id: "query-builder-publish-success", }); - if (hasPublishedNodes) onClose(); + if (hasPublishedNodes) closeDialog(); } catch (e) { internalError({ error: e as Error, @@ -981,11 +1003,33 @@ const ExportDialog: ExportDialogComponent = ({ + +
+
{error} -
- -
-
+ +
+ +
+
From 9eae87b42adefc0dfe19162208e21882a8697e03 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sat, 5 Sep 2026 19:12:07 -0400 Subject: [PATCH 06/13] ENG-2185 Discourse context on its own line above the export options row Review feedback: the checkbox sits right-aligned on the line above; the line below is the Export options toggle on the left and the result count on the right. Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/components/Export.tsx | 58 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 6d76d657b..e8ab30a57 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -978,7 +978,30 @@ const ExportDialog: ExportDialogComponent = ({ /> -
+
+ + { + setIncludeDiscourseContext( + (e.target as HTMLInputElement).checked, + ); + }} + labelElement={ + + Discourse context + + } + /> + +
+
From 4202f91281fae3712d00da0431dbe3e73aa0a77a Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 7 Sep 2026 18:20:13 -0400 Subject: [PATCH 07/13] ENG-2185 Drop unit tests and trim comments to the decisions they record Co-Authored-By: Claude Fable 5.1 --- apps/roam/src/components/Export.tsx | 18 +++++------------- .../components/settings/utils/settingsTabs.ts | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index e8ab30a57..6d96250ac 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -234,20 +234,15 @@ const ExportDialog: ExportDialogComponent = ({ const [includeDiscourseContext, setIncludeDiscourseContext] = useState(false); const [exportOptionsOpen, setExportOptionsOpen] = useState(false); const exportOptionsOpened = useRef(false); - // Re-read on every open rather than once at mount: Collapse unmounts the option - // panels while closed, so each open seeds them from the current stored values - // instead of whatever the dialog saw when it first rendered. + // Collapse unmounts the panels while closed, so each open seeds them from current values. const exportGlobalSettings = useMemo( () => bulkReadSettings().globalSettings, // eslint-disable-next-line react-hooks/exhaustive-deps [exportOptionsOpen], ); - // The export option panels write legacy config blocks alongside block props, so - // the cached config tree has to be refreshed the way SettingsDialog does. Gated - // on the section having been opened because refreshConfigTree re-reads every - // node page and re-registers the datalog translators, which is too heavy to run - // on every close of a dialog that is opened for each export. + // Option panels also write legacy config blocks, so refresh the tree as SettingsDialog + // does, but only if they were opened: refreshConfigTree re-reads every node page. const closeDialog = (): void => { if (exportOptionsOpened.current) refreshConfigTree(); onClose(); @@ -1055,11 +1050,8 @@ const ExportDialog: ExportDialogComponent = ({ // eslint-disable-next-line @typescript-eslint/no-misused-promises setTimeout(async () => { try { - // The export reads settings inside its callback, and the number - // and select panels defer their write behind a short timer. The - // await matters as much as the flush: committing only starts the - // Roam block update, so an option edited a moment ago would - // otherwise still read as its previous value here. + // Awaited, not just fired: a commit only starts the Roam update, and an option edited + // a moment ago would otherwise still read as its previous value here. await flushPendingSettingWrites(); const exportType = exportTypes.find( (e) => e.name === activeExportType, diff --git a/apps/roam/src/components/settings/utils/settingsTabs.ts b/apps/roam/src/components/settings/utils/settingsTabs.ts index 77a23f94f..0c0e8e163 100644 --- a/apps/roam/src/components/settings/utils/settingsTabs.ts +++ b/apps/roam/src/components/settings/utils/settingsTabs.ts @@ -23,7 +23,7 @@ export const SETTINGS_TAB_ALIASES: Record = { "canvas-shortcuts-personal-settings": SETTINGS_TAB_IDS.featuresCanvas, "left-sidebar-personal-settings": SETTINGS_TAB_IDS.featuresLeftSidebar, "left-sidebar-global-settings": SETTINGS_TAB_IDS.featuresLeftSidebar, - // Export options left Settings in ENG-2185; they now live in the Export dialog. + // Export options now live in the Export dialog. "discourse-graph-export": SETTINGS_TAB_IDS.preferencesGeneral, "discourse-nodes": SETTINGS_TAB_IDS.grammarNodes, "discourse-relations": SETTINGS_TAB_IDS.grammarRelations, From 90fd2638f9593e680032421078c185003814b548 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 31 Aug 2026 13:50:40 -0400 Subject: [PATCH 08/13] ENG-2183 Build the new setting-item row component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add one shared settings row — scope indicator, label, always-visible description, control — and route every panel-based row through it. Descriptions move out of the hover popover that dismissed before its doc links could be clicked (ENG-2080). The fix is structural: the description renders as a sibling of the label, never a descendant, so a link inside it can no longer toggle the row's control. The rewire happens at SettingTitle, the one helper all five Base*Panels funnelled label and description through, so ~70 call sites are unchanged. Flag rows swap Checkbox for Switch to match the design. Scope is derived from the wrapper rather than passed per call site, because the wrapper already binds the setter that decides who a value is written for. Rows that cannot derive it pass `scope` explicitly. Layout is Tailwind, which resolves because Roam itself bundles Tailwind. Anything depending on Tailwind's preflight is hand-rolled instead: `border-b` and `border` set only a width, and `* { border-style: solid }` is absent, so those borders never paint. This repo's own `neutral-*` theme colours are likewise absent from Roam's build and are mirrored as CSS variables. Supersedes ENG-2186's SettingsDrillDownRow, whose two callers now compose the shared row with a trailing drill-down summary. Co-Authored-By: Claude Opus 5 --- .../roam/src/components/DiscourseNodeMenu.tsx | 5 +- .../components/DiscourseNodeSearchMenu.tsx | 4 +- .../settings/KeyboardShortcutInput.tsx | 58 ++-- .../src/components/settings/NodeConfig.tsx | 143 +++++---- .../components/settings/PageGroupPanel.tsx | 198 ++++++------ .../settings/PreferencesGeneral.tsx | 57 ++-- .../src/components/settings/QuerySettings.tsx | 35 +-- .../settings/SettingsDescription.tsx | 19 +- .../components/BlockPropSettingPanels.tsx | 291 +++++++++++------- .../components/EphemeralBlocksPanel.tsx | 32 +- .../components/SettingDrillDownSummary.tsx | 33 ++ .../settings/components/SettingItemRow.tsx | 112 +++++++ .../components/SettingKeycapInput.tsx | 21 ++ .../components/SettingsDrillDownRow.tsx | 31 -- apps/roam/src/styles/settingsStyles.css | 29 ++ 15 files changed, 674 insertions(+), 394 deletions(-) create mode 100644 apps/roam/src/components/settings/components/SettingDrillDownSummary.tsx create mode 100644 apps/roam/src/components/settings/components/SettingItemRow.tsx create mode 100644 apps/roam/src/components/settings/components/SettingKeycapInput.tsx delete mode 100644 apps/roam/src/components/settings/components/SettingsDrillDownRow.tsx diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index 46fdcc8ed..f5e569aea 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -4,11 +4,11 @@ import { Popover, Position, Button, - InputGroup, getKeyCombo, IKeyCombo, Icon, } from "@blueprintjs/core"; +import SettingKeycapInput from "~/components/settings/components/SettingKeycapInput"; import React, { useCallback, useEffect, @@ -496,7 +496,8 @@ export const NodeMenuTriggerComponent = ({ const shortcut = useMemo(() => comboToString(comboKey), [comboKey]); return ( - - {label} - - setIsActive(true)} - onBlur={() => setIsActive(false)} - rightElement={ -
)} - + + { + if (colorWriteTimeoutRef.current) { + window.clearTimeout(colorWriteTimeoutRef.current); + colorWriteTimeoutRef.current = null; + } + pendingColorRef.current = null; + setColor(""); + persistColorValue(""); + }} + /> + + + } + /> ); }; @@ -293,11 +290,16 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { return (
- nav.push(nodeConfigSegmentIds.index)} + scope="nodeType" + control={ + nav.push(nodeConfigSegmentIds.index)} + /> + } /> { parentUid={node.type} uid={shortcutUid} /> - nav.push(nodeConfigSegmentIds.template)} + scope="nodeType" + control={ + nav.push(nodeConfigSegmentIds.template)} + /> + } /> @@ -394,26 +401,28 @@ const NodeConfig = ({ node }: { node: DiscourseNode }) => { - + { + validate({ + tag: tagValue, + format: formatValue, + isSpecificationEnabled, + }); + }} + /> + } + /> {isSyncEnabled() && ( diff --git a/apps/roam/src/components/settings/PageGroupPanel.tsx b/apps/roam/src/components/settings/PageGroupPanel.tsx index 584cb83b5..c68bf84fc 100644 --- a/apps/roam/src/components/settings/PageGroupPanel.tsx +++ b/apps/roam/src/components/settings/PageGroupPanel.tsx @@ -1,12 +1,12 @@ import React, { useState, useCallback } from "react"; -import { Label, Button, Intent, Tag, InputGroup } from "@blueprintjs/core"; -import Description from "~/components/settings/SettingsDescription"; +import { Button, Intent, Tag, InputGroup } from "@blueprintjs/core"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import createBlock from "roamjs-components/writes/createBlock"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import getAllPageNames from "roamjs-components/queries/getAllPageNames"; import { type PageGroup } from "~/utils/getSuggestiveModeConfigSettings"; import { setGlobalSetting } from "~/components/settings/utils/accessors"; +import SettingItemRow from "./components/SettingItemRow"; import { GLOBAL_KEYS, SUGGESTIVE_MODE_KEYS, @@ -129,107 +129,109 @@ const PageGroupsPanel = ({ }, []); return ( -