@antadesign/anta 0.3.31 → 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.
@@ -17,15 +17,25 @@ const SHADOW_STYLE = `
17
17
  align-items: stretch;
18
18
  }
19
19
 
20
+ :host([indicator-placement="end"]) .header {
21
+ display: grid;
22
+ grid-template-columns: minmax(0, 1fr) auto var(--_expander-indicator-track);
23
+ }
24
+
20
25
  slot[name="actions"] { display: none; }
21
26
  .header.has-actions slot[name="actions"] {
22
27
  display: flex;
23
28
  align-items: center;
24
- gap: 2px;
29
+ gap: var(--_expander-actions-gap);
25
30
  flex-shrink: 0;
26
- margin-inline-start: 8px;
31
+ margin-inline-start: var(--_expander-actions-start);
27
32
  /* Inset from the right edge (the host has no padding of its own). */
28
- margin-inline-end: 4px;
33
+ margin-inline-end: var(--_expander-actions-end);
34
+ }
35
+ :host([indicator-placement="end"]) .header.has-actions slot[name="actions"] {
36
+ grid-column: 2;
37
+ grid-row: 1;
38
+ z-index: 1;
29
39
  }
30
40
 
31
41
  button {
@@ -34,14 +44,11 @@ const SHADOW_STYLE = `
34
44
  background: none;
35
45
  border: none;
36
46
  margin: 0;
37
- /* The button is the full-bleed header: it spans the host's width (flex: 1)
38
- and height (.header stretch). The title is inset by --expander-gutter
39
- (the chevron lives absolutely inside that gutter, below \u2014 it never pushes
40
- the title), so the title + the body share one inset value; 6px block
41
- gives the header a touch more height. Restyle it edge-to-edge via
42
- ::part(summary). */
47
+ /* The default button fills the header beside its actions. The title and
48
+ body share --expander-gutter, and the indicator hangs inside that gutter.
49
+ End placement switches to the header's subgrid. */
43
50
  position: relative;
44
- padding: 6px 4px 6px var(--expander-gutter);
51
+ padding: var(--_expander-header-padding-block) var(--_expander-header-padding-end) var(--_expander-header-padding-block) var(--expander-gutter);
45
52
  flex: 1;
46
53
  min-width: 0;
47
54
  font: inherit;
@@ -58,49 +65,108 @@ const SHADOW_STYLE = `
58
65
  outline: none;
59
66
  }
60
67
 
68
+ :host([indicator-placement="end"]) button {
69
+ grid-column: 1 / -1;
70
+ grid-row: 1;
71
+ display: grid;
72
+ grid-template-columns: subgrid;
73
+ padding: var(--_expander-header-padding-block) 0;
74
+ }
75
+ :host([indicator-placement="end"]) slot[name="title"] {
76
+ grid-column: 1;
77
+ display: block;
78
+ min-width: 0;
79
+ margin-inline-start: var(--expander-gutter);
80
+ }
81
+
61
82
  button:focus-visible {
62
83
  outline: 1px solid var(--focus-ring);
63
84
  outline-offset: 0px;
64
85
  }
65
86
 
66
- /* The chevron sits absolutely INSIDE the gutter \u2014 out of flow, so it never
67
- pushes the title. Its right edge lands 2px before the gutter (left = gutter
68
- - 16px - 2px), leaving a 2px gap before the title; at the default 24px
69
- gutter the chevron box is [6px, 22px], and on outdent (gutter 0) it
70
- auto-hangs at -18px in the negative gutter. One var drives the title inset,
71
- the body inset, and the chevron position. */
72
- button::before {
73
- content: '';
87
+ .indicator {
74
88
  position: absolute;
75
- left: calc(var(--expander-gutter) - 16px - 2px);
89
+ inset-inline-start: var(--_expander-indicator-start);
76
90
  top: 50%;
77
- width: 16px;
78
- height: 16px;
91
+ width: var(--_expander-indicator-size);
92
+ height: var(--_expander-indicator-size);
93
+ pointer-events: none;
94
+ opacity: 0.6;
95
+ transform: translateY(-50%);
96
+ transition: transform 150ms ease, opacity 150ms ease;
97
+ }
98
+ .indicator slot { display: grid; place-items: center; width: 100%; height: 100%; }
99
+ .indicator slot::slotted(*) { pointer-events: none; }
100
+ .indicator .glyph {
101
+ display: block;
102
+ width: var(--_expander-indicator-size);
103
+ height: var(--_expander-indicator-size);
79
104
  background-color: currentColor;
80
- -webkit-mask-image: var(--_expander-chevron);
81
- mask-image: var(--_expander-chevron);
105
+ -webkit-mask-image: var(--_expander-indicator-mask);
106
+ mask-image: var(--_expander-indicator-mask);
82
107
  -webkit-mask-size: contain;
83
108
  mask-size: contain;
84
109
  -webkit-mask-repeat: no-repeat;
85
110
  mask-repeat: no-repeat;
86
111
  -webkit-mask-position: center;
87
112
  mask-position: center;
88
- opacity: 0.6;
89
- /* translateY centers it; the open state adds rotate (same transform so the
90
- transition animates only the rotation). */
91
- transform: translateY(-50%);
92
- transition: transform 150ms ease, opacity 150ms ease, background-color 150ms ease;
93
113
  }
94
- button:enabled:hover::before { opacity: 1; }
95
- button[aria-expanded="true"]::before { transform: translateY(-50%) rotate(90deg); opacity: 1; }
114
+ :host([indicator="plus"]) .indicator .glyph { position: relative; background: none; mask: none; -webkit-mask: none; }
115
+ :host([indicator="plus"]) .indicator .glyph::before,
116
+ :host([indicator="plus"]) .indicator .glyph::after {
117
+ content: '';
118
+ position: absolute;
119
+ background: currentColor;
120
+ border-radius: calc(var(--_expander-indicator-stroke) / 2);
121
+ }
122
+ :host([indicator="plus"]) .indicator .glyph::before {
123
+ width: calc(var(--_expander-indicator-size) - 2 * var(--_expander-indicator-stroke));
124
+ height: var(--_expander-indicator-stroke);
125
+ left: var(--_expander-indicator-stroke);
126
+ top: calc((var(--_expander-indicator-size) - var(--_expander-indicator-stroke)) / 2);
127
+ }
128
+ :host([indicator="plus"]) .indicator .glyph::after {
129
+ width: var(--_expander-indicator-stroke);
130
+ height: calc(var(--_expander-indicator-size) - 2 * var(--_expander-indicator-stroke));
131
+ left: calc((var(--_expander-indicator-size) - var(--_expander-indicator-stroke)) / 2);
132
+ top: var(--_expander-indicator-stroke);
133
+ transition: transform 150ms ease;
134
+ }
135
+ button:enabled:hover .indicator { opacity: 1; }
136
+ button[aria-expanded="true"] .indicator { transform: translateY(-50%) rotate(90deg); opacity: 1; }
137
+ :host([indicator="plus"]) button[aria-expanded="true"] .indicator,
138
+ button[aria-expanded="true"] .indicator.has-pair { transform: translateY(-50%); }
139
+ :host([indicator="plus"]) button[aria-expanded="true"] .glyph::after { transform: scaleY(0); }
140
+ button[aria-expanded="false"] slot[name="indicator"]::slotted([data-when="open"]),
141
+ button[aria-expanded="true"] slot[name="indicator"]::slotted([data-when="closed"]) { display: none; }
142
+
143
+ :host([indicator-placement="end"]) .indicator {
144
+ position: static;
145
+ grid-column: 3;
146
+ grid-row: 1;
147
+ justify-self: end;
148
+ align-self: center;
149
+ margin-inline-end: var(--_expander-indicator-edge);
150
+ transform: rotate(90deg);
151
+ }
152
+ :host([indicator-placement="end"]) button[aria-expanded="true"] .indicator { transform: rotate(270deg); }
153
+ :host([indicator-placement="end"]) .indicator.has-custom,
154
+ :host([indicator-placement="end"]) .indicator.has-pair { transform: none; }
155
+ :host([indicator-placement="end"][indicator="plus"]) .indicator { transform: none; }
156
+ :host([indicator-placement="end"]) button[aria-expanded="true"] .indicator.has-custom { transform: rotate(180deg); }
157
+ :host([indicator-placement="end"]) button[aria-expanded="true"] .indicator.has-pair,
158
+ :host([indicator-placement="end"][indicator="plus"]) button[aria-expanded="true"] .indicator { transform: none; }
96
159
 
97
160
  button:enabled:hover { color: var(--expander-text-hover); }
98
161
  :host([priority="tertiary"]) button:enabled:hover { --_summary-underline: underline; }
99
162
  button:enabled:active { color: var(--expander-text); }
100
163
  button:disabled { cursor: default; }
101
- button:enabled:not([aria-expanded="true"]):active::before {
164
+ button:enabled:not([aria-expanded="true"]):active .indicator {
102
165
  opacity: 0.6;
103
166
  }
167
+ @media (prefers-reduced-motion: reduce) {
168
+ .indicator, :host([indicator="plus"]) .indicator .glyph::after { transition: none; }
169
+ }
104
170
 
105
171
  .region {
106
172
  display: grid;
@@ -109,11 +175,8 @@ const SHADOW_STYLE = `
109
175
  }
110
176
  .header:has(button[aria-expanded="true"]) + .region { grid-template-rows: 1fr; }
111
177
 
112
- /* Outdent flush correction: the host keeps its 1px (transparent) border for
113
- cross-priority box stability, which insets the content-box by 1px. Pull the
114
- header + region back by that 1px so the title and body land exactly on the
115
- surrounding content (the chevron, absolutely positioned inside the header,
116
- rides along). Logical start so it follows RTL. */
178
+ /* The transparent tertiary border still insets the content by 1px. Pull the
179
+ header and region back so outdent lands flush with surrounding content. */
117
180
  :host([priority="tertiary"][outdent]) .header,
118
181
  :host([priority="tertiary"][outdent]) .region {
119
182
  margin-inline-start: -1px;
@@ -145,8 +208,7 @@ class AExpanderElement extends HTMLElementBase {
145
208
  // including custom header content. Guarded for engines without ResizeObserver.
146
209
  roundObserver;
147
210
  // ElementInternals exposes the open/closed state as a custom state
148
- // (`:state(open)`) for consumer styling — e.g. rotating a chevron that lives
149
- // in `actions`, or matching an open header to a design. It's NOT a host
211
+ // (`:state(open)`) for consumer styling, including the indicator. It's NOT a host
150
212
  // attribute (declarative-DOM safe: no host mutation, no reconciliation
151
213
  // churn), just element-internal state the browser exposes for CSS matching.
152
214
  // Guarded for engines without attachInternals / CustomStateSet (no-op there).
@@ -164,7 +226,24 @@ class AExpanderElement extends HTMLElementBase {
164
226
  const titleSlot = document.createElement("slot");
165
227
  titleSlot.name = "title";
166
228
  this.titleSlot = titleSlot;
167
- this.summary.append(titleSlot);
229
+ const indicator = document.createElement("span");
230
+ indicator.className = "indicator";
231
+ indicator.setAttribute("part", "indicator");
232
+ indicator.setAttribute("aria-hidden", "true");
233
+ indicator.inert = true;
234
+ const indicatorSlot = document.createElement("slot");
235
+ indicatorSlot.name = "indicator";
236
+ const glyph = document.createElement("span");
237
+ glyph.className = "glyph";
238
+ indicatorSlot.append(glyph);
239
+ indicatorSlot.addEventListener("slotchange", () => {
240
+ const assigned = indicatorSlot.assignedElements();
241
+ const hasPair = assigned.some((el) => el.hasAttribute("data-when"));
242
+ indicator.classList.toggle("has-custom", assigned.length > 0 && !hasPair);
243
+ indicator.classList.toggle("has-pair", hasPair);
244
+ });
245
+ indicator.append(indicatorSlot);
246
+ this.summary.append(titleSlot, indicator);
168
247
  this.summary.addEventListener("click", this.onSummaryClick);
169
248
  const header = document.createElement("div");
170
249
  header.className = "header";
@@ -22,9 +22,20 @@ const openStack = [];
22
22
  let docBound = false;
23
23
  let boundDoc = null;
24
24
  let boundView = null;
25
+ let mouseX = null;
26
+ let mouseY = 0;
27
+ function trackMouse(e) {
28
+ mouseX = e.pointerType === "mouse" ? e.clientX : null;
29
+ if (mouseX !== null) mouseY = e.clientY;
30
+ }
31
+ function mouseInside(rect) {
32
+ return mouseX !== null && mouseX >= rect.left && mouseX < rect.right && mouseY >= rect.top && mouseY < rect.bottom;
33
+ }
25
34
  function bindDocListeners(doc, view) {
26
35
  if (docBound) return;
27
36
  doc.addEventListener("pointerdown", onDocPointerDown, true);
37
+ doc.addEventListener("pointermove", trackMouse, { capture: true, passive: true });
38
+ doc.addEventListener("pointerleave", clearMouse);
28
39
  doc.addEventListener("keydown", onDocKeyDown, true);
29
40
  doc.addEventListener("keyup", onDocKeyUp, true);
30
41
  doc.addEventListener("contextmenu", onDocContextMenu, true);
@@ -36,6 +47,8 @@ function bindDocListeners(doc, view) {
36
47
  function unbindDocListeners() {
37
48
  if (!docBound) return;
38
49
  boundDoc?.removeEventListener("pointerdown", onDocPointerDown, true);
50
+ boundDoc?.removeEventListener("pointermove", trackMouse, true);
51
+ boundDoc?.removeEventListener("pointerleave", clearMouse);
39
52
  boundDoc?.removeEventListener("keydown", onDocKeyDown, true);
40
53
  boundDoc?.removeEventListener("keyup", onDocKeyUp, true);
41
54
  boundDoc?.removeEventListener("contextmenu", onDocContextMenu, true);
@@ -43,6 +56,10 @@ function unbindDocListeners() {
43
56
  boundDoc = null;
44
57
  boundView = null;
45
58
  docBound = false;
59
+ mouseX = null;
60
+ }
61
+ function clearMouse() {
62
+ mouseX = null;
46
63
  }
47
64
  function menuIndexForEvent(e, primaryClick = false) {
48
65
  const path = e.composedPath();
@@ -75,6 +92,7 @@ setMenuPresence({
75
92
  contains: nodeHitsMenus
76
93
  });
77
94
  function onDocPointerDown(e) {
95
+ trackMouse(e);
78
96
  if (!openStack.length) return;
79
97
  const index = menuIndexForEvent(e, e.button === 0);
80
98
  openStack[index + 1]?.requestClose(e);
@@ -145,6 +163,7 @@ class AMenuElement extends HTMLElementBase {
145
163
  // Submenu hover-intent timers.
146
164
  openTimer;
147
165
  closeTimer;
166
+ #holdHoverClose = false;
148
167
  // Typeahead state (root navigation).
149
168
  typeBuffer = "";
150
169
  typeTimer;
@@ -307,7 +326,10 @@ class AMenuElement extends HTMLElementBase {
307
326
  this.surface.append(header, scroll, footer);
308
327
  this.surface.addEventListener("pointerenter", (e) => {
309
328
  if (e.pointerType !== "mouse") return;
310
- if (this.isSubmenu) this.cancelCloseTimer();
329
+ if (this.isSubmenu) {
330
+ this.#holdHoverClose = false;
331
+ this.cancelCloseTimer();
332
+ }
311
333
  });
312
334
  this.surface.addEventListener("pointerleave", (e) => {
313
335
  if (e.pointerType !== "mouse") return;
@@ -850,6 +872,7 @@ class AMenuElement extends HTMLElementBase {
850
872
  _doShow(coord, instant = false) {
851
873
  if (this.surface.isConnected && !this._shown) this.surface.showPopover();
852
874
  this._shown = true;
875
+ this.#holdHoverClose = false;
853
876
  this._dismissNotified = false;
854
877
  this.reflectOpen(true);
855
878
  this.hideAnchorTooltip();
@@ -866,9 +889,18 @@ class AMenuElement extends HTMLElementBase {
866
889
  }
867
890
  /** Shadow-only hide. */
868
891
  _doHide() {
892
+ const parent = this.parentElement?.closest("a-menu");
893
+ if (this._shown && parent && mouseX !== null && mouseInside(this.surface.getBoundingClientRect())) {
894
+ for (let ancestor = parent; ancestor; ancestor = ancestor.parentElement?.closest("a-menu")) {
895
+ if (!ancestor.isSubmenu || !ancestor.#isHover || !ancestor._shown || ancestor.surface.matches(":hover")) continue;
896
+ ancestor.#holdHoverClose = true;
897
+ ancestor.cancelCloseTimer();
898
+ }
899
+ }
869
900
  this.#stopAnchorTracking();
870
901
  if (this.surface.isConnected && this._shown) this.surface.hidePopover();
871
902
  this._shown = false;
903
+ this.#holdHoverClose = false;
872
904
  this.reflectOpen(false);
873
905
  this.cancelOpenTimer();
874
906
  this.cancelCloseTimer();
@@ -1242,7 +1274,9 @@ class AMenuElement extends HTMLElementBase {
1242
1274
  let onLeave;
1243
1275
  if (this.#isHover) {
1244
1276
  onEnter = (e) => {
1245
- if (e.pointerType === "mouse") this.scheduleOpen();
1277
+ if (e.pointerType !== "mouse") return;
1278
+ this.#holdHoverClose = false;
1279
+ this.scheduleOpen();
1246
1280
  };
1247
1281
  onLeave = (e) => {
1248
1282
  if (e.pointerType === "mouse") this.scheduleClose();
@@ -1320,9 +1354,12 @@ class AMenuElement extends HTMLElementBase {
1320
1354
  scheduleClose() {
1321
1355
  this.cancelOpenTimer();
1322
1356
  if (!this._shown) return;
1357
+ if (this.#holdHoverClose) return;
1323
1358
  if (this.#hasKeyboardFocusInside) return;
1324
- if (openStack.some((m) => m !== this && (this.contains(m) || m.triggerAnchor != null && this.contains(m.triggerAnchor))))
1359
+ if (openStack.some((m) => m !== this && (this.contains(m) || m.triggerAnchor != null && this.contains(m.triggerAnchor)))) {
1360
+ this.cancelCloseTimer();
1325
1361
  return;
1362
+ }
1326
1363
  this.cancelCloseTimer();
1327
1364
  this.closeTimer = setTimeout(() => {
1328
1365
  this.closeTimer = void 0;
@@ -334,6 +334,10 @@ export interface ABoxAttributes extends BaseAttributes {
334
334
  * typed `attr()` read it from `--box-gap` in the host's inline style
335
335
  * instead, which is what the JSX wrapper always sets. */
336
336
  gap?: boolean | '' | number | string;
337
+ /** Adds matching descendant rects to each measurement snapshot. With
338
+ * `observe="scroll"`, use `throttle` when many descendants match: rects are
339
+ * read for each emitted event, and CSS state updates follow the interval. */
340
+ 'include-rects-for'?: string;
337
341
  /** CSS padding shorthand (`padding="8px 16px"`). Without typed `attr()`
338
342
  * support, set `--box-padding` as well. The JSX wrapper supplies it. */
339
343
  padding?: boolean | '' | number | string;
@@ -348,8 +352,12 @@ export interface ABoxAttributes extends BaseAttributes {
348
352
  * attribute means `all`. Omission runs no observers unless `fade` is set;
349
353
  * a native event listener alone does not enable observation. */
350
354
  observe?: string;
351
- /** Minimum interval between measurement events in milliseconds. Omit or
352
- * pass `0` for frame-based reporting. Invalid or negative values use `0`. */
355
+ /** Keeps measurement observation active outside the viewport when `observe`
356
+ * or `fade` selects measurement. */
357
+ 'observe-offscreen'?: boolean | '';
358
+ /** Minimum interval between measurement reads, CSS state updates, and
359
+ * events in milliseconds. The initial read is immediate. Omit or pass `0`
360
+ * for frame-based updates. Invalid or negative values use `0`. */
353
361
  throttle?: number | string;
354
362
  /** Depth of that mask, as a length value (`fade-size="2rem"`). Engines
355
363
  * without typed `attr()` read it from `--box-fade-size` in the host's
@@ -423,6 +431,11 @@ export interface ATagAttributes extends BaseAttributes {
423
431
  * use `Expander` from `@antadesign/anta`.
424
432
  */
425
433
  export interface AExpanderAttributes extends BaseAttributes {
434
+ /** Built-in disclosure mark. Custom visuals use `slot="indicator"`.
435
+ * Defaults to `chevron`. */
436
+ indicator?: 'chevron' | 'triangle' | 'plus';
437
+ /** Logical side for the disclosure mark. Defaults to `start`. */
438
+ 'indicator-placement'?: 'start' | 'end';
426
439
  /** Controlled open state (`'open'` / `'closed'`). Present → controlled: the
427
440
  * attribute is the source of truth, clicks only dispatch the cancelable
428
441
  * `statechange` event, and the consumer answers by updating it. Absent →
@@ -434,10 +447,9 @@ export interface AExpanderAttributes extends BaseAttributes {
434
447
  /** Surface emphasis. `secondary` (default) is a subtle fill; `primary`
435
448
  * is a stronger raised fill; `tertiary` is transparent. */
436
449
  priority?: 'primary' | 'secondary' | 'tertiary';
437
- /** Outdent the chevron into the left gutter so the title + body sit
438
- * flush with surrounding content (the docs-header layout). Tertiary
439
- * only — a no-op on the filled priorities, where the container edge
440
- * has to bound the chevron. Presence-based. */
450
+ /** Align the title and body with surrounding content on a tertiary
451
+ * expander. The start indicator hangs in the gutter; an end indicator
452
+ * stays inside its edge. Presence-based. */
441
453
  outdent?: boolean | '';
442
454
  /** Disables the header: not clickable or focusable, hover affordance
443
455
  * off, text dimmed. The open state freezes as-is. Presence-based. */
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export { Panel } from './components/Panel';
34
34
  export type { PanelProps } from './components/Panel';
35
35
  export type { PanelState } from './general_types';
36
36
  export type { CaptureInputCancelReason, CaptureInputDirection, CaptureInputDirections, CaptureInputGeometry, CaptureInputModifier, CapturePan, CapturePanInertia, CapturePanInput, CapturePointerActivationReason, CapturePointerCapture, CapturePointerInput, CapturePointerStart, CapturePointerType, CaptureWheelActivation, CaptureWheelActivationReason, CaptureWheelInput, CaptureWheelSettle, SerializedMouseEvent, SerializedPointerEvent, SerializedWheelEvent, } from './capture-types';
37
- export type { BoxChange, BoxContext, BoxContextChange, BoxDisplay, BoxFont, BoxInset, BoxMeasurement, BoxMeasurementChange, BoxObservation, BoxMode, BoxOS, BoxBrowser, BoxPointer, } from './box-types';
37
+ export type { BoxChange, BoxContext, BoxContextChange, BoxDisplay, BoxFont, BoxInset, BoxMeasurement, BoxMeasurementChange, BoxObservation, BoxRect, BoxMode, BoxOS, BoxBrowser, BoxPointer, } from './box-types';
38
38
  export { Title } from './components/Title';
39
39
  export type { TitleProps } from './components/Title';
40
40
  export { Tag } from './components/Tag';
package/docs/changelog.md CHANGED
@@ -3,6 +3,40 @@
3
3
  This page tracks changes that ship in `@antadesign/anta`. Documentation-only
4
4
  changes are not listed.
5
5
 
6
+ ## 0.3.32 — September 25, 2026
7
+
8
+ ### Added
9
+
10
+ - [Expander indicators](/expander/#indicator) can sit after header actions and
11
+ use built-in chevron, triangle, or plus visuals. Custom indicators can rotate
12
+ as one node or switch between closed and open nodes.
13
+ - [Box measurements](/box/#measurements) can include
14
+ descendant rects with `includeRectsFor`, including positions relative to the
15
+ Box, width, and height. Changed rects appear in `detail.changed.rects` when
16
+ another observed field triggers a report.
17
+ - [Box measurements](/box/#measurements) can stay active outside the viewport
18
+ with `observeOffscreen` when a virtualized layout needs updates there.
19
+
20
+ ### Changed
21
+
22
+ - [Box layout](/box/#display) now uses `position: relative` by default, so
23
+ absolutely positioned descendants use the Box as their containing block. Set
24
+ `position: static` on the Box if they should use another positioned ancestor.
25
+ - [Box measurements](/box/#measurements) now apply `throttle` to measurement
26
+ reads and CSS states as well as events. The edge mask eases between states;
27
+ larger intervals delay the fade response during scrolling.
28
+
29
+ ### Fixed
30
+
31
+ - [Expander indicators](/expander/#indicator) leave the mark empty for
32
+ unrecognized strings and numbers instead of displaying them as text.
33
+ - [Menu submenus](/menu/#submenus) stay open when a nested
34
+ date picker closes under the pointer outside the submenu. Hover dismissal
35
+ resumes after the pointer returns. Closing a nested popup away from the
36
+ pointer leaves hover dismissal available.
37
+ - [Dialog headers](/dialog/#zones) wrap long unbroken text
38
+ and clip content that still overflows their bounds.
39
+
6
40
  ## 0.3.31 — September 18, 2026
7
41
 
8
42
  ### Added