Skip to content

feat(ui): header slot + SidebarLayout.DefaultHeader for the top bar#344

Draft
interacsean wants to merge 2 commits into
mainfrom
feat/ui/484-customizable-header-actions
Draft

feat(ui): header slot + SidebarLayout.DefaultHeader for the top bar#344
interacsean wants to merge 2 commits into
mainfrom
feat/ui/484-customizable-header-actions

Conversation

@interacsean

@interacsean interacsean commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an official, composable extension point for the app-shell top bar. SidebarLayout gains a full-region header?: ReactNode slot (mirroring the existing sidebar slot), defaulting to the built-in SidebarLayout.DefaultHeader.

Note: this PR was reworked following review (discussion). It originally added a narrow headerActions prop; it now uses a full-region header slot + building blocks, for consistency with sidebar/children and to avoid a proliferation of one-off header props.

API

Three tiers of customization:

// 1. Default — built-in header
<SidebarLayout />

// 2. Extend the built-in header (common case: add a notification bell, user menu…)
<SidebarLayout
  header={
    <SidebarLayout.DefaultHeader
      actions={[
        <NotificationBell key="bell" />,
        // `actions` REPLACES the right-hand cluster, so include the switcher to keep it
        <AppearanceSwitcher key="appearance" />,
      ]}
    />
  }
/>

// 3. Replace the header entirely
<SidebarLayout header={<MyCustomHeader />} />

SidebarLayout.DefaultHeader

The built-in top bar: sidebar trigger + breadcrumb on the left, an actions cluster on the right.

  • actionsReactNode | ReactNode[] (optional). The entire right-hand cluster, in an opinionated horizontal, vertically-centered row.
    • Defaults to [<AppearanceSwitcher />], so the zero-config header is unchanged.
    • ⚠️ Passing actions replaces the whole cluster, including the appearance switcher — include <AppearanceSwitcher /> explicitly to keep it (AppearanceSwitcher is a public export). actions={[]} renders an empty right side.
    • This keeps the API a single slot rather than accumulating one-off props (hideAppearanceSwitcher, headerLeft, …).

Namespacing

  • DefaultHeader is exposed as SidebarLayout.DefaultHeader and as a top-level DefaultHeader export.
  • DefaultSidebar is now also exposed as SidebarLayout.DefaultSidebar (top-level DefaultSidebar export retained for backwards compatibility).

Why

Resolves tailor-inc/platform-planning#484 — Customizable NavBar (header). Previously there was no supported way to extend the top bar, so consumers (e.g. TRM) queried the header DOM, created a portal target with a MutationObserver, and createPortal-ed their controls in — fragile and tied to internal structure. This replaces that with a first-class slot.

Changes

  • packages/core/src/components/sidebar/default-header.tsx — new DefaultHeader (trigger + breadcrumb + actions)
  • packages/core/src/components/sidebar/sidebar-layout.tsxheader slot; namespace SidebarLayout.DefaultHeader / .DefaultSidebar
  • packages/core/src/components/sidebar/index.ts, packages/core/src/index.ts — exports
  • packages/core/src/components/sidebar/sidebar-layout.test.tsx — 13 tests (header slot, actions replace/default/empty, namespace)
  • Docs: docs/components/{sidebar-layout,default-header (new),default-sidebar,appearance-switcher}.md, catalogue/src/fundamental/components.md + regenerated skill reference
  • examples/vite-app/src/App.tsx — demonstrates the new API
  • .changeset/minor bump

Follow-ups (out of scope)

  • Publicly export lower-level header primitives (breadcrumb, sidebar trigger) so tier-3 custom headers can reuse them.
  • Migrate other internal consumers to the namespaced SidebarLayout.DefaultSidebar.

Testing

  • oxlint — 0 warnings/errors · tsc --noEmit — clean · vitest — 1206 passed
  • Emitted .d.ts carries namespace SidebarLayout { DefaultSidebar; DefaultHeader }; example app type-checks as a real consumer
  • Browser-verified: actions render next to / in place of the switcher; switcher dropdown works when composed into actions

🤖 Generated with Claude Code

Provides an official extension point for the app-shell top bar: a
`headerActions` prop on SidebarLayout renders custom action components
(notification bell, user menu, global search, etc.) on the right side,
immediately before the appearance switcher. Accepts a single node or an
array, laid out in a horizontal, vertically-centered row.

Replaces fragile consumer workarounds that queried the header DOM and
injected a React portal to place controls next to the appearance switcher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@interacsean interacsean force-pushed the feat/ui/484-customizable-header-actions branch from 7d5b787 to 2b2b0e5 Compare June 30, 2026 01:54
@IzumiSy

IzumiSy commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@interacsean

WIP API thought: this may already be in flux, but sharing the comment anyway in case it’s useful.

My understanding of SidebarLayout’s customization API so far was that it is mostly organized around replacing whole regions, such as sidebar and children. From that perspective, headerActions feels a bit different, since it introduces a partial customization point inside the built-in header rather than replacing the header as a whole.

One reason I’m slightly hesitant about headerActions specifically is that once we add a narrowly scoped prop like this, it tends to invite more props in the same direction over time — e.g. headerLeft, headerRight, headerContent, hideAppearanceSwitcher, renderBreadcrumbs, etc. At that point the built-in header starts becoming incrementally configurable through many special-case props, which makes the API feel less coherent.

If we want to open up the header as an extension point, I wonder if something like header?: ReactNode would be more consistent with the existing API shape.

More generally, one thing I’ve been trying to optimize for in this kind of API is to keep extension points like header broadly open as ReactNode, and then provide the pieces that go into that slot as building blocks at different levels. That tends to balance flexibility and convention well: the slot itself stays simple and general, while consumers can choose between full replacement, default helpers like SidebarLayout.DefaultHeader, or lower-level building blocks depending on how much customization they need.

Then, for cases where consumers only want to slightly extend the built-in header, we could provide a helper like SidebarLayout.DefaultHeader, following the same idea as DefaultSidebar: the main component exposes a full-region customization point, while a default implementation remains available as a convenience for partial customization. Since AppearanceSwitcher is already public, this would still make it easy to reconstruct the default setup when needed, without adding a one-off prop like headerActions.

Related to that, thinking about symmetry, I’m also starting to feel that DefaultSidebar itself wants to live under the SidebarLayout namespace as well. In practice it is rarely useful outside the context of SidebarLayout, so SidebarLayout.DefaultSidebar / SidebarLayout.DefaultHeader feels like a more cohesive API shape. We could still keep the top-level DefaultSidebar export for compatibility, but the namespaced form seems like a nicer direction for discoverability and consistency.

…ltHeader (#484)

Pivots the top-bar extension API per PR review (IzumiSy): instead of a
one-off `headerActions` prop, `SidebarLayout` now exposes a full-region
`header?: ReactNode` slot (mirroring `sidebar`), defaulting to the built-in
`SidebarLayout.DefaultHeader`.

- `SidebarLayout.DefaultHeader` (also exported as `DefaultHeader`) renders the
  trigger + breadcrumb and an `actions` cluster. `actions` defaults to
  `[<AppearanceSwitcher />]`; passing `actions` replaces the whole right-hand
  cluster (include `<AppearanceSwitcher />` to keep it) — avoiding a
  proliferation of one-off header props.
- `DefaultSidebar` is now also namespaced as `SidebarLayout.DefaultSidebar`
  (top-level export retained for compat).
- Docs updated across docs/components (sidebar-layout, default-header [new],
  default-sidebar, appearance-switcher) and the catalogue + generated skill
  reference. Example app demonstrates the new API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@interacsean

Copy link
Copy Markdown
Contributor Author

Thanks @IzumiSy — this steer makes sense, and I've reworked the PR to follow it. Dropped the one-off headerActions prop in favour of a full-region slot + building blocks:

SidebarLayout gains a header?: ReactNode slot (mirrors sidebar), defaulting to the built-in SidebarLayout.DefaultHeader. Three tiers of customization:

// 1. default
<SidebarLayout />

// 2. extend the built-in header (common case)
<SidebarLayout
  header={
    <SidebarLayout.DefaultHeader
      actions={[<NotificationBell key="bell" />, <AppearanceSwitcher key="appearance" />]}
    />
  }
/>

// 3. replace entirely
<SidebarLayout header={<MyCustomHeader />} />

SidebarLayout.DefaultHeader (also a top-level DefaultHeader export) renders trigger + breadcrumb on the left and an actions cluster on the right. Key decision, following your "switcher is a building block" point:

  • actions defaults to [<AppearanceSwitcher />] — so the zero-config header is unchanged.
  • actions replaces the entire right-hand cluster, including the switcher. If you customize and still want it, you include <AppearanceSwitcher /> in the array. This deliberately avoids a hideAppearanceSwitcher-style boolean and keeps it a single slot. (AppearanceSwitcher is already a public export.)

Symmetry: DefaultSidebar is now also exposed as SidebarLayout.DefaultSidebar; the top-level export stays for back-compat, matching your suggestion.

Docs updated across docs/components (sidebar-layout, new default-header, default-sidebar, appearance-switcher) + the catalogue/skill reference, and the example app demonstrates it.

Two things I intentionally scoped out of this PR as follow-ups, lmk if you'd rather fold them in:

  1. Publicly exporting the lower-level header primitives (breadcrumb, sidebar trigger) so tier-3 full-replacement headers can reuse them — right now a full custom header would rebuild those.
  2. Migrating other internal consumers to the namespaced SidebarLayout.DefaultSidebar.

Does this shape match what you had in mind?

@interacsean interacsean changed the title feat(ui): add headerActions prop to SidebarLayout feat(ui): header slot + SidebarLayout.DefaultHeader for the top bar Jul 3, 2026
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