feat(ui): browse collections by folder with a card grid, drag-and-drop moves, and nav redesign - #17877
Draft
tylandavis wants to merge 41 commits into
Draft
feat(ui): browse collections by folder with a card grid, drag-and-drop moves, and nav redesign#17877tylandavis wants to merge 41 commits into
tylandavis wants to merge 41 commits into
Conversation
The nav scroll container is now edge to edge, with each section owning its own inset. Links and group headers span the full sidebar width so the gutters and the gap between rows are clickable, and the visible row is painted by a ::before inset from that box. Row geometry follows Figma's UI3 list row: a 24px highlight inside a 32px box, inset 8px from the sidebar edge. Group headers pick up --color-text-secondary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move the UserMenu component from the AppHeader to the sidebar's nav__controls, placing it alongside the SettingsMenuButton at the bottom of the sidebar. Update the Nav component to accept and render UserMenu with the necessary props for custom avatar, logout button, and settings groups. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The nav close button now displays in the header regardless of screen size, allowing users to close the sidebar from any device.
Prevent elastic scroll bounce effect in the navigation sidebar by containing overscroll behavior, keeping scrolling within the nav boundaries.
The user menu and close button now render in a sticky-free header above .nav__scroll, so the header stays put while only the tabs/links area scrolls. NavWrapper takes a header prop rendered before the scroll div.
Un-revert a commented-out effect that was swept into the prior commit by mistake.
The toggle button only needs to be visible when the sidebar is closed; showing it while the sidebar is already open is redundant.
- Rename .nav__mobile-close to .nav__close since it's now shown at all screen sizes, not just mobile - Remove the imperative overscrollBehavior toggling in NavProvider in favor of a plain overscroll-behavior: none on .nav__wrap, and drop the now-redundant scroll wrapper div in NavWrapper - Remove stray spacing/padding from .nav__close and .nav__controls
Lock the main layout grid to height: 100vh with overflow: hidden instead of min-height, so the sidebar and main content area are both strictly sized to the viewport rather than allowed to grow beyond it.
- Show the user's title/email in the account trigger, using the auth collection's useAsTitle field with a fallback to email/username - Theme/Language/Settings submenus now open on hover instead of click, using a ported Floating UI safePolygon-style funnel to decide when the cursor has genuinely left the trigger/panel area, rather than a simple bounding-box union which incorrectly stayed "open" over unrelated items below a short submenu - A shared HoverSubmenuGroupProvider ensures only one of the three submenus can be open at a time, mirroring how hovering a sibling item force-closes any previously open submenu - Submenu triggers are now rendered inside PopupList.MenuItem and reuse the shared icon/label classes so their spacing matches the Log Out row exactly - Add an opt-in showScrim prop to Popup that renders a full-viewport click-capturing layer while the (outermost) popup is open, so a click must dismiss the menu before it can reach anything else on the page, and fix a related side-positioning bug where the popup's content sat a few pixels lower than the trigger due to internal scroll-container padding
Replace the JS-tracked isContentHovered approximation with a CSS-only :has() selector querying from a shared ancestor. The submenu content panel is portaled to document.body, so it isn't a DOM descendant of its trigger and can't be reached with a plain descendant selector - :has() bridges the two via body, reflecting real-time :hover state directly instead of an approximation that could drift out of sync during mouseout/funnel transit. The trigger now stays brand-colored for its whole open (aria-expanded) state by default, switching to bg-secondary only while the cursor is actually over an item inside that specific submenu's content.
…rendering Reconciles loadedCount/totalDocs across hydrate() calls instead of letting a shallower fetch clobber a deeper one, and dedupes fetchPage's page concat by id as a defense-in-depth guard against page-number drift.
Applies to sidebar rows, tree nodes, and the main nav links. Also fixes the sidebar toggle button to a fixed square size and lets the admin layout scroll instead of clipping content.
Generalizes ViewModeToggle out of the Hierarchy-only view into a shared elements component backed by a new SegmentedControl, and wires it into the default List view so any collection can switch between table and card grid. Renames the CollectionPreferences field from hierarchyViewMode to documentViewMode to reflect the wider scope.
Move the border, fill, focus ring and marks onto a `__visuals` sibling of the input so every state is driven by the input's own pseudo-classes rather than classes React has to keep in sync. Both marks are always rendered and revealed by CSS, so an uncontrolled input that changes without React knowing still shows the right mark. - Write the real `indeterminate` property to the node so the mixed state is reported to assistive tech instead of being faked with a class - Draw each mark twice - a wider halo stroke under the mark itself - to keep it legible against the brand fill - Animate the mark in with `stroke-dashoffset`, gated so a document that loads already-checked doesn't replay it - Invert `variant="muted"` to mean "keep the neutral surface when checked", which is now the table default, so the explicit props can be dropped Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reshape the hierarchy browser around what the two modes actually mean: a single-select modal moves one document, so its current value is the origin of the move rather than a pre-made choice, and picking a row always replaces the destination. Multi-select edits a set, so existing values start checked and the checkbox is what toggles them. Modal: - New footer showing the move as `previous -> destination` breadcrumb chips, or the selection count and Clear in multi-select. Clicking a chip expands the browser down to that item and focuses it via a new `revealPath` handle - Primary actions moved out of the header into the footer; the header gained a close button and an optional title, e.g. `Move "My Post"` - `fetchAncestorPath` is now batched and returns titled breadcrumbs, which is what both the chips and the descendant counts are built from Column browser: - Rows are a real button rather than a `role="button"` div. Single-select picks and opens a row in one click, mirroring the macOS column view; in multi-select the row body opens children and the chevron is its own button - Replace the has-selected-descendants dot with a count badge, so a collapsed branch shows how many picks live inside it - Only single-select rows carry a selected fill - in multi-select the checkbox already says what is picked, and filling those rows drowns out the expanded fill that marks the current path - Re-opening an already-expanded ancestor truncates columns instead of refetching, and the expanded path always resets so a reveal back to the root doesn't leave stale expansion Field: - Render the hierarchy field as a standard relationship input, swapping the "create new document" button for one that opens the browser. Drops the bespoke pill list in favour of the shared select - Options are labelled with their full ancestor path while browsing so same-named items stay distinguishable, via new `formatOptionLabel` and `selectOptionFields` props on `RelationshipInput` Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Swap the round secondary-fill pill for the design system's filled badge: tertiary fill, `--radius-medium`, and body-medium type at full text colour rather than small-strong at secondary. Height is now explicit with flex centring instead of leaning on `line-height` to size the box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a text-only "link" button variant, uses it for the hierarchy modal clear action, matches the relationship add-new button to the secondary style, and cleans up hierarchy card selection rings and spacing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Names the upload field a card grid should render as a document's thumbnail, validated like `useAsTitle` and defaulting to the collection's first `upload` field so non-upload collections still get a preview. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Render thumbnails from `admin.useAsThumbnail` when the collection isn't upload-enabled, and always reserve the thumbnail slot so cards stay uniform - Label the root band of documents that aren't filed under a hierarchy item, and stop listing related documents there since "everything unfiled from everywhere" isn't a meaningful grouping - Name each per-collection pagination control, which the merged band otherwise leaves ambiguous - Disable dragging on the flat collection grid, which has nowhere to drop - Give `DndContext` an explicit `useId` so `aria-describedby` matches across hydration, and auto-fill grid columns before the plane is measured Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Stacked work across the hierarchy/folder views, the list view, and the nav sidebar. Grouped by theme since it's a large branch (41 commits, 136 files).
Browse a collection by folder
Drag and drop
moveDocumentsextracted so drag-drop and the Move button share one pathDndContextgets an explicituseIdsoaria-describedbymatches across hydration ("Warning: Proparia-describedbydid not match." in Next.js clauderic/dnd-kit#926)Grid view
documentViewModelist preference (defaults totable)admin.useAsThumbnailcollection option names the upload field a card renders as its thumbnail, validated likeuseAsTitleand defaulting to the collection's firstuploadfield — so non-upload collections get previews tooNav and account menu
Fixes and cleanup
:has()instead of JShasManyhierarchy fields now validate per-IDlinkbutton styleBreaking changes
useNav()/NavContextno longer exposenavRef— publicly exported from@payloadcms/ui, so consumers destructuring it break. The mobile overscroll lock it powered is now handled in CSS.hierarchy:itemsMovedTo/itemsMovedToRootchanged placeholders —{{title}}→{{count}} {{label}}. Every non-English locale still carries the old form and needs atranslateNewKeyspass.HierarchyViewData.hierarchySlugis now required — new non-optional field on an exported type, so anything constructing it fails to typecheck. Could be made optional if that matters.Deep-import-only breaks:
HierarchyColumnBrowserProps(ancestorsWithSelections→ requiredselectedDescendantCounts),HierarchySearchProps(droppedcollectionSpecificOptions/onFilterChange/selectedFilters, added requiredparentId),ColumnItemProps(hasSelectedDescendants→selectedDescendantCount),TreeNodeProps(droppedicon). None are inexports/client.Notes
useAsTitleandupdatedAt, even when those columns are hidden, since the grid cards always render both.🤖 Generated with Claude Code