@antadesign/anta 0.1.1-dev.7 → 0.1.1-dev.8

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.
package/README.md CHANGED
@@ -36,6 +36,8 @@ Anta exposes four independent imports. Tokens + elements + the JSX layer are the
36
36
 
37
37
  The chain matters: the per-element CSS that ships with `/elements` references variables like `var(--text-1)` and `var(--bg-2)`. Those variables are *only defined* by `tokens.css`. Skip the tokens import and the components render with whatever the surrounding cascade provides — usually nothing styled at all.
38
38
 
39
+ `@antadesign/anta/elements` registers **all** elements — convenient, but it includes every element's code (and deps). To keep your bundle lean, import only the elements you use from their per-element entry points instead: `import '@antadesign/anta/elements/a-tooltip'` registers just `<a-tooltip>` **and loads just its CSS**. Unused elements — and their dependencies, e.g. `lottie-web` (needed only by `<a-sticker-animated>`) — then never enter your bundle. See [Registering elements](#registering-elements).
40
+
39
41
  ### Cascade layers
40
42
 
41
43
  Anta's reset and element CSS live in `@layer anta`. `tokens.css` pre-declares an order of `@layer base, anta, components, utilities;`, which keeps Anta's defaults above any preflight resets (Tailwind's `@layer base`, Normalize, etc.) while letting your own `@layer components` rules and utility-class frameworks like Tailwind (`@layer utilities`) override Anta when you ask them to.
@@ -70,6 +72,15 @@ The JSX wrappers (React components) as `Progress` render custom DOM elements as
70
72
  import '@antadesign/anta/elements' // auto-registers all elements
71
73
  ```
72
74
 
75
+ **Register only what you use.** `/elements` is the all-in-one convenience import. To trim your bundle, import the specific element entry points instead — each registers just that element and loads just its CSS:
76
+
77
+ ```ts
78
+ import '@antadesign/anta/elements/a-tooltip' // only <a-tooltip> + its CSS
79
+ import '@antadesign/anta/elements/a-button' // only <a-button> + its CSS
80
+ ```
81
+
82
+ Both styles are side-effect imports (the act of importing registers the element), and both are idempotent and SSR-safe. The granular form keeps unused elements — and their dependencies, e.g. `lottie-web`, which only `<a-sticker-animated>` pulls in — out of your bundle.
83
+
73
84
  The cleanest pattern is a **static, synchronous import at your app's entry file** — outside any component, outside any hook:
74
85
 
75
86
  ```ts
@@ -0,0 +1,55 @@
1
+ import type { BaseProps } from "../general_types";
2
+ export interface TooltipProps extends BaseProps {
3
+ /** Tooltip content. Renders anything — text, markup, an icon + text.
4
+ * Lives in the light DOM, so it's styleable with your own plain CSS. */
5
+ children?: React.ReactNode;
6
+ /** Show delay in milliseconds after hover / focus. Never use `0` — use
7
+ * ~`50` for a near-instant tooltip (0 has caused issues in practice).
8
+ * @defaultValue 250 */
9
+ delay?: number;
10
+ /** Which side of the anchor the bubble prefers. Auto-flips to the other
11
+ * side when there isn't room.
12
+ * @defaultValue bottom */
13
+ placement?: 'top' | 'bottom';
14
+ /** Pin the bubble under the anchor instead of following the cursor. */
15
+ static?: boolean;
16
+ /** Make the bubble hoverable and clickable — enables pointer events and
17
+ * keeps it open while the cursor is over it, so its content (links,
18
+ * buttons) can be interacted with. Implies `static` (an interactive
19
+ * bubble can't follow the cursor). */
20
+ interactive?: boolean;
21
+ }
22
+ /**
23
+ * Tooltip — a small floating bubble that describes the element it's placed
24
+ * inside. Render `<Tooltip>` as a child of the element it annotates; it
25
+ * doesn't affect that element's layout.
26
+ *
27
+ * Shows on hover (after `delay`) and on keyboard focus; dismisses on mouse
28
+ * leave, blur, Escape, or when the anchor scrolls away. Follows the cursor
29
+ * by default — pass `static` to pin it under the anchor instead.
30
+ *
31
+ * Requires `@antadesign/anta/elements` to be imported (client-side only)
32
+ * to register the underlying custom element.
33
+ *
34
+ * @example Basic usage
35
+ * ```tsx
36
+ * import { Tooltip } from '@antadesign/anta'
37
+ * import '@antadesign/anta/elements'
38
+ *
39
+ * <button>
40
+ * Save
41
+ * <Tooltip>Saves your work</Tooltip>
42
+ * </button>
43
+ * ```
44
+ *
45
+ * @example Pinned above, rich content
46
+ * ```tsx
47
+ * <span style={{ cursor: 'help' }}>
48
+ * Status
49
+ * <Tooltip placement="top" static>
50
+ * <strong>Healthy</strong> — last checked 2m ago
51
+ * </Tooltip>
52
+ * </span>
53
+ * ```
54
+ */
55
+ export declare const Tooltip: ({ delay, placement, static: isStatic, interactive, className, children, ...rest }: TooltipProps) => any;
@@ -0,0 +1,26 @@
1
+ import { jsx } from "@antadesign/anta/jsx-runtime";
2
+ const Tooltip = ({
3
+ delay,
4
+ placement,
5
+ static: isStatic,
6
+ interactive,
7
+ className,
8
+ children,
9
+ ...rest
10
+ }) => {
11
+ return /* @__PURE__ */ jsx(
12
+ "a-tooltip",
13
+ {
14
+ delay: delay != null ? String(delay) : void 0,
15
+ placement: placement === "top" ? "top" : void 0,
16
+ static: isStatic ? "" : void 0,
17
+ interactive: interactive ? "" : void 0,
18
+ class: className,
19
+ ...rest,
20
+ children
21
+ }
22
+ );
23
+ };
24
+ export {
25
+ Tooltip
26
+ };
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-button.css";
2
3
  declare global {
3
4
  interface Document {
4
5
  hasKeyListenerForAButton?: boolean;
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-button.css";
2
3
  class AButtonElement extends HTMLElementBase {
3
4
  connectedCallback() {
4
5
  if (!document.hasKeyListenerForAButton) {
@@ -70,6 +71,7 @@ function register_a_button() {
70
71
  customElements.define("a-button", AButtonElement);
71
72
  }
72
73
  }
74
+ register_a_button();
73
75
  export {
74
76
  AButtonElement,
75
77
  register_a_button
@@ -1,4 +1,6 @@
1
1
  import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-icon.css';
3
+ import './a-icon.shapes.css';
2
4
  /**
3
5
  * `<a-icon shape="…">` — pure declarative icon element.
4
6
  *
@@ -1,4 +1,6 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-icon.css";
3
+ import "./a-icon.shapes.css";
2
4
  class AIconElement extends HTMLElementBase {
3
5
  }
4
6
  function register_a_icon() {
@@ -7,6 +9,7 @@ function register_a_icon() {
7
9
  customElements.define("a-icon", AIconElement);
8
10
  }
9
11
  }
12
+ register_a_icon();
10
13
  export {
11
14
  AIconElement,
12
15
  register_a_icon
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-progress.css';
2
3
  export declare class AProgressElement extends HTMLElementBase {
3
4
  static observedAttributes: string[];
4
5
  indicator: HTMLDivElement;
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-progress.css";
2
3
  class AProgressElement extends HTMLElementBase {
3
4
  static observedAttributes = ["value", "max", "tone"];
4
5
  indicator;
@@ -60,6 +61,7 @@ function register_a_progress() {
60
61
  customElements.define("a-progress", AProgressElement);
61
62
  }
62
63
  }
64
+ register_a_progress();
63
65
  export {
64
66
  AProgressElement,
65
67
  register_a_progress
@@ -1,5 +1,6 @@
1
1
  import { type AnimationItem } from 'lottie-web';
2
2
  import { HTMLElementBase } from '../anta_helpers';
3
+ import './a-sticker-animated.css';
3
4
  /**
4
5
  * `<a-sticker-animated>` — Lottie-driven sticker carrier.
5
6
  *
@@ -1,5 +1,6 @@
1
1
  import lottie from "lottie-web";
2
2
  import { HTMLElementBase } from "../anta_helpers";
3
+ import "./a-sticker-animated.css";
3
4
  class AStickerAnimatedElement extends HTMLElementBase {
4
5
  static observedAttributes = ["animation", "paused"];
5
6
  container;
@@ -73,6 +74,7 @@ function register_a_sticker_animated() {
73
74
  customElements.define("a-sticker-animated", AStickerAnimatedElement);
74
75
  }
75
76
  }
77
+ register_a_sticker_animated();
76
78
  export {
77
79
  AStickerAnimatedElement,
78
80
  register_a_sticker_animated
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-sticker.css';
2
3
  /**
3
4
  * `<a-sticker>` — static sticker carrier.
4
5
  *
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-sticker.css";
2
3
  class AStickerElement extends HTMLElementBase {
3
4
  static observedAttributes = ["svg"];
4
5
  container;
@@ -24,6 +25,7 @@ function register_a_sticker() {
24
25
  customElements.define("a-sticker", AStickerElement);
25
26
  }
26
27
  }
28
+ register_a_sticker();
27
29
  export {
28
30
  AStickerElement,
29
31
  register_a_sticker
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-text.css';
2
3
  export declare class ATextElement extends HTMLElementBase {
3
4
  static observedAttributes: string[];
4
5
  private slotEl;
@@ -1,4 +1,5 @@
1
1
  import { HTMLElementBase } from "../anta_helpers";
2
+ import "./a-text.css";
2
3
  const SHADOW_STYLE = `
3
4
  :host {
4
5
  display: block;
@@ -137,6 +138,7 @@ function register_a_text() {
137
138
  customElements.define("a-text", ATextElement);
138
139
  }
139
140
  }
141
+ register_a_text();
140
142
  export {
141
143
  ATextElement,
142
144
  register_a_text
@@ -0,0 +1,42 @@
1
+ @layer anta {
2
+ /* Before the custom element upgrades on the client, <a-tooltip> is an
3
+ unknown inline element and its content would flash *inside the anchor*.
4
+ Hide it until defined; once defined this stops matching and the shadow
5
+ `:host { display: contents }` governs (content renders only in the
6
+ popover via the slot). */
7
+ a-tooltip:not(:defined) {
8
+ display: none;
9
+ }
10
+
11
+ /* Tooltip exposes only the box "chrome" that lives inside the shadow
12
+ popover and is therefore impossible to reach with plain consumer CSS.
13
+ The bubble's content is slotted light DOM, so its font / colour / size
14
+ are already styleable from the page — those are intentionally NOT
15
+ tokens. These custom properties inherit across the shadow boundary
16
+ into the popover container (see a-tooltip.ts). The host itself draws
17
+ nothing and stays out of layout (`:host { display: contents }` in the
18
+ shadow style). */
19
+ a-tooltip {
20
+ /* Frosted translucent surface (matches the legacy look): a mostly-
21
+ opaque white that, paired with the container's `backdrop-filter:
22
+ blur(8px)`, reads as a frosted bubble over the page. --bg-1 is
23
+ #ffffff in light / #000000 in dark, so this flips to a dark frost
24
+ automatically (alpha is dialed down in the .dark override). */
25
+ --tooltip-bg: color-mix(in oklch, var(--bg-1) 80%, transparent);
26
+ --tooltip-shadow:
27
+ 0 1px 8px color-mix(in oklch, var(--text-1) 15%, transparent),
28
+ 0 0 1px color-mix(in oklch, var(--text-1) 50%, transparent);
29
+ --tooltip-radius: 3px;
30
+ --tooltip-max-width: min(calc(100vw - 20px), 80ch);
31
+ }
32
+
33
+ /* Dark: a slightly more transparent black frost. A solid black cast shadow
34
+ for lift, plus an inset 1px white ring so the bubble's edge stays crisp
35
+ against dark page content. */
36
+ .dark a-tooltip {
37
+ --tooltip-bg: color-mix(in oklch, var(--bg-1) 70%, transparent);
38
+ --tooltip-shadow:
39
+ 0 2px 16px color-mix(in oklch, black 70%, transparent),
40
+ inset 0 0 0 1px color-mix(in oklch, white 25%, transparent);
41
+ }
42
+ }
@@ -0,0 +1,61 @@
1
+ import { HTMLElementBase } from '../anta_helpers';
2
+ import './a-tooltip.css';
3
+ export declare class ATooltipElement extends HTMLElementBase {
4
+ static observedAttributes: string[];
5
+ /** Shadow-internal popover surface — the only thing we ever mutate. */
6
+ private container;
7
+ private anchor;
8
+ listening: boolean;
9
+ private shown;
10
+ private lastMouse?;
11
+ private debouncedShow?;
12
+ private teardown?;
13
+ private closeTimer?;
14
+ private fading;
15
+ private fadeTimer?;
16
+ private docMoveBound;
17
+ constructor();
18
+ connectedCallback(): void;
19
+ disconnectedCallback(): void;
20
+ attributeChangedCallback(name: string): void;
21
+ /** (Re)build the show debounce with the CURRENT `delay` (rebuilt when the
22
+ * `delay` attribute changes). Re-armed on each hover move (trailing), and
23
+ * fed the latest cursor event, so it shows at the cursor and so moving back
24
+ * onto an anchor after the tooltip was dismissed re-arms it. */
25
+ private makeDebouncedShow;
26
+ /** The element's OWN window / document. The class may be defined in a
27
+ * different realm than the element lives in (e.g. the docs playground
28
+ * renders into an iframe but reuses the parent page's element class), so
29
+ * the module-global `window`/`document` can point at the wrong frame.
30
+ * Everything viewport- or document-scoped (clamping, scroll/key/move
31
+ * listeners) must go through these so it's correct in any frame. */
32
+ private get view();
33
+ private get doc();
34
+ private get isInteractive();
35
+ /** Pinned under the anchor (no cursor-following). Interactive implies this —
36
+ * you can't move into a bubble that's chasing the cursor. */
37
+ private get isStatic();
38
+ private get prefersTop();
39
+ private get delay();
40
+ private positionToTarget;
41
+ private positionToMouse;
42
+ private position;
43
+ show: (e?: MouseEvent) => void;
44
+ hide: () => void;
45
+ /** While shown, track the cursor even after it has left the anchor (during
46
+ * the close delay). Cursor-following only — static/interactive bubbles
47
+ * stay pinned. */
48
+ private onDocMove;
49
+ /** Hide after CLOSE_DELAY unless something cancels it first (re-enter, or
50
+ * another tooltip claiming the slot). Lets the bubble bridge the gap to a
51
+ * neighbouring anchor — and survive the trip into an interactive bubble. */
52
+ private scheduleHide;
53
+ private cancelHide;
54
+ private onKeyDown;
55
+ /** Open now if another tooltip is hot (skip delay), else (re)arm the delayed
56
+ * show with the latest cursor event. */
57
+ private trigger;
58
+ setupListeners(): void;
59
+ teardownListeners(): void;
60
+ }
61
+ export declare function register_a_tooltip(): void;
@@ -0,0 +1,411 @@
1
+ import { HTMLElementBase } from "../anta_helpers";
2
+ import { debounce } from "es-toolkit";
3
+ import "./a-tooltip.css";
4
+ const MARGIN = 4;
5
+ const CURSOR_SIZE = 16;
6
+ const PADDING_X = 8;
7
+ const DEFAULT_DELAY = 250;
8
+ const FADE_MS = 150;
9
+ const GRACE_MS = 300;
10
+ const CLOSE_DELAY = 120;
11
+ let currentOpen = null;
12
+ let graceTimer;
13
+ function graceActive() {
14
+ return graceTimer !== void 0;
15
+ }
16
+ function isHot() {
17
+ return currentOpen !== null || graceActive();
18
+ }
19
+ function clearGrace() {
20
+ if (graceTimer !== void 0) {
21
+ clearTimeout(graceTimer);
22
+ graceTimer = void 0;
23
+ }
24
+ }
25
+ function claimOpen(el) {
26
+ if (currentOpen && currentOpen !== el) currentOpen.hide();
27
+ currentOpen = el;
28
+ clearGrace();
29
+ }
30
+ function releaseOpen(el) {
31
+ if (currentOpen !== el) return;
32
+ currentOpen = null;
33
+ clearGrace();
34
+ graceTimer = setTimeout(() => {
35
+ graceTimer = void 0;
36
+ }, GRACE_MS);
37
+ }
38
+ const anchorToTooltip = /* @__PURE__ */ new WeakMap();
39
+ function handleIntersection(entries) {
40
+ for (const entry of entries) {
41
+ const tooltip = anchorToTooltip.get(entry.target);
42
+ if (!tooltip) continue;
43
+ if (!tooltip.listening && entry.isIntersecting) {
44
+ requestAnimationFrame(() => tooltip.setupListeners());
45
+ } else if (tooltip.listening && !entry.isIntersecting) {
46
+ requestAnimationFrame(() => tooltip.teardownListeners());
47
+ }
48
+ }
49
+ }
50
+ const lazyObserver = typeof IntersectionObserver !== "undefined" ? new IntersectionObserver(handleIntersection, { root: null, rootMargin: "0px", threshold: 0 }) : null;
51
+ class ATooltipElement extends HTMLElementBase {
52
+ // Observe `delay` so changing it at runtime — e.g. the docs playground
53
+ // patches the attribute on the live element — rebuilds the show debounce
54
+ // with the new value instead of keeping the one captured at setup.
55
+ static observedAttributes = ["delay"];
56
+ /** Shadow-internal popover surface — the only thing we ever mutate. */
57
+ container;
58
+ anchor = null;
59
+ listening = false;
60
+ shown = false;
61
+ lastMouse;
62
+ debouncedShow;
63
+ teardown;
64
+ closeTimer;
65
+ fading = false;
66
+ fadeTimer;
67
+ docMoveBound = false;
68
+ constructor() {
69
+ super();
70
+ const shadow = this.attachShadow({ mode: "open" });
71
+ const style = document.createElement("style");
72
+ style.textContent = `
73
+ :host { display: contents; }
74
+
75
+ .container {
76
+ --_dur: ${FADE_MS}ms;
77
+
78
+ position: fixed;
79
+ left: 0;
80
+ top: 0;
81
+ margin: 0;
82
+ box-sizing: border-box;
83
+ width: fit-content;
84
+ max-width: var(--tooltip-max-width, min(calc(100vw - 20px), 80ch));
85
+ max-height: calc(100vh - 20px);
86
+ overflow: clip;
87
+ pointer-events: none;
88
+ text-align: left;
89
+
90
+ background: var(--tooltip-bg, Canvas);
91
+ color: var(--text-1, CanvasText);
92
+ box-shadow: var(--tooltip-shadow, 0 1px 8px rgba(0, 0, 0, 0.2));
93
+ backdrop-filter: blur(8px);
94
+ padding: 5px 8px;
95
+ border: none;
96
+ border-radius: var(--tooltip-radius, 3px);
97
+ outline: none;
98
+
99
+ font-size: 14px;
100
+ line-height: 130%;
101
+ font-weight: 400;
102
+ white-space: break-spaces;
103
+ word-break: break-word;
104
+ overflow-wrap: break-word;
105
+
106
+ /* Clean enter/exit fade. allow-discrete keeps the bubble rendered
107
+ through its fade-out after hidePopover(), and @starting-style
108
+ gives every open a from-opacity:0 \u2014 so the first paint at a
109
+ not-yet-computed transform is invisible (no flash at a stale
110
+ spot), and re-appearing is always clean. */
111
+ opacity: 0;
112
+ transition:
113
+ opacity var(--_dur) ease,
114
+ overlay var(--_dur) ease allow-discrete,
115
+ display var(--_dur) ease allow-discrete;
116
+ }
117
+
118
+ .container:popover-open { opacity: 1; }
119
+
120
+ @starting-style {
121
+ .container:popover-open { opacity: 0; }
122
+ }
123
+
124
+ /* Interactive tooltips accept pointer events so their content (links,
125
+ buttons) is clickable; the default click-through bubble does not. */
126
+ :host([interactive]) .container { pointer-events: auto; }
127
+ `;
128
+ this.container = document.createElement("div");
129
+ this.container.className = "container";
130
+ this.container.setAttribute("popover", "manual");
131
+ this.container.setAttribute("role", "tooltip");
132
+ this.container.append(document.createElement("slot"));
133
+ this.container.addEventListener("mouseenter", () => {
134
+ if (this.isInteractive) this.cancelHide();
135
+ });
136
+ this.container.addEventListener("mouseleave", () => {
137
+ if (this.isInteractive && this.shown) this.scheduleHide();
138
+ });
139
+ shadow.append(style, this.container);
140
+ }
141
+ connectedCallback() {
142
+ const parent = this.parentElement;
143
+ if (!parent) return;
144
+ this.anchor = parent;
145
+ anchorToTooltip.set(parent, this);
146
+ lazyObserver?.observe(parent);
147
+ }
148
+ disconnectedCallback() {
149
+ this.hide();
150
+ if (this.fadeTimer !== void 0) {
151
+ clearTimeout(this.fadeTimer);
152
+ this.fadeTimer = void 0;
153
+ }
154
+ this.fading = false;
155
+ if (this.docMoveBound) {
156
+ this.doc.removeEventListener("mousemove", this.onDocMove);
157
+ this.docMoveBound = false;
158
+ }
159
+ this.teardownListeners();
160
+ if (this.anchor) {
161
+ if (anchorToTooltip.get(this.anchor) === this) {
162
+ anchorToTooltip.delete(this.anchor);
163
+ lazyObserver?.unobserve(this.anchor);
164
+ }
165
+ this.anchor = null;
166
+ }
167
+ }
168
+ attributeChangedCallback(name) {
169
+ if (name === "delay" && this.listening) this.makeDebouncedShow();
170
+ }
171
+ /** (Re)build the show debounce with the CURRENT `delay` (rebuilt when the
172
+ * `delay` attribute changes). Re-armed on each hover move (trailing), and
173
+ * fed the latest cursor event, so it shows at the cursor and so moving back
174
+ * onto an anchor after the tooltip was dismissed re-arms it. */
175
+ makeDebouncedShow() {
176
+ this.debouncedShow?.cancel();
177
+ this.debouncedShow = debounce((e) => this.show(e), this.delay);
178
+ }
179
+ /** The element's OWN window / document. The class may be defined in a
180
+ * different realm than the element lives in (e.g. the docs playground
181
+ * renders into an iframe but reuses the parent page's element class), so
182
+ * the module-global `window`/`document` can point at the wrong frame.
183
+ * Everything viewport- or document-scoped (clamping, scroll/key/move
184
+ * listeners) must go through these so it's correct in any frame. */
185
+ get view() {
186
+ return this.ownerDocument?.defaultView ?? window;
187
+ }
188
+ get doc() {
189
+ return this.ownerDocument ?? document;
190
+ }
191
+ get isInteractive() {
192
+ return this.hasAttribute("interactive");
193
+ }
194
+ /** Pinned under the anchor (no cursor-following). Interactive implies this —
195
+ * you can't move into a bubble that's chasing the cursor. */
196
+ get isStatic() {
197
+ return this.hasAttribute("static") || this.isInteractive;
198
+ }
199
+ get prefersTop() {
200
+ return this.getAttribute("placement") === "top";
201
+ }
202
+ get delay() {
203
+ const attr = this.getAttribute("delay");
204
+ if (attr == null) return DEFAULT_DELAY;
205
+ const n = parseInt(attr, 10);
206
+ return Number.isFinite(n) ? n : DEFAULT_DELAY;
207
+ }
208
+ // --- positioning (sets only the shadow container's own transform) ---
209
+ positionToTarget = () => {
210
+ requestAnimationFrame(() => {
211
+ if (!this.anchor || !this.shown) return;
212
+ const a = this.anchor.getBoundingClientRect();
213
+ const box = this.container.getBoundingClientRect();
214
+ const view = this.view;
215
+ const vw = view.innerWidth;
216
+ const vh = view.innerHeight;
217
+ let left = a.left;
218
+ if (left + box.width > vw) left = vw - box.width - MARGIN;
219
+ left = Math.max(MARGIN, left);
220
+ let top;
221
+ if (this.prefersTop) {
222
+ top = a.top - box.height - MARGIN;
223
+ if (top < MARGIN) top = a.bottom + MARGIN;
224
+ } else {
225
+ top = a.bottom + MARGIN;
226
+ if (top + box.height > vh) top = a.top - box.height - MARGIN;
227
+ }
228
+ top = Math.max(MARGIN, top);
229
+ this.container.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
230
+ });
231
+ };
232
+ positionToMouse = (e) => {
233
+ requestAnimationFrame(() => {
234
+ if (!this.shown && !this.fading) return;
235
+ const box = this.container.getBoundingClientRect();
236
+ const view = this.view;
237
+ const vw = view.innerWidth;
238
+ const vh = view.innerHeight;
239
+ let left = e.clientX - PADDING_X;
240
+ if (left + box.width > vw) left = vw - box.width - MARGIN;
241
+ left = Math.max(MARGIN, left);
242
+ let top;
243
+ if (this.prefersTop) {
244
+ top = e.clientY - box.height - MARGIN * 2;
245
+ if (top < MARGIN) top = e.clientY + CURSOR_SIZE;
246
+ } else {
247
+ top = e.clientY + CURSOR_SIZE;
248
+ if (top + box.height > vh) top = e.clientY - box.height - MARGIN * 2;
249
+ }
250
+ top = Math.max(MARGIN, top);
251
+ this.container.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
252
+ });
253
+ };
254
+ position(e) {
255
+ if (!this.isStatic && e) this.positionToMouse(e);
256
+ else this.positionToTarget();
257
+ }
258
+ // --- show / hide ---
259
+ show = (e) => {
260
+ if (!this.anchor) return;
261
+ this.cancelHide();
262
+ if (currentOpen && currentOpen !== this && this.anchor.contains(currentOpen.anchor)) {
263
+ return;
264
+ }
265
+ claimOpen(this);
266
+ if (!this.shown) {
267
+ this.shown = true;
268
+ this.fading = false;
269
+ if (this.fadeTimer !== void 0) {
270
+ clearTimeout(this.fadeTimer);
271
+ this.fadeTimer = void 0;
272
+ }
273
+ this.container.showPopover();
274
+ this.view.addEventListener("scroll", this.hide, { capture: true, passive: true });
275
+ this.doc.addEventListener("keydown", this.onKeyDown, true);
276
+ if (!this.docMoveBound) {
277
+ this.doc.addEventListener("mousemove", this.onDocMove);
278
+ this.docMoveBound = true;
279
+ }
280
+ }
281
+ this.position(e);
282
+ };
283
+ hide = () => {
284
+ this.cancelHide();
285
+ if (!this.shown) return;
286
+ this.shown = false;
287
+ this.container.hidePopover();
288
+ this.view.removeEventListener("scroll", this.hide, { capture: true });
289
+ this.doc.removeEventListener("keydown", this.onKeyDown, true);
290
+ this.fading = true;
291
+ if (this.fadeTimer !== void 0) clearTimeout(this.fadeTimer);
292
+ this.fadeTimer = setTimeout(() => {
293
+ this.fading = false;
294
+ this.fadeTimer = void 0;
295
+ if (this.docMoveBound) {
296
+ this.doc.removeEventListener("mousemove", this.onDocMove);
297
+ this.docMoveBound = false;
298
+ }
299
+ }, FADE_MS);
300
+ releaseOpen(this);
301
+ };
302
+ /** While shown, track the cursor even after it has left the anchor (during
303
+ * the close delay). Cursor-following only — static/interactive bubbles
304
+ * stay pinned. */
305
+ onDocMove = (e) => {
306
+ if ((this.shown || this.fading) && !this.isStatic) {
307
+ this.lastMouse = e;
308
+ this.positionToMouse(e);
309
+ }
310
+ };
311
+ /** Hide after CLOSE_DELAY unless something cancels it first (re-enter, or
312
+ * another tooltip claiming the slot). Lets the bubble bridge the gap to a
313
+ * neighbouring anchor — and survive the trip into an interactive bubble. */
314
+ scheduleHide = () => {
315
+ this.cancelHide();
316
+ this.closeTimer = setTimeout(() => {
317
+ this.closeTimer = void 0;
318
+ this.hide();
319
+ }, CLOSE_DELAY);
320
+ };
321
+ cancelHide() {
322
+ if (this.closeTimer !== void 0) {
323
+ clearTimeout(this.closeTimer);
324
+ this.closeTimer = void 0;
325
+ }
326
+ }
327
+ onKeyDown = (e) => {
328
+ if (e.key === "Escape") this.hide();
329
+ };
330
+ /** Open now if another tooltip is hot (skip delay), else (re)arm the delayed
331
+ * show with the latest cursor event. */
332
+ trigger(e) {
333
+ if (isHot()) {
334
+ this.debouncedShow?.cancel();
335
+ this.show(e);
336
+ } else {
337
+ this.debouncedShow?.(e);
338
+ }
339
+ }
340
+ // --- lazy listener wiring (called by the intersection observer) ---
341
+ setupListeners() {
342
+ if (this.listening || !this.anchor) {
343
+ this.listening = true;
344
+ return;
345
+ }
346
+ const anchor = this.anchor;
347
+ const view = this.view;
348
+ const doc = this.doc;
349
+ this.makeDebouncedShow();
350
+ const onMouseMove = (e) => {
351
+ this.lastMouse = e;
352
+ if (this.shown) {
353
+ if (!this.isStatic) this.positionToMouse(e);
354
+ } else {
355
+ this.trigger(e);
356
+ }
357
+ };
358
+ const onLeave = () => {
359
+ this.debouncedShow?.cancel();
360
+ anchor.removeEventListener("mousemove", onMouseMove);
361
+ anchor.removeEventListener("mouseleave", onLeave);
362
+ anchor.removeEventListener("blur", onLeave);
363
+ view.removeEventListener("pagehide", onLeave);
364
+ doc.removeEventListener("visibilitychange", onLeave);
365
+ this.scheduleHide();
366
+ };
367
+ const onEnter = (e) => {
368
+ this.lastMouse = e;
369
+ anchor.addEventListener("mousemove", onMouseMove);
370
+ anchor.addEventListener("mouseleave", onLeave);
371
+ view.addEventListener("pagehide", onLeave);
372
+ doc.addEventListener("visibilitychange", onLeave);
373
+ this.trigger(e);
374
+ };
375
+ const onFocus = () => {
376
+ anchor.addEventListener("blur", onLeave);
377
+ view.addEventListener("pagehide", onLeave);
378
+ doc.addEventListener("visibilitychange", onLeave);
379
+ this.trigger();
380
+ };
381
+ anchor.addEventListener("mouseenter", onEnter);
382
+ anchor.addEventListener("focus", onFocus);
383
+ this.teardown = () => {
384
+ this.debouncedShow?.cancel();
385
+ anchor.removeEventListener("mousemove", onMouseMove);
386
+ anchor.removeEventListener("mouseenter", onEnter);
387
+ anchor.removeEventListener("mouseleave", onLeave);
388
+ anchor.removeEventListener("focus", onFocus);
389
+ anchor.removeEventListener("blur", onLeave);
390
+ view.removeEventListener("pagehide", onLeave);
391
+ doc.removeEventListener("visibilitychange", onLeave);
392
+ this.listening = false;
393
+ };
394
+ this.listening = true;
395
+ }
396
+ teardownListeners() {
397
+ this.teardown?.();
398
+ this.teardown = void 0;
399
+ }
400
+ }
401
+ function register_a_tooltip() {
402
+ if (typeof customElements === "undefined") return;
403
+ if (!customElements.get("a-tooltip")) {
404
+ customElements.define("a-tooltip", ATooltipElement);
405
+ }
406
+ }
407
+ register_a_tooltip();
408
+ export {
409
+ ATooltipElement,
410
+ register_a_tooltip
411
+ };
@@ -1,14 +1,24 @@
1
- import './a-progress.css';
2
- import './a-text.css';
3
- import './a-title.css';
4
- import './a-icon.css';
5
- import './a-icon.shapes.css';
6
- import './a-button.css';
7
- import './a-sticker.css';
8
- import './a-sticker-animated.css';
1
+ /**
2
+ * Barrel: registers ALL Anta custom elements (the convenience path).
3
+ *
4
+ * Each `a-{name}` module self-registers and imports its own CSS when loaded,
5
+ * so re-exporting them here (which evaluates each module) registers the whole
6
+ * set + injects every element's CSS. Importing this barrel for side effects —
7
+ * `import '@antadesign/anta/elements'` — gives you everything.
8
+ *
9
+ * For a SMALLER footprint, import just the element(s) you use instead:
10
+ * import '@antadesign/anta/elements/a-tooltip' // registers a-tooltip + its CSS, nothing else
11
+ * That granular path pulls in only that element's code (e.g. it won't drag in
12
+ * `lottie-web`, which only `a-sticker-animated` needs).
13
+ *
14
+ * Must only be imported client-side — registration is guarded against missing
15
+ * `customElements` (SSR), but there's no reason to load it server-side.
16
+ */
9
17
  export { AProgressElement, register_a_progress } from './a-progress';
10
18
  export { ATextElement, register_a_text } from './a-text';
11
19
  export { AIconElement, register_a_icon } from './a-icon';
12
20
  export { AButtonElement, register_a_button } from './a-button';
13
21
  export { AStickerElement, register_a_sticker } from './a-sticker';
14
22
  export { AStickerAnimatedElement, register_a_sticker_animated } from './a-sticker-animated';
23
+ export { ATooltipElement, register_a_tooltip } from './a-tooltip';
24
+ import './a-title.css';
@@ -1,31 +1,11 @@
1
- import { register_a_progress } from "./a-progress";
2
- import { register_a_text } from "./a-text";
3
- import { register_a_icon } from "./a-icon";
4
- import { register_a_button } from "./a-button";
5
- import { register_a_sticker } from "./a-sticker";
6
- import { register_a_sticker_animated } from "./a-sticker-animated";
7
- import "./a-progress.css";
8
- import "./a-text.css";
1
+ import { AProgressElement, register_a_progress } from "./a-progress";
2
+ import { ATextElement, register_a_text } from "./a-text";
3
+ import { AIconElement, register_a_icon } from "./a-icon";
4
+ import { AButtonElement, register_a_button } from "./a-button";
5
+ import { AStickerElement, register_a_sticker } from "./a-sticker";
6
+ import { AStickerAnimatedElement, register_a_sticker_animated } from "./a-sticker-animated";
7
+ import { ATooltipElement, register_a_tooltip } from "./a-tooltip";
9
8
  import "./a-title.css";
10
- import "./a-icon.css";
11
- import "./a-icon.shapes.css";
12
- import "./a-button.css";
13
- import "./a-sticker.css";
14
- import "./a-sticker-animated.css";
15
- import { AProgressElement, register_a_progress as register_a_progress2 } from "./a-progress";
16
- import { ATextElement, register_a_text as register_a_text2 } from "./a-text";
17
- import { AIconElement, register_a_icon as register_a_icon2 } from "./a-icon";
18
- import { AButtonElement, register_a_button as register_a_button2 } from "./a-button";
19
- import { AStickerElement, register_a_sticker as register_a_sticker2 } from "./a-sticker";
20
- import { AStickerAnimatedElement, register_a_sticker_animated as register_a_sticker_animated2 } from "./a-sticker-animated";
21
- if (typeof customElements !== "undefined") {
22
- register_a_progress();
23
- register_a_text();
24
- register_a_icon();
25
- register_a_button();
26
- register_a_sticker();
27
- register_a_sticker_animated();
28
- }
29
9
  export {
30
10
  AButtonElement,
31
11
  AIconElement,
@@ -33,10 +13,12 @@ export {
33
13
  AStickerAnimatedElement,
34
14
  AStickerElement,
35
15
  ATextElement,
36
- register_a_button2 as register_a_button,
37
- register_a_icon2 as register_a_icon,
38
- register_a_progress2 as register_a_progress,
39
- register_a_sticker2 as register_a_sticker,
40
- register_a_sticker_animated2 as register_a_sticker_animated,
41
- register_a_text2 as register_a_text
16
+ ATooltipElement,
17
+ register_a_button,
18
+ register_a_icon,
19
+ register_a_progress,
20
+ register_a_sticker,
21
+ register_a_sticker_animated,
22
+ register_a_text,
23
+ register_a_tooltip
42
24
  };
@@ -76,7 +76,7 @@ export interface ATextAttributes extends BaseAttributes {
76
76
  /** Type scale. `small` = 13/16, `medium` (default) = 15/20, `large` = 17/24. */
77
77
  size?: 'small' | 'medium' | 'large';
78
78
  /** Render as inline-block instead of the default block. */
79
- inline?: boolean | string;
79
+ inline?: boolean | '';
80
80
  /** Truncate to N lines with a trailing ellipsis. The attribute value
81
81
  * carries the line count (e.g. `"1"`, `"3"`); the count is also
82
82
  * available via the `--line-clamp` CSS custom property set inline. */
@@ -84,7 +84,7 @@ export interface ATextAttributes extends BaseAttributes {
84
84
  /** Marks the host as expandable when paired with `truncate`. Adds
85
85
  * the fade-out mask; the JSX wrapper renders the chevron and owns
86
86
  * the click/keyboard expansion logic. */
87
- expandable?: boolean | string;
87
+ expandable?: boolean | '';
88
88
  /** ARIA disclosure state, mirrors the JSX wrapper's `expanded` flag. */
89
89
  'aria-expanded'?: boolean | 'true' | 'false';
90
90
  }
@@ -173,6 +173,25 @@ export interface AIconAttributes extends BaseAttributes {
173
173
  /** Hides decorative icons from screen readers. */
174
174
  'aria-hidden'?: 'true' | 'false' | boolean;
175
175
  }
176
+ /**
177
+ * Attributes for the `<a-tooltip>` custom element. Placed as a child of the
178
+ * element it annotates (content as children). For the typed JSX wrapper use
179
+ * `Tooltip` from `@antadesign/anta`.
180
+ */
181
+ export interface ATooltipAttributes extends BaseAttributes {
182
+ /** Show delay in milliseconds. Never use `0` — use ~`50`. Defaults to 250. */
183
+ delay?: number | string;
184
+ /** Preferred side; auto-flips when there's no room. Defaults to `'bottom'`. */
185
+ placement?: 'top' | 'bottom';
186
+ /** Pin under the anchor instead of following the cursor. Presence-based
187
+ * (`''` on, omit off). */
188
+ static?: boolean | '';
189
+ /** Make the bubble hoverable/clickable (pointer events on, stays open while
190
+ * hovered). Implies `static`. Presence-based (`''` on, omit off). */
191
+ interactive?: boolean | '';
192
+ /** HTML `id`. */
193
+ id?: string;
194
+ }
176
195
  /**
177
196
  * Attributes for the `<a-button>` custom element. For the typed JSX
178
197
  * wrapper use `Button` from `@antadesign/anta`.
@@ -188,13 +207,13 @@ export interface AButtonAttributes extends BaseAttributes {
188
207
  size?: 'small' | 'medium' | 'large';
189
208
  /** Drop outer padding to zero. Only takes effect on `priority="quaternary"`.
190
209
  * Presence-based: `''` (or any value) turns it on; omit to turn off. */
191
- paddingless?: '' | 'true' | 'false' | boolean;
210
+ paddingless?: boolean | '';
192
211
  /** Loading state. Presence-based (`''` on, omit off). */
193
- loading?: '' | 'true' | 'false' | boolean;
212
+ loading?: boolean | '';
194
213
  /** Disabled state. Presence-based (`''` on, omit off). */
195
- disabled?: '' | 'true' | 'false' | boolean;
214
+ disabled?: boolean | '';
196
215
  /** Toggled-on / pressed state. Presence-based (`''` on, omit off). */
197
- selected?: '' | 'true' | 'false' | boolean;
216
+ selected?: boolean | '';
198
217
  /** Submit/reset semantics. */
199
218
  type?: 'button' | 'submit' | 'reset';
200
219
  /** Associate with a form by id when not nested inside it. */
package/dist/index.d.ts CHANGED
@@ -31,6 +31,8 @@ export { Sticker } from './components/Sticker';
31
31
  export type { StickerProps } from './components/Sticker';
32
32
  export { StickerAnimated } from './components/StickerAnimated';
33
33
  export type { StickerAnimatedProps } from './components/StickerAnimated';
34
+ export { Tooltip } from './components/Tooltip';
35
+ export type { TooltipProps } from './components/Tooltip';
34
36
  export type { BaseProps, BaseAttributes } from './general_types';
35
37
  export { configure } from './jsx-runtime';
36
38
  /**
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { Button } from "./components/Button";
6
6
  import { ICON_SHAPES, ICON_SYNONYMS } from "./elements/a-icon.shapes";
7
7
  import { Sticker } from "./components/Sticker";
8
8
  import { StickerAnimated } from "./components/StickerAnimated";
9
+ import { Tooltip } from "./components/Tooltip";
9
10
  import { configure } from "./jsx-runtime";
10
11
  export {
11
12
  Button,
@@ -17,5 +18,6 @@ export {
17
18
  StickerAnimated,
18
19
  Text,
19
20
  Title,
21
+ Tooltip,
20
22
  configure
21
23
  };
@@ -22,7 +22,7 @@ export declare function configure(jsx: JsxFunction, Fragment?: ComponentType): v
22
22
  export declare function jsx(type: ComponentType, props: Record<string, unknown> | null, key?: string | number): unknown;
23
23
  export declare function jsxs(type: ComponentType, props: Record<string, unknown> | null, key?: string | number): unknown;
24
24
  export { _Fragment as Fragment };
25
- import type { AProgressAttributes, ATextAttributes, ATitleAttributes, AIconAttributes, AButtonAttributes, AStickerAttributes, AStickerAnimatedAttributes, BaseAttributes } from './general_types';
25
+ import type { AProgressAttributes, ATextAttributes, ATitleAttributes, AIconAttributes, AButtonAttributes, AStickerAttributes, AStickerAnimatedAttributes, ATooltipAttributes, BaseAttributes } from './general_types';
26
26
  export declare namespace JSX {
27
27
  type IntrinsicElements = React.JSX.IntrinsicElements & {
28
28
  'a-progress': AProgressAttributes;
@@ -37,5 +37,6 @@ export declare namespace JSX {
37
37
  'a-button-label': BaseAttributes;
38
38
  'a-sticker': AStickerAttributes;
39
39
  'a-sticker-animated': AStickerAnimatedAttributes;
40
+ 'a-tooltip': ATooltipAttributes;
40
41
  };
41
42
  }
package/dist/reset.css CHANGED
@@ -108,9 +108,14 @@
108
108
  /* Global link defaults. Hairline underline at 75% alpha;
109
109
  underline goes to currentColor (100%) and 1px on hover. Color
110
110
  comes from --link-color tokens, which auto-flip in dark mode.
111
- Components or specific contexts (nav, headings, buttons)
112
- should override as needed. */
113
- a, a:link, a:visited {
111
+ `:not([role="button"])` excludes anchor-buttons (`<a role="button">`,
112
+ e.g. `<Button href>`): `a:link`/`a:visited` are pseudo-classes with the
113
+ same (0,1,1) specificity as `a[role="button"]`, so without the exclusion
114
+ they'd tie and win on source order, giving buttons link colour +
115
+ underline. Other contexts (nav, headings) still override as needed. */
116
+ a:not([role="button"]),
117
+ a:not([role="button"]):link,
118
+ a:not([role="button"]):visited {
114
119
  color: var(--link-color);
115
120
  text-decoration: underline;
116
121
  text-decoration-style: solid;
@@ -121,14 +126,14 @@
121
126
  /* Hover only on real-pointer devices — gating avoids the sticky hover
122
127
  color that lingers after a tap on touch screens. */
123
128
  @media (hover: hover) and (pointer: fine) {
124
- a:hover {
129
+ a:not([role="button"]):hover {
125
130
  color: var(--link-color-hover);
126
131
  /* Underline color is left as the resting hairline (the base rule's
127
132
  `currentColor` mix) — only the thickness grows on hover. */
128
133
  text-decoration-thickness: 1px;
129
134
  }
130
135
  }
131
- a:active {
136
+ a:not([role="button"]):active {
132
137
  text-decoration-color: color-mix(in srgb, currentColor 75%, transparent);
133
138
  }
134
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antadesign/anta",
3
- "version": "0.1.1-dev.7",
3
+ "version": "0.1.1-dev.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -64,6 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "clsx": "^2.1.0",
67
+ "es-toolkit": "^1.47.0",
67
68
  "lottie-web": "^5.13.0"
68
69
  },
69
70
  "peerDependencies": {