@galaxy-io/dls 2.6.1 → 2.7.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.
@@ -23,6 +23,18 @@ interface DrawerBaseProps extends AriaAttributes {
23
23
  footer?: ReactNode;
24
24
  /** Controls in the title row, before the close button, for the thing the drawer shows: one or two icon-only `TERTIARY` `SMALL` buttons and a ⋯ `Menu` for the rest. Not for Save or Cancel (those are `footer`). */
25
25
  actions?: ReactNode;
26
+ /** A strip between the title row and the body that stays in place while the body scrolls: an `Alert isBanner`. */
27
+ banner?: ReactNode;
28
+ /**
29
+ * Hairlines between the title row and the body, and between the body and the footer. For a body that scrolls.
30
+ * @default false
31
+ */
32
+ hasDividers?: boolean;
33
+ /**
34
+ * No body padding: the app owns it (sections of `Box padding={16}` and `Divider`s, a table edge to edge). The title row and footer keep theirs.
35
+ * @default false
36
+ */
37
+ isFlush?: boolean;
26
38
  /**
27
39
  * The edge the drawer sits on and slides from.
28
40
  * @default Side.RIGHT
@@ -6,6 +6,7 @@ import { useControlledOpenState } from "../hooks/useControlledOpenState.js";
6
6
  import Portal from "../layout/Portal.js";
7
7
  import { OverlayLayer, useOverlay } from "../overlay/useOverlay.js";
8
8
  import { usePrefersReducedMotion } from "../hooks/usePrefersReducedMotion.js";
9
+ import Divider from "../layout/Divider.js";
9
10
  import { OVERLAY_CONTENT_SHOWN, withContentFirstExit } from "../overlay/internal/contentFirstExit.js";
10
11
  import { OverlayNode } from "../overlay/OverlayProvider.js";
11
12
  import Button, { ButtonSize, ButtonVariant } from "../buttons/Button.js";
@@ -32,6 +33,11 @@ import { AnimatePresence, motion } from "framer-motion";
32
33
  * link) and a ⋯ `Menu` for the rest (rename, duplicate, delete). `footer` finishes the drawer's
33
34
  * task (Cancel, then the `PRIMARY` Save); a read-only detail drawer usually has no footer.
34
35
  *
36
+ * The regions are plain by default, as in `Modal`. `hasDividers` draws a hairline between the
37
+ * title row and the body and between the body and the footer. `isFlush` drops the body's padding
38
+ * so the app owns it. `banner` is a slot between the title row and the body that stays put while
39
+ * the body scrolls, for an `Alert isBanner`.
40
+ *
35
41
  * By default it is modal, like `Modal`: a portal over a dimmed, locked page with a focus trap,
36
42
  * stacked through the overlay stack, closing on Escape or a backdrop click. With
37
43
  * `isModal={false}` it is a docked panel: it renders in place in the layout and pushes the content
@@ -186,14 +192,22 @@ var Body = /*#__PURE__*/ styled("div")({
186
192
  class: "gx-Drawer-Body-b45j7yl",
187
193
  propsAsIs: false
188
194
  });
195
+ /** `isFlush`: the body has no inset; the content pads itself. */
196
+ var FLUSH_BODY_CSS = "gx-Drawer-FLUSH_BODY_CSS-fov58xm";
197
+ /** `banner`: a strip pinned between the title row and the body; it never shrinks or scrolls. */
198
+ var Banner = /*#__PURE__*/ styled("div")({
199
+ name: "Banner",
200
+ class: "gx-Drawer-Banner-bckczr5",
201
+ propsAsIs: false
202
+ });
189
203
  var Footer = /*#__PURE__*/ styled("div")({
190
204
  name: "Footer",
191
- class: "gx-Drawer-Footer-fov58xm",
205
+ class: "gx-Drawer-Footer-f1v64nx4",
192
206
  propsAsIs: false
193
207
  });
194
208
  var CloseSlot = /*#__PURE__*/ styled("span")({
195
209
  name: "CloseSlot",
196
- class: "gx-Drawer-CloseSlot-cckczr5",
210
+ class: "gx-Drawer-CloseSlot-ct6k1c3",
197
211
  propsAsIs: false
198
212
  });
199
213
  /**
@@ -202,17 +216,17 @@ var CloseSlot = /*#__PURE__*/ styled("span")({
202
216
  * the stylesheet.
203
217
  */
204
218
  var DRAWER_SIDE_TO_HANDLE_CSS_MAP = {
205
- [Side.RIGHT]: "gx-Drawer-Side_RIGHT-s1v64nx4",
206
- [Side.LEFT]: "gx-Drawer-Side_LEFT-st6k1c3",
207
- [Side.TOP]: "gx-Drawer-Side_TOP-s15lcrsa",
208
- [Side.BOTTOM]: "gx-Drawer-Side_BOTTOM-s1ob4oh8"
219
+ [Side.RIGHT]: "gx-Drawer-Side_RIGHT-s15lcrsa",
220
+ [Side.LEFT]: "gx-Drawer-Side_LEFT-s1ob4oh8",
221
+ [Side.TOP]: "gx-Drawer-Side_TOP-s14nwavk",
222
+ [Side.BOTTOM]: "gx-Drawer-Side_BOTTOM-s1ifjfam"
209
223
  };
210
224
  /** The viewport length on the side's axis, less the 48px strip. */
211
225
  function readMaxSize(side) {
212
226
  return (DRAWER_SIDE_TO_IS_HORIZONTAL_MAP[side] ? window.innerWidth : window.innerHeight) - DRAWER_VIEWPORT_GAP_PX;
213
227
  }
214
228
  /** A panel that slides in from an edge: title row, scrolling body, actions row. */
215
- var Drawer = forwardRef(function Drawer({ children, header, subheader, icon, footer, actions, side = Side.RIGHT, size = "MEDIUM", isResizable = false, isModal = true, isDismissable = true, isOpen: isOpenProp, defaultIsOpen = false, onOpenChange, ariaLabel, id, className, style, ...rest }, ref) {
229
+ var Drawer = forwardRef(function Drawer({ children, header, subheader, icon, footer, actions, banner, hasDividers = false, isFlush = false, side = Side.RIGHT, size = "MEDIUM", isResizable = false, isModal = true, isDismissable = true, isOpen: isOpenProp, defaultIsOpen = false, onOpenChange, ariaLabel, id, className, style, ...rest }, ref) {
216
230
  const generatedId = useId();
217
231
  const titleId = useId();
218
232
  const subheaderId = useId();
@@ -283,8 +297,13 @@ var Drawer = forwardRef(function Drawer({ children, header, subheader, icon, foo
283
297
  subheaderId,
284
298
  hasClose: isDismissable
285
299
  }),
286
- /* @__PURE__ */ jsx(Body, { children: typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Text, { children }) : children }),
287
- footer !== void 0 && /* @__PURE__ */ jsx(Footer, { children: footer }),
300
+ hasDividers && hasHeaderRow && /* @__PURE__ */ jsx(Divider, {}),
301
+ banner !== void 0 && /* @__PURE__ */ jsx(Banner, { children: banner }),
302
+ /* @__PURE__ */ jsx(Body, {
303
+ className: cx(isFlush && FLUSH_BODY_CSS),
304
+ children: typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Text, { children }) : children
305
+ }),
306
+ footer !== void 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [hasDividers && /* @__PURE__ */ jsx(Divider, {}), /* @__PURE__ */ jsx(Footer, { children: footer })] }),
288
307
  isResizable && /* @__PURE__ */ jsx(ResizeHandle, {
289
308
  orientation: DRAWER_SIDE_TO_HANDLE_ORIENTATION_MAP[side],
290
309
  isReversed: DRAWER_SIDE_TO_IS_HANDLE_REVERSED_MAP[side],
@@ -10,6 +10,11 @@ export interface ResizablePanelsProps extends AriaAttributes {
10
10
  orientation?: Orientation;
11
11
  /** Persists each uncontrolled panel's size in `localStorage` under this key, by panel `id`. Off when omitted. */
12
12
  storageKey?: string;
13
+ /**
14
+ * A hairline on the edge where two panels meet: a sized panel's edge facing its handle, so the handle's strip lies just inside the fluid panel.
15
+ * @default false
16
+ */
17
+ hasDividers?: boolean;
13
18
  /** DOM id on the root `<div>`. */
14
19
  id?: string;
15
20
  /** Any `data-*` attribute, forwarded to the root `<div>`. */
@@ -20,6 +20,11 @@ import { useMergeRefs } from "@floating-ui/react";
20
20
  * Pass `ResizablePanel`s as direct children (Fragments are not flattened); `null` / `false`
21
21
  * children are skipped, so a panel can be shown conditionally. A panel with neither `value` nor
22
22
  * `defaultValue` is the fluid one; with none, the last panel is.
23
+ *
24
+ * The panels have no frame of their own. `hasDividers` draws a hairline on the edge where two
25
+ * panels meet: on a sized panel's edge that faces its handle (so the line sits on the sidebar's
26
+ * edge and the handle's strip lies just inside the fluid panel), and on the later panel's start
27
+ * edge where two panels meet without a handle.
23
28
  */
24
29
  var ResizablePanelsContext = createContext(null);
25
30
  /** The storage key while `storageKey` is unset; it lives in a per-instance memory adapter. */
@@ -68,6 +73,15 @@ var RESIZABLE_PANEL_ORIENTATION_TO_CSS_MAP = {
68
73
  var SIZED_CSS = "gx-ResizablePanels-SIZED_CSS-s16ojr42";
69
74
  /** The fluid panel takes the remaining space. */
70
75
  var FLUID_CSS = "gx-ResizablePanels-FLUID_CSS-f1k6jei3";
76
+ /** `hasDividers`: the hairline on a panel's start or end edge along the layout axis. */
77
+ var RESIZABLE_PANEL_ORIENTATION_TO_START_DIVIDER_CSS_MAP = {
78
+ [Orientation.HORIZONTAL]: "gx-ResizablePanels-Orientation_HORIZONTAL-o1ikvfoe",
79
+ [Orientation.VERTICAL]: "gx-ResizablePanels-Orientation_VERTICAL-oa3x64k"
80
+ };
81
+ var RESIZABLE_PANEL_ORIENTATION_TO_END_DIVIDER_CSS_MAP = {
82
+ [Orientation.HORIZONTAL]: "gx-ResizablePanels-Orientation_HORIZONTAL-o1kf7v08",
83
+ [Orientation.VERTICAL]: "gx-ResizablePanels-Orientation_VERTICAL-o12hjusf"
84
+ };
71
85
  /** Rounds and keeps `value` within `[min, max]`. */
72
86
  function clamp(value, min, max) {
73
87
  return Math.min(Math.max(Math.round(value), min), max);
@@ -76,7 +90,7 @@ function isPanelElement(node) {
76
90
  return isValidElement(node) && node.type === ResizablePanel;
77
91
  }
78
92
  /** A split layout of `ResizablePanel`s with a `ResizeHandle` between each sized–fluid pair. */
79
- var ResizablePanels = forwardRef(function ResizablePanels({ children, orientation = Orientation.HORIZONTAL, storageKey, className, style, ...rest }, ref) {
93
+ var ResizablePanels = forwardRef(function ResizablePanels({ children, orientation = Orientation.HORIZONTAL, storageKey, hasDividers = false, className, style, ...rest }, ref) {
80
94
  const rootRef = useRef(null);
81
95
  const mergedRef = useMergeRefs([rootRef, ref]);
82
96
  const rootSize = useElementClientSize(rootRef);
@@ -130,10 +144,26 @@ var ResizablePanels = forwardRef(function ResizablePanels({ children, orientatio
130
144
  }
131
145
  return Math.max(target.min, Math.min(target.max, Math.floor(room)));
132
146
  };
133
- const layoutById = new Map(panels.map((panel) => [panel.props.id, {
147
+ const dividerEdges = panels.map(() => ({
148
+ start: false,
149
+ end: false
150
+ }));
151
+ if (hasDividers) handleTargets.forEach((targetIndex, index) => {
152
+ if (index === 0) return;
153
+ if (targetIndex === void 0) {
154
+ const edges = dividerEdges[index];
155
+ if (edges !== void 0) edges.start = true;
156
+ } else {
157
+ const edges = dividerEdges[targetIndex];
158
+ if (edges !== void 0) edges[targetIndex === index ? "start" : "end"] = true;
159
+ }
160
+ });
161
+ const layoutById = new Map(panels.map((panel, index) => [panel.props.id, {
134
162
  size: panel.isFluid ? void 0 : sizeOf(panel),
135
163
  min: panel.min,
136
- isFluid: panel.isFluid
164
+ isFluid: panel.isFluid,
165
+ hasStartDivider: dividerEdges[index]?.start ?? false,
166
+ hasEndDivider: dividerEdges[index]?.end ?? false
137
167
  }]));
138
168
  const contextValue = {
139
169
  orientation,
@@ -185,7 +215,7 @@ var ResizablePanel = forwardRef(function ResizablePanel({ id, children, value: _
185
215
  id,
186
216
  role: name ? "region" : void 0,
187
217
  "aria-label": name,
188
- className: cx(RESIZABLE_PANEL_ORIENTATION_TO_CSS_MAP[orientation], isFluid ? FLUID_CSS : SIZED_CSS, className),
218
+ className: cx(RESIZABLE_PANEL_ORIENTATION_TO_CSS_MAP[orientation], isFluid ? FLUID_CSS : SIZED_CSS, layout?.hasStartDivider && RESIZABLE_PANEL_ORIENTATION_TO_START_DIVIDER_CSS_MAP[orientation], layout?.hasEndDivider && RESIZABLE_PANEL_ORIENTATION_TO_END_DIVIDER_CSS_MAP[orientation], className),
189
219
  style: {
190
220
  ...style,
191
221
  ...variables
@@ -83,6 +83,16 @@ export interface DescriptionListProps extends AriaAttributes {
83
83
  * @default false
84
84
  */
85
85
  isValueTrailing?: boolean;
86
+ /**
87
+ * A hairline between rows, with 8px (`HORIZONTAL`) or 12px (`VERTICAL`) above and below it; none above the first row or below the last.
88
+ * @default false
89
+ */
90
+ hasDividers?: boolean;
91
+ /**
92
+ * A hairline frame with `lg` corners around the list, 16px from the first label and the last value, with a line between rows inside it (implies `hasDividers`) and 12px above and below every row: the detail card.
93
+ * @default false
94
+ */
95
+ hasBorder?: boolean;
86
96
  /** DOM id on the root `<dl>`. */
87
97
  id?: string;
88
98
  /** Any `data-*` attribute, forwarded to the root `<dl>`. */
@@ -19,6 +19,11 @@ import { cx } from "@linaria/core";
19
19
  * by side. Rows take the copy button's height when any item is copyable, so the list keeps one
20
20
  * rhythm.
21
21
  *
22
+ * `hasDividers` draws a hairline under every row but the last (never above the first) and
23
+ * moves the grid gaps onto the cells so the line is continuous across the label and value tracks.
24
+ * `hasBorder` frames the list with a hairline and `lg` corners, pads the rows' outer edges (so the
25
+ * lines reach the frame) and implies `hasDividers`: the detail card.
26
+ *
22
27
  * Read-only: editing is a form (`Field` + inputs); a list of plain lines is `BulletedList`.
23
28
  */
24
29
  /** Type step of labels and values; canonical rungs `SMALL MEDIUM LARGE`. */
@@ -68,17 +73,42 @@ var DESCRIPTION_LIST_ORIENTATION_TO_CSS_MAP = {
68
73
  [Orientation.HORIZONTAL]: "gx-DescriptionList-Orientation_HORIZONTAL-oarm5a8",
69
74
  [Orientation.VERTICAL]: "gx-DescriptionList-Orientation_VERTICAL-o1itek1z"
70
75
  };
76
+ /**
77
+ * `hasDividers`: the grid gaps become cell padding (`--gx-dl-col-gap` at the end of every item
78
+ * but the last in its row, `--gx-dl-row-pad` above and below every row boundary) and the rows
79
+ * after the first draw a hairline on their top edge. Which items start or end a row depends on
80
+ * `columns`, so the per-row custom properties are set in the columns map and read here. In
81
+ * `HORIZONTAL` the item leaves `display: contents` for a subgrid over its two tracks, so it is a
82
+ * box that can carry the border and padding while its label stays in the shared label track.
83
+ */
84
+ var DIVIDERS_CSS = "gx-DescriptionList-DIVIDERS_CSS-d19oquh";
85
+ var DESCRIPTION_LIST_DIVIDERS_ORIENTATION_TO_CSS_MAP = {
86
+ [Orientation.HORIZONTAL]: "gx-DescriptionList-Orientation_HORIZONTAL-o108p3ql",
87
+ [Orientation.VERTICAL]: "gx-DescriptionList-Orientation_VERTICAL-o10x7q9y"
88
+ };
89
+ var DESCRIPTION_LIST_DIVIDERS_COLUMNS_TO_CSS_MAP = {
90
+ 1: "gx-DescriptionList-1-_1g4xmgd",
91
+ 2: "gx-DescriptionList-2-_1s7hmki",
92
+ 3: "gx-DescriptionList-3-_1t8g3vn",
93
+ 4: "gx-DescriptionList-4-_1k8wc0x"
94
+ };
95
+ /**
96
+ * `hasBorder`: a hairline frame with `lg` corners; the first and last columns pad 16 from it, every
97
+ * row pads 12 above and below (the outer rows through the edge fallbacks the cells read), and the
98
+ * `hasDividers` blocks draw the lines. Declared after the orientation map so its row pad wins.
99
+ */
100
+ var BORDER_CSS = "gx-DescriptionList-BORDER_CSS-b6xkh13";
71
101
  var DESCRIPTION_LIST_COLUMNS_TO_CSS_MAP = {
72
- 1: "gx-DescriptionList-1-_19oquh",
73
- 2: "gx-DescriptionList-2-_108p3ql",
74
- 3: "gx-DescriptionList-3-_10x7q9y",
75
- 4: "gx-DescriptionList-4-_1g4xmgd"
102
+ 1: "gx-DescriptionList-1-_x7vzfq",
103
+ 2: "gx-DescriptionList-2-_1wbhzfx",
104
+ 3: "gx-DescriptionList-3-_6u7lmt",
105
+ 4: "gx-DescriptionList-4-_10jrxrj"
76
106
  };
77
107
  /** Applied only when some item is copyable: every row is as tall as a row with a copy button. */
78
108
  var DESCRIPTION_LIST_SIZE_TO_ROW_CSS_MAP = {
79
- ["SMALL"]: "gx-DescriptionList-SMALL-s1s7hmki",
80
- ["MEDIUM"]: "gx-DescriptionList-MEDIUM-m1t8g3vn",
81
- ["LARGE"]: "gx-DescriptionList-LARGE-l1k8wc0x"
109
+ ["SMALL"]: "gx-DescriptionList-SMALL-s1xbu6g3",
110
+ ["MEDIUM"]: "gx-DescriptionList-MEDIUM-m1okymsl",
111
+ ["LARGE"]: "gx-DescriptionList-LARGE-l15jcjvy"
82
112
  };
83
113
  var DESCRIPTION_LIST_SIZE_TO_TEXT_SIZE_MAP = {
84
114
  ["SMALL"]: TextSize.BODY_SM,
@@ -99,9 +129,10 @@ var getCopyText = (item) => {
99
129
  return isText(item.value) && !isEmpty(item.value) ? String(item.value) : void 0;
100
130
  };
101
131
  /** Labelled values in a `<dl>`: labels aligned in one column, or above their values. */
102
- var DescriptionList = forwardRef(function DescriptionList({ items, size = "MEDIUM", orientation = Orientation.HORIZONTAL, columns = 1, isValueTrailing = false, id, className, style, ...rest }, ref) {
132
+ var DescriptionList = forwardRef(function DescriptionList({ items, size = "MEDIUM", orientation = Orientation.HORIZONTAL, columns = 1, isValueTrailing = false, hasDividers = false, hasBorder = false, id, className, style, ...rest }, ref) {
103
133
  const textSize = DESCRIPTION_LIST_SIZE_TO_TEXT_SIZE_MAP[size];
104
134
  const hasCopy = items.some((item) => item.isCopyable === true);
135
+ const hasLines = hasDividers || hasBorder;
105
136
  const entries = items.map((item, position) => ({
106
137
  item,
107
138
  position
@@ -110,7 +141,7 @@ var DescriptionList = forwardRef(function DescriptionList({ items, size = "MEDIU
110
141
  ...rest,
111
142
  ref,
112
143
  id,
113
- className: cx(DESCRIPTION_LIST_ORIENTATION_TO_CSS_MAP[orientation], DESCRIPTION_LIST_COLUMNS_TO_CSS_MAP[columns], hasCopy && DESCRIPTION_LIST_SIZE_TO_ROW_CSS_MAP[size], isValueTrailing && VALUE_TRAILING_CSS, className),
144
+ className: cx(DESCRIPTION_LIST_ORIENTATION_TO_CSS_MAP[orientation], DESCRIPTION_LIST_COLUMNS_TO_CSS_MAP[columns], hasCopy && DESCRIPTION_LIST_SIZE_TO_ROW_CSS_MAP[size], isValueTrailing && VALUE_TRAILING_CSS, hasLines && DIVIDERS_CSS, hasLines && DESCRIPTION_LIST_DIVIDERS_ORIENTATION_TO_CSS_MAP[orientation], hasLines && DESCRIPTION_LIST_DIVIDERS_COLUMNS_TO_CSS_MAP[columns], hasBorder && BORDER_CSS, className),
114
145
  style,
115
146
  children: entries.map(({ item, position }) => {
116
147
  const copyText = getCopyText(item);
@@ -17,6 +17,18 @@ interface ModalBaseProps extends AriaAttributes {
17
17
  children?: ReactNode;
18
18
  /** The actions row, end-aligned with an 8px gap. Put the least destructive action first. */
19
19
  footer?: ReactNode;
20
+ /** A strip between the title row and the body that stays in place while the body scrolls: an `Alert isBanner`. */
21
+ banner?: ReactNode;
22
+ /**
23
+ * Hairlines between the title row and the body, and between the body and the footer. For a body that scrolls.
24
+ * @default false
25
+ */
26
+ hasDividers?: boolean;
27
+ /**
28
+ * No body padding: the app owns it (sections of `Box padding={16}` and `Divider`s, a table edge to edge). The title row and footer keep theirs.
29
+ * @default false
30
+ */
31
+ isFlush?: boolean;
20
32
  /**
21
33
  * Panel width: 400 / 560 / 720px, or `X_LARGE` to fill the viewport less 32px on each side.
22
34
  * @default ModalSize.MEDIUM
@@ -3,6 +3,7 @@ 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 Divider from "../layout/Divider.js";
6
7
  import { OverlayScaleFade } from "../overlay/internal/OverlayScaleFade.js";
7
8
  import { OverlayNode } from "../overlay/OverlayProvider.js";
8
9
  import Button, { ButtonSize, ButtonVariant } from "../buttons/Button.js";
@@ -11,7 +12,7 @@ import Fade from "../transform/Fade.js";
11
12
  /* empty css */
12
13
  import { styled } from "@linaria/react";
13
14
  import { forwardRef, useId, useRef, useState } from "react";
14
- import { jsx, jsxs } from "react/jsx-runtime";
15
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
15
16
  import { cx } from "@linaria/core";
16
17
  import { XIcon } from "@phosphor-icons/react";
17
18
  //#region src/modal/Modal.tsx
@@ -23,6 +24,12 @@ import { XIcon } from "@phosphor-icons/react";
23
24
  * and out (`Fade` backdrop, a scale-fade panel whose content leaves first). It has no trigger: the
24
25
  * app opens it.
25
26
  *
27
+ * The regions are plain by default. `hasDividers` draws a hairline between the title row and the
28
+ * body and between the body and the footer, for a body that scrolls. `isFlush` drops the body's
29
+ * padding so the app owns it (sections of `Box padding={16}` and `Divider`s, a table edge to
30
+ * edge). `banner` is a slot between the title row and the body that stays put while the body
31
+ * scrolls, for an `Alert isBanner`.
32
+ *
26
33
  * A short yes/no question is `ConfirmDialog`; a panel beside the page is `Drawer`; an anchored
27
34
  * panel is `Popover`.
28
35
  *
@@ -80,18 +87,26 @@ var Body = /*#__PURE__*/ styled("div")({
80
87
  class: "gx-Modal-Body-b1w4b032",
81
88
  propsAsIs: false
82
89
  });
90
+ /** `isFlush`: the body has no inset; the content pads itself. */
91
+ var FLUSH_BODY_CSS = "gx-Modal-FLUSH_BODY_CSS-fbri8ij";
92
+ /** `banner`: a strip pinned between the title row and the body; it never shrinks or scrolls. */
93
+ var Banner = /*#__PURE__*/ styled("div")({
94
+ name: "Banner",
95
+ class: "gx-Modal-Banner-bxdonbm",
96
+ propsAsIs: false
97
+ });
83
98
  var Footer = /*#__PURE__*/ styled("div")({
84
99
  name: "Footer",
85
- class: "gx-Modal-Footer-fbri8ij",
100
+ class: "gx-Modal-Footer-f1c1q51w",
86
101
  propsAsIs: false
87
102
  });
88
103
  var CloseSlot = /*#__PURE__*/ styled("span")({
89
104
  name: "CloseSlot",
90
- class: "gx-Modal-CloseSlot-cxdonbm",
105
+ class: "gx-Modal-CloseSlot-c1aye683",
91
106
  propsAsIs: false
92
107
  });
93
108
  /** A blocking dialog over a dimmed page: title row, scrolling body, actions row. */
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) {
109
+ var Modal = forwardRef(function Modal({ children, header, subheader, icon, footer, banner, hasDividers = false, isFlush = false, size = "MEDIUM", isDismissable = true, isOpen: isOpenProp, defaultIsOpen = false, onOpenChange, ariaLabel, id, className, style, ...rest }, ref) {
95
110
  const generatedId = useId();
96
111
  const titleId = useId();
97
112
  const subheaderId = useId();
@@ -165,8 +180,13 @@ var Modal = forwardRef(function Modal({ children, header, subheader, icon, foote
165
180
  subheaderId,
166
181
  hasClose: isDismissable
167
182
  }),
168
- /* @__PURE__ */ jsx(Body, { children: typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Text, { children }) : children }),
169
- footer !== void 0 && /* @__PURE__ */ jsx(Footer, { children: footer }),
183
+ hasDividers && hasHeaderRow && /* @__PURE__ */ jsx(Divider, {}),
184
+ banner !== void 0 && /* @__PURE__ */ jsx(Banner, { children: banner }),
185
+ /* @__PURE__ */ jsx(Body, {
186
+ className: cx(isFlush && FLUSH_BODY_CSS),
187
+ children: typeof children === "string" || typeof children === "number" ? /* @__PURE__ */ jsx(Text, { children }) : children
188
+ }),
189
+ footer !== void 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [hasDividers && /* @__PURE__ */ jsx(Divider, {}), /* @__PURE__ */ jsx(Footer, { children: footer })] }),
170
190
  isDismissable && /* @__PURE__ */ jsx(CloseSlot, { children: /* @__PURE__ */ jsx(Button, {
171
191
  variant: ButtonVariant.TERTIARY,
172
192
  size: ButtonSize.SMALL,
@@ -1,6 +1,6 @@
1
1
  import { type Icon as PhosphorIcon } from "@phosphor-icons/react";
2
2
  import { type AriaAttributes, type CSSProperties, type ElementType, type ReactElement, type ReactNode, type RefAttributes } from "react";
3
- import { Orientation } from "../theme/enums.js";
3
+ import { Orientation, type Space } from "../theme/enums.js";
4
4
  /** Mode (Constitution V-A local mode set: Tabs UNDERLINE / PILL; declared in scripts/lint-dls.ts MODE_SETS). */
5
5
  export declare enum TabsVariant {
6
6
  /** Tabs on a hairline; the selected tab has a `text.primary` indicator on the edge facing the panel. The default. */
@@ -81,6 +81,12 @@ export interface TabsProps<TId extends string = string> extends AriaAttributes {
81
81
  * @default false
82
82
  */
83
83
  fillWidth?: boolean;
84
+ /**
85
+ * Space between the ends of the row (or column) and its first and last tab, `actions` included; the `UNDERLINE` hairline still runs the full length.
86
+ * For a row flush with a panel's edges whose tabs should sit on the page's padding line (`16` in the island), instead of a padded wrapper, which would cut the hairline short.
87
+ * @default 0
88
+ */
89
+ inset?: Space;
84
90
  /** Controls at the end of the tab row (or the bottom of the column), outside the tablist, usually `Button`s one rung smaller. */
85
91
  actions?: ReactNode;
86
92
  /**
@@ -2,6 +2,7 @@ import { useIsomorphicLayoutEffect } from "../hooks/useIsomorphicLayoutEffect.js
2
2
  import { Orientation, Placement } from "../theme/enums.js";
3
3
  import Text, { TextSize, TextVariant, TextWeight } from "../text/Text.js";
4
4
  import { useControlledState } from "../hooks/useControlledState.js";
5
+ import { spaceToCss } from "../utils/css.js";
5
6
  import Icon, { IconVariant } from "../icons/Icon.js";
6
7
  import Chip, { ChipSize, ChipVariant } from "../chips/Chip.js";
7
8
  import Menu, { MenuItem, MenuSize } from "../menu/Menu.js";
@@ -19,7 +20,9 @@ import { CaretDownIcon } from "@phosphor-icons/react";
19
20
  * navigates. Each tab can show an icon and a count `Chip`. `UNDERLINE` is the page-level mode (a
20
21
  * hairline with a `text.primary` indicator under the selected tab); `PILL` is a quieter second level (the
21
22
  * selected tab is a filled pill). Horizontal tabs that do not fit collapse into a "More" menu at
22
- * the end of the row, so the row never wraps or scrolls.
23
+ * the end of the row, so the row never wraps or scrolls. `inset` pads the row (or column) from its
24
+ * ends while the `UNDERLINE` hairline keeps its full length: a row flush with a panel's edges, its
25
+ * tabs on the page's padding line.
23
26
  *
24
27
  * Height: a horizontal row takes the height it is given. Alone it is its rung tall; stretched by
25
28
  * a taller flex row (the default `align-items: stretch`, e.g. a 48px app header) it fills that
@@ -58,7 +61,7 @@ var TabsSize = /* @__PURE__ */ function(TabsSize) {
58
61
  }({});
59
62
  /** Sub-pixel slack when comparing measured widths. */
60
63
  var FIT_EPSILON_PX = .5;
61
- /** The root block: bar, then panel (a horizontal bar with no panel fills the root's height). */
64
+ /** The root block: bar, then panel (a horizontal bar with no panel fills the root's height). `--gx-tabs-inset` is the `inset` prop. */
62
65
  var Root = /*#__PURE__*/ styled("div")({
63
66
  name: "Root",
64
67
  class: "gx-Tabs-r1fvk33l",
@@ -114,6 +117,7 @@ var TABS_ORIENTATION_TO_CSS_MAP = {
114
117
  var FILL_WIDTH_CSS = "gx-Tabs-FILL_WIDTH_CSS-fs6iug3";
115
118
  /** Vertical `fillWidth`: a column with no panel beside it spans its parent, so the hairline sits on the parent's edge. */
116
119
  var VERTICAL_FILL_WIDTH_CSS = "gx-Tabs-VERTICAL_FILL_WIDTH_CSS-vvxftzj";
120
+ /** The bar's `inset` is padding on the bar itself, so the `UNDERLINE` hairline (drawn on the bar's edge) spans it. */
117
121
  var TABS_ORIENTATION_TO_BAR_CSS_MAP = {
118
122
  [Orientation.HORIZONTAL]: "gx-Tabs-Orientation_HORIZONTAL-oduyhd6",
119
123
  [Orientation.VERTICAL]: "gx-Tabs-Orientation_VERTICAL-oux4b2d"
@@ -252,7 +256,7 @@ function fitVisibleIndexes(count, selectedIndex, measurement, available) {
252
256
  }
253
257
  /** Tabs and their panel; the value is the selected tab's id. */
254
258
  function TabsImpl(props, ref) {
255
- const { items, value, defaultValue, onChange, variant = "UNDERLINE", size = "MEDIUM", orientation = Orientation.HORIZONTAL, fillWidth = false, actions, as, ariaLabel, id, className, style, ...rest } = props;
259
+ const { items, value, defaultValue, onChange, variant = "UNDERLINE", size = "MEDIUM", orientation = Orientation.HORIZONTAL, fillWidth = false, inset = 0, actions, as, ariaLabel, id, className, style, ...rest } = props;
256
260
  const list = items;
257
261
  const isLinks = list.some((item) => item.href !== void 0);
258
262
  const isHorizontal = orientation === Orientation.HORIZONTAL;
@@ -281,13 +285,14 @@ function TabsImpl(props, ref) {
281
285
  const [available, setAvailable] = useState(null);
282
286
  const isMeasuring = isOverflowMode && (measurement === null || measurement.signature !== signature);
283
287
  const hasActions = actions !== void 0 && actions !== null && actions !== false;
284
- /** The row's width for tabs and the trigger: the bar minus `actions` and its gap. */
288
+ /** The row's width for tabs and the trigger: the bar minus its `inset` padding, `actions` and its gap. */
285
289
  const readAvailable = useCallback(() => {
286
290
  const bar = barRef.current;
287
291
  if (bar === null) return;
288
- let width = bar.getBoundingClientRect().width;
292
+ const barStyle = getComputedStyle(bar);
293
+ let width = bar.getBoundingClientRect().width - pxOf(barStyle.paddingInlineStart) - pxOf(barStyle.paddingInlineEnd);
289
294
  const actionsElement = actionsRef.current;
290
- if (actionsElement !== null) width -= actionsElement.getBoundingClientRect().width + pxOf(getComputedStyle(bar).columnGap);
295
+ if (actionsElement !== null) width -= actionsElement.getBoundingClientRect().width + pxOf(barStyle.columnGap);
291
296
  setAvailable((current) => current === width ? current : width);
292
297
  }, []);
293
298
  useIsomorphicLayoutEffect(() => {
@@ -472,7 +477,10 @@ function TabsImpl(props, ref) {
472
477
  return /* @__PURE__ */ jsxs(Root, {
473
478
  ref,
474
479
  className: cx(TABS_ORIENTATION_TO_CSS_MAP[orientation], fillWidth && (isHorizontal ? FILL_WIDTH_CSS : VERTICAL_FILL_WIDTH_CSS), className),
475
- style,
480
+ style: inset === 0 ? style : {
481
+ ...style,
482
+ "--gx-tabs-inset": spaceToCss(inset)
483
+ },
476
484
  children: [/* @__PURE__ */ jsxs(Bar, {
477
485
  ref: barRef,
478
486
  className: cx(TABS_SIZE_TO_CSS_MAP[size], TABS_VARIANT_TO_BAR_CSS_MAP[variant][orientation], TABS_ORIENTATION_TO_BAR_CSS_MAP[orientation]),