Deck
Browse the catalog

Build / Organisms

AccountSheet

  • organism
  • play-fn

The account drawer, opened from the avatar in the app bar.

Answers "who am I signed in as, and what do I hand people" in one surface. That pairing is the reason this is not a generic menu: the cards are not a settings sub-page reached through a menu item, they are the account, and showing them with their public links means the common question — *which of these is the one I give out* — is answered without opening anything.

A side drawer rather than a bottom sheet, at every width. It is a list of destinations, and it hangs off a control in the top-right corner of the bar; a panel that flew to the bottom of the screen would leave the thing that opened it behind.

The profile block is this sheet's visible header, so Sheet's own title is hidden — it survives as the dialog's accessible name and nothing more, and the close control lifts into the corner beside the avatar.

Deck has no router and no session. Every row takes an href from the composition layer and falls back to a callback without one, so an app with routing gets real links and a prototype without it gets working controls rather than a drawer full of addresses that 404.

import { AccountSheet } from '@meetcard/deck'

Example

<AccountSheet open={open} onClose={close} name="Ben Ackles" handle="/ben" settingsHref="/settings/profile" cards={[{ id: 'personal', label: 'Personal card', link: 'meetcard.io/ben', href: '/cards/ben' }]} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Open, as it arrives from the avatar in the app bar: who you are signed in as, the cards you can hand over with the links that tell them apart, and the way into settings.

Default
Source
const Default = () => <AccountSheet
    name="Ben Ackles"
    handle="/ben"
    cards={[
      {
        id: 'personal',
        label: 'Personal card',
        link: 'meetcard.io/ben',
      },
      {
        id: 'business',
        label: 'Business card',
        link: 'meetcard.io/ben@meetcard',
        icon: BriefcaseIcon,
        isDefault: true,
      },
    ]}
    onSelectCard={fn()}
    onClose={fn()}
    open />;

With routing. Given an href a row becomes a real link — openable in a new tab, copyable, and announced as a link — which is what an app with routes should pass. The rows above are buttons only because Deck has nowhere to send them.

/settings/profile is the product's own destination: this entry point comes from a person's own face, so the section about them is the one they meant, rather than a settings index.

As Links
Source
const AsLinks = () => <AccountSheet
    name="Ben Ackles"
    handle="/ben"
    cards={cards.map((card) => ({
      ...card,
      href: `/cards/${card.link?.replace('meetcard.io/', '')}`,
    }))}
    onSelectCard={fn()}
    onClose={fn()}
    open
    settingsHref="/settings/profile" />;

From the control that opens it. The drawer is a real modal — focus moves into it, Escape closes it, and the page behind is inert — because it is a native <dialog> rather than a positioned <div>.

From The App Bar
Source
const FromTheAppBar = () => {
    const [open, setOpen] = useState(false)

    return (
        <>
            <Button onClick={() => setOpen(true)}>Account</Button>
            <AccountSheet
                name="Ben Ackles"
                handle="/ben"
                cards={[
                  {
                    id: 'personal',
                    label: 'Personal card',
                    link: 'meetcard.io/ben',
                  },
                  {
                    id: 'business',
                    label: 'Business card',
                    link: 'meetcard.io/ben@meetcard',
                    icon: BriefcaseIcon,
                    isDefault: true,
                  },
                ]}
                onSelectCard={fn()}
                open={open}
                onClose={() => setOpen(false)} />
        </>
    );
};

Phone. The drawer keeps its edge at every width rather than becoming a bottom sheet — it is a list of destinations hanging off a control in the top-right corner, and a panel that flew to the bottom would leave the thing that opened it behind. The sliver of page at the start edge is what says this is over the app rather than a screen the app navigated to.

Mobile
Source
const Mobile = () => <AccountSheet
    name="Ben Ackles"
    handle="/ben"
    cards={[
      {
        id: 'personal',
        label: 'Personal card',
        link: 'meetcard.io/ben',
      },
      {
        id: 'business',
        label: 'Business card',
        link: 'meetcard.io/ben@meetcard',
        icon: BriefcaseIcon,
        isDefault: true,
      },
    ]}
    onSelectCard={fn()}
    onClose={fn()}
    open />;

A new account, before any card exists. The card section drops out entirely rather than showing an empty heading, and Settings stays — it is the one row that is always reachable.

No Cards
Source
const NoCards = () => <AccountSheet
    name="Ben Ackles"
    handle="/ben"
    cards={[]}
    onSelectCard={fn()}
    onClose={fn()}
    open />;

Extra rows go after Settings, as <li>s. Signing out lives at the foot because it is the one action here you cannot undo by pressing back.

With Sign Out
Source
const WithSignOut = () => <AccountSheet
    name="Ben Ackles"
    handle="/ben"
    cards={[
      {
        id: 'personal',
        label: 'Personal card',
        link: 'meetcard.io/ben',
      },
      {
        id: 'business',
        label: 'Business card',
        link: 'meetcard.io/ben@meetcard',
        icon: BriefcaseIcon,
        isDefault: true,
      },
    ]}
    onSelectCard={fn()}
    onClose={fn()}
    open>(<li>
      <AccountSheetRow label="Sign out" onClick={fn()} />
    </li>)</AccountSheet>;

Props

PropTypeDefaultDescription
namerequiredstring—The signed-in person.
onCloserequired() => void——
openrequiredboolean——
avatarSrcstring—Photo. Falls back to initials, as Avatar does everywhere.
cardsAccountCard[][]—
cardsLabelstring'My cards'Heading over the card list.
childrenReactNode—Extra rows after Settings — signing out, switching account. Appended inside the list, so pass <li>s.
handlestring—Public handle, shown under the name — "/ben".
labelstring'Account'The dialog's accessible name, announced on open. "Account" rather than the person's name: a screen reader reads the dialog's name and then its content, and naming it after the person makes the first thing you hear a repeat of the second. This says what the surface is, which is the part the name does not cover.
onSelectCard(id: string) => void——
onSelectSettings() => void——
settingsHrefstring—Where Settings goes — "/settings/profile" in the product, which opens on the profile section rather than a settings index: this entry point comes from a person's own face, so the section about them is the one they meant. Deliberately undefaulted. Every row here follows one rule — a link when it has a route, a button when it does not — and a row that invented an href nobody had routed yet would be the one exception, which is how a component starts shipping links that 404.
settingsLabelstring'Settings'—

Open in Storybookfor controls, the a11y panel and the interaction runner.