[6.x] Design updates - #19648
[6.x] Design updates#19648brianjhanson wants to merge 166 commits into
Conversation
`craft-action-item` was the only one of its neighbours whose shadow root never picked up the box-sizing reset from `styles/host.styles.ts`, and a shadow root doesn't inherit the page's. So `.action-item` was content-box: its `width: 100%` sat inside its own inline padding and overflowed whatever contained it by twice the spacing. In a menu that's past `.popover-pane`'s max-width, which `overflow: auto` then drew as a scrollbar. `craft-nav-item` never showed it because its host isn't `display: contents` and its inner box isn't `width: 100%`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Bound per item, the handler only ever covered the items present when the overlay was set up, so anything a consumer added afterwards stayed open on click. Slot-based mode is exactly where that happens — a framework rendering the list swaps items in long after setup. `event.target` retargets to the `craft-action-item` host on its way out of the shadow root, so `closest()` still finds the item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
A nav entry, a breadcrumb and a menu item are the same thing drawn three ways, but they were three shapes: a `NavItem` class, hand-written crumb arrays, and hand-written action-menu arrays. They spelled the same field differently — `NavItem.url` against the crumbs' `href` — and supported different subsets of the same ideas, which is how the breadcrumb switcher came to lose the groups the sources sidebar shows. `Cp\Data\ActionItem` carries the whole vocabulary and `NavItem` extends it without adding anything, keeping the name the navigation and plugins use. Everything is optional: a crumb often has no link, a group heading isn't a destination, and `NavItem.url` being required had already forced a `'#'` placeholder in `EditUserScreens`. `href` is the spelling, since it maps straight onto HTML. `url` stays a real alias rather than a break — a setter, a getter, and a constructor key — because plugins return these from `getCpNavItem()` and the Twig-rendered screens read `item.url`, where a Component throws on an unknown property instead of returning null. Only `href` is serialized. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Crumbs were loose arrays in 33 files, which is how two of them came to be keyed `url` while `Breadcrumbs.vue` reads `href` — nothing normalized between the two, so the Utilities and Asset Transformers screens rendered their first crumbs as plain text that led nowhere. Naming the shape settles it; `CpScreenResponse::crumbs()` still takes arrays and configures them. The source switcher now keeps the headings the sources sidebar shows, grouping the sources under them rather than flattening into an ungrouped run beside a grouped list. `ContentIndexControllerTest` pins the two against each other by outline so they can't drift apart again. `UserEditViewModel` loses a hop that re-resolved a `url` key which had stopped existing — a no-op for core and quietly wrong for anything else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
`utilities/Show.vue` filled `SecondaryNav`'s default slot with its own markup and passed no items. The nav's collapsed action menu is built from the items, so below the large breakpoint it listed nothing at all. The controller sends the nav as data now and the page draws none of it, which also means the breadcrumbs pick the current utility up with a switcher. Its crumbs move to `ActionItem` with the rest, and its first crumb gets the `t()` it was missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
`ActionList` turns `ActionItem` descriptors into real controls, as buttons where there's room and as menu items where there isn't. `craft-button` and `craft-action-item` both take the same declarative action/feedback primitives, so one descriptor behaves the same in either shape. `ActionMenu` now renders through it rather than handing `craft-action-menu` an array to build itself. That's what lets a link action be a `CpLink` and make an Inertia visit — the element's own `href` renders a plain anchor in its shadow root, so every link in every menu was a full page load. Link actions that should stay that way say `external`, which is why `NewEntryButton` sets it: its own primary button does `window.location`. Two details the switch turned up. The menu's content wrapper is ours on purpose: `craft-popover` auto-wraps unslotted children into a container it makes once and appends, so anything Vue added afterwards landed outside it and never reached the slot. And binding `:variant="undefined"` doesn't mean "leave it alone" — Vue assigns it as a property and Lit reflects the result, so an unset variant became `variant=""` instead of the element's own default; only set props are passed now. The tests that broke were asserting on `craft-action-menu.actions` — the plumbing rather than the output — and now read the rendered items. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
`subnav-actions` was a slot, so a page filling it had to write the control twice — once as a button for the expanded nav, once as an item for the menu the nav collapses into. They're one thing, so they're described once now and travel through `useAppLayout`, which already carries action descriptors and already handles reactivity, per-navigation reset and the slideout scoping. The nav's own items feed the collapsed menu through the same mapper, so a group survives as a heading over its children rather than as a link to the `#` its URL usually is. Items carry their icons and badges through both renderings, and the collapsed menu honours `external`, which it ignored. Nav and controls are one list rather than two, so the checkmark gutter is decided once — split up, the labels wouldn't line up across the rule. The slot stays for anything descriptors can't express. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
The nav already says where you are, but only while it's on screen — and it collapses below the large breakpoint. The trail joins the crumbs now, each level carrying the same switcher menu an element index's source crumb has, so you can move between siblings without opening the nav. On an index screen the derived crumb replaces the one the controller wrote by hand, since they name the same thing; where the trail ends somewhere else — a user's account screens end in a chip for the user — it adds the level that was never named at all. Both the switcher and the nav's own collapsed menu come from `navItemActions`, so they show the same items, the same groups and the same mark on the current one. They were drifting apart precisely because they were two renderings. `BreadcrumbItem` moves to `@/common/types` so plain modules can build crumbs; a `.ts` file can't import a type out of an SFC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Breadcrumbs move onto their own row under the toolbar rather than sitting beside the sidebar toggle, and `SystemInfo` moves out of the top bar into a sidebar header alongside a close button — so the toggle no longer lives in the sidebar footer. Below the large breakpoint the top bar's toggle becomes a `bars` button and `SystemInfo` drops out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
The rename to `href` missed the expanded nav list, which kept binding `item.url` — so every item in the docked sidebar linked nowhere while the collapsed menu, which goes through `navItemActions`, was fine. Nothing caught it: the expanded list is its own rendering and no test asserted its links, and `craft-nav-item.href` is a non-reflecting property, so it fails quietly rather than rendering an obviously empty attribute. The new test reads the property and fails against the old binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
The sources sidebar was its own component with its own markup, so it never collapsed into an action menu the way every other nav does, and its list and the breadcrumb switcher beside it were two derivations of the same thing. The behaviour that made it its own component moves to `useElementSourceActions`, which describes the sources as action items carrying it: selecting one is a partial visit that leaves the source list and publishable sections alone and keeps the list's scroll and state, it activates before the round-trip so the selection doesn't lag the pointer, it prefetches on mousedown, it hands off to an `indexVisitor` where there is one, and folder sources keep the attributes an asset drag needs. `SecondaryNav` now takes those descriptors rather than `NavItem[]` — one array rendered as a list and as the menu it collapses into, with `NavEntry` drawing an entry either way. Callers with a `NavItem[]` map it at the boundary with `navItemActions()`. `ElementSources` stays for the selector modal, which has no page chrome to hang a nav on, but renders the same descriptors over the same composable instead of repeating the logic. `Customize sources` becomes a nav action. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
`useElementIndexPage` hands back a reactive object, not refs — every other reader says `elementIndex.viewModes`, not `.value`. The sources getter I added reached through `.value`, so it read `undefined.sources` and threw in setup, leaving the page as nothing behind the layout's own bars. Typecheck let it past, so the guard is a test that mounts the page with the reactive shape the composable really returns and asserts the sources reach the nav. It fails against the `.value` access. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Same miss as the secondary nav: the rename to `href` left `MainNav` binding `item.url`, so every top-level and subnav item rendered with no link and resolved to the current page. Clicking one reloaded where you already were. The test fixtures still said `url` too, and nothing asserted an href, so it passed throughout. It now checks each item links where it says it does, and fails against the old binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Loading the entries index showed the sources as plain text: `craft-nav-item` only renders an interactive anchor when it has an href, and describing a source as a button left it without one. Unfocusable, no role, no keyboard. They carry the source URL again, with the click intercepted for the partial visit — which is what the component they replaced did, and said so. Routed straight to the element rather than through `CpLink` when the descriptor brings its own `onClick`: `CpLink` took its props from Inertia's `Link`, which declares `onClick`, so the handler was consumed as a prop and never reached the DOM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
It declared Inertia's whole prop surface through `InertiaLinkProps` but forwarded only `href`, so `method`, `data`, `headers`, `replace`, `preserveScroll`, `only`, `onClick` and the rest were consumed as props and silently never arrived. `prefetch` was hardcoded to `click`, so a caller couldn't turn it off either. It now declares only what it adds and lets the rest fall through `$attrs`. Forwarding the declared props instead doesn't work: Vue casts an absent Boolean prop to `false` rather than `undefined`, so `replace`, `preserveScroll`, `instant` and friends would arrive as explicit `false` and override Inertia's own defaults — and `prefetch` would arrive as `false` rather than being unset. The cost is that consumers no longer get prop-level types for Inertia's options from `CpLink` itself; they reach `Link` unchecked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Two things stopped `match-invoker-width` doing anything visible. Lion sets the inherited width on the overlay *wrapper*, and `.popover-pane` inside it kept its own `max-width: 320px`, so a 464px invoker got a 320px panel. And the pane's 1px border sat outside its width, because a shadow root doesn't inherit the page's box-sizing reset — the same gap `craft-action-item` had. Measured against a 464px invoker: 322px before, 464px after. `placement`, `distance` and `match-invoker-width` also never took effect unless they were set before the overlay was built: `_defineOverlayConfig()` is read once when the controller is created, and `updated()` only pushed `for`/`anchor` changes through. Setting `distance` at runtime moved the panel by exactly nothing; it now moves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Restructures PageScreen around explicit `cp-page` / `cp-content` regions with a sticky page footer, renames the layout custom properties from `--content-layout-*` to `--cp-content-*`, and moves the top bar and sidebar onto `useCpBreakpoints`. SecondaryNav switches to a popover-based disclosure, and the element index sources nav picks up a gear icon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Completes 0295fdb, which renamed the slots everywhere except the two files that define and fill them — so `ScreenSlots` declared `content-sidebar` while the shell still rendered `<slot name="sidebar">` and read `slots.contentSidebar`. Also settles that slot on kebab-case: `contentSidebar` never matched the `content-sidebar` the template rendered, so `hasSidebar` was falling through to its other conditions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
The nav came through `useCraftData()`, which hands back `page.props.craft` as it stood at setup — a plain object, so a computed over it had no reactive dependency and could never update. MainNav lives in the sidebar and never remounts, so it kept highlighting whichever section you first landed on. Reads `page.props.craft.nav` inside the computed instead, making Inertia's new props object on each visit the dependency. The test drives through the page prop rather than mocking the composable, so it fails against the old binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
Condenses 18 comment blocks without dropping any of the reasoning: the teleport-target constraint behind `v-show`, the `--details-width` name collision with legacy `_cp.scss`, the `#details` 350px pin, and the `inline-size` vs `size` containment note all survive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
The flyout was tied to `icon-only`: collapsed to a rail an item has nowhere to indent to, so its subnav moved into a popover. Depth has the same problem for a different reason — a 220px column runs out of horizontal room long before the nav runs out of levels — but a labelled item had no way to ask for it. Adds `subnav-display="inline|flyout"`, with `icon-only` still forcing `flyout`, and extracts the popover both paths now share. A flyout leaves nothing to collapse, so it suppresses the disclosure toggle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhkE6EKcrbrFvL1WZ22A9z
build:cms can be used to only build the assets in the resources folder
… design-updates
A rail has nowhere to indent into, but the branch you were in still opened its subnav in place while every other branch flew out. That ran at two levels: the trail indented in the rail, and its children resumed trail expansion once inside the flyout, so the open branch's flyout came out a pre-expanded tree rather than the flat list every other item gets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
A branch on the trail took a thin rule beside its icon while the page itself took a filled row, so a rail marked the same selection two ways depending on whether the item had a subnav. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The six are defined in cp.css but read from twelve other files, the legacy CP's SCSS among them — a var() that no longer resolves fails silently, so the consumers move with the definitions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
[6.x] Add safe area to main navigation
# Conflicts: # resources/js/common/components/CpLink.vue
|
This PR had conflicts with Claude's response
|
# Conflicts: # resources/js/pages/assets/Index.vue
|
This PR had conflicts with Claude's response
|
# Conflicts: # resources/js/modules/elements/components/NewEntryButton.vue
|
This PR had conflicts with Claude's response
|
# Conflicts: # resources/js/pages/settings/entry-types/Index.vue # resources/js/pages/settings/routes/Index.vue Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This PR had conflicts with Claude's response
|
contentMaxWidthand center it withcenterContent. Settings and form pages now use both.?debug-slots=0)Navigation
Element editor and indexes
ElementEditorhandles full pages as well as slideouts.Components and tokens
craft-buttonhas aflushoption that offsets its padding, and spaces an icon from its label when the variant containsplain. Otherwise it does nothing.--c-layer-*) and Tailwind utilities for surfaces, border colors, spacing and layers. A Storybook page documents the utilities.--c-container-paddinggutter.diron the root element.Housekeeping
preparescript ran husky outside the repository root, where it could have redirected git hooks away fromvp's.