@galaxy-io/dls 2.0.2 → 2.2.0

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.
@@ -1,3 +1,4 @@
1
+ import { type Icon as PhosphorIcon } from "@phosphor-icons/react";
1
2
  import { type AriaAttributes, type CSSProperties, type ReactNode } from "react";
2
3
  /** Panel width. Canonical rung names (Constitution V); the values are widths, not control heights. */
3
4
  export declare enum ModalSize {
@@ -46,8 +47,12 @@ interface ModalBaseProps extends AriaAttributes {
46
47
  }
47
48
  /** A title row names the dialog. */
48
49
  interface ModalTitledProps {
49
- /** The title row. A string renders as an `h2` `Text` and names the dialog; a node renders as it is. Required unless `ariaLabel` is set. */
50
+ /** The title row. A string renders as an `h2` `Text` and names the dialog; a node renders as it is, in a first line one control tall. Required unless `ariaLabel` is set. */
50
51
  header: ReactNode;
52
+ /** A secondary line under the title. Describes the dialog. */
53
+ subheader?: string | undefined;
54
+ /** A leading icon before the title. Pass the component, not an element. */
55
+ icon?: PhosphorIcon | undefined;
51
56
  /** Accessible name of the dialog. Required by type when there is no `header`. */
52
57
  ariaLabel?: string;
53
58
  }
@@ -55,6 +60,10 @@ interface ModalTitledProps {
55
60
  interface ModalUntitledProps {
56
61
  /** The title row. A string renders as an `h2` `Text` and names the dialog; a node renders as it is. Required unless `ariaLabel` is set. */
57
62
  header?: undefined;
63
+ /** A secondary line under the title. Needs `header`. */
64
+ subheader?: undefined;
65
+ /** A leading icon before the title. Needs `header`. */
66
+ icon?: undefined;
58
67
  /** Accessible name of the dialog. Required by type when there is no `header`. */
59
68
  ariaLabel: string;
60
69
  }
@@ -1,11 +1,12 @@
1
1
  import FocusScope, { FocusScopeAutoFocus } from "../a11y/FocusScope.js";
2
- import Text, { TextSize, TextWeight } from "../text/Text.js";
2
+ import Text from "../text/Text.js";
3
3
  import { useControlledOpenState } from "../hooks/useControlledOpenState.js";
4
4
  import Portal from "../layout/Portal.js";
5
5
  import { OverlayLayer, useOverlay } from "../overlay/useOverlay.js";
6
+ import { OverlayScaleFade } from "../overlay/internal/OverlayScaleFade.js";
6
7
  import { OverlayNode } from "../overlay/OverlayProvider.js";
7
- import ScaleFade from "../transform/ScaleFade.js";
8
8
  import Button, { ButtonSize, ButtonVariant } from "../buttons/Button.js";
9
+ import OverlayTitleRow from "../overlay/internal/OverlayTitleRow.js";
9
10
  import Fade from "../transform/Fade.js";
10
11
  /* empty css */
11
12
  import { styled } from "@linaria/react";
@@ -19,7 +20,8 @@ import { XIcon } from "@phosphor-icons/react";
19
20
  * or dismiss before going back to the page. Modal draws the panel and its three regions (a title
20
21
  * row, a scrolling body and an actions row), dims and locks the page behind it, traps focus inside
21
22
  * and restores it on close, stacks above other overlays through the overlay stack, and animates in
22
- * and out (`Fade` backdrop, `ScaleFade` panel). It has no trigger: the app opens it.
23
+ * and out (`Fade` backdrop, a scale-fade panel whose content leaves first). It has no trigger: the
24
+ * app opens it.
23
25
  *
24
26
  * A short yes/no question is `ConfirmDialog`; a panel beside the page is `Drawer`; an anchored
25
27
  * panel is `Popover`.
@@ -59,7 +61,7 @@ var Positioner = /*#__PURE__*/ styled("div")({
59
61
  class: "gx-Modal-Positioner-ptpk8hn",
60
62
  propsAsIs: false
61
63
  });
62
- /** The panel's ScaleFade root: the flex chain that lets a tall body scroll. */
64
+ /** The panel's scale-fade root: the flex chain that lets a tall body scroll. */
63
65
  var FRAME_CSS = "gx-Modal-FRAME_CSS-fedvh3d";
64
66
  var MODAL_SIZE_TO_CSS_MAP = {
65
67
  ["SMALL"]: "gx-Modal-SMALL-s1jpkb4q",
@@ -73,38 +75,26 @@ var Panel = /*#__PURE__*/ styled("div")({
73
75
  class: "gx-Modal-Panel-p14wx16q",
74
76
  propsAsIs: false
75
77
  });
76
- var Header = /*#__PURE__*/ styled("div")({
77
- name: "Header",
78
- class: "gx-Modal-Header-h1w4b032",
79
- propsAsIs: false
80
- });
81
- /** While the close button shows: a long title never runs under it. */
82
- var HAS_CLOSE_CSS = "gx-Modal-HAS_CLOSE_CSS-hbri8ij";
83
- /** A node `header`: takes the row and may shrink. */
84
- var HeaderNode = /*#__PURE__*/ styled("div")({
85
- name: "HeaderNode",
86
- class: "gx-Modal-HeaderNode-hxdonbm",
87
- propsAsIs: false
88
- });
89
78
  var Body = /*#__PURE__*/ styled("div")({
90
79
  name: "Body",
91
- class: "gx-Modal-Body-b1c1q51w",
80
+ class: "gx-Modal-Body-b1w4b032",
92
81
  propsAsIs: false
93
82
  });
94
83
  var Footer = /*#__PURE__*/ styled("div")({
95
84
  name: "Footer",
96
- class: "gx-Modal-Footer-f1aye683",
85
+ class: "gx-Modal-Footer-fbri8ij",
97
86
  propsAsIs: false
98
87
  });
99
88
  var CloseSlot = /*#__PURE__*/ styled("span")({
100
89
  name: "CloseSlot",
101
- class: "gx-Modal-CloseSlot-c1uq3md9",
90
+ class: "gx-Modal-CloseSlot-cxdonbm",
102
91
  propsAsIs: false
103
92
  });
104
93
  /** A blocking dialog over a dimmed page: title row, scrolling body, actions row. */
105
- var Modal = forwardRef(function Modal({ children, header, footer, size = "MEDIUM", isDismissable = true, isOpen: isOpenProp, defaultIsOpen = false, onOpenChange, ariaLabel, id, className, style, ...rest }, ref) {
94
+ var Modal = forwardRef(function Modal({ children, header, subheader, icon, footer, size = "MEDIUM", isDismissable = true, isOpen: isOpenProp, defaultIsOpen = false, onOpenChange, ariaLabel, id, className, style, ...rest }, ref) {
106
95
  const generatedId = useId();
107
96
  const titleId = useId();
97
+ const subheaderId = useId();
108
98
  const { isOpen, setOpen } = useControlledOpenState({
109
99
  isOpen: isOpenProp,
110
100
  defaultIsOpen,
@@ -133,6 +123,7 @@ var Modal = forwardRef(function Modal({ children, header, footer, size = "MEDIUM
133
123
  if (!hasOpened) return null;
134
124
  const hasHeaderRow = header !== void 0 || isDismissable;
135
125
  const labelling = ariaLabel !== void 0 ? { "aria-label": ariaLabel } : rest["aria-labelledby"] !== void 0 || header === void 0 ? {} : { "aria-labelledby": titleId };
126
+ const describing = subheader !== void 0 && header !== void 0 && rest["aria-describedby"] === void 0 ? { "aria-describedby": subheaderId } : {};
136
127
  const zStyle = { "--gx-modal-z": zIndex };
137
128
  return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsxs(OverlayNode, {
138
129
  overlayId,
@@ -148,7 +139,7 @@ var Modal = forwardRef(function Modal({ children, header, footer, size = "MEDIUM
148
139
  })
149
140
  }), /* @__PURE__ */ jsx(Positioner, {
150
141
  style: zStyle,
151
- children: /* @__PURE__ */ jsx(ScaleFade, {
142
+ children: /* @__PURE__ */ jsx(OverlayScaleFade, {
152
143
  isOpen,
153
144
  shouldAnimateOnMount,
154
145
  className: cx(FRAME_CSS, MODAL_SIZE_TO_CSS_MAP[size]),
@@ -158,6 +149,7 @@ var Modal = forwardRef(function Modal({ children, header, footer, size = "MEDIUM
158
149
  children: /* @__PURE__ */ jsxs(Panel, {
159
150
  ...rest,
160
151
  ...labelling,
152
+ ...describing,
161
153
  ref,
162
154
  id: id ?? generatedId,
163
155
  role: "dialog",
@@ -165,18 +157,13 @@ var Modal = forwardRef(function Modal({ children, header, footer, size = "MEDIUM
165
157
  className,
166
158
  style,
167
159
  children: [
168
- hasHeaderRow && /* @__PURE__ */ jsx(Header, {
169
- className: cx(isDismissable && HAS_CLOSE_CSS),
170
- children: typeof header === "string" ? /* @__PURE__ */ jsx(Text, {
171
- as: "h2",
172
- size: TextSize.BODY_LG,
173
- weight: TextWeight.MEDIUM,
174
- id: titleId,
175
- children: header
176
- }) : header !== void 0 ? /* @__PURE__ */ jsx(HeaderNode, {
177
- id: titleId,
178
- children: header
179
- }) : null
160
+ hasHeaderRow && /* @__PURE__ */ jsx(OverlayTitleRow, {
161
+ header,
162
+ subheader,
163
+ icon,
164
+ titleId,
165
+ subheaderId,
166
+ hasClose: isDismissable
180
167
  }),
181
168
  /* @__PURE__ */ jsx(Body, { children: typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Text, { children }) : children }),
182
169
  footer !== void 0 && /* @__PURE__ */ jsx(Footer, { children: footer }),
@@ -7,8 +7,8 @@ import Portal from "../layout/Portal.js";
7
7
  import { OverlayLayer, useOverlay } from "../overlay/useOverlay.js";
8
8
  import Kbd, { KbdSize, KbdVariant } from "../text/Kbd.js";
9
9
  import Divider from "../layout/Divider.js";
10
+ import { OverlayScaleFade } from "../overlay/internal/OverlayScaleFade.js";
10
11
  import { OverlayNode } from "../overlay/OverlayProvider.js";
11
- import ScaleFade from "../transform/ScaleFade.js";
12
12
  import { useHotkey } from "../hooks/useHotkey.js";
13
13
  import Icon, { IconVariant } from "../icons/Icon.js";
14
14
  import Fade from "../transform/Fade.js";
@@ -28,10 +28,10 @@ import { cx } from "@linaria/core";
28
28
  * move a highlight while focus stays in the field; Enter runs the highlighted command and closes
29
29
  * the palette. Commands come from the app as data (`items`), each with its own `onSelect`.
30
30
  *
31
- * Built from Modal's primitives (Portal, the overlay stack, Fade, ScaleFade, FocusScope, the
32
- * backdrop and its scroll lock), not by rendering `Modal`, and its rows copy Menu's `MEDIUM` look
33
- * in a `role="listbox"` driven by `aria-activedescendant`. It does not route, fetch or rank:
34
- * `onSearch` hands the term to the app, and commands do the work in `onSelect`.
31
+ * Built from Modal's primitives (Portal, the overlay stack, Fade, the content-first scale-fade,
32
+ * FocusScope, the backdrop and its scroll lock), not by rendering `Modal`, and its rows copy Menu's
33
+ * `MEDIUM` look in a `role="listbox"` driven by `aria-activedescendant`. It does not route, fetch
34
+ * or rank: `onSearch` hands the term to the app, and commands do the work in `onSelect`.
35
35
  *
36
36
  * SSR: nothing renders on the server (closed by default; `Portal` renders nothing there).
37
37
  * `useHotkey` and `useLocalStorage` are SSR-safe; nothing touches `window`, `document` or
@@ -68,7 +68,7 @@ function createMemoryStorage() {
68
68
  }
69
69
  /**
70
70
  * Focuses `target` outside the palette while the palette's FocusScope is still mounted for its
71
- * exit. The exiting panel keeps the props it had while open (ScaleFade freezes them), so its trap
71
+ * exit. The exiting panel keeps the props it had while open (AnimatePresence holds them), so its trap
72
72
  * would pull this focus back; the one `focusin` this call dispatches is stopped before it reaches
73
73
  * the trap's document listener.
74
74
  */
@@ -107,7 +107,7 @@ var Positioner = /*#__PURE__*/ styled("div")({
107
107
  class: "gx-CommandPalette-Positioner-pjnwubt",
108
108
  propsAsIs: false
109
109
  });
110
- /** The panel's ScaleFade root: it grows from the top center. */
110
+ /** The panel's scale-fade root: it grows from the top center. */
111
111
  var FRAME_CSS = "gx-CommandPalette-FRAME_CSS-f1kamwsl";
112
112
  var FOCUS_SCOPE_CSS = "gx-CommandPalette-FOCUS_SCOPE_CSS-fi5b22w";
113
113
  var Panel = /*#__PURE__*/ styled("div")({
@@ -407,7 +407,7 @@ var CommandPalette = forwardRef(function CommandPalette({ items, isOpen: isOpenP
407
407
  })
408
408
  }), /* @__PURE__ */ jsx(Positioner, {
409
409
  style: zStyle,
410
- children: /* @__PURE__ */ jsx(ScaleFade, {
410
+ children: /* @__PURE__ */ jsx(OverlayScaleFade, {
411
411
  isOpen,
412
412
  shouldAnimateOnMount,
413
413
  className: FRAME_CSS,
@@ -0,0 +1,16 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ /** @internal Props of `OverlayScaleFade`. */
3
+ export interface OverlayScaleFadeProps {
4
+ /** Shown; changing it scales and fades the panel in, or fades its content then the panel out. */
5
+ isOpen: boolean;
6
+ /** Pop in when mounting open (see `ScaleFade`). */
7
+ shouldAnimateOnMount?: boolean;
8
+ /** The panel. */
9
+ children?: ReactNode;
10
+ /** May set `transform-origin`; must not set `opacity`, `transform` or `visibility`. */
11
+ className?: string | undefined;
12
+ /** Only custom properties. */
13
+ style?: CSSProperties | undefined;
14
+ }
15
+ /** @internal An overlay panel's enter and content-first exit. */
16
+ export declare function OverlayScaleFade({ isOpen, shouldAnimateOnMount, className, style, children, }: OverlayScaleFadeProps): import("react").JSX.Element;
@@ -0,0 +1,60 @@
1
+ import { DURATION } from "../../theme/tokens/scales.js";
2
+ import { usePrefersReducedMotion } from "../../hooks/usePrefersReducedMotion.js";
3
+ import { OVERLAY_CONTENT_SHOWN, withContentFirstExit } from "./contentFirstExit.js";
4
+ /* empty css */
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { cx } from "@linaria/core";
7
+ import { AnimatePresence, motion } from "framer-motion";
8
+ //#region src/overlay/internal/OverlayScaleFade.tsx
9
+ /**
10
+ * @internal `ScaleFade` for overlay panels (Modal, Popover, CommandPalette), with a content-first
11
+ * exit (`contentFirstExit`): on close the content fades in place, then the empty panel scales and
12
+ * fades. Entering matches `ScaleFade`. The public `ScaleFade` keeps its single-step exit.
13
+ */
14
+ var ENTER_TRANSITION = {
15
+ duration: Number.parseFloat(DURATION.base) / 1e3,
16
+ ease: "easeOut"
17
+ };
18
+ var REDUCED_TRANSITION = { duration: 0 };
19
+ var SCALE = .95;
20
+ var START = {
21
+ opacity: 0,
22
+ scale: SCALE,
23
+ ...OVERLAY_CONTENT_SHOWN
24
+ };
25
+ /**
26
+ * `visibility: visible` inline, like `ScaleFade`: a closing host may hide its own wrapper
27
+ * (Popover's positioner), and the exit must still show through it.
28
+ */
29
+ var OPEN = {
30
+ opacity: 1,
31
+ scale: 1,
32
+ visibility: "visible",
33
+ pointerEvents: "auto",
34
+ ...OVERLAY_CONTENT_SHOWN
35
+ };
36
+ var CLOSED = {
37
+ opacity: 0,
38
+ scale: SCALE,
39
+ visibility: "visible",
40
+ pointerEvents: "none"
41
+ };
42
+ var ROOT_CSS = "gx-OverlayScaleFade-ROOT_CSS-r1woxc6d";
43
+ /** @internal An overlay panel's enter and content-first exit. */
44
+ function OverlayScaleFade({ isOpen, shouldAnimateOnMount = false, className, style, children }) {
45
+ const isReducedMotion = usePrefersReducedMotion();
46
+ return /* @__PURE__ */ jsx(AnimatePresence, {
47
+ initial: shouldAnimateOnMount,
48
+ children: isOpen && /* @__PURE__ */ jsx(motion.div, {
49
+ initial: START,
50
+ animate: OPEN,
51
+ exit: withContentFirstExit(CLOSED, isReducedMotion),
52
+ transition: isReducedMotion ? REDUCED_TRANSITION : ENTER_TRANSITION,
53
+ className: cx(ROOT_CSS, className),
54
+ ...style === void 0 ? {} : { style },
55
+ children
56
+ }, "overlay-scale-fade")
57
+ });
58
+ }
59
+ //#endregion
60
+ export { OverlayScaleFade };
@@ -0,0 +1,23 @@
1
+ import type { Icon as PhosphorIcon } from "@phosphor-icons/react";
2
+ import type { ReactNode, Ref } from "react";
3
+ /** @internal Props of `OverlayTitleRow`. */
4
+ export interface OverlayTitleRowProps {
5
+ /** The title: a string renders as an `h2`; a node renders as it is in the first line. */
6
+ header?: ReactNode;
7
+ /** A secondary line under the title. */
8
+ subheader?: string | undefined;
9
+ /** A leading icon before the title. */
10
+ icon?: PhosphorIcon | undefined;
11
+ /** Drawer's title-row controls, before the close button. */
12
+ actions?: ReactNode;
13
+ /** Drawer's ref on the actions group (its development check counts the controls). */
14
+ actionsRef?: Ref<HTMLDivElement>;
15
+ /** Id the title takes, so the overlay can point `aria-labelledby` at it. */
16
+ titleId: string;
17
+ /** Id the subheader takes, so the overlay can point `aria-describedby` at it. */
18
+ subheaderId: string;
19
+ /** The overlay shows its close button: the row keeps clear of it. */
20
+ hasClose: boolean;
21
+ }
22
+ /** @internal The title row: icon, title over subheader, actions; the close button is the host's. */
23
+ export default function OverlayTitleRow({ header, subheader, icon, actions, actionsRef, titleId, subheaderId, hasClose, }: OverlayTitleRowProps): import("react").JSX.Element;
@@ -0,0 +1,90 @@
1
+ import Text, { TextSize, TextVariant, TextWeight } from "../../text/Text.js";
2
+ import Icon, { IconVariant } from "../../icons/Icon.js";
3
+ import IconSlot from "../../icons/internal/IconSlot.js";
4
+ /* empty css */
5
+ import { styled } from "@linaria/react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { cx } from "@linaria/core";
8
+ //#region src/overlay/internal/OverlayTitleRow.tsx
9
+ /**
10
+ * @internal The title row of Modal and Drawer: an optional leading icon, the title (a string as an
11
+ * `h2`, or a node) over an optional subheader, and Drawer's `actions`. The close button is not in
12
+ * here: each overlay renders it last, so it stays last in the tab order, pinned 12px from the top.
13
+ *
14
+ * The row aligns to its top and every control sits in a first line one `control.small` tall: the
15
+ * icon, the title line and `actions` all center on the same line as the pinned close button, so a
16
+ * subheader (or any taller header) grows the row downward without pulling them apart.
17
+ */
18
+ /** The leading icon: Widget's `LARGE` header icon, odd, so it sits in an `IconSlot`. */
19
+ var OVERLAY_TITLE_ICON_PX = 15;
20
+ var Root = /*#__PURE__*/ styled("div")({
21
+ name: "Root",
22
+ class: "gx-OverlayTitleRow-r1ppt2kb",
23
+ propsAsIs: false
24
+ });
25
+ /** While the close button shows: a long title never runs under it. */
26
+ var HAS_CLOSE_CSS = "gx-OverlayTitleRow-HAS_CLOSE_CSS-h147kmzk";
27
+ /** With `actions`, the close button joins them: the same 4px apart, one group at the row's end. */
28
+ var HAS_CLOSE_WITH_ACTIONS_CSS = "gx-OverlayTitleRow-HAS_CLOSE_WITH_ACTIONS_CSS-h6yq484";
29
+ /** One `control.small` tall, its content centered: the line the close button centers on. */
30
+ var Line = /*#__PURE__*/ styled("div")({
31
+ name: "Line",
32
+ class: "gx-OverlayTitleRow-Line-l16sm8vd",
33
+ propsAsIs: false
34
+ });
35
+ /** Title over subheader; takes the free width. */
36
+ var Titles = /*#__PURE__*/ styled("div")({
37
+ name: "Titles",
38
+ class: "gx-OverlayTitleRow-Titles-tiu9n96",
39
+ propsAsIs: false
40
+ });
41
+ /** A node `header`: takes the line and may shrink. */
42
+ var HeaderNode = /*#__PURE__*/ styled("div")({
43
+ name: "HeaderNode",
44
+ class: "gx-OverlayTitleRow-HeaderNode-h8nok1a",
45
+ propsAsIs: false
46
+ });
47
+ /** `actions`: at the end of the first line, 4px apart and 4px before the close button. */
48
+ var Actions = /*#__PURE__*/ styled("div")({
49
+ name: "Actions",
50
+ class: "gx-OverlayTitleRow-Actions-a157lrfo",
51
+ propsAsIs: false
52
+ });
53
+ /** @internal The title row: icon, title over subheader, actions; the close button is the host's. */
54
+ function OverlayTitleRow({ header, subheader, icon, actions, actionsRef, titleId, subheaderId, hasClose }) {
55
+ const hasHeader = header !== void 0;
56
+ return /* @__PURE__ */ jsxs(Root, {
57
+ className: cx(hasClose && (actions === void 0 ? HAS_CLOSE_CSS : HAS_CLOSE_WITH_ACTIONS_CSS)),
58
+ children: [
59
+ hasHeader && icon !== void 0 && /* @__PURE__ */ jsx(Line, { children: /* @__PURE__ */ jsx(IconSlot, {
60
+ px: OVERLAY_TITLE_ICON_PX,
61
+ children: /* @__PURE__ */ jsx(Icon, {
62
+ component: icon,
63
+ size: OVERLAY_TITLE_ICON_PX,
64
+ variant: IconVariant.PRIMARY
65
+ })
66
+ }) }),
67
+ /* @__PURE__ */ jsxs(Titles, { children: [hasHeader && /* @__PURE__ */ jsx(Line, { children: typeof header === "string" ? /* @__PURE__ */ jsx(Text, {
68
+ as: "h2",
69
+ size: TextSize.BODY_LG,
70
+ weight: TextWeight.MEDIUM,
71
+ id: titleId,
72
+ children: header
73
+ }) : /* @__PURE__ */ jsx(HeaderNode, {
74
+ id: titleId,
75
+ children: header
76
+ }) }), hasHeader && subheader !== void 0 && /* @__PURE__ */ jsx(Text, {
77
+ id: subheaderId,
78
+ size: TextSize.BODY_MD,
79
+ variant: TextVariant.SECONDARY,
80
+ children: subheader
81
+ })] }),
82
+ actions !== void 0 && /* @__PURE__ */ jsx(Actions, {
83
+ ref: actionsRef,
84
+ children: actions
85
+ })
86
+ ]
87
+ });
88
+ }
89
+ //#endregion
90
+ export { OverlayTitleRow as default };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @internal How Modal, Drawer and Popover (with Dropdown and Menu on it) leave: the content first,
3
+ * then the shell. On close the panel's content fades out in place over `duration.fast` (opacity
4
+ * only, so its text is never resampled, never half-transparent over a blurred backdrop and never
5
+ * squeezed); then the empty shell scales, slides or collapses away over the next `duration.fast`.
6
+ * The whole exit stays `duration.base`. The exiting panel keeps the props it last rendered with
7
+ * (`AnimatePresence` holds the removed element), so an app that clears its state on close does not
8
+ * change the panel mid-exit.
9
+ *
10
+ * The shell's motion element animates `--gx-overlay-content-opacity`; the content reads it
11
+ * (`OVERLAY_CONTENT_OPACITY_CSS`). Entering is unchanged: the content shows with the shell.
12
+ */
13
+ import type { TargetAndTransition } from "framer-motion";
14
+ /** The custom property the shell animates and the content reads. */
15
+ export declare const OVERLAY_CONTENT_OPACITY_VAR = "--gx-overlay-content-opacity";
16
+ /** Declarations for the content: its opacity follows the shell's exit. */
17
+ export declare const OVERLAY_CONTENT_OPACITY_CSS = "opacity: var(--gx-overlay-content-opacity, 1);";
18
+ /** Merged into a shell's open (and enter-start) target: the content fully shown. */
19
+ export declare const OVERLAY_CONTENT_SHOWN: TargetAndTransition;
20
+ /**
21
+ * A shell's exit target, made content-first: the content variable drops to 0 over
22
+ * `duration.fast`, then the shell's own values animate over the next `duration.fast`. Under
23
+ * reduced motion everything snaps.
24
+ */
25
+ export declare function withContentFirstExit(exitTarget: TargetAndTransition, isReducedMotion: boolean): TargetAndTransition;
@@ -0,0 +1,34 @@
1
+ import { DURATION } from "../../theme/tokens/scales.js";
2
+ //#region src/overlay/internal/contentFirstExit.ts
3
+ /** The custom property the shell animates and the content reads. */
4
+ var OVERLAY_CONTENT_OPACITY_VAR = "--gx-overlay-content-opacity";
5
+ /** Declarations for the content: its opacity follows the shell's exit. */
6
+ var OVERLAY_CONTENT_OPACITY_CSS = `opacity: var(${OVERLAY_CONTENT_OPACITY_VAR}, 1);`;
7
+ /** `duration.fast` in seconds (framer needs a number). */
8
+ var FAST_S = Number.parseFloat(DURATION.fast) / 1e3;
9
+ /** Merged into a shell's open (and enter-start) target: the content fully shown. */
10
+ var OVERLAY_CONTENT_SHOWN = { [OVERLAY_CONTENT_OPACITY_VAR]: 1 };
11
+ /**
12
+ * A shell's exit target, made content-first: the content variable drops to 0 over
13
+ * `duration.fast`, then the shell's own values animate over the next `duration.fast`. Under
14
+ * reduced motion everything snaps.
15
+ */
16
+ function withContentFirstExit(exitTarget, isReducedMotion) {
17
+ const transition = isReducedMotion ? { duration: 0 } : {
18
+ duration: FAST_S,
19
+ delay: FAST_S,
20
+ ease: "easeOut",
21
+ [OVERLAY_CONTENT_OPACITY_VAR]: {
22
+ duration: FAST_S,
23
+ delay: 0,
24
+ ease: "easeOut"
25
+ }
26
+ };
27
+ return {
28
+ ...exitTarget,
29
+ [OVERLAY_CONTENT_OPACITY_VAR]: 0,
30
+ transition
31
+ };
32
+ }
33
+ //#endregion
34
+ export { OVERLAY_CONTENT_OPACITY_CSS, OVERLAY_CONTENT_OPACITY_VAR, OVERLAY_CONTENT_SHOWN, withContentFirstExit };
@@ -5,8 +5,8 @@ import { useControlledOpenState } from "../hooks/useControlledOpenState.js";
5
5
  import { PortalContainerContext } from "../theme/useGalaxyTheme.js";
6
6
  import { OverlayLayer, useOverlay } from "../overlay/useOverlay.js";
7
7
  import Box from "../layout/Box.js";
8
+ import { OverlayScaleFade } from "../overlay/internal/OverlayScaleFade.js";
8
9
  import { OverlayNode } from "../overlay/OverlayProvider.js";
9
- import ScaleFade from "../transform/ScaleFade.js";
10
10
  /* empty css */
11
11
  import { styled } from "@linaria/react";
12
12
  import { cloneElement, forwardRef, useCallback, useContext, useId, useReducer, useRef, useState, version } from "react";
@@ -22,7 +22,8 @@ import { FloatingFocusManager, FloatingNode, FloatingPortal, FloatingTree, autoP
22
22
  *
23
23
  * Positioning, interactions and non-modal focus come from `@floating-ui/react`. The computed
24
24
  * `x`/`y` and the overlay stack's `zIndex` are written as custom properties on the positioner and
25
- * read by static CSS. The panel enters and exits through `ScaleFade`, growing out of the trigger.
25
+ * read by static CSS. The panel scale-fades in, growing out of the trigger; on close its content fades
26
+ * first, then the empty panel shrinks back toward the trigger.
26
27
  * Escape goes through the overlay stack (`useOverlay`), so it reaches only the topmost overlay.
27
28
  *
28
29
  * Popover has no slots and no padding: Dropdown, Menu, ContextMenu, SelectInput, DateInput and
@@ -110,7 +111,7 @@ var POPOVER_ROLE_TO_FLOATING_ROLE_MAP = {
110
111
  ["LISTBOX"]: "listbox"
111
112
  };
112
113
  /**
113
- * The resolved placement → ScaleFade's `transform-origin` (the ScaleFade host contract), on the
114
+ * The resolved placement → the scale-fade root's `transform-origin`, on the
114
115
  * edge that faces the trigger. The `AUTO*` entries never resolve at runtime; the `Record` is
115
116
  * exhaustive.
116
117
  */
@@ -154,11 +155,11 @@ var POPOVER_VARIANT_TO_CSS_MAP = {
154
155
  var NO_SURFACE_CSS = "gx-Popover-NO_SURFACE_CSS-n1lgpldq";
155
156
  /** `shouldMatchTriggerWidth`: exactly as wide as the trigger. */
156
157
  var MATCH_TRIGGER_WIDTH_CSS = "gx-Popover-MATCH_TRIGGER_WIDTH_CSS-mgy0n2s";
157
- /** Closed (the exit, if any, still shows: ScaleFade's root sets its own visibility). */
158
+ /** Closed (the exit, if any, still shows: the panel's scale-fade root sets its own visibility). */
158
159
  var CLOSED_CSS = "gx-Popover-CLOSED_CSS-c17lvfn2";
159
160
  /**
160
161
  * Not positioned yet, or the trigger scrolled fully out of view. Clipped rather than
161
- * `visibility: hidden`, because ScaleFade's root sets `visibility: visible` inline, which an
162
+ * `visibility: hidden`, because the scale-fade root sets `visibility: visible` inline, which an
162
163
  * ancestor's `hidden` cannot override; a clip hides the descendants and their hit area.
163
164
  */
164
165
  var REFERENCE_HIDDEN_CSS = "gx-Popover-REFERENCE_HIDDEN_CSS-r1gfapfk";
@@ -351,7 +352,7 @@ function PopoverRoot({ children, body, trigger = "CLICK", placement = Placement.
351
352
  ref: isShown ? refs.setFloating : void 0,
352
353
  className: cx(!isShown && CLOSED_CSS, isShown && (!isPositioned || isReferenceHidden) && REFERENCE_HIDDEN_CSS),
353
354
  style: position,
354
- children: /* @__PURE__ */ jsx(ScaleFade, {
355
+ children: /* @__PURE__ */ jsx(OverlayScaleFade, {
355
356
  isOpen: isShown,
356
357
  shouldAnimateOnMount: !isOpenOnFirstRender,
357
358
  className: POPOVER_PLACEMENT_TO_TRANSFORM_ORIGIN_MAP[resolvedPlacement],