Skip to content

Make the Content sidebar easier to scan - #5719

Merged
3mdistal merged 10 commits into
mainfrom
t3code/audit-content-sidebar-legibility
Sep 24, 2026
Merged

3mdistal merged 10 commits into
mainfrom
t3code/audit-content-sidebar-legibility

Conversation

@3mdistal

@3mdistal 3mdistal commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What changed

A legibility pass on the Content sidebar, using Clips' navigation as the reference, plus one shared row-actions layer for every section.

Legibility (first commit)

  • Where you are. One filled active row style shared by Recent, Pinned and Files, with hover lighter than active. Recent now highlights the open page. In Files, the open page scrolls into view unless it's already visible elsewhere in the sidebar.
  • One type scale. All rows use 14px full-foreground text (the 85% opacity is gone). Emoji and glyph icons share a fixed 16px slot. Show more / Show less are row height, with the chevron in the icon column.
  • Grouping and nesting. More space between sections. Trash moved into a fixed group below the scroller. Nested pages get a guide line under each ancestor's icon, and rows keep one 2px rhythm at every depth.
  • Quieter chrome. Row actions fade the end of the title instead of re-truncating it, so text doesn't shift on hover. Section … menus appear on hover or focus, and always show on devices without hover. Settings appears once, in the footer. Search is a quiet row.
  • Creation stays where it was. After merging main, the header keeps its space switcher (which holds workspace sources) and the + beside it for a new Page or Collection (feat(content): clarify page and collection creation #5592). This branch's earlier Files-header + was removed.
  • Accessibility. Tree toggles announced "Expand sidebar {title}". They now say "Expand {title}" / "Collapse {title}", with translations for every locale.

Shared row actions and a personal Recent menu (second commit)

  • One actions layer. The hover/focus overlay (… menu, title fade, stay-open-while-menu-open, pin item) moved out of the Files row into SidebarRowActions. Files and Pinned rows render through it unchanged.
  • Recent gets a personal-only menu: Pin/Unpin and Remove from Recent. Delete, add child and reorder stay on Files, since Recent is personal history rather than hierarchy. A guard test asserts this.
  • Server. get-content-recent now returns the requester's pinned state, looked up only for rows that already resolved under access. A new remove-content-recent action forgets one destination for the current user and context. It's available to the agent, and pages, pins and other users' Recent are untouched.
  • Client. Pin and remove both update Recent optimistically; remove rolls back with a toast if the save fails.
  • Records. Parity matrix row updated and matrix.md regenerated. The content.navigation.sidebar capability record now states the personal-actions promise and has a "Forget a Recent destination" acceptance story. New strings are translated in every locale.

One Page menu with rename, duplicate, and move (third commit)

  • Pinned rows show … again. Reorderable rows were wider than the sidebar and clipped their actions. They now stay inside its width.
  • One menu, one order. Files, Pinned and Recent all render SidebarPageMenu, grouped Notion-style: Pin | Copy link, Open in new tab | Rename, Duplicate, Move to | Remove from Recent or Move to Trash | "Last edited by … · when". Each section passes only what it allows. The menu is 240px wide to fit the footer.
  • Recent stays hands-off: Pin, Copy link, Open in new tab, Remove from Recent. A guard test asserts it offers no rename, duplicate, move, trash, add child or reorder.
  • Rename edits the title inline in the row. It starts after the menu closes, so Radix's focus trap can't blur and commit the input early. The title updates optimistically, including in Pinned.
  • Duplicate keeps the copy beside its original. duplicate-database-item now preserves the parent and, when given a Page id, prefers the Page's own space Files membership over a Favorites membership, so duplicating a pinned Page no longer puts a stray copy in Pinned. There's a db test for nested, root and pinned sources.
  • Move to opens a same-space picker: the space's top-level Pages plus "Top level", or a title search with each result's parent as context. It calls move-document, which still rejects moving a Page under its own descendants.
  • Last edited comes from a new readonly get-document-activity action (viewer access). It is read only while the menu is open.
  • Local-file Pages mirror disk, so they get no Rename, Duplicate or Move, and their copied link is the in-app URL.

Move and duplicate between spaces (fourth commit)

Product decisions: the person moving becomes the owner, sharing changes to match the destination (with a warning), and sub-pages move with the page and are copied on Duplicate.

  • Move to picks a space first. It opens on the page's own space. It offers every space you can add pages to, and never local-folder spaces. Picking a place in another space shows a warning that names the space and what changes. Nothing moves until you confirm.
  • move-document accepts spaceId. It walks the page's whole subtree regardless of owner, including trashed sub-pages that would restore under it. You need edit access to every page in it. In one transaction it:
    • sets space, owner (the mover), org and visibility on every page. Access becomes organization-wide in an organization space, private otherwise, or the new parent's sharing;
    • deletes existing shares, so old grants and public links stop working;
    • swaps Files membership through the existing reconciler;
    • rewrites the owner on comments, versions, property values and block identities. Readers filter these rows by the page's owner, so without this they would disappear;
    • re-keys block identities to the new Files Blocks field, so block ids survive the move;
    • drops values of the old Files table's own properties, since those columns don't exist in the new space.
  • Nesting in another space is allowed only under a page the mover owns, matching move-document's existing same-owner rule.
  • Refused with a reason: pages that contain collections, collection rows, local-folder files, and Notion- or Builder-linked pages. Carrying a collection's rows, properties, views and sources across spaces is the remaining open question on the capability record.
  • New duplicate-page action. It copies a page and its sub-pages beside the original, or into another space. The sidebar's Duplicate now uses it. Each copy is created through create-document, so placement, sharing and Files membership follow the normal creation rules. Collections inside the page aren't copied. Property values carry over within the same space. Open editors are flushed first when they respond.
  • Fixed: Move from Pinned or Recent used the sidebar's selected space rather than the page's own.

Verification

  • tsc --noEmit for Content passes.
  • Sidebar, database editor, Recent action, shared, parity and i18n tests: 1048 pass. Database tests, each run on its own, all pass: move-page-to-space (new, 6), move-document (6), space-aware-writers (6), content-files (17), move-database-item (5).
  • Three tests fail only in this Windows checkout, all on files this PR doesn't touch. Two layout tests and one database-row-batch-actions source check assume LF line endings, and pass once the files are converted to LF. shared/__nfm_probe.test.ts writes to /tmp, which doesn't exist on Windows.
  • guard:content-product-docs validates all records with LF line endings; in the CRLF checkout it fails on every record.
  • guard:no-default-chrome, guard:content-product-conformance, guard:no-action-twin-routes, guard:help-icon-scale, guard:modal-layer-integrity and guard:i18n-changed-copy pass.
  • In a local browser, checked:
    • First commit: the active row in Recent and Files, depth guides, Trash's fixed group, the single Settings entry, dark mode, and the mobile drawer layout.
    • Second commit: the Recent menu shows only "Pin to sidebar" / "Remove from Recent". Pinning adds the page to Pinned and flips the label to "Unpin". Remove drops the row immediately, the server confirms it, and the page stays in Files. The Files menu still offers Unpin / Delete plus add child.
    • Third commit:
      • Pinned rows fit the sidebar, and their menu shows all seven items plus the footer.
      • Rename saves on Enter and updates Pinned.
      • Duplicate lands beside the original under the same parent.
      • Move opens with focus in search, finds nested Pages, and moves to Top level.
      • Move to Trash opens the existing confirmation.
      • Recent shows only Pin, Copy link, Open in new tab and Remove from Recent, plus the footer.
    • Fourth commit:
      • Duplicating "Research notes" copied its sub-page and placed the copy right after the original.
      • Moving that copy to "Dev's workspace" showed the warning. After confirming, a toast appeared, the copy and its sub-page left Personal, and both appeared in Dev's workspace Files.
content_product_impact:
  lane: product_decision_candidate
  features:
    - content.feature.find-your-place-again
  capabilities:
    - content.navigation.sidebar
  record_change: included
  proof:
    - pnpm --filter content exec vitest --run app/components/sidebar app/components/editor/database actions/content-recent.test.ts shared/content-personal-navigation.test.ts parity app/i18n
    - pnpm --filter content exec vitest --run actions/content-files.db.test.ts
    - pnpm --filter content exec vitest --run actions/move-page-to-space.db.test.ts
    - pnpm --filter content exec tsc --noEmit -p .
  rationale: Gives every sidebar row one Page menu (pin, copy link, open in new tab, rename, duplicate with sub-pages, move within or between spaces, trash, last edited); a cross-space move follows the approved decisions (mover owns, access matches the destination after a warning, sub-pages move), and Recent keeps only non-mutating personal actions. Collections changing space remains an open question.

🤖 Generated with Claude Code

Bring the Content sidebar in line with the Clips navigation grammar:

- One filled active row style shared by Recent, Pinned, and Files, with
  hover lighter than active; the open page scrolls into view in Files
  unless it is already visible elsewhere in the sidebar.
- One row type scale (14px, full foreground), fixed 16px icon slot for
  emoji and glyphs, and row-height Show more/less aligned to the icon
  column.
- Wider section spacing, Trash moved into a fixed lifecycle group below
  the scroller, and depth guides under each ancestor's icon column with
  a consistent 2px row rhythm at every depth.
- Row actions fade the title end instead of re-truncating it; section
  menus appear on hover or focus; Settings lives only in the footer.
- New page is a visible Files header action, and workspace sources moved
  into the space switcher; search is a quiet row.
- Tree toggles are named after the item ("Expand Research") instead of
  "Expand sidebar Research", with translations for every locale.
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Visual recap — generation failed

The visual recap could not be generated for this pull request. This is informational only and does not block the PR.

Diagnostic:

No plan URL: Repair changed too much of targeted file plan.mdx; expected a localized parser fix.

Agent output: Repaired the malformed wireframe html expressions in recap-source.json by closing their JavaScript expressions. --- ⠀ 🟢 Corrected recap source written; ready for the hosted parser retry. stderr: Reading additional input from stdin...

Pull the hover/focus row-actions layer (overlay, "..." menu, title fade,
pin item) out of the Files row into SidebarRowActions so every sidebar
section uses one implementation.

Recent rows now use it with personal-only actions: Pin/Unpin and Remove
from Recent. Shared mutations (delete, add child, reorder) stay on Files.

- get-content-recent returns the requester's pinned state for rows that
  already resolved under access.
- New remove-content-recent action forgets one Recent destination for
  the current user and context; agents can use it too.
- Recent updates optimistically for both pin and remove.
- Parity matrix, capability record, and all locale catalogs updated.
builder-io-integration[bot]

This comment was marked as outdated.

@github-actions
github-actions Bot temporarily deployed to pr-5719-forms September 23, 2026 17:50 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-analytics September 23, 2026 17:51 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-slides September 23, 2026 17:51 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-design September 23, 2026 17:52 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-mail September 23, 2026 17:54 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-starter September 23, 2026 17:54 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-content September 23, 2026 17:54 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-assets September 23, 2026 17:55 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-calendar September 23, 2026 17:55 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-clips September 23, 2026 17:56 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-plan September 23, 2026 17:56 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-dispatch September 23, 2026 17:56 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-fw September 23, 2026 17:57 Destroyed
Files, Pinned, and Recent now render the same SidebarPageMenu in one
order: pin; copy link and open in a new tab; rename, duplicate, and
move; remove from Recent or move to Trash; then who last edited the
Page. Each section passes only the actions it allows, so Recent keeps
to actions that leave the Page unchanged.

- Rename edits the title inline and starts after the menu closes, so
  the menu's focus trap cannot commit the input early.
- Duplicate keeps the copy beside its original and in the Page's own
  space Files membership, even when the Page is pinned.
- Move to opens a same-space picker over move-document.
- The footer reads the new get-document-activity action while open.
- Reorderable rows stay inside the sidebar width, so Pinned rows show
  their actions again.
builder-io-integration[bot]

This comment was marked as outdated.

Move to now picks a Content space first. Moving a Page into another
space moves its sub-pages too: the mover becomes the owner, access
resets to the destination's (organization-wide, private, or the new
parent's sharing), existing shares and public access are removed, and
comments, history, and block identities travel with the Pages. The
dialog warns and asks before a cross-space move.

- move-document takes spaceId and runs the move in one transaction,
  swapping Files membership and rewriting owner-scoped rows. Pages with
  collections, collection rows, local-folder files, or Notion/Builder
  links are refused with a reason.
- New duplicate-page action copies a Page and its sub-pages, beside the
  original or into another space; the sidebar's Duplicate uses it.
- The Move picker now opens on the Page's own space, fixing moves of
  Pinned and Recent pages from other spaces.
builder-io-integration[bot]

This comment was marked as outdated.

@github-actions
github-actions Bot temporarily deployed to pr-5719-mail September 23, 2026 20:37 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-analytics September 23, 2026 20:38 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-fw September 23, 2026 20:38 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-content September 23, 2026 20:39 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-dispatch September 23, 2026 20:40 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-clips September 23, 2026 20:40 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-design September 23, 2026 20:40 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-starter September 23, 2026 20:41 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-plan September 23, 2026 20:41 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-calendar September 23, 2026 20:41 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-slides September 23, 2026 20:41 Destroyed
@github-actions
github-actions Bot temporarily deployed to pr-5719-assets September 23, 2026 20:41 Destroyed
…cher

Take main's workspace menu (sources inside the space switcher) and its
+ menu for a new Page or Collection, and drop this branch's Files
header + along with the split WorkspaceSourceMenu it relied on.
builder-io-integration[bot]

This comment was marked as outdated.

- Duplicating a collection row keeps the collection page as its parent
  again; only Files pages keep their own parent. Top-level position
  shifts stay within the source space and root section.
- duplicate-page requires edit access and refuses, instead of silently
  trimming, a subtree with sub-pages the caller can't read.
- Move search leaves out the page's own subtree via a new
  search-documents excludeSubtreeOf filter.
- The row menu reads last-edit activity only while it is open.
- Recent copies in-app links for local-file pages, reloads pin state
  when a pin fails, and a failed removal restores only that entry.
builder-io-integration[bot]

This comment was marked as outdated.

@github-actions
github-actions Bot temporarily deployed to pr-5719-content September 23, 2026 23:33 Destroyed
- Format with oxfmt (the repo's formatter), including locale files.
- Point the pin-glyph layout test at the shared SidebarRowActions menu.
- duplicate-page: no silent catches, reject collection pages up front,
  require a parent in the destination space, copy extra Blocks field
  content within a space, and report pages copied from their last save.
- Cross-space move refuses pages with content in the old Files table's
  extra Blocks fields and aborts if a sub-page appears mid-move.

@builder-io-integration builder-io-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.

Builder reviewed your changes and found 1 potential issue 🟡

Review Details

Incremental Code Review Summary

The latest commits fix four previously reported issues, and those stale threads were resolved: duplicate destination-parent validation, secondary Blocks-field copying during duplication, live-editor flush status reporting, and collection-backed duplicate handling. The concurrent subtree-move race and cross-space Blocks-field preservation comments remain unresolved and were not reposted.

New Finding

🟡 MEDIUM — duplicate-page still omits description from every createDocument payload. Since createDocument defaults the field to empty, duplicating a Page silently loses its stored description even though the action advertises duplicating the Page and its sub-pages.

Risk assessment: High because the PR contains document duplication and cross-space hierarchy mutations; this review's new finding is medium severity.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment thread templates/content/actions/duplicate-page.ts
@github-actions
github-actions Bot temporarily deployed to pr-5719-content September 24, 2026 02:42 Destroyed

@builder-io-integration builder-io-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.

Builder reviewed your changes and found 5 potential issues 🟡

Review Details

Incremental Code Review Summary

The existing duplicate-page description-preservation comment remains unresolved and was not reposted. The latest changes improve duplicate destination validation and add more subtree consistency checks, but several edge cases remain around all-or-nothing duplication and concurrent cross-space moves.

New Findings

🟡 MEDIUM — Failed duplication uses soft-delete cleanup, and that cleanup can also require stricter access than the editor who started the copy. A later failure can therefore leave a visible partial subtree in Trash or leave shared-editor copies behind.

🟡 MEDIUM — Cross-space move checks still race with child creation and reparenting: the source tree is snapshotted before the relevant source locks, and already-loaded descendants are not checked for changed parent relationships.

🟡 MEDIUM — Cross-space duplication can silently drop secondary Blocks fields, omit nested collection pages and their descendants, and permit a local-folder/source-backed destination even though those Pages are disk-backed and should not receive SQL-managed copies.

Risk assessment: High because this PR mutates document hierarchy, ownership, and user-authored content across spaces.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment thread templates/content/actions/duplicate-page.ts
Comment thread templates/content/actions/_move-page-to-space.ts
Comment thread templates/content/actions/duplicate-page.ts
Comment thread templates/content/actions/duplicate-page.ts
Comment thread templates/content/actions/duplicate-page.ts
@github-actions
github-actions Bot temporarily deployed to pr-5719-content September 24, 2026 02:55 Destroyed
Keep Trash from highlighting the current page in Pinned and Recent.
@github-actions
github-actions Bot temporarily deployed to pr-5719-content September 24, 2026 15:56 Destroyed
@3mdistal
3mdistal enabled auto-merge (squash) September 24, 2026 16:42
@3mdistal
3mdistal merged commit 9dd4757 into main Sep 24, 2026
46 of 47 checks passed
@3mdistal
3mdistal deleted the t3code/audit-content-sidebar-legibility branch September 24, 2026 16:55
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.

2 participants