Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dialog-hero-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astryxdesign/core': patch
---

[feat] DialogHeroHeader: prominent, hero-style header for Dialog — the high-emphasis sibling of DialogHeader. Adds an optional media/visual slot and eyebrow above a display-scale title, centered by default (`align="start"` for left alignment), with the same onOpenChange-driven close button and aria-labelledby title behavior. For featured, marketing, or onboarding dialogs.
@Lee-Dongwook
162 changes: 162 additions & 0 deletions apps/storybook/stories/DialogHeroHeader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

import {useState} from 'react';
import type {Meta, StoryObj} from '@storybook/react';
import {Dialog, DialogHeroHeader} from '@astryxdesign/core/Dialog';
import {
Layout,
LayoutContent,
LayoutFooter,
HStack,
} from '@astryxdesign/core/Layout';
import {Button} from '@astryxdesign/core/Button';
import {Icon} from '@astryxdesign/core/Icon';
import {Text} from '@astryxdesign/core/Text';

const meta: Meta<typeof DialogHeroHeader> = {
title: 'Core/DialogHeroHeader',
component: DialogHeroHeader,
tags: ['autodocs'],
argTypes: {
align: {
control: 'inline-radio',
options: ['center', 'start'],
description: 'Horizontal alignment of the hero content',
},
hasDivider: {
control: 'boolean',
description: 'Adds a border at the bottom edge',
},
},
};

export default meta;
type Story = StoryObj<typeof DialogHeroHeader>;

/**
* The prominent hero treatment: media, eyebrow, display-scale title, and
* supporting text, centered by default. Actions live in LayoutFooter.
*/
function CenteredExample() {
const [isOpen, setIsOpen] = useState(false);

return (
<>
<Button
label="Open hero dialog"
variant="secondary"
onClick={() => setIsOpen(true)}
/>
<Dialog isOpen={isOpen} onOpenChange={open => setIsOpen(open)}>
<Layout
header={
<DialogHeroHeader
media={<Icon icon="success" size="lg" color="accent" />}
eyebrow="Welcome"
title="You're all set up"
subtitle="Your workspace is ready. Invite your team to start collaborating."
onOpenChange={open => setIsOpen(open)}
/>
}
content={
<LayoutContent>
<Text type="body" color="secondary">
Body content for the featured moment goes here.
</Text>
</LayoutContent>
}
footer={
<LayoutFooter>
<HStack gap={2} hAlign="end">
<Button
label="Maybe later"
variant="secondary"
onClick={() => setIsOpen(false)}
/>
<Button
label="Invite team"
variant="primary"
onClick={() => setIsOpen(false)}
/>
</HStack>
</LayoutFooter>
}
/>
</Dialog>
</>
);
}

export const Default: Story = {
render: () => <CenteredExample />,
};

/**
* Start-aligned variant — left-aligned like DialogHeader, but at hero scale.
*/
function StartAlignedExample() {
const [isOpen, setIsOpen] = useState(false);

return (
<>
<Button
label="Open start-aligned"
variant="secondary"
onClick={() => setIsOpen(true)}
/>
<Dialog isOpen={isOpen} onOpenChange={open => setIsOpen(open)}>
<Layout
header={
<DialogHeroHeader
align="start"
eyebrow="New feature"
title="Introducing Insights"
subtitle="Track how your team uses the workspace over time."
onOpenChange={open => setIsOpen(open)}
/>
}
content={
<LayoutContent>
<Text type="body" color="secondary">
Body content goes here.
</Text>
</LayoutContent>
}
/>
</Dialog>
</>
);
}

export const StartAligned: Story = {
render: () => <StartAlignedExample />,
};

/**
* Inline preview (isInline) — the same rendering used by docs/showcases, with
* autofocus suppressed. Handy for visual review without opening a modal.
*/
export const InlinePreview: Story = {
render: () => (
<Dialog isOpen isInline onOpenChange={() => {}}>
<Layout
header={
<DialogHeroHeader
media={<Icon icon="success" size="lg" color="accent" />}
eyebrow="Payment received"
title="Thanks for your order"
subtitle="A receipt has been sent to your email. Your plan is now active."
onOpenChange={() => {}}
/>
}
content={
<LayoutContent>
<Text type="body" color="secondary">
Dialog body content goes here.
</Text>
</LayoutContent>
}
/>
</Dialog>
),
};
173 changes: 135 additions & 38 deletions packages/cli/lib/xle/registry-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,45 +26,132 @@
*/
export const ALIAS_TABLE = {
// Layout core
A: 'AppShell', L: 'Layout', LH: 'LayoutHeader', LC: 'LayoutContent',
LF: 'LayoutFooter', LP: 'LayoutPanel', V: 'VStack', H: 'HStack',
SI: 'StackItem', G: 'Grid', GS: 'GridSpan', S: 'Section', Ctr: 'Center',
F: 'FormLayout', D: 'Divider', Tbar: 'Toolbar', AR: 'AspectRatio',
A: 'AppShell',
L: 'Layout',
LH: 'LayoutHeader',
LC: 'LayoutContent',
LF: 'LayoutFooter',
LP: 'LayoutPanel',
V: 'VStack',
H: 'HStack',
SI: 'StackItem',
G: 'Grid',
GS: 'GridSpan',
S: 'Section',
Ctr: 'Center',
F: 'FormLayout',
D: 'Divider',
Tbar: 'Toolbar',
AR: 'AspectRatio',
// Navigation
TN: 'TopNav', TNH: 'TopNavHeading', TNI: 'TopNavItem',
SN: 'SideNav', SNI: 'SideNavItem', SNS: 'SideNavSection', SNH: 'SideNavHeading',
MN: 'MobileNav', MNT: 'MobileNavToggle',
BC: 'Breadcrumbs', BCI: 'BreadcrumbItem',
TL: 'TabList', Tab: 'Tab', PG: 'Pagination',
SG: 'SegmentedControl', SGI: 'SegmentedControlItem',
TN: 'TopNav',
TNH: 'TopNavHeading',
TNI: 'TopNavItem',
SN: 'SideNav',
SNI: 'SideNavItem',
SNS: 'SideNavSection',
SNH: 'SideNavHeading',
MN: 'MobileNav',
MNT: 'MobileNavToggle',
BC: 'Breadcrumbs',
BCI: 'BreadcrumbItem',
TL: 'TabList',
Tab: 'Tab',
PG: 'Pagination',
SG: 'SegmentedControl',
SGI: 'SegmentedControlItem',
// Data display
T: 'Table', TH: 'TableHeader', TB: 'TableBody', TF: 'TableFooter',
TR: 'TableRow', TC: 'TableCell', TD: 'TableCell', THC: 'TableHeaderCell',
UL: 'List', LI: 'ListItem', ML: 'MetadataList', MLI: 'MetadataListItem',
C: 'Card', CC: 'ClickableCard', ES: 'EmptyState', Bd: 'Badge',
SD: 'StatusDot', Av: 'Avatar', AvG: 'AvatarGroup', Tmb: 'Thumbnail',
Ts: 'Timestamp', OFL: 'OverflowList', Cs: 'Carousel', It: 'Item',
T: 'Table',
TH: 'TableHeader',
TB: 'TableBody',
TF: 'TableFooter',
TR: 'TableRow',
TC: 'TableCell',
TD: 'TableCell',
THC: 'TableHeaderCell',
UL: 'List',
LI: 'ListItem',
ML: 'MetadataList',
MLI: 'MetadataListItem',
C: 'Card',
CC: 'ClickableCard',
ES: 'EmptyState',
Bd: 'Badge',
SD: 'StatusDot',
Av: 'Avatar',
AvG: 'AvatarGroup',
Tmb: 'Thumbnail',
Ts: 'Timestamp',
OFL: 'OverflowList',
Cs: 'Carousel',
It: 'Item',
// Forms & inputs
Fd: 'Field', IG: 'InputGroup', IGT: 'InputGroupText',
TI: 'TextInput', TA: 'TextArea', NI: 'NumberInput',
DI: 'DateInput', DR: 'DateRangeInput', DT: 'DateTimeInput', TM: 'TimeInput',
FI: 'FileInput', CB: 'CheckboxInput', CL: 'CheckboxList', CLI: 'CheckboxListItem',
RL: 'RadioList', RLI: 'RadioListItem', SW: 'Switch', SL: 'Slider',
SE: 'Selector', MS: 'MultiSelector', TY: 'Typeahead', Tkz: 'Tokenizer',
PS: 'PowerSearch', CAL: 'Calendar',
Fd: 'Field',
IG: 'InputGroup',
IGT: 'InputGroupText',
TI: 'TextInput',
TA: 'TextArea',
NI: 'NumberInput',
DI: 'DateInput',
DR: 'DateRangeInput',
DT: 'DateTimeInput',
TM: 'TimeInput',
FI: 'FileInput',
CB: 'CheckboxInput',
CL: 'CheckboxList',
CLI: 'CheckboxListItem',
RL: 'RadioList',
RLI: 'RadioListItem',
SW: 'Switch',
SL: 'Slider',
SE: 'Selector',
MS: 'MultiSelector',
TY: 'Typeahead',
Tkz: 'Tokenizer',
PS: 'PowerSearch',
CAL: 'Calendar',
// Overlay & feedback
Dlg: 'Dialog', DH: 'DialogHeader', AD: 'AlertDialog',
Po: 'Popover', HC: 'HoverCard', Tt: 'Tooltip', Bn: 'Banner', Ov: 'Overlay',
CM: 'ContextMenu', DM: 'DropdownMenu', MM: 'MoreMenu', CP: 'CommandPalette',
Col: 'Collapsible', ColG: 'CollapsibleGroup',
Sp: 'Spinner', PB: 'ProgressBar', Sk: 'Skeleton',
Dlg: 'Dialog',
DH: 'DialogHeader',
DHH: 'DialogHeroHeader',
AD: 'AlertDialog',
Po: 'Popover',
HC: 'HoverCard',
Tt: 'Tooltip',
Bn: 'Banner',
Ov: 'Overlay',
CM: 'ContextMenu',
DM: 'DropdownMenu',
MM: 'MoreMenu',
CP: 'CommandPalette',
Col: 'Collapsible',
ColG: 'CollapsibleGroup',
Sp: 'Spinner',
PB: 'ProgressBar',
Sk: 'Skeleton',
// Content & chat
Tx: 'Text', Hd: 'Heading', MD: 'Markdown', Cd: 'CodeBlock', BQ: 'Blockquote',
K: 'Kbd', Ic: 'Icon', Lk: 'Link', Tk: 'Token',
B: 'Button', IB: 'IconButton', BG: 'ButtonGroup', Tg: 'ToggleButton', TgG: 'ToggleButtonGroup',
ChL: 'ChatLayout', ChML: 'ChatMessageList', ChM: 'ChatMessage',
ChB: 'ChatMessageBubble', ChC: 'ChatComposer', ChCD: 'ChatComposerDrawer',
ChS: 'ChatSystemMessage', ChT: 'ChatToolCalls',
Tx: 'Text',
Hd: 'Heading',
MD: 'Markdown',
Cd: 'CodeBlock',
BQ: 'Blockquote',
K: 'Kbd',
Ic: 'Icon',
Lk: 'Link',
Tk: 'Token',
B: 'Button',
IB: 'IconButton',
BG: 'ButtonGroup',
Tg: 'ToggleButton',
TgG: 'ToggleButtonGroup',
ChL: 'ChatLayout',
ChML: 'ChatMessageList',
ChM: 'ChatMessage',
ChB: 'ChatMessageBubble',
ChC: 'ChatComposer',
ChCD: 'ChatComposerDrawer',
ChS: 'ChatSystemMessage',
ChT: 'ChatToolCalls',
};

/** SpacingStep — the canonical spacing enum (matches the doc.mjs prose). */
Expand All @@ -89,8 +176,14 @@ export function parseEnumValues(type) {
const values = [];
for (const part of parts) {
const str = part.match(/^'([^']*)'$/) || part.match(/^"([^"]*)"$/);
if (str) { values.push(str[1]); continue; }
if (/^\d+(\.\d+)?$/.test(part)) { values.push(Number(part)); continue; }
if (str) {
values.push(str[1]);
continue;
}
if (/^\d+(\.\d+)?$/.test(part)) {
values.push(Number(part));
continue;
}
return null; // union of non-literal types (e.g. number|string) — not an enum
}
return values;
Expand Down Expand Up @@ -155,7 +248,9 @@ export function serializeRegistry(registry) {
dirName: c.dirName,
importPath: c.importPath,
undocumented: c.undocumented || false,
props: /** @type {Array<Record<string, unknown>>} */ (/** @type {unknown} */ ([...c.props.values()])),
props: /** @type {Array<Record<string, unknown>>} */ (
/** @type {unknown} */ ([...c.props.values()])
),
})),
aliases: [...registry.aliases.entries()],
componentNames: registry.componentNames,
Expand All @@ -175,7 +270,9 @@ export function hydrateRegistry(json) {
/** @type {Map<string, import('./xle-ast').RegistryProp>} */
const props = new Map();
for (const p of c.props) {
const prop = /** @type {import('./xle-ast').RegistryProp} */ (/** @type {unknown} */ (p));
const prop = /** @type {import('./xle-ast').RegistryProp} */ (
/** @type {unknown} */ (p)
);
props.set(String(p.name), prop);
}
components.set(c.name, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
export const doc = {
type: 'block',
exampleFor: 'DialogHeroHeader',
name: 'DialogHeroHeaderBasic',
displayName: 'Dialog Hero Header Basic',
description:
'A hero-style dialog header with a media slot, eyebrow, prominent title, and supporting text — the high-emphasis treatment for onboarding or featured moments.',
isReady: true,
aspectRatio: 16 / 9,
componentsUsed: ['Dialog', 'DialogHeroHeader'],
};
Loading
Loading