@astryxdesign/core 0.7.2-canary.c9f2659 → 0.7.2-canary.ccf1a1c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/Drawer/Drawer.d.ts +191 -0
  2. package/dist/Drawer/Drawer.d.ts.map +1 -0
  3. package/dist/Drawer/Drawer.js +382 -0
  4. package/dist/Drawer/DrawerHeader.d.ts +85 -0
  5. package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
  6. package/dist/Drawer/DrawerHeader.js +134 -0
  7. package/dist/Drawer/index.d.ts +11 -0
  8. package/dist/Drawer/index.d.ts.map +1 -0
  9. package/dist/Drawer/index.js +11 -0
  10. package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
  11. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
  12. package/dist/Drawer/useDrawerDialogPresence.js +236 -0
  13. package/dist/Layer/useLayer.d.ts.map +1 -1
  14. package/dist/Layer/useLayer.js +7 -1
  15. package/dist/Layer/useLayerDismissal.d.ts +1 -1
  16. package/dist/Layer/useLayerDismissal.js +1 -1
  17. package/dist/Layout/container.stylex.d.ts +5 -5
  18. package/dist/Layout/container.stylex.js +4 -4
  19. package/dist/TabList/TabMenu.d.ts +2 -2
  20. package/dist/TabList/TabMenu.d.ts.map +1 -1
  21. package/dist/TabList/TabMenu.js +6 -3
  22. package/dist/astryx.css +10 -0
  23. package/dist/hooks/useEntryAnimation.d.ts +2 -1
  24. package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
  25. package/dist/hooks/useEntryAnimation.js +14 -12
  26. package/dist/hooks/useIsHydrating.d.ts +6 -0
  27. package/dist/hooks/useIsHydrating.d.ts.map +1 -0
  28. package/dist/hooks/useIsHydrating.js +30 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  33. package/dist/theme/derivedVarRegistry.js +0 -3
  34. package/package.json +8 -3
  35. package/src/Drawer/Drawer.doc.mjs +472 -0
  36. package/src/Drawer/Drawer.spec.md +357 -0
  37. package/src/Drawer/Drawer.test.tsx +1300 -0
  38. package/src/Drawer/Drawer.tsx +613 -0
  39. package/src/Drawer/DrawerDirection.test.tsx +127 -0
  40. package/src/Drawer/DrawerHeader.doc.mjs +293 -0
  41. package/src/Drawer/DrawerHeader.test.tsx +102 -0
  42. package/src/Drawer/DrawerHeader.tsx +235 -0
  43. package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
  44. package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
  45. package/src/Drawer/index.ts +13 -0
  46. package/src/Drawer/useDrawerDialogPresence.ts +284 -0
  47. package/src/Layer/layerViewportInset.test.ts +1 -0
  48. package/src/Layer/useLayer.tsx +10 -1
  49. package/src/Layer/useLayerDismissal.ts +1 -1
  50. package/src/Layout/container.stylex.ts +6 -6
  51. package/src/TabList/TabList.test.tsx +67 -0
  52. package/src/TabList/TabMenu.doc.mjs +4 -3
  53. package/src/TabList/TabMenu.tsx +7 -4
  54. package/src/Tooltip/Tooltip.test.tsx +31 -0
  55. package/src/hooks/useEntryAnimation.doc.mjs +4 -4
  56. package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
  57. package/src/hooks/useEntryAnimation.ts +15 -16
  58. package/src/hooks/useIsHydrating.test.tsx +70 -0
  59. package/src/hooks/useIsHydrating.ts +31 -0
  60. package/src/index.ts +1 -0
  61. package/src/theme/derivedVarRegistry.test.ts +0 -3
  62. package/src/theme/derivedVarRegistry.ts +0 -3
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @file Drawer.tsx
3
+ * @input Uses React, StyleX, theme tokens, the shared layer text/structure resets and content boundary, the Dialog purpose type, shared focus/dismissal/depth primitives, container padding lowering, scroll locking/dialog presence, BaseProps, merged refs/props, themeProps
4
+ * @output Exports Drawer component and DrawerProps
5
+ * @position Core implementation; consumed by index.ts, tested by Drawer.test.tsx, demonstrated in Storybook
6
+ *
7
+ * Overlay panel for inspectors and detail views — the "click a table row,
8
+ * see its details" pattern. Slides in from the inline start or end edge and
9
+ * floats above the page content: unlike a docked panel it never reflows the
10
+ * layout underneath, it overlays it (with or without a scrim).
11
+ *
12
+ * Inline axis only (start/end). Block-axis sheets are BottomSheet's job;
13
+ * a drawer is always a full-height side panel.
14
+ *
15
+ * Sizing is viewport-aware: `width` is the desktop budget, and below
16
+ * the mobile breakpoint it preserves a 56px reveal of the page behind, capped
17
+ * by the requested width (or fills the viewport with the experimental
18
+ * `experimental.isFullWidthOnMobile` option).
19
+ *
20
+ * Uses the native `<dialog>` element (same precedent as Dialog/MobileNav):
21
+ * - `showModal()` when `hasScrim` (default) — top-layer rendering, focus
22
+ * trapping, `::backdrop`, no z-index management.
23
+ * - `showPopover()` when `hasScrim={false}` — non-modal top-layer overlay;
24
+ * the page behind stays interactive (e.g. master-detail inspectors).
25
+ *
26
+ * Entry animation uses `@starting-style`; exit slides out before the active
27
+ * modal-dialog or manual-popover host releases the top layer and focus returns
28
+ * to the element that opened the drawer. React owns `display` for both legs
29
+ * rather than a discrete `display` transition, so the panel stops painting in
30
+ * the same commit as the host closes — see the `rendered` style for why.
31
+ *
32
+ * Sibling drawers use the shared layer dismissal stack for topmost-only Escape
33
+ * handling and the browser top layer's chronological paint order.
34
+ *
35
+ * Dismissal follows Dialog: `purpose` decides whether Escape and a scrim click
36
+ * request close, and Drawer renders no close button of its own. Compose
37
+ * `DrawerHeader` with `onOpenChange` for a visible close action, as with
38
+ * `DialogHeader`.
39
+ *
40
+ * The scrolling content area is the drawer's container, like Dialog's inner
41
+ * box: the `padding` prop, else the theme's `padding` on `drawer`, insets it
42
+ * through container tokens, defaulting to `--spacing-4` like Dialog, and the
43
+ * applied inset is published so a lone Section child escapes it and a Layout
44
+ * redistributes it to its regions. Compose `DrawerHeader` inside a `Layout`, as
45
+ * `DialogHeader` is composed in Dialog, so the header is not padded twice. The
46
+ * root dialog stays the overlay boundary (`overlayPaddingReset`).
47
+ *
48
+ * SYNC: When modified, update these files to stay in sync:
49
+ * - /packages/core/src/Drawer/Drawer.doc.mjs (props table, features, usage)
50
+ * - /packages/core/src/Drawer/Drawer.test.tsx (tests for new/changed behavior)
51
+ * - /packages/core/src/Drawer/Drawer.spec.md (component contract)
52
+ * - /packages/core/src/Drawer/index.ts (exports if types change)
53
+ * - /packages/cli/assets/templates/blocks/components/Drawer/ (showcase blocks)
54
+ * - /apps/storybook/stories/Drawer.stories.tsx (examples and visual coverage)
55
+ */
56
+ import { type ReactNode } from 'react';
57
+ import type { BaseProps } from '../BaseProps';
58
+ import type { DialogPurpose } from '../Dialog';
59
+ import type { SpacingStep } from '../Layout';
60
+ export interface DrawerProps extends BaseProps<HTMLDialogElement> {
61
+ /** Ref forwarded to the root <dialog> element */
62
+ ref?: React.Ref<HTMLDialogElement>;
63
+ /**
64
+ * Whether the drawer is open. Fully controlled — pair with `onOpenChange`.
65
+ */
66
+ isOpen: boolean;
67
+ /**
68
+ * Called when the drawer requests an open-state change. Escape and a scrim
69
+ * click call it with `false` as `purpose` allows; a `DrawerHeader` close
70
+ * button calls it when you pass the same callback to the header. The caller
71
+ * owns the open state. When sibling drawers are open, Escape only closes the
72
+ * top (last-opened) drawer.
73
+ */
74
+ onOpenChange: (isOpen: boolean) => void;
75
+ /**
76
+ * Which edge the drawer slides from.
77
+ * - `'end'` — inline-end edge (right in LTR) — the inspector convention
78
+ * - `'start'` — inline-start edge (left in LTR)
79
+ * @default 'end'
80
+ */
81
+ side?: 'start' | 'end';
82
+ /**
83
+ * Desktop width budget. A number is pixels; a string is any CSS length
84
+ * (`'50%'`, `'32rem'`). Below the mobile breakpoint (640px), this
85
+ * remains the maximum while the drawer preserves a 56px reveal of the page
86
+ * behind — see `experimental.isFullWidthOnMobile`.
87
+ * @default 400
88
+ */
89
+ width?: number | string;
90
+ /**
91
+ * Experimental options, outside the stable compatibility promise: anything
92
+ * in this object may change or be removed in a patch release. Omit it for
93
+ * the stable behavior.
94
+ * @experimental
95
+ */
96
+ experimental?: {
97
+ /**
98
+ * Cover the full viewport width on mobile (below 640px) instead of
99
+ * preserving the default 56px reveal of the page behind. The reveal makes
100
+ * the drawer read as an overlay rather than a navigation.
101
+ * @experimental
102
+ * @default false
103
+ */
104
+ isFullWidthOnMobile?: boolean;
105
+ };
106
+ /**
107
+ * Accessible label for the drawer (required — the drawer has no
108
+ * built-in heading to derive a name from).
109
+ */
110
+ label: string;
111
+ /**
112
+ * Whether to render a modal scrim behind the drawer.
113
+ * - `true` (default) — `showModal()`: top layer, focus trap, body scroll
114
+ * lock, click-outside-to-close.
115
+ * - `false` — `showPopover()`: non-modal top-layer overlay; the page behind
116
+ * stays interactive. Escape still closes through the shared layer stack.
117
+ * @default true
118
+ */
119
+ hasScrim?: boolean;
120
+ /**
121
+ * Configures how the drawer enables dismissals, matching Dialog.
122
+ * - required: Disables Escape and scrim click (for mandatory flows); a
123
+ * modal required drawer is exposed as an `alertdialog`
124
+ * - form: Prevents scrim click, allows the Escape key
125
+ * - info: Allows Escape and scrim click
126
+ *
127
+ * A non-modal drawer has no scrim, so `form` and `info` behave the same.
128
+ * @default 'info'
129
+ */
130
+ purpose?: DialogPurpose;
131
+ /**
132
+ * Internal padding of the drawer content using the spacing scale, matching
133
+ * Dialog. Accepts numeric spacing steps: 0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8,
134
+ * 10. When omitted, uses the theme default for drawers (`--spacing-4`, 16px,
135
+ * unless the theme sets `padding` on `drawer`), as Dialog does. Pass `0` for
136
+ * a full-bleed content area. The content area is a container: a Section that
137
+ * is its only child escapes the padding, bleed children such as Table and
138
+ * Divider compensate against it, and a Layout picks it up for its regions.
139
+ */
140
+ padding?: SpacingStep;
141
+ /**
142
+ * Drawer content. Rendered inside a full-height scrollable area.
143
+ * Focus the element with `data-autofocus` on open, if present.
144
+ */
145
+ children: ReactNode;
146
+ /**
147
+ * Test ID for the root element.
148
+ */
149
+ 'data-testid'?: string;
150
+ }
151
+ /**
152
+ * An overlay panel for inspectors and detail views.
153
+ *
154
+ * Slides in from the logical start or end edge and floats above the page
155
+ * using the native `<dialog>` element: modal with a scrim by default, or a
156
+ * non-modal overlay with `hasScrim={false}` that leaves the page behind
157
+ * interactive. `width` is the desktop budget; below 640px the panel preserves
158
+ * a 56px page reveal without exceeding that budget (or fills the viewport
159
+ * with the experimental `experimental.isFullWidthOnMobile`). Escape
160
+ * closes the top-most open drawer; focus returns to the element that
161
+ * opened it.
162
+ *
163
+ * Dismissal matches Dialog: `purpose` decides whether Escape and a scrim click
164
+ * close the drawer, and `DrawerHeader` renders a close button when given
165
+ * `onOpenChange`.
166
+ *
167
+ * Like Dialog, the drawer is a container: `padding` (or a theme's `padding`
168
+ * on `drawer`) insets the content area, a lone Section child escapes that
169
+ * inset, and a Layout inside picks it up for its header, content, and footer
170
+ * regions. The default inset is `--spacing-4`, matching Dialog.
171
+ *
172
+ * @example
173
+ * ```
174
+ * const [selected, setSelected] = useState(null);
175
+ * const handleOpenChange = isOpen => !isOpen && setSelected(null);
176
+ * <Drawer
177
+ * isOpen={selected != null}
178
+ * onOpenChange={handleOpenChange}
179
+ * label={`Details: ${selected?.name}`}>
180
+ * <Layout
181
+ * header={<DrawerHeader title={selected?.name} onOpenChange={handleOpenChange} />}
182
+ * content={<LayoutContent><HostDetails host={selected} /></LayoutContent>}
183
+ * />
184
+ * </Drawer>
185
+ * ```
186
+ */
187
+ export declare function Drawer({ isOpen, onOpenChange, side, width, experimental, label, hasScrim, purpose, padding, children, xstyle, className, style, onClick: onClickProp, onKeyDown: onKeyDownProp, ref, ...props }: DrawerProps): import("react").JSX.Element;
188
+ export declare namespace Drawer {
189
+ var displayName: string;
190
+ }
191
+ //# sourceMappingURL=Drawer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../src/Drawer/Drawer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,EAAgC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAEpE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAQ5C,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,WAAW,CAAC;AAe7C,OAAO,KAAK,EAAC,WAAW,EAAe,MAAM,WAAW,CAAC;AAgMzD,MAAM,WAAW,WAAY,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IAC/D,iDAAiD;IACjD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEnC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAEvB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAExB;;;;;OAKG;IACH,YAAY,CAAC,EAAE;QACb;;;;;;WAMG;QACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;KAC/B,CAAC;IAEF;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,MAAM,CAAC,EACrB,MAAM,EACN,YAAY,EACZ,IAAY,EACZ,KAAW,EACX,YAAY,EACZ,KAAK,EACL,QAAe,EACf,OAAgB,EAChB,OAAO,EACP,QAAQ,EACR,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,aAAa,EACxB,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,+BA4Kb;yBA9Le,MAAM"}
@@ -0,0 +1,382 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file Drawer.tsx
7
+ * @input Uses React, StyleX, theme tokens, the shared layer text/structure resets and content boundary, the Dialog purpose type, shared focus/dismissal/depth primitives, container padding lowering, scroll locking/dialog presence, BaseProps, merged refs/props, themeProps
8
+ * @output Exports Drawer component and DrawerProps
9
+ * @position Core implementation; consumed by index.ts, tested by Drawer.test.tsx, demonstrated in Storybook
10
+ *
11
+ * Overlay panel for inspectors and detail views — the "click a table row,
12
+ * see its details" pattern. Slides in from the inline start or end edge and
13
+ * floats above the page content: unlike a docked panel it never reflows the
14
+ * layout underneath, it overlays it (with or without a scrim).
15
+ *
16
+ * Inline axis only (start/end). Block-axis sheets are BottomSheet's job;
17
+ * a drawer is always a full-height side panel.
18
+ *
19
+ * Sizing is viewport-aware: `width` is the desktop budget, and below
20
+ * the mobile breakpoint it preserves a 56px reveal of the page behind, capped
21
+ * by the requested width (or fills the viewport with the experimental
22
+ * `experimental.isFullWidthOnMobile` option).
23
+ *
24
+ * Uses the native `<dialog>` element (same precedent as Dialog/MobileNav):
25
+ * - `showModal()` when `hasScrim` (default) — top-layer rendering, focus
26
+ * trapping, `::backdrop`, no z-index management.
27
+ * - `showPopover()` when `hasScrim={false}` — non-modal top-layer overlay;
28
+ * the page behind stays interactive (e.g. master-detail inspectors).
29
+ *
30
+ * Entry animation uses `@starting-style`; exit slides out before the active
31
+ * modal-dialog or manual-popover host releases the top layer and focus returns
32
+ * to the element that opened the drawer. React owns `display` for both legs
33
+ * rather than a discrete `display` transition, so the panel stops painting in
34
+ * the same commit as the host closes — see the `rendered` style for why.
35
+ *
36
+ * Sibling drawers use the shared layer dismissal stack for topmost-only Escape
37
+ * handling and the browser top layer's chronological paint order.
38
+ *
39
+ * Dismissal follows Dialog: `purpose` decides whether Escape and a scrim click
40
+ * request close, and Drawer renders no close button of its own. Compose
41
+ * `DrawerHeader` with `onOpenChange` for a visible close action, as with
42
+ * `DialogHeader`.
43
+ *
44
+ * The scrolling content area is the drawer's container, like Dialog's inner
45
+ * box: the `padding` prop, else the theme's `padding` on `drawer`, insets it
46
+ * through container tokens, defaulting to `--spacing-4` like Dialog, and the
47
+ * applied inset is published so a lone Section child escapes it and a Layout
48
+ * redistributes it to its regions. Compose `DrawerHeader` inside a `Layout`, as
49
+ * `DialogHeader` is composed in Dialog, so the header is not padded twice. The
50
+ * root dialog stays the overlay boundary (`overlayPaddingReset`).
51
+ *
52
+ * SYNC: When modified, update these files to stay in sync:
53
+ * - /packages/core/src/Drawer/Drawer.doc.mjs (props table, features, usage)
54
+ * - /packages/core/src/Drawer/Drawer.test.tsx (tests for new/changed behavior)
55
+ * - /packages/core/src/Drawer/Drawer.spec.md (component contract)
56
+ * - /packages/core/src/Drawer/index.ts (exports if types change)
57
+ * - /packages/cli/assets/templates/blocks/components/Drawer/ (showcase blocks)
58
+ * - /apps/storybook/stories/Drawer.stories.tsx (examples and visual coverage)
59
+ */
60
+ import { useCallback, useRef, useState } from 'react';
61
+ import * as stylex from '@stylexjs/stylex';
62
+ import "../theme/tokens.stylex.js";
63
+ import { borderVars, colorVars, durationVars, easeVars, shadowVars } from "../theme/tokens.stylex.js";
64
+ import { useFocusTrap, useMergedRefs, useScrollLock } from "../hooks/index.js";
65
+ import { LayerDepthProvider, useLayerDismissal } from "../Layer/index.js";
66
+ import { layerTextReset } from "../Layer/layerTextReset.stylex.js";
67
+ import { layerStructureReset } from "../Layer/layerStructureReset.stylex.js";
68
+ import { LayerContentBoundary } from "../Layer/layerScopedContext.js";
69
+ import { composeEventHandlers, mergeProps, themeProps } from "../utils/index.js";
70
+ import { container, overlayPaddingReset } from "../Layout/index.js";
71
+ import { containerPaddingBlockEndVarStyles, containerPaddingBlockStartVarStyles, containerPaddingInlineVarStyles, paddingStyles, spacingStepToToken } from "../Layout/padding.stylex.js";
72
+ import { useDrawerDialogPresence } from "./useDrawerDialogPresence.js";
73
+
74
+ // =============================================================================
75
+ // Styles
76
+ // =============================================================================
77
+
78
+ // Below this viewport width the drawer preserves a fixed reveal of the page
79
+ // behind instead of growing proportionally with the viewport. 640px is the
80
+ // repo's mobile breakpoint.
81
+ import { jsx as _jsx } from "react/jsx-runtime";
82
+ const MOBILE_BREAKPOINT = 640;
83
+
84
+ // Material's established mobile drawer pattern leaves a 56dp reveal. Using
85
+ // the same value in CSS pixels gives the overlay a stable visual relationship
86
+ // to the page behind while the requested width remains an upper bound.
87
+ const MOBILE_PAGE_REVEAL = 56;
88
+ const MOBILE_WIDTH_FULL = '100dvw';
89
+ const styles = {
90
+ dialog: {
91
+ kVAEAm: "xixxii4",
92
+ kogj98: "x1ghz6dp",
93
+ kmVPX3: "x1717udv",
94
+ kMzoRj: "xc342km",
95
+ ksu8eU: "xng3xce",
96
+ kaIpWk: "x2u8bby",
97
+ ks0D6T: "x1x1rfll",
98
+ kskxy: "x7ab17h",
99
+ kB7OPa: "x9f619",
100
+ kXwgrk: "xdt5ytf",
101
+ kWkggS: "x10xzikg",
102
+ kGVxlE: "x1kcpxr7",
103
+ kVQacm: "xb3r6kr",
104
+ kZeWKH: "xish69e",
105
+ kI3sdo: "x1a2a7pz",
106
+ k87sOh: "x13vifvy",
107
+ krVfgx: "x1ey2m1c",
108
+ kZKoxP: "xtdtrs8",
109
+ k1xSpc: "x1s85apg",
110
+ k1ekBW: "x11xpdln",
111
+ kIyJzY: "x80gvsz",
112
+ kAMwcw: "xlr8y92",
113
+ k6CgDc: "xzg1mie",
114
+ $$css: true
115
+ },
116
+ rendered: {
117
+ k1xSpc: "x78zum5",
118
+ $$css: true
119
+ },
120
+ end: {
121
+ kt4wiu: "xtijo5x",
122
+ kLqNvP: "xhi6v0a",
123
+ k2ei4v: "xgbv0en",
124
+ kVhnKS: "x1t7ytsu",
125
+ kGJrpR: "x1j92z86",
126
+ k3aq6I: "xumwmo6 xbw113y",
127
+ $$css: true
128
+ },
129
+ endOpen: {
130
+ k3aq6I: "xbryuvx x1yqmsfc xrxqmjf",
131
+ $$css: true
132
+ },
133
+ start: {
134
+ kLqNvP: "x1o0tod",
135
+ kt4wiu: "x1woyocn",
136
+ ke9TFa: "xw8tdv1",
137
+ k8ry5P: "x18b5jzi",
138
+ kBCPoo: "x1gejf6u",
139
+ k3aq6I: "x5i6ehr x1eqdj8u",
140
+ $$css: true
141
+ },
142
+ startOpen: {
143
+ k3aq6I: "xbryuvx x6mt36l x1r0dqvm",
144
+ $$css: true
145
+ },
146
+ scrim: {
147
+ kGyWv1: "xnixb3f",
148
+ kba3nw: "x1abwkk1",
149
+ k5sjJv: "xph5o2a",
150
+ kND0Po: "x167zut7",
151
+ k9an0g: "xft5bk6",
152
+ kb4ib: "x15h3t91",
153
+ kA5Tbj: "x1viac0w",
154
+ $$css: true
155
+ },
156
+ scrimOpen: {
157
+ k5sjJv: "xb3n6bw xxiuuzi",
158
+ $$css: true
159
+ },
160
+ content: {
161
+ kzQI83: "x1iyjqo2",
162
+ kAzted: "x2lwn1j",
163
+ kzqmXN: "xh8yej3",
164
+ kORKVm: "x1odjw0f",
165
+ kXHlph: "x6ikm8r",
166
+ kZeWKH: "xish69e",
167
+ kFalU9: "xx69xxh",
168
+ kI3sdo: "x1a2a7pz",
169
+ kWkggS: "x10xzikg",
170
+ $$css: true
171
+ },
172
+ contentSafeArea: {
173
+ kGO01o: "xvmia7e",
174
+ $$css: true
175
+ }
176
+ };
177
+ const _temp = {
178
+ ks0D6T: "xlbgzzq",
179
+ "$$css": true
180
+ };
181
+ const dynamicStyles = {
182
+ inlineSize: (desktopWidth, mobileWidth) => [_temp, {
183
+ kzqmXN: (desktopWidth != null ? "x11ymkkh " : desktopWidth) + (mobileWidth != null ? "xci7e5a" : mobileWidth),
184
+ $$css: true
185
+ }, {
186
+ "--x-1xmrurk": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(desktopWidth),
187
+ "--x-fqzwsx": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(mobileWidth)
188
+ }]
189
+ };
190
+
191
+ // =============================================================================
192
+ // Types
193
+ // =============================================================================
194
+
195
+ // =============================================================================
196
+ // Component
197
+ // =============================================================================
198
+
199
+ /**
200
+ * An overlay panel for inspectors and detail views.
201
+ *
202
+ * Slides in from the logical start or end edge and floats above the page
203
+ * using the native `<dialog>` element: modal with a scrim by default, or a
204
+ * non-modal overlay with `hasScrim={false}` that leaves the page behind
205
+ * interactive. `width` is the desktop budget; below 640px the panel preserves
206
+ * a 56px page reveal without exceeding that budget (or fills the viewport
207
+ * with the experimental `experimental.isFullWidthOnMobile`). Escape
208
+ * closes the top-most open drawer; focus returns to the element that
209
+ * opened it.
210
+ *
211
+ * Dismissal matches Dialog: `purpose` decides whether Escape and a scrim click
212
+ * close the drawer, and `DrawerHeader` renders a close button when given
213
+ * `onOpenChange`.
214
+ *
215
+ * Like Dialog, the drawer is a container: `padding` (or a theme's `padding`
216
+ * on `drawer`) insets the content area, a lone Section child escapes that
217
+ * inset, and a Layout inside picks it up for its header, content, and footer
218
+ * regions. The default inset is `--spacing-4`, matching Dialog.
219
+ *
220
+ * @example
221
+ * ```
222
+ * const [selected, setSelected] = useState(null);
223
+ * const handleOpenChange = isOpen => !isOpen && setSelected(null);
224
+ * <Drawer
225
+ * isOpen={selected != null}
226
+ * onOpenChange={handleOpenChange}
227
+ * label={`Details: ${selected?.name}`}>
228
+ * <Layout
229
+ * header={<DrawerHeader title={selected?.name} onOpenChange={handleOpenChange} />}
230
+ * content={<LayoutContent><HostDetails host={selected} /></LayoutContent>}
231
+ * />
232
+ * </Drawer>
233
+ * ```
234
+ */
235
+ export function Drawer({
236
+ isOpen,
237
+ onOpenChange,
238
+ side = 'end',
239
+ width = 400,
240
+ experimental,
241
+ label,
242
+ hasScrim = true,
243
+ purpose = 'info',
244
+ padding,
245
+ children,
246
+ xstyle,
247
+ className,
248
+ style,
249
+ onClick: onClickProp,
250
+ onKeyDown: onKeyDownProp,
251
+ ref,
252
+ ...props
253
+ }) {
254
+ const dialogRef = useRef(null);
255
+ const {
256
+ containerRef: focusTrapRef
257
+ } = useFocusTrap({
258
+ isActive: isOpen && hasScrim
259
+ });
260
+ const mergedDialogRef = useMergedRefs(ref, dialogRef, focusTrapRef);
261
+ // Derive dismissal behavior from purpose, as Dialog does.
262
+ const allowEscape = purpose !== 'required';
263
+ const allowScrimClick = purpose === 'info';
264
+ // Same lowering as Dialog: with no padding prop the content area reads the
265
+ // theme's --astryx-drawer-padding chain (--spacing-4 when unset), else the
266
+ // explicit step.
267
+ const usesThemePadding = padding == null;
268
+ const effectivePadding = padding ?? 4;
269
+ const paddingToken = spacingStepToToken[effectivePadding];
270
+ // Whether the panel paints: true while open and for the whole slide-out.
271
+ const [isRendered, setIsRendered] = useState(isOpen);
272
+
273
+ // Adjusted during render, not in an effect: the panel has to be rendered in
274
+ // the same commit that targets the open transform, or @starting-style has
275
+ // nothing to animate from.
276
+ if (isOpen && !isRendered) {
277
+ setIsRendered(true);
278
+ }
279
+ useDrawerDialogPresence({
280
+ dialogRef,
281
+ isOpen,
282
+ isModal: hasScrim,
283
+ setIsRendered
284
+ });
285
+ const handleDismiss = useCallback(() => {
286
+ onOpenChange(false);
287
+ }, [onOpenChange]);
288
+
289
+ // The shared stack owns Escape delivery across every overlay family. The
290
+ // provider below gives layers opened from Drawer content a greater logical
291
+ // depth even when they render elsewhere in the DOM or browser top layer.
292
+ const {
293
+ shouldDismissOnCloseRequest
294
+ } = useLayerDismissal({
295
+ // The native host stays present through the slide-out. Keep this entry on
296
+ // the stack until the same commit that hides the host, so a second Escape
297
+ // cannot fall through to a lower layer during the exit animation.
298
+ isActive: isRendered,
299
+ // A `required` drawer consumes Escape without closing, so the press cannot
300
+ // fall through and dismiss a layer behind it either.
301
+ escapeBehavior: allowEscape ? 'close' : 'block',
302
+ onDismiss: handleDismiss,
303
+ getContainer: () => dialogRef.current
304
+ });
305
+
306
+ // Lock body scroll while a modal drawer is open (iOS Safari workaround).
307
+ useScrollLock(isOpen && hasScrim);
308
+
309
+ // A modal dialog's native cancel event is a platform close request (for
310
+ // example Android Back). Keep controlled state authoritative and apply the
311
+ // same top-most/IME rules as the shared Escape listener.
312
+ const handleCancel = useCallback(event => {
313
+ event.preventDefault();
314
+ if (shouldDismissOnCloseRequest() && allowEscape) {
315
+ handleDismiss();
316
+ }
317
+ }, [allowEscape, handleDismiss, shouldDismissOnCloseRequest]);
318
+
319
+ // Clicks on the ::backdrop target the <dialog> element itself; clicks on
320
+ // drawer content always target a child (the content area fills the panel).
321
+ const handleClick = useCallback(event => {
322
+ if (event.target === event.currentTarget && hasScrim && allowScrimClick) {
323
+ onOpenChange(false);
324
+ }
325
+ }, [allowScrimClick, hasScrim, onOpenChange]);
326
+ const widthValue = typeof width === 'number' ? `${width}px` : width;
327
+ const isFullWidthOnMobile = experimental?.isFullWidthOnMobile ?? false;
328
+ const mobileWidth = isFullWidthOnMobile ? MOBILE_WIDTH_FULL : `min(${widthValue}, calc(100dvw - ${MOBILE_PAGE_REVEAL}px))`;
329
+
330
+ // The side the panel is ANCHORED to, which is the side it must slide back
331
+ // out to. Latched at open, because a consumer commonly derives `side` from
332
+ // the same state that drives `isOpen` (`side={selected?.side ?? 'end'}`):
333
+ // that state clears on close, so the live prop flips mid-exit and the panel
334
+ // teleports to the other edge and slides out the wrong way. Children stay
335
+ // mounted for the exit for the same reason; so does the anchor.
336
+ const exitSideRef = useRef(side);
337
+ if (isOpen) {
338
+ exitSideRef.current = side;
339
+ }
340
+ const anchoredSide = isOpen ? side : exitSideRef.current;
341
+ const sideStyle = anchoredSide === 'start' ? styles.start : styles.end;
342
+ const sideOpenStyle = anchoredSide === 'start' ? styles.startOpen : styles.endOpen;
343
+
344
+ // Filter out native `open` to prevent InvalidStateError when passed
345
+ const {
346
+ open: _open,
347
+ ...safeProps
348
+ } = props;
349
+ return /*#__PURE__*/_jsx("dialog", {
350
+ ref: mergedDialogRef,
351
+ ...mergeProps(themeProps('drawer', {
352
+ side: anchoredSide
353
+ }), stylex.props(layerTextReset.reset, layerStructureReset.reset, styles.dialog, overlayPaddingReset.reset, sideStyle, dynamicStyles.inlineSize(widthValue, mobileWidth), isRendered && styles.rendered, isOpen && sideOpenStyle, hasScrim && styles.scrim, hasScrim && isOpen && styles.scrimOpen, xstyle), className, style),
354
+ ...safeProps,
355
+ popover: hasScrim ? undefined : 'manual',
356
+ "aria-label": label,
357
+ "aria-modal": hasScrim ? 'true' : undefined,
358
+ ...(purpose === 'required' && hasScrim ? {
359
+ role: 'alertdialog'
360
+ } : undefined),
361
+ onClick: composeEventHandlers(onClickProp, handleClick),
362
+ onKeyDown: onKeyDownProp,
363
+ onCancel: handleCancel,
364
+ children: /*#__PURE__*/_jsx(LayerDepthProvider, {
365
+ children: /*#__PURE__*/_jsx(LayerContentBoundary, {
366
+ children: /*#__PURE__*/_jsx("div", {
367
+ tabIndex: -1,
368
+ ...stylex.props(styles.content, ...container(usesThemePadding ? {
369
+ useThemeDefault: 'drawer'
370
+ } : {
371
+ paddingInnerX: paddingToken,
372
+ paddingInnerY: paddingToken,
373
+ paddingOuterX: paddingToken,
374
+ paddingOuterY: paddingToken
375
+ }), !usesThemePadding && effectivePadding !== 4 && paddingStyles[effectivePadding], !usesThemePadding && effectivePadding !== 4 && containerPaddingInlineVarStyles[effectivePadding], !usesThemePadding && effectivePadding !== 4 && containerPaddingBlockStartVarStyles[effectivePadding], !usesThemePadding && effectivePadding !== 4 && containerPaddingBlockEndVarStyles[effectivePadding], styles.contentSafeArea),
376
+ children: children
377
+ })
378
+ })
379
+ })
380
+ });
381
+ }
382
+ Drawer.displayName = 'Drawer';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file DrawerHeader.tsx
3
+ * @input Uses React, StyleX, LayoutHeader, Button, Icon, Heading, Text, i18n, BaseProps, isRenderable, mergeProps, themeProps
4
+ * @output Exports DrawerHeader component and DrawerHeaderProps
5
+ * @position Drawer header; composed in a Layout header slot inside Drawer, tested by DrawerHeader.test.tsx
6
+ *
7
+ * Mirrors DialogHeader's API: a title, an optional subtitle, start/end content
8
+ * slots, and a close button that renders only when `onOpenChange` is passed.
9
+ * Two differences follow from Drawer's contract:
10
+ * - It does not move focus on mount. Drawer owns focus entry through
11
+ * `data-autofocus` once its native host is presented.
12
+ * - It does not name the drawer. Drawer's required `label` does.
13
+ *
14
+ * SYNC: When modified, update these files to stay in sync:
15
+ * - /packages/core/src/Drawer/DrawerHeader.doc.mjs (props, anatomy, theming)
16
+ * - /packages/core/src/Drawer/DrawerHeader.test.tsx (tests for new/changed behavior)
17
+ * - /packages/core/src/Drawer/index.ts (exports if types change)
18
+ * - /packages/cli/assets/templates/blocks/components/Drawer/ (showcase blocks)
19
+ * - /packages/cli/assets/templates/blocks/components/DrawerHeader/ (showcase blocks)
20
+ * - /apps/storybook/stories/Drawer.stories.tsx (examples and visual coverage)
21
+ * - /apps/storybook/stories/DrawerHeader.stories.tsx (component audit fixture)
22
+ */
23
+ import type { ReactNode } from 'react';
24
+ import type { BaseProps } from '../BaseProps';
25
+ export interface DrawerHeaderProps extends BaseProps<HTMLDivElement> {
26
+ /** Ref forwarded to the root element */
27
+ ref?: React.Ref<HTMLDivElement>;
28
+ /**
29
+ * The title of the drawer, rendered as an h2. Keep it inline and
30
+ * non-interactive. It does not name the drawer; Drawer's `label` does.
31
+ */
32
+ title: ReactNode;
33
+ /**
34
+ * Optional subtitle displayed below the title in smaller, secondary text.
35
+ * Accepts inline content such as a Link; it renders inside a span, so
36
+ * avoid block elements. Nothing renders for `null`, `undefined`, booleans,
37
+ * or an empty string.
38
+ */
39
+ subtitle?: ReactNode;
40
+ /**
41
+ * Called with `false` when the close button is clicked. Pass the Drawer's
42
+ * own `onOpenChange`. If not provided, no close button is rendered.
43
+ */
44
+ onOpenChange?: (isOpen: boolean) => unknown;
45
+ /**
46
+ * Content to render before the title (e.g., a back button).
47
+ */
48
+ startContent?: ReactNode;
49
+ /**
50
+ * Content to render after the title, before the close button (e.g., action
51
+ * buttons).
52
+ */
53
+ endContent?: ReactNode;
54
+ /**
55
+ * Overrides automatic end-slot compensation. When omitted, rendering the
56
+ * close action applies block and logical inline-end compensation. Use
57
+ * `inline`, `block`, or `all` to select the axes explicitly.
58
+ */
59
+ endContentEdgeCompensation?: 'inline' | 'block' | 'all';
60
+ /**
61
+ * Adds a themed border at the bottom edge.
62
+ * When false, spacing collapse is applied automatically for seamless visual flow.
63
+ * Defaults to the parent Layout's `defaultHasDividers` context value.
64
+ */
65
+ hasDivider?: boolean;
66
+ }
67
+ /**
68
+ * Header for a Drawer: a title, optional subtitle and start/end content, and a
69
+ * close button when `onOpenChange` is passed. Same API as DialogHeader.
70
+ *
71
+ * Uses LayoutHeader internally for consistent styling with other layout headers.
72
+ *
73
+ * @example
74
+ * ```
75
+ * <Drawer isOpen={isOpen} onOpenChange={setIsOpen} label="Details">
76
+ * <DrawerHeader title="Details" onOpenChange={setIsOpen} />
77
+ * <Section padding={4}>Content</Section>
78
+ * </Drawer>
79
+ * ```
80
+ */
81
+ export declare function DrawerHeader({ title, subtitle, onOpenChange, startContent, endContent, endContentEdgeCompensation, hasDivider, xstyle, className, style, ref, ...rest }: DrawerHeaderProps): import("react").JSX.Element;
82
+ export declare namespace DrawerHeader {
83
+ var displayName: string;
84
+ }
85
+ //# sourceMappingURL=DrawerHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DrawerHeader.d.ts","sourceRoot":"","sources":["../../src/Drawer/DrawerHeader.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAErC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA0C5C,MAAM,WAAW,iBAAkB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAClE,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAEhC;;;OAGG;IACH,KAAK,EAAE,SAAS,CAAC;IAEjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAE5C;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IAEzB;;;OAGG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IAExD;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,0BAA0B,EAC1B,UAAU,EACV,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,IAAI,EACR,EAAE,iBAAiB,+BAoFnB;yBAjGe,YAAY"}