Skip to content

ENG-2186 Grammar › Nodes drill-down and settings navigation primitive - #1375

Open
trangdoan982 wants to merge 11 commits into
eng-2189-reorganize-roam-settings-replace-the-personalglobal-splitfrom
eng-2186-grammar-nodes-drill-down-and-settings-navigation-primitive
Open

ENG-2186 Grammar › Nodes drill-down and settings navigation primitive#1375
trangdoan982 wants to merge 11 commits into
eng-2189-reorganize-roam-settings-replace-the-personalglobal-splitfrom
eng-2186-grammar-nodes-drill-down-and-settings-navigation-primitive

Conversation

@trangdoan982

@trangdoan982 trangdoan982 commented Aug 31, 2026

Copy link
Copy Markdown
Member

https://www.loom.com/share/55852dd333654bb08fd8c3c03ba8b797

I decided to merge the work of ENG-2186: Grammar › Nodes drill-down and settings navigation primitive
and ENG-2188: Node type settings are spread across seven inner tabs; put them on one page into 1 PR.

Reviewer brief

  • Result: the rail no longer grows one tab per node type. Grammar › Nodes drills node list → node type → Index/Template, with a back button and breadcrumb at each depth. A node type's settings are one vertical page instead of seven inner tabs.
  • Review focus: EphemeralBlocksPanel.tsx is the riskiest change and the only one touching persistence. Everything else is navigation and layout.
  • Review focus: this stacks on ENG-2189 and consumes its settingsTabs registry instead of adding a second source of tab ids. resolveInitialSettingsPath extends resolveSettingsTabId: an id that is not a known tab is treated as a node type uid and opens that node inside Grammar › Nodes, which is how the old per-node deep links keep working.
**Risk or follow-up:** - the Index row's description is proposed copy and needs sign-off — ENG-2188 notes Index has no description in code today. - Suggestive mode stays on the node page rather than moving to the Admin panel. That is in ENG-2188's Solution but not its Done When, and it needs its own node selector in Admin. Needs a ticket. - `apps/roam` has no Tailwind build (`tailwind.config.ts` sets `content: []`), so utility classes only apply when Roam's own stylesheet ships them. New layout is therefore hand-rolled CSS. Existing Tailwind in this app is partly inert as a result, `text-neutral-dark` included. Worth its own ticket; not touched here.
Verification
  • pnpm install --frozen-lockfile, then pnpm ci:validate from the repo root: green.
  • pnpm lint in apps/roam: 0 errors. Prettier clean. Production build: built with 0 errors.
  • Driven in Roam: rail lists no node types, L1 → L2 → L3 for Index and Template, breadcrumb and back at each depth, legacy selectedTabId openers still land.
  • No unit tests. The pure route module was covered by tests in an earlier revision; those were removed on request.
## Scope check
  • Ran $scope-check against the ENG ticket and final diff.
  • Scope beyond Done When:
    • ENG-2188 (node type settings on one page) is included; the drill-down primitive has no consumer without it, and its Index/Template rows have no home without the primitive. Sections are Identity / Recognition / Creation plus Attributes, Canvas and Suggestive mode — the ticket names the first three and gives the others no home, so they stayed put rather than becoming unreachable.
    • EphemeralBlocksPanel flush fix is a prerequisite, not a drive-by: unmount discarded the pending debounced write and then deleted the block carrying it. A back button makes that a fast, keyboard-adjacent path.

Local delegated full review

  • Ran a comprehensive review of the entire final diff in a subagent with a fresh context.

@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown

ENG-2186

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
discourse-graph Skipped Skipped Sep 5, 2026 10:55pm UTC

Request Review

@supabase

supabase Bot commented Aug 31, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread apps/roam/src/components/settings/navigation/SettingsPageHeader.tsx
Comment thread apps/roam/src/utils/settingsNavigation.ts Outdated
@trangdoan982
trangdoan982 force-pushed the eng-2186-grammar-nodes-drill-down-and-settings-navigation-primitive branch from ac26028 to 17ad1b7 Compare August 31, 2026 15:57
@trangdoan982
trangdoan982 changed the base branch from main to eng-2189-reorganize-roam-settings-replace-the-personalglobal-split August 31, 2026 15:57
cancelled = true;
window.clearTimeout(debounceRef.current);
// Navigating away lands right after a keystroke, and the buffer block is deleted next.
flushPendingChanges();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing bug this PR has to fix rather than inherit: the cleanup used to clearTimeout the pending 250ms write without running it, and the buffer effect then deleted the block carrying that edit. Only a deliberate tab click unmounted this before, so the window was rarely hit. A back button and a breadcrumb are fast, keyboard-adjacent unmount paths landing right after a keystroke, which turns it into a reproducible edit loss. Active only when isNewSettingsStoreEnabled().

Known limit: this recovers an edit Roam has already committed and debounced. A keystroke Roam has not committed yet is still lost.

* 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 = (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the ENG-2186 "existing deep links still resolve" requirement. resolveSettingsTabId (ENG-2189) maps the renamed tabs; its passthrough comment assumes per-node tabs still exist, keyed by node page uid. This PR removes those tabs, so an id that is not a known tab is now read as a node type uid and opens that node inside Grammar › Nodes instead of selecting a tab that no longer exists.

Deliberately no separate deep-link API: selectedTabId plus this resolution covers it, and ENG-2189 already addresses individual rows via settingAnchor.

--dg-secondary: #5f57c0;
}

/* Hand-rolled, not Tailwind: apps/roam has no Tailwind build (tailwind.config.ts sets

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md says prefer Tailwind, so this needs justifying. apps/roam/tailwind.config.ts sets content: [] and is commented "required for editor support" — there is no Tailwind build for this app, and the built extension.css contains no utilities. Classes only work when Roam's own stylesheet happens to ship them.

Found the hard way: a <h3 className="m-0"> here fell back to host heading margins and opened a ~440px gap. flex-shrink: 0 below is load-bearing for the same class of reason — without it the header is a shrinkable flex item and overlaps the scrolling body.

// 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);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path outlives this panel — renderActiveTabPanelOnly unmounts and remounts it on every tab switch — so it can point at a node type deleted in the meantime, or arrive stale from a saved link. goToDepth(0) rather than pop() so a depth-2 stale path converges in one dispatch instead of two.

@graphite-app

graphite-app Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

@trangdoan982
trangdoan982 force-pushed the eng-2186-grammar-nodes-drill-down-and-settings-navigation-primitive branch from bd1b4fe to 592a07c Compare September 3, 2026 20:30
@trangdoan982
trangdoan982 force-pushed the eng-2186-grammar-nodes-drill-down-and-settings-navigation-primitive branch from 592a07c to 5c92bfd Compare September 4, 2026 21:57
trangdoan982 and others added 10 commits September 4, 2026 18:42
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Supersedes the per-panel pendingWriteRef added earlier on this branch. That
version stored the inner 100ms timer id in the same ref as the outer 250ms one,
so unmounting inside that window cleared the inner timer and then ran the
pending write, issuing syncToBlock and setter twice.

useDeferredWrite keeps one timer and one registry entry per panel, so a commit
runs exactly once whether it is triggered by the timer, by unmount, or by a
caller flushing before it reads. The number and select panels now go through it
too, which is what lets a reader commit them.

flushPendingSettingWrites is awaitable: the commits only start a Roam block
update, so a caller that read straight after a synchronous flush could still
observe the previous value. setBlockProps tracks its in-flight updates so the
flush can wait for them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
settingsNavReducer and resolveInitialSettingsPath decide which tab and
drill-down a deep link opens, and both fail silently when wrong: the user simply
lands somewhere else. The alias map is pinned entry by entry for the same
reason, plus a check that every alias target is a real tab.

Widens the vitest include so tests can sit next to the settings modules they
cover rather than only under src/utils.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
resolveInitialSettingsPath rebuilt the list of known tab ids to decide whether
an incoming id is a tab or a node type uid. That classification belongs next
to the ids themselves, so the passthrough in resolveSettingsTabId and the
branch here read from one definition.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review feedback on the Grammar > Nodes drill-down:

- Recognition is gone. Index and Format join Identity, Index first, and
  Specification moves to a Legacy group that holds whatever is mid-migration.
  Format's "DEPRECATED" note goes with it: Specification is the one in flux.
- The Specification toggle reads "Enabled" — its lowercase block text stays
  the storage key via blockKey — and the query builder only renders while it
  is on, rather than sitting greyed out underneath.
- The back button moves inline with the page title; the breadcrumb beneath
  already says where it leads, so a second row naming the parent was noise.
- The node list page gets the same inset as every other panel, so the
  add-node input's focus ring is no longer clipped by the scrolling body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@trangdoan982
trangdoan982 force-pushed the eng-2186-grammar-nodes-drill-down-and-settings-navigation-primitive branch from 5c92bfd to 92a4176 Compare September 4, 2026 22:46
.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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant