Skip to content

chore(components): Refactor Page to be built from composed parts#2867

Merged
nad182 merged 43 commits intomasterfrom
CLEANUP/page-refactor-compound
Feb 27, 2026
Merged

chore(components): Refactor Page to be built from composed parts#2867
nad182 merged 43 commits intomasterfrom
CLEANUP/page-refactor-compound

Conversation

@scotttjob
Copy link
Copy Markdown
Contributor

@scotttjob scotttjob commented Jan 7, 2026

Motivations

We chatted in a recent meeting about refactoring Page to be built from composed parts + using data-attributes.

Using visual regression tests as a refactor helper, I was able to refactor page to keep the exact same structure as it does today while exposing a variety of new compound pieces to build in a compositional way if required.

Each compound piece accepts data-attributes, so in the edge case where someone needs very specific data-attributes on pieces of Page, they can now accomplish that! :)

Changes

  1. Page is now built from compound pieces in a composed way, and everything is exposed to the consumer for advanced applications.
  2. No visual tests were modified, because they all passed! :)

Code Notes

We could prooooobably break down that ternary statement in PageTitleMeta that's deciding what to render. That still feels a bit smelly to me.

Testing

Page should work completely 100% as-is with no changes by consumers.

However, you should also now be able to build composed pages for advanced use cases as well!

Changes can be
tested via Pre-release


In Atlantis we use Github's built in pull request reviews.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jan 7, 2026

Deploying atlantis with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7ebc98a
Status: ✅  Deploy successful!
Preview URL: https://605af9f8.atlantis.pages.dev
Branch Preview URL: https://cleanup-page-refactor-compou.atlantis.pages.dev

View logs

Comment thread packages/components/src/Page/index.ts Outdated
Comment thread packages/components/src/Page/Page.tsx
Comment thread packages/components/src/Page/Page.tsx Outdated
<Page.TitleMeta
title={title}
titleMetaData={titleMetaData}
subtitle={subtitle}
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.

is there a reason we'd want to stop at this level rather than continuing to a point where we have: Page.Subtitle, Page.Title, and Page.TitleMetaData?

all 3 of those feel like pieces that people would reasonably want to customize since 2/3 are already ReactNodes.

so we'd have something like

<Page.Wrapper>

</Page.Wrapper>

actually, do we even need a Page.Wrapper? or would it be possible to just have Page as the top level container?

either way, going back to what I was about to say!

<Page.Wrapper>
 <Page.Header>
  <Page.TitleBar>
   <Page.Title>
     // this is the default, you can give it different content but it'll be the predefined component
     <Page.TitleLabel><Trans>Hello friend</Trans></Page.TitleLabel>
    // this is a fully custom value that may get out of sync
    <Heading level=3><Trans>Hola Amigo</Trans></Heading>
   </Page.Title>

   <Page.TitleMetaData> 
     // doesn't seem like we have a default? so it's just a slot really
     <SomeComponent/>
   </Page.TitleMetaData>

   <Page.Subtitle> 
     // this is the default
     <Page.SubtitleLabel><Trans>This is my subtitle that gets the default appearance</Page.Subtitle>
     // this is for fully custom
     <Text><Trans>I want my subtitle to look entirely different for some reason</Trans></Text>
   </Page.Subtitle>

  </Page.TitleBar>
 // other parts
</Page.Header>
</Page.Wrapper>

I'm not 100% on the "label" naming but that's what I did in Menu so just went with the same idea.

basically we are providing the slot, that way you can customize heavily, but we also provide the default if all you want is a declarative style but are otherwise happy with what we provide

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.

Implemented in e5c7132.

Page.TitleMeta has been replaced with separate Page.Title and Page.Subtitle components. Page.Wrapper is gone - Page itself is the top-level container and handles width internally.

  • Page.Title accepts children (the heading text) and an optional metadata prop for badges/status labels.
  • Page.Subtitle accepts either a string (gets the default Text/Emphasis/Markdown "treatment") or a ReactNode for full customization.
  • I didn't go as deep as "Page.TitleLabel" / "Page.SubtitleLabel" since the string VS ReactNode pattern on children covers the same use case with less nesting.

Comment thread packages/components/src/Page/Page.tsx Outdated
ref={primaryAction?.ref}
visible={!!primaryAction}
>
<Button {...getActionProps(primaryAction)} fullWidth />
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.

this getActionProps feels a bit clunky. I wonder if we couldn't find a way to abstract that or avoid people having to use this on their instances.

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.

getActionProps is only used internally by the legacy renderer now.
In the composable API, Page.PrimaryAction and Page.SecondaryAction now handle their own defaults, so consumers never touch this. But if/when Button gets native ref support (as you suggested below), the need for this will likely go away.

Comment thread packages/components/src/Page/Page.tsx Outdated
<Page.ActionGroup visible={!!showActionGroup}>
<Page.PrimaryAction
ref={primaryAction?.ref}
visible={!!primaryAction}
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.

I feel like there's gotta be an alternate approach here.

having a visible prop with a declarative style feels redundant.

ie. if I don't want it to be visible, then wouldn't I simply not render it?

{isAdmin && <Page.PrimaryAction ...>}

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.

I agree! The composable API is fully declarative now. I removed this prop.

Comment thread packages/components/src/Page/Page.tsx Outdated
>
<Button {...getActionProps(primaryAction)} fullWidth />
</Page.PrimaryAction>
<Page.ActionButton
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.

curious how this would work now. previously we would limit you to a primary action and a single secondary.

looking at how it works now, I assume I would be able to add as many ActionButtons as I want. maybe that's fine, but if we have opinions on how many buttons a Page should have, that would be worth documenting.

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.

I replaced Page.ActionButton with moredistinct Page.PrimaryAction, Page.SecondaryAction, and Page.Menu.
Hopefully, this way we "document" our opinion on the page structure (at most these three action slots) while making each one self-documenting. I also added sensible Button defaults for both PrimaryAction and SecondaryAction, while accepting children (in case the consumers would want to provide a custom content).

Comment thread packages/components/src/Page/Page.tsx Outdated
type="secondary"
/>
</Page.ActionButton>
<Page.ActionButton visible={!!showMenu}>
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.

same idea for the ActionButtons, a visible prop feels odd to me. I wonder if we can avoid that and leverage the declarative style.

also kinda coming back to the topic of having N ActionButtons, it seems to me these are quite different.

one is a regular Button, and one is a Menu. while yes it will look the same at a glance, it has very different behavior.

how would someone know the default configuration? and how to build it?

I'm wondering about maybe a Page.PrimaryAction Page.SecondaryAction and Page.TertiaryAction or Page.ActionMenu

that way our opinions on how many elements should be there, in what order, and what they are is easy to understand and implement.

like with some other pieces, we'd still need to do a bit more work to create good simple defaults for people to use, and then also offer a "slot" where heavy customization can be applied - but they don't have to sort out the order/position/layout of these 3 actions.

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.

Responded in the comment above.

Comment thread packages/components/src/Page/Page.tsx Outdated
...rest
}: {
readonly title: ReactNode;
readonly titleMetaData: ReactNode;
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.

currently titleMetaData is optional. let's stick to that unless there's a compelling reason to make it mandatory.

https://github.com/GetJobber/atlantis/blob/master/packages/components/src/Page/Page.tsx#L37

Copy link
Copy Markdown
Contributor

@nad182 nad182 Feb 10, 2026

Choose a reason for hiding this comment

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

I left it as optional on Page.Title.

@ZakaryH
Copy link
Copy Markdown
Contributor

ZakaryH commented Jan 14, 2026

I'll update this comment as I progress, but immediately trying to use this compound version I hit a block with

Page

<Page>
...
</Page>

it tells me that title is a required prop, but with the sub components that's no longer the way to provide it. we'll need a way to identify the sub component invocation, and provide different props.

Subtitle
we have 2 APIs for this that I believe do the same thing

<Page.TitleMeta
 title="My Title"
 subtitle="A subtitle"
>
<Page.Subtitle>Also subtitle</Page.Subtitle>
image

I would be in favor of further breaking down TitleMeta so it's not 3 pieces in one, and keeping Page.Subtitle though I am realizing it's a bit hard to tell where to put it. we'll definitely need some strong docs and examples. that way, Page.Subtitle sticks around, and we get rid of the second subtitle prop.

I'm finding that the subtitle being Text > Emphasis > Markdown is pretty restrictive/opinionated. it's a sub component for sure, but I wouldn't say we have meaningfully improved composition/flexibility. it is more declarative though! assuming the Page.Subtitle invocation anyway.

Page.Wrapper

I'm not quite sure how I'm supposed to use this piece? it requires the pageStyles but I don't feel like that's something a consumer should have to worry about. it also introduces an awkward scenario due to the props required for usePage which isn't exported.

if anything I'd prefer to inverse that. where we provide it, but if you don't like it you can UNSAFE override it. actually that might be something we're missing on all the subcomponents, is the UNSAFE props.

I'd also prefer to avoid exporting styles unless we absolutely must. I find it creates tough situations for us where we can't safely change internal style details without worrying about how it will affect those using them outside.

I think it would feel much better if Page.Wrapper wasn't necessary. we just have Page that handles this. rather than having to fuss with pageStyles on Page.Wrapper you just provide Page the relevant props like width and we handle that for you.

Page.Intro

the externalIntroLinks prop is kind of awkward. I wonder if we can't make this easier to use? if I don't want a link to be external, is there a way to do that? basically I'm feeling like if I just had control over what this rendered, it would be so much more flexible. is there a reason we need to be so strict here? or could we have the default with Text + Markdown, and then have it just be a slot for when you don't want that?

Page.ActionGroup
the visible prop being required on this and the actions adds a lot of friction.

if we could just leverage the declarative nature, I think it would improve it significantly.

it's also kinda hard to know where to put this. there are so many pieces and you have to already know that the Actions are in the Header. it still renders if I put it outside Page.Header so it's up to me to know how Page is supposed to look.

I know there are so many components here that it's going to be very verbose but I wonder if we can't have it be self documenting to some degree with the names Page.Header.Actions or Page.HeaderActions.

Page.ActionButton and Page.PrimaryAction

I'm probably doing it wrong but I can't get these to sit beside each other? they keep stacking no matter what. it might be the pageStyles I'm missing? update: ah I got it, it needs to be inside the TitleBar. that's tricky and not super obvious.

the order is important here. if I don't place my primary button first, it doesn't space correctly. on one hand, I guess we can just say you should look up the order and do it that way, but on the other hand it's trivial for us to make it order independent with CSS grid. we already have access to the elements, assuming we make new unique ones for the secondary/menu actions that is.

Page.PrimaryAction accepting a Button means we don't provide a default to stay in sync with. I would prefer we did. if ever down the road we change the defaults on Page, it's simple to make sweeping changes. if each instance of Page.PrimaryAction is detached, it creates much more work. same idea applies to the other actions.

I do feel like sub components with stronger identities would be helpful. so rather than Page.ActionButton it's Page.SecondaryAction and Page.ActionMenu. I'm not sure we have a need for infinite actions? or if we even want that. I think it's perfectly valid for us to have opinions that a Page has at most 3 buttons if we have rationale to back it up. that it gets overwhelming and that's the whole point of the More Actions.

Page.ActionButton Menu

the menu behavior is difficult to implement, and you have to know to put a Menu there at all. I feel like that should be a responsibility of the (sub) component. since we don't provide the default, you have to know and repeat the exact phrasing every other "more actions" menu has on a Page.

this turns into a decent amount of markup to implement. if I want to customize it, sure that's fair. but if I just want data attributes, and the defaults I'm not sure it's fair to make people do all this. the recurring theme is for sure that we need to provide defaults IMO.

update: knowing about the TSR linking problems, I do feel it's worth spending more time to focus on the Page.Menu API, to figure out how to best allow a consumer to provide a TSR createLink'd Menu.Item that allows them to focus on what they care about, while still providing the ability for a more customized Menu if that's needed (maybe they want to wrap it in an Intercom target or PrivacyMask)

Content

largely a personal preference, I don't feel super strong about it, but I find it a bit weird to just plop my actual content inside the Page.Wrapper when everything else had a proper slot.

maybe a Page.Content or Page.Body? I think one other benefit to that outside of preference is that it allows us to target/have easier access to that wrapping element if we need to change anything, or if we eventually have other sibling sub components.

here's my final example code

const BasicTemplate: ComponentStory<typeof Page> = args => {
  const { pageStyles } = usePage({ width: "standard" });

  return (
    <Page.Wrapper pageStyles={pageStyles}>
      <Page.Header>
        <Page.TitleBar>
          <Page.TitleMeta
            title="My Title"
            subtitle="a subtitle"
            titleMetaData={
              <StatusLabel
                label={"In Progress"}
                alignment={"start"}
                status={"warning"}
              />
            }
          />
          <Page.Subtitle>check</Page.Subtitle>
          <Page.ActionGroup visible={true}>
            <Page.PrimaryAction visible={true}>
              <Button
                label="Primary Action"
                onClick={() => alert("Primary Action")}
              />
            </Page.PrimaryAction>
            <Page.ActionButton visible={true}>
              <Button
                label="Secondary Action"
                onClick={() => alert("Secondary Action")}
                type="secondary"
              />
            </Page.ActionButton>
            <Page.ActionButton visible={true}>
              <Button label="Who knows" onClick={() => alert("Who knows")} />
            </Page.ActionButton>
            <Page.ActionButton visible={true}>
              <Menu>
                <Menu.Trigger>
                  <Button label="More Actions" />
                </Menu.Trigger>
                <Menu.Content>
                  <Menu.Item
                    textValue="Action 1"
                    onClick={() => alert("Action 1")}
                  >
                    <Menu.ItemLabel>Action 1</Menu.ItemLabel>
                  </Menu.Item>
                  <Menu.Item
                    textValue="Action 2"
                    onClick={() => alert("Action 2")}
                  >
                    <Menu.ItemLabel>Action 2</Menu.ItemLabel>
                  </Menu.Item>
                  <Menu.Item
                    textValue="Action 3"
                    onClick={() => alert("Action 3")}
                  >
                    <Menu.ItemLabel>Action 3</Menu.ItemLabel>
                  </Menu.Item>
                </Menu.Content>
              </Menu>
            </Page.ActionButton>
          </Page.ActionGroup>
        </Page.TitleBar>
        <Page.Intro externalIntroLinks={true}>
          This is my intro with a link to [Jobber](https://www.jobber.com)
        </Page.Intro>
      </Page.Header>
      <Content>
        <Text>Page content here</Text>
      </Content>
    </Page.Wrapper>
  );
};

edit: thinking about it more, I wonder about the need for so many header/title sub components. would it be possible to merge or abstract one or more of these components and handle it for the consumer?

@ZakaryH
Copy link
Copy Markdown
Contributor

ZakaryH commented Jan 15, 2026

after trying to build with these pieces, I would say there are a handful of things we should strongly consider changing and a few others that would also be worthwhile improvements. that said, this is a great start!

one final piece of FUD I have is what we are asking of consumers through all of this.

if someone has an existing Page instance, and all they want is to apply data attributes to their actions - there's no way for them to only replace that module. they are going to have to fully refactor their entire Page to this new pattern.

that's a pretty big chunk of work for a relatively small addition. something to keep in mind.

import { type SectionProps } from "../Menu";

export type ButtonActionProps = ButtonProps & {
ref?: React.RefObject<HTMLDivElement | null>;
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.

ugh, if we get that ref support on Button done, this can go away. I was wondering why we had these extra refs that we are stripping from the props with that extra helper function.

@ZakaryH
Copy link
Copy Markdown
Contributor

ZakaryH commented Jan 15, 2026

whoops, I forgot to try the entire point of all this 😅

I feel like giving the data attributes to Page.PrimaryAction isn't ideal. if we make the changes to Button, then data attributes can simply be given directly to that rather than this wrapping div.

a bit to sort out with the defaults cases, but for the fully custom where it's acting as a slot you'd just do <Button data-hello="world" /> and you're done! I do get that this means we might need to also do it for Menu, which is increasing the scope by a large margin, so I'm not totally against shipping without those alongside as long as we have a clear plan for how this works once those are available. I'd hate to have to immediately deprecate or otherwise change course if we get that done in the near future.

another argument for more atomic pieces for defaults that we receive the props for and can abstract with a very thin layer.

oh also Page.Header isn't applying data attributes. presumably because it's just a Content and that would require Content supporting it?

- Uses the same TS overload pattern as Menu
- Replaces ts-xor XOR with native discriminated union
- Props-based usage is fully preserved; composable mode activates when title is not passed as a prop
- Page.Menu uses the composable Menu internally, enabling consumers to provide Menu.Item children compatible with client-side routers
- PrimaryAction/SecondaryAction provide sensible Button defaults but accept children for custom elements
- All compound components support data-attributes via filterDataAttributes
- Creates PageNotes.mdx covering all sub-components, the slot pattern for custom actions,
and an explanation of TSR integration via createLink(Menu.Item).
@nad182
Copy link
Copy Markdown
Contributor

nad182 commented Feb 19, 2026

some thoughts trying to build with the Page component.

@ZakaryH , this is a pretty thorough and large write-up and I have looked into every point you raised. I'm struggling a bit with how to respond best. We can get on a call to decide what to do with the points that you raised. But I will try my best to provide my thoughts concisely in a single comment.


  1. metadata as a prop vs Page.TitleMetaData component

I chose the prop approach because I though that it was simpler and metadata is always a single element displayed alongside the title. Making it a sub-component would add a layer without adding flexibility - you'd still just pass a ReactNode into it. Having it as a prop is kind of like a named slot, which is a valid pattern (React Aria uses this extensively). You do have a point about consistency. So I'm happy to change it to Page.TitleMetaData if you'd prefer full consistency.


  1. Page Actions layout / margin alignment

I used the same CSS classes, so this behaviour is inherited from whatever we have on master right now. It probably would be cleaner with gap on the action group instead of individual margins. But that's a layout change that would affect the legacy version of Page as well. If we want to do that, it would be better to do it separately (IMHO) to avoid breaking some existing usage and needing to do a full revert of all other changes that this PR has.


  1. Page.Menu customization / slot separation

This is a big one. While I agree that from an extensibility perspective it is a vaild concern, I think we could do it later if the need arises. For the immediate need (TSR menu links), the current Page.Menu would be sufficient.
If we need to add a positional slot component later, we can restructure Page.Menu internally to use it. That way current usages would keep working and consumers who need more control would get the escape hatch.


  1. Menu dropdown alignment

This one can be big or small depending on what we want to do. If you check Storybook, the "incorrect" alignment happens only on width: "fill" variant (on "narrow" and "standard" it matches the prop driven Page).
image
image
image

I was able to pinpoint it to the fact that legacy Page uses Floating UI with flip middleware (with fallbackPlacements: ["bottom-end", "top-start", "top-end"]; while the composable Page uses React Aria's AriaPopover with placement="bottom start" hard-coded (in Menu.Content) and no "flip" fallbacks configured.
We could solve it by adding placement as a prop in MenuContentComposableProps and in changing it to placement={placement ?? "bottom start"} in Menu.tsx, which would allow to configure this in Page (and any other component that uses composable Menu).
Let me know if that make sense and how you would like me to proceed.


  1. PrimaryAction / SecondaryAction slot separation

If I understood your concern correctly, the current PageActionProps union already "solves this: when you pass children, the component just renders the "positional" wrapper div with your custom content inside. So the "slot" exists, it's just implicit rather than a separate named component. However, if you mean that you envision for the "wrapper" itself to be customizable as well (e.g. its class, element type, styles), then I guess we would need a slot + content pair. But I have a feeling that I'm just not fully or correctly understanding the issue, so I'd be happy to discuss it over a call if I'm off with interpreting it.

@nad182 nad182 requested review from ZakaryH and jdeichert February 19, 2026 03:14
… metadata to be a sub-component

- Replace metadata prop with Page.TitleMetaData sub-component
- Split actions into positional slots (PrimarySlot, SecondarySlot, TertiarySlot) and opinionated buttons (PrimaryAction, SecondaryAction)
- Page.Menu no longer owns its positional wrapper, goes inside TertiarySlot
- Remove typeof children === "string" checks from Subtitle and Intro; default styling always applies, fixing i18n/Trans compatibility
- Remove hasCustomChildren type guard and discriminated union (no longer needed)
@ZakaryH
Copy link
Copy Markdown
Contributor

ZakaryH commented Feb 24, 2026

the only thing I'm wondering about is the naming. we haven't used the term "slot" before, and where I think it makes things a bit less obvious is what to put where.

<Page.PrimarySlot>
// our opinionated/default primary Button goes here
</Page.PrimarySlot>

<Page.SecondarySlot>
// our opinionated/default secondary Button goes here
</Page.SecondarySlot>

<Page.TertiarySlot>
// our opinionated/default Menu goes here
</Page.TertiarySlot>

so while we want to offer flexibility, there is still a standard structure that we want to not necessarily enforce, but at least guide people towards.

<Page.TertiarySlot>
 <Combobox />
</Page.TertiarySlot>

to me that seems valid. nothing in here indicates this is even remotely undesirable.

vs

<Page.ActionMenu>
 <Combobox />
</Page.ActionMenu>

it feels a bit weird to be putting a Combobox inside a component named "Menu" and it at least guides towards or communicates that yes we still have some opinions on what a Page contains. you're welcome to modify it but this is what it should be for consistency.

so instead what I would propose is:

<Page.Actions>
 <Page.ActionPrimary>
  <Page.PrimaryButton />
 </Page.ActionPrimary>

 <Page.ActionSecondary>
  <Page.SecondaryButton />
 </Page.ActionSecondary>

 <Page.ActionMenu>
  <Page.Menu> ... </Page.Menu>
 </Page.ActionMenu>

</Page.Actions>

I do think it can be a bit unclear where Page.ActionMenu goes. to solve that we can either rely on docs and examples, OR we can use CSS grid to position it regardless of order. the latter is probably something we could follow up with if it becomes an issue.

to me an "action" is a Page concept, whereas as Button is a literal element - so while there is a bit of redundancy between Page.ActionPrimary -> Page.PrimaryButton, in my head that makes sense and is conceptually coherent.

also I realize that there's some asymmetry here. the first two action containers are more generic, and only the last one says "Menu". I think that might be ok, while we should use Buttons for the first two - it's really the last one, the Menu, that we probably have the strongest opinion on. this is where overflow actions go that weren't the primary nor secondary. I think putting some light JSDocs on the ActionPrimary and ActionSecondary saying "these should be Buttons" could suffice.

what do you think about that? does this naming make sense to you?

- Replace PrimarySlot, SecondarySlot, and TertiarySlot with ActionPrimary, ActionSecondary, and ActionMenu.
- Update action components to use PrimaryButton and SecondaryButton for better consistency.
- Modify related tests and documentation to reflect the new structure.
@nad182
Copy link
Copy Markdown
Contributor

nad182 commented Feb 24, 2026

what do you think about that? does this naming make sense to you?

@ZakaryH , I think this naming convention is reasonable. It might be somewhat inconsistent with the legacy API which uses primaryAction/secondaryAction as prop names for the Button config objects, while we are now providing PrimaryButton/SecondaryButton.
But also having the first two actions as generic "...Action") and the last one as a "Menu" specifically can actually be considered a "feature" - where we specifically communicates intent. I have added to the JSDoc comments guiding consumers to use Button inside ActionPrimary/ActionSecondary: ea4a8f6

Comment thread packages/components/src/Page/Page.tsx Outdated
let metaDataElement: ReactNode = null;
const otherChildren: ReactNode[] = [];

Children.forEach(children, child => {
Copy link
Copy Markdown
Contributor

@ZakaryH ZakaryH Feb 25, 2026

Choose a reason for hiding this comment

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

ah I missed this earlier. with very few exceptions, we should avoid iterating over the children prop. it's a strong signal/smell that we've missed a step while constructing our component or API.

rather than having Page.Title accept two things, let's make each part so simple.

function PageTitleBar({ children, ...rest }: { readonly children: ReactNode }) {
  const dataAttrs = filterDataAttributes(rest);

  return (
    <div className={styles.titleRow} {...dataAttrs}>
      {children}
    </div>
  );
}

/** Renders the page heading (H1). */
function PageTitle({ children, ...rest }: PageTitleProps) {
 // this probably doesn't work yet but it's not advertised so that might be fine
  const dataAttrs = filterDataAttributes(rest);

  return (
    <Heading level={1} {...dataAttrs}>
      {children}
    </Heading>
  );
}

/** Metadata displayed alongside the page title (e.g. status badges). Use inside Page.Title. */
function PageTitleMetaData({ children }: PageTitleMetaDataProps) {
  return <>{children}</>;
}

now, similar to the actions and whatnot - if someone really really wants to have a h2 in the title, they can do

<Page.TitleBar>
 <Heading level={2}>...</Heading>
</Page.TitleBar>

they shouldn't do that, but that's part of the deal we are making now. more power, and more responsibility.

the PageTitleMetaData sticks out to me now. it's literally nothing. we have no opinion of what it should or could be. it's just some arbitrary element that is a sibling to the Heading/Title element.

so now I question if it's even worth having? what would be the downside of

<Page.TitleBar>
  <Page.Title>Hello World</Page.Title>
  <StatusLabel label="In Progress" alignment="start" status="warning" />
</Page.TitleBar>

to me that seems fine. the whole TitleMetaData concept is too nebulous. it lacks any kind of definitive shape, and because of that I would say it's not the responsibility or part of the Page's identity.

so with that in mind, what if we instead go for...

<Page.Title>
  <Page.TitleLabel>
    <Trans>Hello World</Trans>
  <Page.TitleLabel>
  <StatusLabel label="In Progress" alignment="start" status="warning" />
</Page.Title>

I don't feel too strongly about any of the names. Page.Title as the container is OK. not my favorite, and the Page.TitleLabel also feels meh.

Page.TitleBar or Page.TitleRow are similar. I like Row less because visually, the Actions are on that row too and in my mind I think of a row as spanning the entire width.

so Page.TitleBar + Page.Title might be my preference but I'll leave it up to to you to decide.

really, the big things I want us to do here are:

  • avoid iterating over children
  • simplify building blocks as much as we can within reason
  • discard the concept of TitleMetaData. it can now be totally delegated outside the component, and achieved with composition

this will mean a small additional example or note about how to implement a TitleMetaData in the new version

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.

I believe I addressed all of your points in e0c23d8.

Comment thread packages/components/src/Page/Page.tsx Outdated
let actionsElement: ReactNode = null;
const otherChildren: ReactNode[] = [];

Children.forEach(children, child => {
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.

same feedback here. I would love to avoid iterating over children. lemme think about how to split this up...

Copy link
Copy Markdown
Contributor

@ZakaryH ZakaryH Feb 25, 2026

Choose a reason for hiding this comment

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

so one simple answer is to split it up more and add another layer. Page.HeaderContent.

/** Wraps the title area (title, subtitle). Renders in the left slot of the header layout. */
function PageHeaderContent({ children }: { readonly children: ReactNode }) {
  return <div>{children}</div>;
}

which allows PageHeader to be purely layout, no logic or children iteration

function PageHeader({ children, ...rest }: PageHeaderProps) {
  const dataAttrs = filterDataAttributes(rest);

  return (
    <Content>
      <Container
        name="page-titlebar"
        autoWidth
        dataAttributes={dataAttrs as CommonAtlantisProps["dataAttributes"]}
      >
        <Container.Apply autoWidth>
          <div className={styles.titleBar}>
            {children}
          </div>
        </Container.Apply>
      </Container>
    </Content>
  );
}

and finally used like

<Page.Header>
  <Page.HeaderContent>
    <Page.TitleBar>
      <Page.Title>Kitchen Renovation Project</Page.Title>
      <StatusLabel label="In Progress" ... />
    </Page.TitleBar>
    <Page.Subtitle>Everything but the Kitchen Sink</Page.Subtitle>
  </Page.HeaderContent>
  <Page.Actions>
    ...
  </Page.Actions>
</Page.Header>

the downside is that we're getting so deeply nested with all this and it's really tough to know what goes where. we are going to rely heavily on examples and documentation, and perhaps trial-and-error. having Header > HeaderContent > TitleBar > Title, then siblings of Subtitle, and Actions is a lot..... part of me wonders if we create a named CSS grid, if that makes it simpler. you don't have to worry about so much nesting, we give you one container and then know where to put everything. it might save a layer or two of nesting.

also I do feel pretty weird about PageHeaderContent being a single unclassed div though. it makes me wonder what it is there for and if we can't solve that some other way...

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.

Addressed this in e0c23d8.

@ZakaryH
Copy link
Copy Markdown
Contributor

ZakaryH commented Feb 25, 2026

seeing an issue on small screens

image

vs master

image

this might be related to another request we got recently about full width menu buttons

/* Composable Page API: responsive layout via CSS Container Queries */

/* Base composable behavior (always active inside Container) */
@container page-titlebar (min-width: 0) {
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.

if we want something to always be there, or be treated as the default - we can simply define these as the style, no need for a greater than 0 width container query

  .titleBar > .actionGroup {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }

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.

couple questions about this container query approach:

didn't we have to revert it last time? what was the issue?

I don't see a container-type definition, does this work without one? I would have expected to see container-type: inline-size on the overall Page container element.

does this now mean we are managing the layout swaps with 2 mechanisms in the same component? one with the titleBarRef and one with these container queries?

lastly, I wonder if a CSS grid could both solve the layout, assuming we aren't reintroducing whatever issue we had to revert for, AND if it would remove at least one layer of component nesting + make it easier to use as far as putting things in the exact expected order.

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.

basically what I'm thinking is this

Page Header is where we have the vast majority of our elements. we know we want at least 2 things:

  • a layout that responds to the container size
  • minimal layers of components
  • ideally, an easy to use API/structure
Image

using a CSS grid, we can label each area and tell it where to go based on the container query. this solves the layout, AND it makes it so that it is order independent. meaning if I do

<Page.Header>
  <Page.Title>...</Page.Title>
  <Page.Actions>...</Page.Actions>
  <Page.Subtitle>...</Page.Subtitle>
</Page.Header

it yields the same thing as

<Page.Header>
  <Page.Title>...</Page.Title>
  <Page.Subtitle>...</Page.Subtitle>
  <Page.Actions>...</Page.Actions>
</Page.Header

and makes it just a bit easier to use

then it might also remove a layer but I need to double check that. in my head it seems to me we have one other layer here that is trying to achieve this same result that we wouldn't need any more..

Comment thread packages/components/src/Page/Page.tsx Outdated
});

return (
<Content>
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.

do we need this?

also, now I see how this works with the container queries. it's the Container element below.

while it is nice to use our components, the concern I have is that we are solving the same problem in two different ways inside the same component, the titleBarRef and container queries.

what I would be tempted to do instead is set the container-type on .page so both can use that method, and then have our queries based on that.

is there a reason you'd like to do it with 2 different mechanisms for each version?

@ZakaryH
Copy link
Copy Markdown
Contributor

ZakaryH commented Feb 25, 2026

I had a few different comments, so just to summarize in rough priority order

container queries
I know we had to revert this the first time, how have we mitigated that issue?

the props driven Page and the composed Page now handle the same problem, container sensitive layout adjustments, in 2 distinct ways. the old one with the useResizeObserver and the new one with container queries.

I worry that this can introduce drift in experience, and we'll have 2 different implementations to fix and maintain.

if the container query works, I feel we should apply that to both - however that's not necessarily the goal of this PR so sticking to the old way may be preferable.

child iteration
we should try to avoid this whenever possible. I do think we have an opportunity to do that with a bit of shuffling around.

as a consequence of the above - on its own, TitleMetaData becomes shapeless and arguably not a built in part of Page because we seem to have no opinions on what it should be. a webstory example showing how you could do it would suffice.

small screen layout issue
the menu isn't behaving properly and I suspect this is related to another request we have, and would require changes in Menu

CSS Grid
this one is related to a couple other topics, and I would classify as an optimization or totally different way to solve the container layout problem. additionally it could make the sub component DX easier to use, remove a nested layer. if we are happy with our solutions without this, it's not strictly necessary.

extra Content
it seems we have some extra markup that doesn't modify the layout that we could remove. it doesn't seem to hurt to have it, but if it's not providing anything - simplifying our markup is always nice!

nad182 and others added 3 commits February 25, 2026 13:45
… API

Replace implicit child iteration with explicit wrapper components:
- PageTitle: remove TitleMetaData detection loop; introduce Page.TitleBar
  as an explicit flex container for title + metadata siblings
- PageHeader: remove Actions detection loop; introduce Page.HeaderContent
  to group title area content, and remove redundant Content wrapper

Co-authored-by: Cursor <cursoragent@cursor.com>
* instead of Container (container queries)
* this is temporary, and we'll switch both legacy and composable Page to use container queries shortly (hopefully)
Copy link
Copy Markdown
Contributor

@ZakaryH ZakaryH left a comment

Choose a reason for hiding this comment

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

LGTM

thanks for all your hard work on this PR and for putting up with all my comments!

@nad182 nad182 merged commit adc0483 into master Feb 27, 2026
15 checks passed
@nad182 nad182 deleted the CLEANUP/page-refactor-compound branch February 27, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants