Build / Organisms
AccountSheet
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
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.
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.
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>.
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.
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.
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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | string | — | The signed-in person. |
onCloserequired | () => void | — | — |
openrequired | boolean | — | — |
avatarSrc | string | — | Photo. Falls back to initials, as Avatar does everywhere. |
cards | AccountCard[] | [] | — |
cardsLabel | string | 'My cards' | Heading over the card list. |
children | ReactNode | — | Extra rows after Settings — signing out, switching account. Appended
inside the list, so pass <li>s. |
handle | string | — | Public handle, shown under the name — "/ben". |
label | string | '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 | — | — |
settingsHref | string | — | 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. |
settingsLabel | string | 'Settings' | — |