@antadesign/anta 0.3.30 → 0.3.32

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 (50) hide show
  1. package/dist/anta_helpers.d.ts +4 -0
  2. package/dist/anta_helpers.js +10 -0
  3. package/dist/box-types.d.ts +12 -1
  4. package/dist/bundle.css +1 -1
  5. package/dist/bundle.js +140 -66
  6. package/dist/components/Box.d.ts +18 -6
  7. package/dist/components/Box.js +4 -0
  8. package/dist/components/Expander.d.ts +17 -9
  9. package/dist/components/Expander.js +14 -1
  10. package/dist/components/InputDate.js +2 -0
  11. package/dist/components/MenuItem.d.ts +9 -7
  12. package/dist/components/MenuItem.js +21 -2
  13. package/dist/components/Text.js +2 -3
  14. package/dist/components/Title.d.ts +8 -2
  15. package/dist/components/Title.js +17 -6
  16. package/dist/elements/a-box.css +1 -1
  17. package/dist/elements/a-box.d.ts +2 -0
  18. package/dist/elements/a-box.js +74 -32
  19. package/dist/elements/a-button.css +1 -1
  20. package/dist/elements/a-button.d.ts +5 -2
  21. package/dist/elements/a-calendar.js +3 -1
  22. package/dist/elements/a-copy.js +23 -3
  23. package/dist/elements/a-dialog.js +2 -0
  24. package/dist/elements/a-expander.css +1 -1
  25. package/dist/elements/a-expander.js +118 -39
  26. package/dist/elements/a-input-time.js +2 -1
  27. package/dist/elements/a-menu-item.css +1 -1
  28. package/dist/elements/a-menu.js +43 -3
  29. package/dist/elements/a-panel.css +1 -1
  30. package/dist/elements/a-panel.js +1 -1
  31. package/dist/elements/a-radio-group.js +2 -2
  32. package/dist/elements/a-slider.js +2 -2
  33. package/dist/elements/a-tabs.js +2 -2
  34. package/dist/elements/a-title.css +1 -1
  35. package/dist/elements/a-tooltip.js +1 -1
  36. package/dist/general_types.d.ts +22 -6
  37. package/dist/index.d.ts +1 -1
  38. package/dist/theme-antune.css +1 -1
  39. package/docs/changelog.md +64 -0
  40. package/docs/comparison.md +1 -1
  41. package/docs/components/box.md +81 -24
  42. package/docs/components/button.md +127 -381
  43. package/docs/components/expander.md +88 -47
  44. package/docs/components/menu.md +37 -15
  45. package/docs/components/panel.md +5 -6
  46. package/docs/components/select-faceted.md +25 -11
  47. package/docs/components/title.md +19 -0
  48. package/docs/credits.md +3 -0
  49. package/docs/packages/plot.md +234 -19
  50. package/package.json +2 -2
@@ -14,6 +14,13 @@ export interface BoxProps extends BaseProps {
14
14
  * pixels; a string is any CSS length or two-value gap (`'1rem'`,
15
15
  * `'8px 16px'`). Applies while the Box is a flex or grid container. */
16
16
  gap?: number | string;
17
+ /** CSS selector for descendants whose border boxes are included in
18
+ * `measurement.rects` when Box measures. Coordinates are relative to this
19
+ * Box's top-left border edge; matches are in document order. Box reads and
20
+ * compares matching rects for each measurement event. With `observe="scroll"`,
21
+ * use `throttle` when the selector matches many descendants; it also delays
22
+ * CSS state updates after the initial read. */
23
+ includeRectsFor?: string;
17
24
  /** Inner spacing, matching CSS `padding`. Numbers are pixels; strings accept
18
25
  * CSS shorthand, percentages, and custom properties. Omission adds no style. */
19
26
  padding?: number | string;
@@ -31,9 +38,13 @@ export interface BoxProps extends BaseProps {
31
38
  * overflow adds content observation; hidden edges and scroll add scroll reads.
32
39
  * Without handlers or `fade`, omission stays idle. */
33
40
  observe?: BoxObservation | readonly BoxObservation[];
34
- /** Minimum interval between measurement events, in milliseconds. The first
35
- * report has no added delay; a trailing report delivers the latest values.
36
- * Active observers and CSS clipping states are not throttled.
41
+ /** Keeps measurement events and CSS clipping states current outside the
42
+ * viewport. Has no effect unless `observe` or `fade` selects measurement. */
43
+ observeOffscreen?: boolean;
44
+ /** Minimum interval between measurements, in milliseconds. The initial
45
+ * measurement runs immediately; a trailing read updates CSS states and
46
+ * delivers the latest event. Descendant rects are read only for emitted
47
+ * events. The `measurement` getter always reads fresh values.
37
48
  * @defaultValue 0 */
38
49
  throttle?: number;
39
50
  /** Fades out every edge that currently hides clipped content, and drops the
@@ -43,8 +54,9 @@ export interface BoxProps extends BaseProps {
43
54
  * length.
44
55
  * @defaultValue 24 */
45
56
  fadeSize?: number | string;
46
- /** Fired when a selected measurement field changes. `detail` contains all
47
- * fields changed since the last event and a full current snapshot. */
57
+ /** Fired when a selected measurement field changes. `detail.changed` contains
58
+ * fields changed since the previous event; `detail.current` includes the full
59
+ * snapshot with matching rects. Rect changes alone do not trigger an event. */
48
60
  onMeasureChange?: (event: CustomEvent<BoxMeasurementChange>, detail: BoxMeasurementChange) => void;
49
61
  /** Fired after Box's browser and local rendering context changes. `detail`
50
62
  * contains the changed fields and a full current snapshot. */
@@ -65,4 +77,4 @@ export interface BoxProps extends BaseProps {
65
77
  * </Box>
66
78
  * ```
67
79
  */
68
- export declare const Box: ({ display, round, gap, padding, margin, observe, throttle, fade, fadeSize, onMeasureChange, onContextChange, className, style, children, ...rest }: BoxProps) => any;
80
+ export declare const Box: ({ display, round, gap, includeRectsFor, padding, margin, observe, observeOffscreen, throttle, fade, fadeSize, onMeasureChange, onContextChange, className, style, children, ...rest }: BoxProps) => any;
@@ -18,9 +18,11 @@ const Box = ({
18
18
  display,
19
19
  round,
20
20
  gap,
21
+ includeRectsFor,
21
22
  padding,
22
23
  margin,
23
24
  observe,
25
+ observeOffscreen,
24
26
  throttle,
25
27
  fade,
26
28
  fadeSize,
@@ -42,9 +44,11 @@ const Box = ({
42
44
  display: display === "block" ? void 0 : display,
43
45
  round: roundAttr(round),
44
46
  gap: gap != null ? "" : void 0,
47
+ "include-rects-for": includeRectsFor,
45
48
  padding: padding != null ? "" : void 0,
46
49
  margin: margin != null ? "" : void 0,
47
50
  observe: observeAttr(observe, onMeasureChange, onContextChange),
51
+ "observe-offscreen": observeOffscreen ? "" : void 0,
48
52
  throttle,
49
53
  fade: fade ? "" : void 0,
50
54
  "fade-size": fade && fadeSize != null ? cssLength(fadeSize) : void 0,
@@ -23,16 +23,24 @@ export interface ExpanderProps extends Omit<BaseProps, "title"> {
23
23
  * bare disclosure).
24
24
  * @defaultValue 'secondary' */
25
25
  priority?: "primary" | "secondary" | "tertiary";
26
- /** Outdent the chevron into the left gutter so the title and body sit
27
- * flush with surrounding content (the docs-header layout). Only takes
28
- * effect with `priority="tertiary"` — on the filled priorities the
29
- * container edge has to bound the chevron, so it's a no-op there. */
26
+ /** Align the title and body with surrounding content on a tertiary
27
+ * expander. With the default start indicator, the mark hangs in the
28
+ * gutter; an end indicator stays inside its edge. */
30
29
  outdent?: boolean;
31
- /** Header actions (e.g. buttons, tags) rendered at the end of the
32
- * header row, OUTSIDE the toggle trigger — clicking them never
33
- * toggles, they're separately focusable, and screen readers see them
34
- * as separate controls. */
30
+ /** Header actions rendered outside the toggle trigger. With an end
31
+ * indicator, actions sit between the title and indicator. */
35
32
  actions?: React.ReactNode;
33
+ /** Built-in disclosure mark, a decorative node, or separate closed/open
34
+ * visuals. Other primitive values leave the indicator empty. Custom nodes
35
+ * are passive; use `actions` for controls.
36
+ * @defaultValue 'chevron' */
37
+ indicator?: "chevron" | "triangle" | "plus" | React.ReactNode | {
38
+ closed: React.ReactNode;
39
+ open: React.ReactNode;
40
+ };
41
+ /** Place the disclosure mark before the title or after the actions.
42
+ * @defaultValue 'start' */
43
+ indicatorPlacement?: "start" | "end";
36
44
  /** Disables the header: not clickable or focusable, hover affordance
37
45
  * off, text dimmed. The open state freezes as-is — disabling an open
38
46
  * expander keeps it open. `actions` stay live; disable them
@@ -76,4 +84,4 @@ export interface ExpanderProps extends Omit<BaseProps, "title"> {
76
84
  * giving real controlled semantics (a consumer can reject a toggle). See
77
85
  * STATEFUL-COMPONENTS.md.
78
86
  */
79
- export declare const Expander: ({ title, level, tone, priority, outdent, actions, disabled, round, open, defaultOpen, onStateChange, className, style, children, ...rest }: ExpanderProps) => any;
87
+ export declare const Expander: ({ title, level, tone, priority, outdent, actions, indicator, indicatorPlacement, disabled, round, open, defaultOpen, onStateChange, className, style, children, ...rest }: ExpanderProps) => any;
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
2
  import { nativeStateChange, neutralToneAttr, toneStyle, roundStyle, roundAttr } from "../anta_helpers";
3
3
  const Expander = ({
4
4
  title,
@@ -7,6 +7,8 @@ const Expander = ({
7
7
  priority,
8
8
  outdent,
9
9
  actions,
10
+ indicator,
11
+ indicatorPlacement,
10
12
  disabled,
11
13
  round,
12
14
  open,
@@ -18,6 +20,10 @@ const Expander = ({
18
20
  ...rest
19
21
  }) => {
20
22
  const controlled = open !== void 0;
23
+ const presetIndicator = typeof indicator === "string" && (indicator === "chevron" || indicator === "triangle" || indicator === "plus") ? indicator : void 0;
24
+ const pairedIndicator = indicator != null && typeof indicator === "object" && "closed" in indicator && "open" in indicator ? indicator : void 0;
25
+ const useCustomSlot = indicator != null && !presetIndicator && !pairedIndicator;
26
+ const customIndicator = typeof indicator === "object" ? indicator : null;
21
27
  const computedStyle = roundStyle(round, "--expander-round", toneStyle(tone, "--expander-tone-source", style));
22
28
  const titleNode = typeof title === "object" && title !== null ? /* @__PURE__ */ jsx("span", { slot: "title", style: { display: "contents" }, children: title }) : /* @__PURE__ */ jsx("a-expander-summary", { slot: "title", children: title });
23
29
  return /* @__PURE__ */ jsxs(
@@ -31,6 +37,8 @@ const Expander = ({
31
37
  outdent: outdent ? "" : void 0,
32
38
  round: roundAttr(round),
33
39
  disabled: disabled ? "" : void 0,
40
+ indicator: presetIndicator,
41
+ "indicator-placement": indicatorPlacement && indicatorPlacement !== "start" ? indicatorPlacement : void 0,
34
42
  onstatechange: onStateChange ? (e) => {
35
43
  const { event, detail, isOwn } = nativeStateChange(e);
36
44
  if (isOwn && detail)
@@ -45,6 +53,11 @@ const Expander = ({
45
53
  children: [
46
54
  titleNode,
47
55
  actions != null && /* @__PURE__ */ jsx("span", { slot: "actions", children: actions }),
56
+ pairedIndicator && /* @__PURE__ */ jsxs(Fragment, { children: [
57
+ /* @__PURE__ */ jsx("span", { slot: "indicator", "data-when": "closed", "aria-hidden": "true", inert: true, children: pairedIndicator.closed }),
58
+ /* @__PURE__ */ jsx("span", { slot: "indicator", "data-when": "open", "aria-hidden": "true", inert: true, children: pairedIndicator.open })
59
+ ] }),
60
+ useCustomSlot && /* @__PURE__ */ jsx("span", { slot: "indicator", "aria-hidden": "true", inert: true, children: customIndicator }),
48
61
  /* @__PURE__ */ jsx("a-expander-details", { children })
49
62
  ]
50
63
  }
@@ -1,5 +1,6 @@
1
1
  import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
2
  import { useMemo, useState } from "../jsx-runtime";
3
+ import { isModifiedNavigationKey } from "../anta_helpers";
3
4
  import { Temporal } from "temporal-polyfill";
4
5
  import {
5
6
  parseISODate,
@@ -138,6 +139,7 @@ const InputDate = ({
138
139
  },
139
140
  onChange: (e) => resolve(e.currentTarget.value),
140
141
  onKeyDown: (e) => {
142
+ if (isModifiedNavigationKey(e)) return;
141
143
  if ((e.key === "ArrowDown" || e.key === "ArrowUp") && !disabled) {
142
144
  e.preventDefault();
143
145
  setFocusNonce((n) => n + 1);
@@ -28,19 +28,19 @@ export interface MenuItemCommonProps extends BaseProps {
28
28
  /** Mark the item as selected. On a plain row (no `selectionIndicator`) this is
29
29
  * a persistent background tint, the same resting fill a pressed row shows —
30
30
  * also the way to flag the current page on a link item. On a checkable row
31
- * (`selectionIndicator` set) it instead drives the leading `checkbox` / `radio`
32
- * indicator and the row's `aria-checked`. */
31
+ * (`selectionIndicator` set) it instead drives the indicator and the row's
32
+ * `aria-checked`. */
33
33
  selected?: boolean;
34
- /** Semantic tone — colors the label, icon, and hover/selected tint (and the
35
- * `checkbox`/`radio` indicator, which adopts it). A named tone, or any literal
36
- * CSS color (`'#ff1493'`, `'rebeccapurple'`) for a one-off custom tone whose
34
+ /** Semantic tone — colors the label, icon, hover/selected tint, and selection
35
+ * indicator. A named tone, or any literal CSS color (`'#ff1493'`,
36
+ * `'rebeccapurple'`) for a one-off custom tone whose
37
37
  * hue + chroma are kept while the lightness is pinned to match the brand text.
38
38
  * `critical` is the destructive action; `neutral` (the default) is the standard
39
39
  * gray.
40
40
  * @defaultValue neutral */
41
41
  tone?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
42
42
  /** Apply `tone` to every row state, or only while the row is selected. In
43
- * `selected` scope, an unselected row and its checkbox/radio indicator stay
43
+ * `selected` scope, an unselected row and its indicator stay
44
44
  * neutral.
45
45
  * @defaultValue 'all' */
46
46
  toneScope?: ToneScope;
@@ -103,8 +103,10 @@ export type MenuItemActionMode = {
103
103
  * - `'radio'` → `role="menuitemradio"`, a leading passive `<a-radio>`; tint dropped.
104
104
  * - `'check'` → `role="menuitemradio"`, a trailing check glyph on the selected
105
105
  * row *and* the background tint (the canonical single-select look).
106
+ * - `'switch'` → `role="menuitemcheckbox"`, a trailing passive `<a-switch>`;
107
+ * selecting the row keeps its menu open.
106
108
  * Omit for a plain row (the default). */
107
- selectionIndicator?: 'checkbox' | 'radio' | 'check';
109
+ selectionIndicator?: 'checkbox' | 'radio' | 'check' | 'switch';
108
110
  /** Only meaningful with `selectionIndicator="checkbox"`: render the box in the
109
111
  * mixed state (`aria-checked="mixed"`) — e.g. a "Select all" row when some but
110
112
  * not all of its options are selected. */
@@ -93,12 +93,13 @@ const MenuItem = ({
93
93
  ...rest
94
94
  }) => {
95
95
  const leadingIcon = icon;
96
- const checkable = selectionIndicator === "checkbox" || selectionIndicator === "radio" || selectionIndicator === "check";
97
- const role = selectionIndicator === "checkbox" ? "menuitemcheckbox" : selectionIndicator === "radio" || selectionIndicator === "check" ? "menuitemradio" : "menuitem";
96
+ const checkable = selectionIndicator === "checkbox" || selectionIndicator === "radio" || selectionIndicator === "check" || selectionIndicator === "switch";
97
+ const role = selectionIndicator === "checkbox" || selectionIndicator === "switch" ? "menuitemcheckbox" : selectionIndicator === "radio" || selectionIndicator === "check" ? "menuitemradio" : "menuitem";
98
98
  const ariaChecked = !checkable ? void 0 : selectionIndicator === "checkbox" && indeterminate ? "mixed" : selected ? "true" : "false";
99
99
  const keepTint = selected && (selectionIndicator === void 0 || selectionIndicator === "check");
100
100
  const effectiveTone = toneScope === "selected" && !selected ? void 0 : tone;
101
101
  const toneAttr = neutralToneAttr(effectiveTone);
102
+ const switchBehavior = selectionIndicator === "switch" ? { "data-menu-open": "" } : {};
102
103
  const declaredShortcuts = rest["aria-keyshortcuts"];
103
104
  const keyShortcuts = kbd ? shortcutLabel(kbd) : void 0;
104
105
  const kbdNode = kbd ? /* @__PURE__ */ jsx("kbd", { "aria-hidden": declaredShortcuts ?? keyShortcuts ? "true" : void 0, children: kbd }) : null;
@@ -149,6 +150,7 @@ const MenuItem = ({
149
150
  onmenuselect: onSelect ? (e) => onSelect(e, { value, label }) : void 0,
150
151
  class: className,
151
152
  ...rest,
153
+ ...switchBehavior,
152
154
  children: [
153
155
  indicator != null ? (
154
156
  // `min-height: 1lh` makes the wrapper one text line tall and centers the node
@@ -191,6 +193,23 @@ const MenuItem = ({
191
193
  labelNode,
192
194
  children,
193
195
  kbdNode,
196
+ selectionIndicator === "switch" && indicator == null && /* @__PURE__ */ jsx(
197
+ "a-switch",
198
+ {
199
+ "data-menu-item-switch": "",
200
+ "aria-hidden": "true",
201
+ tabIndex: -1,
202
+ state: selected ? "checked" : "unchecked",
203
+ disabled: disabled ? "" : void 0,
204
+ size: "small",
205
+ tone: toneAttr,
206
+ style: toneStyle(
207
+ effectiveTone,
208
+ "--switch-tone-source",
209
+ toneStyle(effectiveTone, "--switch-off-tone-source")
210
+ )
211
+ }
212
+ ),
194
213
  (() => {
195
214
  let trailing = submenu ? iconTrailing ?? "chevron-right" : iconTrailing;
196
215
  if (!submenu && !iconTrailing && selectionIndicator === "check" && indicator == null)
@@ -1,9 +1,8 @@
1
1
  import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
- import { neutralToneAttr, toneStyle } from "../anta_helpers";
2
+ import { lineClamp, neutralToneAttr, toneStyle } from "../anta_helpers";
3
3
  import { Tooltip } from "./Tooltip";
4
4
  const Text = ({ priority, tone, size, inline, truncate, expandable, collapsible, className, style, children, ...rest }) => {
5
- const n = typeof truncate === "number" ? truncate : truncate ? 1 : null;
6
- const lineCount = n != null && n >= 1 ? n : null;
5
+ const lineCount = lineClamp(truncate);
7
6
  const toneAttr = neutralToneAttr(tone);
8
7
  const computedStyle = toneStyle(
9
8
  toneAttr,
@@ -14,9 +14,15 @@ export interface TitleProps extends BaseProps {
14
14
  * kept while lightness/chroma are pinned per priority in oklch.
15
15
  * @defaultValue neutral */
16
16
  tone?: 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'critical' | (string & {});
17
+ /** Truncate with a trailing ellipsis. `true` (or `1`) clamps to a
18
+ * single line; any integer ≥ 2 clamps to that many lines; `0` or a
19
+ * negative value means no truncation. A clipped JSX `Title` shows its
20
+ * text content in a tooltip by default. Nest a `<Tooltip>` to provide
21
+ * your own tooltip instead. */
22
+ truncate?: boolean | number;
17
23
  }
18
24
  /**
19
- * Block-level heading with level 1-6, priority, and tone.
25
+ * Block-level heading with level 1-6, priority, tone, and optional truncation.
20
26
  *
21
27
  * Renders an `<a-title>` styled tag (no JS, no shadow DOM) with
22
28
  * `role="heading"` and `aria-level={level}` set by this wrapper for
@@ -57,4 +63,4 @@ export interface TitleProps extends BaseProps {
57
63
  * </Title>
58
64
  * ```
59
65
  */
60
- export declare const Title: ({ level, priority, tone, className, style, children, ...rest }: TitleProps) => any;
66
+ export declare const Title: ({ level, priority, tone, truncate, className, style, children, ...rest }: TitleProps) => any;
@@ -1,19 +1,30 @@
1
- import { jsx } from "@antadesign/anta/jsx-runtime";
2
- import { neutralToneAttr, toneStyle } from "../anta_helpers";
3
- const Title = ({ level = 2, priority, tone, className, style, children, ...rest }) => {
1
+ import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
2
+ import { lineClamp, neutralToneAttr, toneStyle } from "../anta_helpers";
3
+ import { Tooltip } from "./Tooltip";
4
+ const Title = ({ level = 2, priority, tone, truncate, className, style, children, ...rest }) => {
5
+ const lineCount = lineClamp(truncate);
4
6
  const toneAttr = neutralToneAttr(tone);
5
- return /* @__PURE__ */ jsx(
7
+ const computedStyle = toneStyle(
8
+ toneAttr,
9
+ "--title-tone-source",
10
+ lineCount != null ? { ...style, ["--line-clamp"]: lineCount } : style
11
+ );
12
+ return /* @__PURE__ */ jsxs(
6
13
  "a-title",
7
14
  {
8
15
  level: String(level),
9
16
  priority,
10
17
  tone: toneAttr,
18
+ truncate: lineCount != null ? String(lineCount) : void 0,
11
19
  role: "heading",
12
20
  "aria-level": level,
13
21
  class: className,
14
- style: toneStyle(toneAttr, "--title-tone-source", style),
22
+ style: computedStyle,
15
23
  ...rest,
16
- children
24
+ children: [
25
+ children,
26
+ lineCount != null && /* @__PURE__ */ jsx(Tooltip, { truncatedOnly: true, "data-anta-text-tooltip": "" })
27
+ ]
17
28
  }
18
29
  );
19
30
  };
@@ -1 +1 @@
1
- @layer base,anta,components,utilities;@layer anta.reset,anta.components,anta.theme;@layer anta.components{a-box{display:block;min-width:0}a-box[display=inline-block]{display:inline-block}a-box[display=flex]{display:flex}a-box[display=inline-flex]{display:inline-flex}a-box[display=grid]{display:grid}a-box[display=inline-grid]{display:inline-grid}a-box[gap]{gap:var(--box-gap, normal);@supports (border-radius: attr(probe type(<length>),1px)){--box-gap: attr(gap type(<length>))}}a-box[padding]{padding:var(--box-padding, 0px);@supports (padding: attr(padding type(*))){--box-padding: attr(padding type(*), 0px)}}a-box[margin]{margin:var(--box-margin, 0px);@supports (margin: attr(margin type(*))){--box-margin: attr(margin type(*), 0px)}}a-box[round]{border-radius:var(--box-round, 999px);@supports (border-radius: attr(probe type(<length>),1px)){--box-round: attr(round type(<length>))}}a-box[fade]{--box-fade-size: 24px;--box-fade-x: linear-gradient(transparent, transparent);--box-fade-y: linear-gradient(transparent, transparent);--box-fade-to-start: to left;--box-fade-to-end: to right}a-box[fade]:is(:state(hidden-start-x),:state(hidden-end-x),:state(hidden-start-y),:state(hidden-end-y)){mask-image:linear-gradient(#000,#000),var(--box-fade-x),var(--box-fade-y);mask-clip:border-box,padding-box,padding-box;mask-origin:border-box,padding-box,padding-box;mask-composite:exclude,add,add;mask-repeat:no-repeat;mask-size:100% 100%}a-box[fade][fade-size]{--box-fade-size: 24px;@supports (border-radius: attr(probe type(<length>),1px)){--box-fade-size: attr(fade-size type(<length>), 24px)}}a-box[fade]:dir(rtl){--box-fade-to-start: to right;--box-fade-to-end: to left}a-box[fade]:state(hidden-end-x){--box-fade-x: linear-gradient(var(--box-fade-to-end), transparent calc(100% - var(--box-fade-size)), black)}a-box[fade]:state(hidden-start-x){--box-fade-x: linear-gradient(var(--box-fade-to-start), transparent calc(100% - var(--box-fade-size)), black)}a-box[fade]:state(hidden-start-x):state(hidden-end-x){--box-fade-x: linear-gradient( to right, black, transparent var(--box-fade-size), transparent calc(100% - var(--box-fade-size)), black )}a-box[fade]:state(hidden-end-y){--box-fade-y: linear-gradient(to bottom, transparent calc(100% - var(--box-fade-size)), black)}a-box[fade]:state(hidden-start-y){--box-fade-y: linear-gradient(to top, transparent calc(100% - var(--box-fade-size)), black)}a-box[fade]:state(hidden-start-y):state(hidden-end-y){--box-fade-y: linear-gradient( to bottom, black, transparent var(--box-fade-size), transparent calc(100% - var(--box-fade-size)), black )}}
1
+ @layer base,anta,components,utilities;@layer anta.reset,anta.components,anta.theme;@property --box-fade-start-x-strength{syntax: "<number>"; inherits: false; initial-value: 0;}@property --box-fade-end-x-strength{syntax: "<number>"; inherits: false; initial-value: 0;}@property --box-fade-start-y-strength{syntax: "<number>"; inherits: false; initial-value: 0;}@property --box-fade-end-y-strength{syntax: "<number>"; inherits: false; initial-value: 0;}@layer anta.components{a-box{display:block;min-width:0;position:relative}a-box[display=inline-block]{display:inline-block}a-box[display=flex]{display:flex}a-box[display=inline-flex]{display:inline-flex}a-box[display=grid]{display:grid}a-box[display=inline-grid]{display:inline-grid}a-box[gap]{gap:var(--box-gap, normal);@supports (border-radius: attr(probe type(<length>),1px)){--box-gap: attr(gap type(<length>))}}a-box[padding]{padding:var(--box-padding, 0px);@supports (padding: attr(padding type(*))){--box-padding: attr(padding type(*), 0px)}}a-box[margin]{margin:var(--box-margin, 0px);@supports (margin: attr(margin type(*))){--box-margin: attr(margin type(*), 0px)}}a-box[round]{border-radius:var(--box-round, 999px);@supports (border-radius: attr(probe type(<length>),1px)){--box-round: attr(round type(<length>))}}a-box[fade]{--box-fade-size: 24px;--box-fade-left-strength: var(--box-fade-start-x-strength);--box-fade-right-strength: var(--box-fade-end-x-strength);transition:--box-fade-start-x-strength .14s ease-out,--box-fade-end-x-strength .14s ease-out,--box-fade-start-y-strength .14s ease-out,--box-fade-end-y-strength .14s ease-out}a-box[fade]:state(fade-mask-active){mask-image:linear-gradient(#000,#000),linear-gradient(to right,rgb(0 0 0 / var(--box-fade-left-strength)),transparent var(--box-fade-size),transparent calc(100% - var(--box-fade-size)),rgb(0 0 0 / var(--box-fade-right-strength))),linear-gradient(to bottom,rgb(0 0 0 / var(--box-fade-start-y-strength)),transparent var(--box-fade-size),transparent calc(100% - var(--box-fade-size)),rgb(0 0 0 / var(--box-fade-end-y-strength)));mask-clip:border-box,padding-box,padding-box;mask-origin:border-box,padding-box,padding-box;mask-composite:exclude,add,add;mask-repeat:no-repeat;mask-size:100% 100%}a-box[fade][fade-size]{--box-fade-size: 24px;@supports (border-radius: attr(probe type(<length>),1px)){--box-fade-size: attr(fade-size type(<length>), 24px)}}a-box[fade]:dir(rtl){--box-fade-left-strength: var(--box-fade-end-x-strength);--box-fade-right-strength: var(--box-fade-start-x-strength)}a-box[fade]:state(hidden-end-x){--box-fade-end-x-strength: 1}a-box[fade]:state(hidden-start-x){--box-fade-start-x-strength: 1}a-box[fade]:state(hidden-end-y){--box-fade-end-y-strength: 1}a-box[fade]:state(hidden-start-y){--box-fade-start-y-strength: 1}@media(prefers-reduced-motion:reduce){a-box[fade]{transition:none}}}
@@ -11,6 +11,8 @@ import './a-box.css';
11
11
  * measurement fields or presets, including `context` and `all`. `fade` keeps
12
12
  * clipping states current without selecting event fields. A box with neither
13
13
  * runs no observers at all, including the shared visibility observer.
14
+ * `observe-offscreen` opts out of the pause: measurement keeps running while
15
+ * the box is off screen, and the box skips the visibility observer entirely.
14
16
  * The `measurement` / `context` / `isTruncated` getters still read on demand.
15
17
  *
16
18
  * Attributes rather than "is a listener attached" on purpose. A listener tally
@@ -1,8 +1,9 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
- import { throttle } from "es-toolkit";
2
+ import { isEqual, throttle } from "es-toolkit";
3
3
  import { boxObservation } from "../box-observation";
4
4
  import "./a-box.css";
5
5
  const stores = /* @__PURE__ */ new WeakMap();
6
+ const FADE_TRANSITION_MS = 140;
6
7
  const MODE_CLASS = /(?:^|\s)(?:dark|light)(?:\s|$)/;
7
8
  function hasModeClass(element) {
8
9
  return element.classList.contains("dark") || element.classList.contains("light");
@@ -200,21 +201,13 @@ function deviceSnapshot(navigator) {
200
201
  mobile: uaData.userAgentData?.mobile ?? /android|mobi|iphone|ipad|ipod/.test(source)
201
202
  };
202
203
  }
203
- function equal(a, b) {
204
- if (a === b) return true;
205
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
206
- const left = a;
207
- const right = b;
208
- const keys = Object.keys(left);
209
- return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]);
210
- }
211
204
  function same(a, b) {
212
- return a !== void 0 && Object.keys(b).every((key) => equal(a[key], b[key]));
205
+ return a !== void 0 && Object.keys(b).every((key) => isEqual(a[key], b[key]));
213
206
  }
214
207
  function changed(previous, current) {
215
208
  if (!previous) return { ...current };
216
209
  return Object.fromEntries(
217
- Object.entries(current).filter(([key, value]) => !equal(previous[key], value))
210
+ Object.entries(current).filter(([key, value]) => !isEqual(previous[key], value))
218
211
  );
219
212
  }
220
213
  function px(value) {
@@ -264,7 +257,7 @@ function rounded(value) {
264
257
  return Math.round(value * 100) / 100;
265
258
  }
266
259
  class ABoxElement extends HTMLElementBase {
267
- static observedAttributes = ["fade", "observe", "throttle"];
260
+ static observedAttributes = ["fade", "observe", "observe-offscreen", "throttle"];
268
261
  #internals = this.attachInternals?.();
269
262
  #store;
270
263
  #resizeObserver;
@@ -276,9 +269,10 @@ class ABoxElement extends HTMLElementBase {
276
269
  #measurementFields = /* @__PURE__ */ new Set();
277
270
  #watchContent = false;
278
271
  #watchScroll = false;
279
- // Filtering and throttling compare against the last event, not the last read.
272
+ // Filtering compares against the last event, not the last read.
280
273
  #reportedMeasurement;
281
274
  #throttledReport;
275
+ #fadeMaskTimer;
282
276
  #context;
283
277
  #measuring = false;
284
278
  #clips = false;
@@ -298,6 +292,7 @@ class ABoxElement extends HTMLElementBase {
298
292
  this.#sync();
299
293
  }
300
294
  disconnectedCallback() {
295
+ this.#clearFadeMask();
301
296
  this.#stopMeasuring();
302
297
  this.#stopReportingContext();
303
298
  this.#store?.unobserveVisibility(this);
@@ -311,8 +306,9 @@ class ABoxElement extends HTMLElementBase {
311
306
  and the later connect would see the flags already set and skip it. */
312
307
  attributeChangedCallback(name, previous, current) {
313
308
  if (previous === current) return;
309
+ if (name === "fade" && current === null) this.#clearFadeMask();
314
310
  if (!this.#store) return;
315
- if (name === "fade" || name === "observe") this.#sync();
311
+ if (name === "fade" || name === "observe" || name === "observe-offscreen") this.#sync();
316
312
  if (name === "throttle") this.#configureThrottle();
317
313
  if (name === "observe" || name === "throttle") this.#queueMeasurement();
318
314
  }
@@ -327,7 +323,7 @@ class ABoxElement extends HTMLElementBase {
327
323
  }
328
324
  /** Allows a nested `Tooltip truncatedOnly` to use Box's clipping decision. */
329
325
  get isTruncated() {
330
- const measurement = this.#readMeasurement();
326
+ const measurement = this.#readMeasurement(false);
331
327
  return measurement.clippedX || measurement.clippedY;
332
328
  }
333
329
  /** Called by the per-window context cache. It is intentionally not a JSX prop. */
@@ -355,11 +351,12 @@ class ABoxElement extends HTMLElementBase {
355
351
  this.#watchContent = this.hasAttribute("fade") || content;
356
352
  this.#watchScroll = this.hasAttribute("fade") || scroll;
357
353
  const wantsMeasurement = this.hasAttribute("fade") || fields.size > 0;
358
- const measure = this.isConnected && wantsMeasurement && (this.#visible ?? this.hasAttribute("fade"));
354
+ const offscreen = this.hasAttribute("observe-offscreen");
355
+ const measure = this.isConnected && wantsMeasurement && (offscreen || (this.#visible ?? this.hasAttribute("fade")));
359
356
  if (measure) this.#startMeasuring();
360
357
  else this.#stopMeasuring();
361
358
  if (this.#measuring) this.#syncMeasurementSources();
362
- if (this.isConnected && wantsMeasurement) this.#store?.observeVisibility(this);
359
+ if (this.isConnected && wantsMeasurement && !offscreen) this.#store?.observeVisibility(this);
363
360
  else this.#store?.unobserveVisibility(this);
364
361
  if (this.isConnected && wantsContext) this.#startReportingContext();
365
362
  else this.#stopReportingContext();
@@ -391,7 +388,7 @@ class ABoxElement extends HTMLElementBase {
391
388
  this.#resizeObserver = new this.view.ResizeObserver(this.#queueMeasurement);
392
389
  this.#resizeObserver.observe(this);
393
390
  this.doc.fonts?.addEventListener("loadingdone", this.#queueMeasurement);
394
- this.#measurement = this.#readMeasurement();
391
+ this.#measurement = this.#readMeasurement(false);
395
392
  this.#setMeasurementStates(this.#measurement);
396
393
  this.#syncChildObservation();
397
394
  this.#initialMeasurement = true;
@@ -488,7 +485,8 @@ class ABoxElement extends HTMLElementBase {
488
485
  this.#frame = this.view.requestAnimationFrame(() => {
489
486
  this.#frame = void 0;
490
487
  if (!this.#measuring) return;
491
- this.#reportMeasurement();
488
+ if (this.#throttledReport) this.#throttledReport();
489
+ else this.#reportMeasurement();
492
490
  });
493
491
  };
494
492
  #queueContext = () => {
@@ -502,7 +500,30 @@ class ABoxElement extends HTMLElementBase {
502
500
  this.#reportContext(initial);
503
501
  });
504
502
  };
505
- #readMeasurement() {
503
+ #readRects(rect) {
504
+ let rects = [];
505
+ const selector = this.getAttribute("include-rects-for");
506
+ if (selector) {
507
+ try {
508
+ const boxRect = rect ?? this.getBoundingClientRect();
509
+ rects = Array.from(this.querySelectorAll(selector), (element) => {
510
+ const target = element.getBoundingClientRect();
511
+ return {
512
+ top: rounded(target.top - boxRect.top),
513
+ right: rounded(target.right - boxRect.left),
514
+ bottom: rounded(target.bottom - boxRect.top),
515
+ left: rounded(target.left - boxRect.left),
516
+ width: rounded(target.width),
517
+ height: rounded(target.height)
518
+ };
519
+ });
520
+ } catch (error) {
521
+ if (!(error instanceof this.view.DOMException) || error.name !== "SyntaxError") throw error;
522
+ }
523
+ }
524
+ return rects;
525
+ }
526
+ #readMeasurement(includeRects = true) {
506
527
  const rect = this.getBoundingClientRect();
507
528
  const clientWidth = this.clientWidth;
508
529
  const clientHeight = this.clientHeight;
@@ -524,6 +545,7 @@ class ABoxElement extends HTMLElementBase {
524
545
  return {
525
546
  width: rounded(rect.width),
526
547
  height: rounded(rect.height),
548
+ rects: includeRects ? this.#readRects(rect) : [],
527
549
  clientWidth,
528
550
  clientHeight,
529
551
  scrollWidth,
@@ -559,7 +581,7 @@ class ABoxElement extends HTMLElementBase {
559
581
  this.#throttledReport = void 0;
560
582
  const interval = Number(this.getAttribute("throttle"));
561
583
  if (this.#measuring && Number.isFinite(interval) && interval > 0) {
562
- this.#throttledReport = throttle(this.#emitMeasurement, Math.min(interval, 2147483647));
584
+ this.#throttledReport = throttle(() => this.#reportMeasurement(), Math.min(interval, 2147483647));
563
585
  }
564
586
  }
565
587
  #hasMeasurementChange(current) {
@@ -572,27 +594,22 @@ class ABoxElement extends HTMLElementBase {
572
594
  return false;
573
595
  }
574
596
  #reportMeasurement() {
575
- const current = this.#readMeasurement();
597
+ const current = this.#readMeasurement(false);
576
598
  this.#measurement = current;
577
599
  this.#setMeasurementStates(current);
578
600
  this.#syncChildObservation();
579
601
  if (!this.#hasMeasurementChange(current)) return;
580
- if (this.#throttledReport) this.#throttledReport();
581
- else this.#emitMeasurement();
582
- }
583
- #emitMeasurement = () => {
584
- const current = this.#measurement;
585
- if (!this.#measuring || !current || !this.#hasMeasurementChange(current)) return;
602
+ const snapshot = { ...current, rects: this.#readRects() };
586
603
  const initial = this.#initialMeasurement;
587
604
  const previous = this.#reportedMeasurement;
588
605
  this.#initialMeasurement = false;
589
- this.#reportedMeasurement = current;
606
+ this.#reportedMeasurement = snapshot;
590
607
  const detail = {
591
- changed: initial ? { ...current } : changed(previous, current),
592
- current
608
+ changed: changed(initial ? void 0 : previous, snapshot),
609
+ current: snapshot
593
610
  };
594
611
  this.dispatchEvent(new this.view.CustomEvent("measurechange", { detail }));
595
- };
612
+ }
596
613
  #reportContext(initial) {
597
614
  const current = this.#readContext();
598
615
  const previous = this.#context;
@@ -620,6 +637,31 @@ class ABoxElement extends HTMLElementBase {
620
637
  this.#setState("hidden-end-x", measurement.hiddenEndX);
621
638
  this.#setState("hidden-start-y", measurement.hiddenStartY);
622
639
  this.#setState("hidden-end-y", measurement.hiddenEndY);
640
+ this.#syncFadeMask(measurement);
641
+ }
642
+ #syncFadeMask(measurement) {
643
+ const hidden = measurement.hiddenStartX || measurement.hiddenEndX || measurement.hiddenStartY || measurement.hiddenEndY;
644
+ if (hidden && this.hasAttribute("fade")) {
645
+ if (this.#fadeMaskTimer !== void 0) this.view.clearTimeout(this.#fadeMaskTimer);
646
+ this.#fadeMaskTimer = void 0;
647
+ this.#setState("fade-mask-active", true);
648
+ return;
649
+ }
650
+ if (!this.hasAttribute("fade")) {
651
+ this.#clearFadeMask();
652
+ return;
653
+ }
654
+ if (!this.#internals?.states?.has("fade-mask-active") || this.#fadeMaskTimer !== void 0) return;
655
+ if (this.view.matchMedia("(prefers-reduced-motion: reduce)").matches) {
656
+ this.#clearFadeMask();
657
+ return;
658
+ }
659
+ this.#fadeMaskTimer = this.view.setTimeout(() => this.#clearFadeMask(), FADE_TRANSITION_MS);
660
+ }
661
+ #clearFadeMask() {
662
+ if (this.#fadeMaskTimer !== void 0) this.view.clearTimeout(this.#fadeMaskTimer);
663
+ this.#fadeMaskTimer = void 0;
664
+ this.#setState("fade-mask-active", false);
623
665
  }
624
666
  #setState(name, active) {
625
667
  if (active) this.#internals?.states?.add(name);