@antadesign/anta 0.2.1 → 0.3.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.
Files changed (85) hide show
  1. package/README.md +14 -0
  2. package/dist/anta_helpers.d.ts +39 -1
  3. package/dist/anta_helpers.js +30 -2
  4. package/dist/components/Button.d.ts +7 -4
  5. package/dist/components/Button.js +12 -14
  6. package/dist/components/Checkbox.d.ts +97 -0
  7. package/dist/components/Checkbox.js +77 -0
  8. package/dist/components/Expander.d.ts +74 -0
  9. package/dist/components/Expander.js +53 -0
  10. package/dist/components/Input.d.ts +159 -0
  11. package/dist/components/Input.js +150 -0
  12. package/dist/components/Menu.d.ts +70 -0
  13. package/dist/components/Menu.js +42 -0
  14. package/dist/components/MenuGroup.d.ts +24 -0
  15. package/dist/components/MenuGroup.js +19 -0
  16. package/dist/components/MenuItem.d.ts +50 -0
  17. package/dist/components/MenuItem.js +41 -0
  18. package/dist/components/MenuSeparator.d.ts +14 -0
  19. package/dist/components/MenuSeparator.js +7 -0
  20. package/dist/components/Progress.d.ts +12 -6
  21. package/dist/components/Progress.js +7 -4
  22. package/dist/components/Radio.d.ts +37 -0
  23. package/dist/components/Radio.js +33 -0
  24. package/dist/components/RadioGroup.d.ts +119 -0
  25. package/dist/components/RadioGroup.js +108 -0
  26. package/dist/components/Tag.d.ts +38 -5
  27. package/dist/components/Tag.js +9 -5
  28. package/dist/components/Text.d.ts +27 -12
  29. package/dist/components/Text.js +6 -3
  30. package/dist/components/Title.d.ts +10 -1
  31. package/dist/components/Tooltip.d.ts +19 -9
  32. package/dist/components/Tooltip.js +2 -2
  33. package/dist/elements/a-button.css +1 -1
  34. package/dist/elements/a-button.d.ts +56 -0
  35. package/dist/elements/a-button.js +13 -11
  36. package/dist/elements/a-checkbox.css +1 -0
  37. package/dist/elements/a-checkbox.d.ts +52 -0
  38. package/dist/elements/a-checkbox.js +130 -0
  39. package/dist/elements/a-expander.css +1 -0
  40. package/dist/elements/a-expander.d.ts +28 -0
  41. package/dist/elements/a-expander.js +237 -0
  42. package/dist/elements/a-icon.d.ts +14 -0
  43. package/dist/elements/a-icon.shapes.css +1 -1
  44. package/dist/elements/a-icon.shapes.d.ts +10 -2
  45. package/dist/elements/a-icon.shapes.js +12 -3
  46. package/dist/elements/a-input.css +1 -0
  47. package/dist/elements/a-input.d.ts +68 -0
  48. package/dist/elements/a-input.js +511 -0
  49. package/dist/elements/a-menu-group.css +1 -0
  50. package/dist/elements/a-menu-group.d.ts +13 -0
  51. package/dist/elements/a-menu-group.js +15 -0
  52. package/dist/elements/a-menu-item.css +1 -0
  53. package/dist/elements/a-menu-item.d.ts +47 -0
  54. package/dist/elements/a-menu-item.js +30 -0
  55. package/dist/elements/a-menu-separator.css +1 -0
  56. package/dist/elements/a-menu-separator.d.ts +13 -0
  57. package/dist/elements/a-menu-separator.js +15 -0
  58. package/dist/elements/a-menu.css +1 -0
  59. package/dist/elements/a-menu.d.ts +171 -0
  60. package/dist/elements/a-menu.js +714 -0
  61. package/dist/elements/a-progress.css +1 -1
  62. package/dist/elements/a-progress.d.ts +12 -0
  63. package/dist/elements/a-progress.js +1 -0
  64. package/dist/elements/a-radio-group.css +1 -0
  65. package/dist/elements/a-radio-group.d.ts +33 -0
  66. package/dist/elements/a-radio-group.js +160 -0
  67. package/dist/elements/a-radio.css +1 -0
  68. package/dist/elements/a-radio.d.ts +14 -0
  69. package/dist/elements/a-radio.js +46 -0
  70. package/dist/elements/a-tag.css +1 -1
  71. package/dist/elements/a-text.css +1 -1
  72. package/dist/elements/a-text.d.ts +42 -3
  73. package/dist/elements/a-text.js +73 -33
  74. package/dist/elements/a-title.css +1 -1
  75. package/dist/elements/a-tooltip.d.ts +65 -15
  76. package/dist/elements/a-tooltip.js +155 -115
  77. package/dist/elements/index.d.ts +9 -0
  78. package/dist/elements/index.js +27 -0
  79. package/dist/general_types.d.ts +471 -19
  80. package/dist/index.d.ts +16 -0
  81. package/dist/index.js +16 -0
  82. package/dist/jsx-runtime.d.ts +42 -7
  83. package/dist/jsx-runtime.js +14 -2
  84. package/dist/tokens.css +1 -1
  85. package/package.json +1 -1
@@ -1,9 +1,42 @@
1
1
  import { HTMLElementBase } from '../anta_helpers';
2
2
  import './a-tooltip.css';
3
+ /**
4
+ * `<a-tooltip>` — floating bubble placed as a child of the element it
5
+ * describes.
6
+ *
7
+ * Styling notes (`a-tooltip.css` ships comment-free):
8
+ * - `a-tooltip:not(:defined)` is hidden — before upgrade the host is an
9
+ * unknown inline element and its content would flash *inside the anchor*.
10
+ * Once defined, the shadow `:host { display: contents }` governs and
11
+ * content renders only in the popover via the slot.
12
+ * - Only the bubble "chrome" is tokenized (`--tooltip-*`): it lives inside
13
+ * the shadow popover, unreachable from plain consumer CSS; the custom
14
+ * properties inherit across the shadow boundary. The content is slotted
15
+ * light DOM — its font/color/size are already styleable from the page and
16
+ * are intentionally NOT tokens.
17
+ * - The frost: `--tooltip-bg` is a mostly-opaque mix of `--bg-1` that, with
18
+ * the container's backdrop blur, reads as a frosted bubble. `--bg-1` is
19
+ * white in light / black in dark, so dark mode flips automatically (the
20
+ * `.dark` override just lowers the alpha and adds an inset white ring so
21
+ * the edge stays crisp on dark content). There's no real border by
22
+ * default — the hairline edge comes from `--tooltip-shadow`; set
23
+ * `--tooltip-border` for an actual one.
24
+ * - On coarse/no-hover pointers, an anchor that owns a tooltip
25
+ * (`:where(:has(> a-tooltip))`, specificity 0 for easy override)
26
+ * suppresses long-press text selection and the iOS callout so
27
+ * press-and-hold cleanly reveals the bubble (see the long-press handling
28
+ * below) instead of selecting text or popping the system menu.
29
+ */
3
30
  export declare class ATooltipElement extends HTMLElementBase {
4
31
  static observedAttributes: string[];
5
32
  /** Shadow-internal popover surface — the only thing we ever mutate. */
6
33
  private container;
34
+ /** Inner bubble inside `.container`. This IS the `<slot>` (styled
35
+ * `display:block`), so it both projects the content and paints all visual
36
+ * chrome — one element instead of a div-wrapping-a-slot. It carries the
37
+ * proximity (distance-from-anchor) opacity, kept separate from the
38
+ * container's enter/exit/cross-fade opacity. */
39
+ private bubble;
7
40
  private anchor;
8
41
  listening: boolean;
9
42
  private shown;
@@ -27,31 +60,48 @@ export declare class ATooltipElement extends HTMLElementBase {
27
60
  * fed the latest cursor event, so it shows at the cursor and so moving back
28
61
  * onto an anchor after the tooltip was dismissed re-arms it. */
29
62
  private makeDebouncedShow;
30
- /** The element's OWN window / document. The class may be defined in a
31
- * different realm than the element lives in (e.g. the docs playground
32
- * renders into an iframe but reuses the parent page's element class), so
33
- * the module-global `window`/`document` can point at the wrong frame.
34
- * Everything viewport- or document-scoped (clamping, scroll/key/move
35
- * listeners) must go through these so it's correct in any frame. */
36
- private get view();
37
- private get doc();
38
63
  private get isInteractive();
39
- /** Pinned under the anchor (no cursor-following). Interactive implies this
40
- * you can't move into a bubble that's chasing the cursor. */
41
- private get isStatic();
42
- /** Pinned to the anchor for any reason: the `static`/`interactive` attrs, or
43
- * a touch long-press (a finger can't track a following bubble). */
64
+ /** Pinned under the anchor (no cursor-following) the DEFAULT. Following the
65
+ * cursor is opt-in via the `follow` attribute. `interactive` (you can't move
66
+ * into a bubble that chases the cursor) and a touch long-press (a finger
67
+ * can't track one) force pinned regardless of `follow`. */
44
68
  private get isPinned();
45
69
  private get prefersTop();
46
70
  private get delay();
71
+ private positionFrame?;
72
+ private pendingPosition?;
73
+ private schedulePosition;
74
+ /** Pick the vertical origin: prefer the `above` candidate when `preferAbove`
75
+ * (auto-flipping to `below` if it'd clip the top), else `below` (flipping to
76
+ * `above` if it'd clip the bottom). Shared by anchor- and cursor-anchored
77
+ * placement — only the candidate origins differ. */
78
+ private flipVertical;
79
+ /** Clamp a candidate (left, top) into the viewport and write it as the
80
+ * container's transform — the single place the transform is set. */
81
+ private place;
47
82
  private positionToTarget;
48
83
  private positionToMouse;
49
84
  private position;
85
+ /** Opacity for the inner bubble from the cursor's distance to the anchor rect
86
+ * (0 inside the rect): 1 within PROX_NEAR, a linear ramp to 0 at PROX_FAR.
87
+ * Cursor-follow only — drives the "fade as you move away" behaviour. */
88
+ private proximityOpacity;
50
89
  show: (e?: MouseEvent) => void;
51
90
  hide: () => void;
91
+ /** Remove every document / window listener this element binds and stop the
92
+ * fade timer. Used by hide() (after the exit fade) and by cleanup()
93
+ * (immediately). Idempotent. */
94
+ private detachGlobals;
95
+ /** Full, immediate teardown: close the popover now (no fade), detach every
96
+ * listener, clear all timers, release the coordinator slot, reset state.
97
+ * Idempotent. Called from disconnectedCallback, and from the orphan safety
98
+ * net (claimOpen / onDocMove) when the element is gone but
99
+ * disconnectedCallback didn't fire. Not `private` — the module coordinator
100
+ * calls it. */
101
+ cleanup(): void;
52
102
  /** While shown, track the cursor even after it has left the anchor (during
53
- * the close delay). Cursor-following onlystatic/interactive bubbles
54
- * stay pinned. */
103
+ * the close delay). Only runs for `follow` tooltips pinned bubbles
104
+ * (the default, `interactive`, touch) stay put. */
55
105
  private onDocMove;
56
106
  /** Hide after CLOSE_DELAY unless something cancels it first (re-enter, or
57
107
  * another tooltip claiming the slot). Lets the bubble bridge the gap to a
@@ -8,6 +8,9 @@ const DEFAULT_DELAY = 250;
8
8
  const FADE_MS = 150;
9
9
  const GRACE_MS = 300;
10
10
  const CLOSE_DELAY = 120;
11
+ const PROX_NEAR = 10;
12
+ const PROX_FAR = 100;
13
+ const PROX_FADE_MS = 60;
11
14
  const ENTER_TOUCH_DELAY = 500;
12
15
  const LEAVE_TOUCH_DELAY = 1500;
13
16
  const TOUCH_SLOP = 10;
@@ -26,7 +29,10 @@ function clearGrace() {
26
29
  }
27
30
  }
28
31
  function claimOpen(el) {
29
- if (currentOpen && currentOpen !== el) currentOpen.hide();
32
+ if (currentOpen && currentOpen !== el) {
33
+ if (currentOpen.isConnected) currentOpen.hide();
34
+ else currentOpen.cleanup();
35
+ }
30
36
  currentOpen = el;
31
37
  clearGrace();
32
38
  }
@@ -58,6 +64,12 @@ class ATooltipElement extends HTMLElementBase {
58
64
  static observedAttributes = ["delay"];
59
65
  /** Shadow-internal popover surface — the only thing we ever mutate. */
60
66
  container;
67
+ /** Inner bubble inside `.container`. This IS the `<slot>` (styled
68
+ * `display:block`), so it both projects the content and paints all visual
69
+ * chrome — one element instead of a div-wrapping-a-slot. It carries the
70
+ * proximity (distance-from-anchor) opacity, kept separate from the
71
+ * container's enter/exit/cross-fade opacity. */
72
+ bubble;
61
73
  anchor = null;
62
74
  listening = false;
63
75
  shown = false;
@@ -81,38 +93,48 @@ class ATooltipElement extends HTMLElementBase {
81
93
 
82
94
  .container {
83
95
  --_dur: ${FADE_MS}ms;
84
-
85
96
  position: fixed;
86
97
  left: 0;
87
98
  top: 0;
88
99
  margin: 0;
100
+ padding: 0;
101
+ border: 0;
102
+ background: transparent;
103
+ color: inherit;
104
+ overflow: visible;
105
+ box-sizing: border-box;
106
+ width: fit-content;
107
+ pointer-events: none;
108
+ opacity: 0;
109
+ transition:
110
+ opacity var(--_dur) ease,
111
+ overlay var(--_dur) ease allow-discrete,
112
+ display var(--_dur) ease allow-discrete;
113
+ }
114
+
115
+ .container:popover-open { opacity: 1; }
116
+
117
+ @starting-style {
118
+ .container:popover-open { opacity: 0; }
119
+ }
120
+
121
+ .bubble {
122
+ display: block;
89
123
  box-sizing: border-box;
90
124
  width: fit-content;
91
125
  max-width: var(--tooltip-max-width, min(calc(100vw - 20px), 80ch));
92
126
  max-height: calc(100vh - 20px);
93
127
  overflow: clip;
94
- pointer-events: none;
95
128
  text-align: left;
96
-
97
129
  background: var(--tooltip-bg, Canvas);
98
130
  color: var(--text-3, CanvasText);
99
131
  box-shadow: var(--tooltip-shadow, 0 1px 8px rgba(0, 0, 0, 0.2));
100
132
  -webkit-backdrop-filter: var(--tooltip-backdrop-filter, blur(8px));
101
133
  backdrop-filter: var(--tooltip-backdrop-filter, blur(8px));
102
- padding: 5px 8px;
134
+ padding: var(--tooltip-padding, 4px 8px);
103
135
  border: var(--tooltip-border, none);
104
136
  border-radius: var(--tooltip-radius, 3px);
105
137
  outline: none;
106
-
107
- /* The bubble establishes its own text baseline so inheritable text
108
- properties from the anchor (e.g. a Button's condensed "wdth" 88
109
- axis, its 0.05ch letter-spacing, an uppercase transform, a custom
110
- font) don't bleed into the slotted content. The content is slotted
111
- light DOM, so it inherits these *from this container* \u2014 making this
112
- the single choke point. Values mirror Anta's body text. Consumers
113
- still customise a single tooltip by styling their own content
114
- element directly (a class on the content overrides what it inherits
115
- \u2014 see the Tooltip docs). */
116
138
  font-family: var(--sans-serif, system-ui, sans-serif);
117
139
  font-size: 14px;
118
140
  font-weight: 400;
@@ -126,34 +148,20 @@ class ATooltipElement extends HTMLElementBase {
126
148
  white-space: break-spaces;
127
149
  word-break: break-word;
128
150
  overflow-wrap: break-word;
129
-
130
- /* Clean enter/exit fade. allow-discrete keeps the bubble rendered
131
- through its fade-out after hidePopover(), and @starting-style
132
- gives every open a from-opacity:0 \u2014 so the first paint at a
133
- not-yet-computed transform is invisible (no flash at a stale
134
- spot), and re-appearing is always clean. */
135
- opacity: 0;
136
- transition:
137
- opacity var(--_dur) ease,
138
- overlay var(--_dur) ease allow-discrete,
139
- display var(--_dur) ease allow-discrete;
140
- }
141
-
142
- .container:popover-open { opacity: 1; }
143
-
144
- @starting-style {
145
- .container:popover-open { opacity: 0; }
151
+ opacity: 1;
152
+ transition: opacity ${PROX_FADE_MS}ms linear;
146
153
  }
147
154
 
148
- /* Interactive tooltips accept pointer events so their content (links,
149
- buttons) is clickable; the default click-through bubble does not. */
150
155
  :host([interactive]) .container { pointer-events: auto; }
151
156
  `;
152
157
  this.container = document.createElement("div");
153
158
  this.container.className = "container";
154
159
  this.container.setAttribute("popover", "manual");
155
160
  this.container.setAttribute("role", "tooltip");
156
- this.container.append(document.createElement("slot"));
161
+ this.bubble = document.createElement("slot");
162
+ this.bubble.className = "bubble";
163
+ this.bubble.setAttribute("part", "bubble");
164
+ this.container.append(this.bubble);
157
165
  this.container.addEventListener("mouseenter", () => {
158
166
  if (this.isInteractive) this.cancelHide();
159
167
  });
@@ -170,17 +178,7 @@ class ATooltipElement extends HTMLElementBase {
170
178
  lazyObserver?.observe(parent);
171
179
  }
172
180
  disconnectedCallback() {
173
- this.hide();
174
- if (this.fadeTimer !== void 0) {
175
- clearTimeout(this.fadeTimer);
176
- this.fadeTimer = void 0;
177
- }
178
- this.fading = false;
179
- if (this.docMoveBound) {
180
- this.doc.removeEventListener("mousemove", this.onDocMove);
181
- this.docMoveBound = false;
182
- }
183
- this.teardownListeners();
181
+ this.cleanup();
184
182
  if (this.anchor) {
185
183
  if (anchorToTooltip.get(this.anchor) === this) {
186
184
  anchorToTooltip.delete(this.anchor);
@@ -200,30 +198,16 @@ class ATooltipElement extends HTMLElementBase {
200
198
  this.debouncedShow?.cancel();
201
199
  this.debouncedShow = debounce((e) => this.show(e), this.delay);
202
200
  }
203
- /** The element's OWN window / document. The class may be defined in a
204
- * different realm than the element lives in (e.g. the docs playground
205
- * renders into an iframe but reuses the parent page's element class), so
206
- * the module-global `window`/`document` can point at the wrong frame.
207
- * Everything viewport- or document-scoped (clamping, scroll/key/move
208
- * listeners) must go through these so it's correct in any frame. */
209
- get view() {
210
- return this.ownerDocument?.defaultView ?? window;
211
- }
212
- get doc() {
213
- return this.ownerDocument ?? document;
214
- }
215
201
  get isInteractive() {
216
202
  return this.hasAttribute("interactive");
217
203
  }
218
- /** Pinned under the anchor (no cursor-following). Interactive implies this
219
- * you can't move into a bubble that's chasing the cursor. */
220
- get isStatic() {
221
- return this.hasAttribute("static") || this.isInteractive;
222
- }
223
- /** Pinned to the anchor for any reason: the `static`/`interactive` attrs, or
224
- * a touch long-press (a finger can't track a following bubble). */
204
+ /** Pinned under the anchor (no cursor-following) the DEFAULT. Following the
205
+ * cursor is opt-in via the `follow` attribute. `interactive` (you can't move
206
+ * into a bubble that chases the cursor) and a touch long-press (a finger
207
+ * can't track one) force pinned regardless of `follow`. */
225
208
  get isPinned() {
226
- return this.isStatic || this.touchOpen;
209
+ if (this.isInteractive || this.touchOpen) return true;
210
+ return !this.hasAttribute("follow");
227
211
  }
228
212
  get prefersTop() {
229
213
  return this.getAttribute("placement") === "top";
@@ -235,55 +219,73 @@ class ATooltipElement extends HTMLElementBase {
235
219
  return Number.isFinite(n) ? n : DEFAULT_DELAY;
236
220
  }
237
221
  // --- positioning (sets only the shadow container's own transform) ---
222
+ // Single pending position frame: a burst of mousemoves within one frame
223
+ // coalesces to ONE layout read + transform write, using the LATEST geometry
224
+ // (earlier samples never matter — only where the cursor ends the frame).
225
+ positionFrame;
226
+ pendingPosition;
227
+ schedulePosition(compute) {
228
+ this.pendingPosition = compute;
229
+ if (this.positionFrame != null) return;
230
+ this.positionFrame = this.view.requestAnimationFrame(() => {
231
+ this.positionFrame = void 0;
232
+ const run = this.pendingPosition;
233
+ this.pendingPosition = void 0;
234
+ run?.();
235
+ });
236
+ }
237
+ /** Pick the vertical origin: prefer the `above` candidate when `preferAbove`
238
+ * (auto-flipping to `below` if it'd clip the top), else `below` (flipping to
239
+ * `above` if it'd clip the bottom). Shared by anchor- and cursor-anchored
240
+ * placement — only the candidate origins differ. */
241
+ flipVertical(above, below, boxHeight, vh, preferAbove) {
242
+ if (preferAbove) return above < MARGIN ? below : above;
243
+ return below + boxHeight > vh ? above : below;
244
+ }
245
+ /** Clamp a candidate (left, top) into the viewport and write it as the
246
+ * container's transform — the single place the transform is set. */
247
+ place(left, top, boxWidth, vw) {
248
+ if (left + boxWidth > vw) left = vw - boxWidth - MARGIN;
249
+ left = Math.max(MARGIN, left);
250
+ top = Math.max(MARGIN, top);
251
+ this.container.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
252
+ }
238
253
  positionToTarget = () => {
239
- requestAnimationFrame(() => {
254
+ this.schedulePosition(() => {
240
255
  if (!this.anchor || !this.shown) return;
241
256
  const a = this.anchor.getBoundingClientRect();
242
257
  const box = this.container.getBoundingClientRect();
243
- const view = this.view;
244
- const vw = view.innerWidth;
245
- const vh = view.innerHeight;
246
- let left = a.left;
247
- if (left + box.width > vw) left = vw - box.width - MARGIN;
248
- left = Math.max(MARGIN, left);
249
- let top;
250
- if (this.prefersTop || this.touchOpen) {
251
- top = a.top - box.height - MARGIN;
252
- if (top < MARGIN) top = a.bottom + MARGIN;
253
- } else {
254
- top = a.bottom + MARGIN;
255
- if (top + box.height > vh) top = a.top - box.height - MARGIN;
256
- }
257
- top = Math.max(MARGIN, top);
258
- this.container.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
258
+ const { innerWidth: vw, innerHeight: vh } = this.view;
259
+ const top = this.flipVertical(a.top - box.height - MARGIN, a.bottom + MARGIN, box.height, vh, this.prefersTop || this.touchOpen);
260
+ this.place(a.left, top, box.width, vw);
259
261
  });
260
262
  };
261
263
  positionToMouse = (e) => {
262
- requestAnimationFrame(() => {
264
+ this.schedulePosition(() => {
263
265
  if (!this.shown && !this.fading) return;
264
266
  const box = this.container.getBoundingClientRect();
265
- const view = this.view;
266
- const vw = view.innerWidth;
267
- const vh = view.innerHeight;
268
- let left = e.clientX - PADDING_X;
269
- if (left + box.width > vw) left = vw - box.width - MARGIN;
270
- left = Math.max(MARGIN, left);
271
- let top;
272
- if (this.prefersTop) {
273
- top = e.clientY - box.height - MARGIN * 2;
274
- if (top < MARGIN) top = e.clientY + CURSOR_SIZE;
275
- } else {
276
- top = e.clientY + CURSOR_SIZE;
277
- if (top + box.height > vh) top = e.clientY - box.height - MARGIN * 2;
278
- }
279
- top = Math.max(MARGIN, top);
280
- this.container.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
267
+ const { innerWidth: vw, innerHeight: vh } = this.view;
268
+ const top = this.flipVertical(e.clientY - box.height - MARGIN * 2, e.clientY + CURSOR_SIZE, box.height, vh, this.prefersTop);
269
+ this.place(e.clientX - PADDING_X, top, box.width, vw);
281
270
  });
282
271
  };
283
272
  position(e) {
284
273
  if (!this.isPinned && e) this.positionToMouse(e);
285
274
  else this.positionToTarget();
286
275
  }
276
+ /** Opacity for the inner bubble from the cursor's distance to the anchor rect
277
+ * (0 inside the rect): 1 within PROX_NEAR, a linear ramp to 0 at PROX_FAR.
278
+ * Cursor-follow only — drives the "fade as you move away" behaviour. */
279
+ proximityOpacity(e) {
280
+ if (!this.anchor) return 1;
281
+ const r = this.anchor.getBoundingClientRect();
282
+ const dx = Math.max(r.left - e.clientX, 0, e.clientX - r.right);
283
+ const dy = Math.max(r.top - e.clientY, 0, e.clientY - r.bottom);
284
+ const dist = Math.hypot(dx, dy);
285
+ if (dist <= PROX_NEAR) return 1;
286
+ if (dist >= PROX_FAR) return 0;
287
+ return 1 - (dist - PROX_NEAR) / (PROX_FAR - PROX_NEAR);
288
+ }
287
289
  // --- show / hide ---
288
290
  show = (e) => {
289
291
  if (!this.anchor) return;
@@ -295,6 +297,7 @@ class ATooltipElement extends HTMLElementBase {
295
297
  if (!this.shown) {
296
298
  this.shown = true;
297
299
  this.fading = false;
300
+ this.bubble.style.opacity = "1";
298
301
  if (this.fadeTimer !== void 0) {
299
302
  clearTimeout(this.fadeTimer);
300
303
  this.fadeTimer = void 0;
@@ -315,28 +318,65 @@ class ATooltipElement extends HTMLElementBase {
315
318
  this.shown = false;
316
319
  this.touchOpen = false;
317
320
  this.container.hidePopover();
318
- this.view.removeEventListener("scroll", this.hide, { capture: true });
319
- this.doc.removeEventListener("keydown", this.onKeyDown, true);
320
321
  this.fading = true;
321
322
  if (this.fadeTimer !== void 0) clearTimeout(this.fadeTimer);
322
- this.fadeTimer = setTimeout(() => {
323
- this.fading = false;
324
- this.fadeTimer = void 0;
325
- if (this.docMoveBound) {
326
- this.doc.removeEventListener("mousemove", this.onDocMove);
327
- this.docMoveBound = false;
328
- }
329
- }, FADE_MS);
323
+ this.fadeTimer = setTimeout(() => this.detachGlobals(), FADE_MS);
330
324
  releaseOpen(this);
331
325
  };
326
+ /** Remove every document / window listener this element binds and stop the
327
+ * fade timer. Used by hide() (after the exit fade) and by cleanup()
328
+ * (immediately). Idempotent. */
329
+ detachGlobals() {
330
+ if (this.fadeTimer !== void 0) {
331
+ clearTimeout(this.fadeTimer);
332
+ this.fadeTimer = void 0;
333
+ }
334
+ this.fading = false;
335
+ this.view.removeEventListener("scroll", this.hide, { capture: true });
336
+ this.doc.removeEventListener("keydown", this.onKeyDown, true);
337
+ if (this.docMoveBound) {
338
+ this.doc.removeEventListener("mousemove", this.onDocMove);
339
+ this.docMoveBound = false;
340
+ }
341
+ }
342
+ /** Full, immediate teardown: close the popover now (no fade), detach every
343
+ * listener, clear all timers, release the coordinator slot, reset state.
344
+ * Idempotent. Called from disconnectedCallback, and from the orphan safety
345
+ * net (claimOpen / onDocMove) when the element is gone but
346
+ * disconnectedCallback didn't fire. Not `private` — the module coordinator
347
+ * calls it. */
348
+ cleanup() {
349
+ this.cancelHide();
350
+ if (this.shown) {
351
+ this.shown = false;
352
+ this.touchOpen = false;
353
+ if (this.container.matches(":popover-open")) this.container.hidePopover();
354
+ }
355
+ this.detachGlobals();
356
+ this.teardownListeners();
357
+ releaseOpen(this);
358
+ this.bubble.style.opacity = "";
359
+ }
332
360
  /** While shown, track the cursor even after it has left the anchor (during
333
- * the close delay). Cursor-following onlystatic/interactive bubbles
334
- * stay pinned. */
361
+ * the close delay). Only runs for `follow` tooltips pinned bubbles
362
+ * (the default, `interactive`, touch) stay put. */
335
363
  onDocMove = (e) => {
336
- if ((this.shown || this.fading) && !this.isPinned) {
337
- this.lastMouse = e;
338
- this.positionToMouse(e);
364
+ if (!this.isConnected) {
365
+ this.cleanup();
366
+ return;
367
+ }
368
+ if (!(this.shown || this.fading) || this.isPinned) return;
369
+ this.lastMouse = e;
370
+ const o = this.proximityOpacity(e);
371
+ if (o === 0) {
372
+ this.bubble.style.transition = "none";
373
+ this.bubble.style.opacity = "0";
374
+ if (this.shown) this.hide();
375
+ return;
339
376
  }
377
+ this.positionToMouse(e);
378
+ this.bubble.style.transition = "";
379
+ this.bubble.style.opacity = String(o);
340
380
  };
341
381
  /** Hide after CLOSE_DELAY unless something cancels it first (re-enter, or
342
382
  * another tooltip claiming the slot). Lets the bubble bridge the gap to a
@@ -17,6 +17,15 @@ export { AProgressElement, register_a_progress } from './a-progress';
17
17
  export { ATextElement, register_a_text } from './a-text';
18
18
  export { AIconElement, register_a_icon } from './a-icon';
19
19
  export { AButtonElement, register_a_button } from './a-button';
20
+ export { ACheckboxElement, register_a_checkbox } from './a-checkbox';
21
+ export { AExpanderElement, register_a_expander } from './a-expander';
20
22
  export { ATooltipElement, register_a_tooltip } from './a-tooltip';
23
+ export { AInputElement, register_a_input } from './a-input';
24
+ export { ARadioElement, register_a_radio } from './a-radio';
25
+ export { ARadioGroupElement, register_a_radio_group } from './a-radio-group';
26
+ export { AMenuElement, register_a_menu } from './a-menu';
27
+ export { AMenuItemElement, register_a_menu_item } from './a-menu-item';
28
+ export { AMenuSeparatorElement, register_a_menu_separator } from './a-menu-separator';
29
+ export { AMenuGroupElement, register_a_menu_group } from './a-menu-group';
21
30
  import './a-title.css';
22
31
  import './a-tag.css';
@@ -2,18 +2,45 @@ import { AProgressElement, register_a_progress } from "./a-progress";
2
2
  import { ATextElement, register_a_text } from "./a-text";
3
3
  import { AIconElement, register_a_icon } from "./a-icon";
4
4
  import { AButtonElement, register_a_button } from "./a-button";
5
+ import { ACheckboxElement, register_a_checkbox } from "./a-checkbox";
6
+ import { AExpanderElement, register_a_expander } from "./a-expander";
5
7
  import { ATooltipElement, register_a_tooltip } from "./a-tooltip";
8
+ import { AInputElement, register_a_input } from "./a-input";
9
+ import { ARadioElement, register_a_radio } from "./a-radio";
10
+ import { ARadioGroupElement, register_a_radio_group } from "./a-radio-group";
11
+ import { AMenuElement, register_a_menu } from "./a-menu";
12
+ import { AMenuItemElement, register_a_menu_item } from "./a-menu-item";
13
+ import { AMenuSeparatorElement, register_a_menu_separator } from "./a-menu-separator";
14
+ import { AMenuGroupElement, register_a_menu_group } from "./a-menu-group";
6
15
  import "./a-title.css";
7
16
  import "./a-tag.css";
8
17
  export {
9
18
  AButtonElement,
19
+ ACheckboxElement,
20
+ AExpanderElement,
10
21
  AIconElement,
22
+ AInputElement,
23
+ AMenuElement,
24
+ AMenuGroupElement,
25
+ AMenuItemElement,
26
+ AMenuSeparatorElement,
11
27
  AProgressElement,
28
+ ARadioElement,
29
+ ARadioGroupElement,
12
30
  ATextElement,
13
31
  ATooltipElement,
14
32
  register_a_button,
33
+ register_a_checkbox,
34
+ register_a_expander,
15
35
  register_a_icon,
36
+ register_a_input,
37
+ register_a_menu,
38
+ register_a_menu_group,
39
+ register_a_menu_item,
40
+ register_a_menu_separator,
16
41
  register_a_progress,
42
+ register_a_radio,
43
+ register_a_radio_group,
17
44
  register_a_text,
18
45
  register_a_tooltip
19
46
  };