{"_id":"@affino/tooltip-core","_rev":"2-f7306d1543969da9d964dbe80eaeecba","name":"@affino/tooltip-core","dist-tags":{"alpha":"0.1.0-alpha.1","latest":"1.0.0"},"versions":{"0.1.0-alpha.1":{"name":"@affino/tooltip-core","version":"0.1.0-alpha.1","keywords":["tooltip","surface","headless","ui-core","accessibility","aria","popover"],"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","_id":"@affino/tooltip-core@0.1.0-alpha.1","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"homepage":"https://affino.dev","bugs":{"url":"https://github.com/affinio/affinio/issues"},"dist":{"shasum":"9e0561778d9de6863b5231e64815298145bafafe","tarball":"https://registry.npmjs.org/@affino/tooltip-core/-/tooltip-core-0.1.0-alpha.1.tgz","fileCount":12,"integrity":"sha512-N1o5ch+Ly3CzGn2XkyCVr9PIXrRXDzZdP+l5TCkC39GCxJ5k8+xwgnpFGuzggr8dGNHMMdRO4A2sFTDW3Re+uA==","signatures":[{"sig":"MEUCIQCQUBeFtLH64K/aeouJXqQ9ywwgc0Gdb+zyXGUwxoiScQIgEXNeqJuhy7ghYE/5vLyKIs9nEbkIs4OBsH1sC36LlXk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":9803},"main":"dist/index.js","type":"module","_from":"file:affino-tooltip-core-0.1.0-alpha.1.tgz","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json"},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"_resolved":"/tmp/a305be5a3d6c6e3935aed3f781273d20/affino-tooltip-core-0.1.0-alpha.1.tgz","_integrity":"sha512-N1o5ch+Ly3CzGn2XkyCVr9PIXrRXDzZdP+l5TCkC39GCxJ5k8+xwgnpFGuzggr8dGNHMMdRO4A2sFTDW3Re+uA==","repository":{"url":"git+https://github.com/affinio/affinio.git#main","type":"git"},"_npmVersion":"10.8.2","description":"Headless tooltip controller powered by @affino/surface-core","directories":{},"sideEffects":false,"_nodeVersion":"20.19.6","dependencies":{"@affino/surface-core":"^1.0.0-alpha.1"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.15","@vitest/coverage-v8":"^4.0.15"},"_npmOperationalInternal":{"tmp":"tmp/tooltip-core_0.1.0-alpha.1_1768945129258_0.6127188890469963","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@affino/tooltip-core","version":"1.0.0","author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"type":"module","description":"Headless tooltip controller powered by @affino/surface-core","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"main":"dist/index.js","types":"dist/index.d.ts","sideEffects":false,"repository":{"type":"git","url":"git+https://github.com/affinio/affinio.git#main"},"homepage":"https://affino.dev","keywords":["tooltip","surface","headless","ui-core","accessibility","aria","popover"],"license":"MIT","dependencies":{"@affino/surface-core":"^1.0.0"},"devDependencies":{"@vitest/coverage-v8":"^4.0.15","vitest":"^4.0.15"},"scripts":{"build":"tsc -p tsconfig.json","test":"vitest run"},"_id":"@affino/tooltip-core@1.0.0","bugs":{"url":"https://github.com/affinio/affinio/issues"},"_integrity":"sha512-taq3oyVo5yzOF8dKa9N9oTK6wVVBBl8qvdVZ3oL/C4uM1KSWXy+ugCRLIOMC8gs8l4iQSvmTHgsFJop2ORgOPQ==","_resolved":"/tmp/739b055ce62713c1bbf1f52cd2765b43/affino-tooltip-core-1.0.0.tgz","_from":"file:affino-tooltip-core-1.0.0.tgz","_nodeVersion":"20.19.6","_npmVersion":"10.8.2","dist":{"integrity":"sha512-taq3oyVo5yzOF8dKa9N9oTK6wVVBBl8qvdVZ3oL/C4uM1KSWXy+ugCRLIOMC8gs8l4iQSvmTHgsFJop2ORgOPQ==","shasum":"09747cddbb35f157d6959619e208629ede518f21","tarball":"https://registry.npmjs.org/@affino/tooltip-core/-/tooltip-core-1.0.0.tgz","fileCount":12,"unpackedSize":20490,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIG85RtCalqiVlRfvJLHMp8u791mHEREf998yjmvt8UWaAiEAjsZr5M+o/I3xMYKpb4zbsyT3JyTN8FR2KQ6vRipEqG8="}]},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"directories":{},"maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/tooltip-core_1.0.0_1769943468387_0.38853681083466207"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-20T21:38:49.185Z","modified":"2026-02-01T10:57:48.653Z","0.1.0-alpha.1":"2026-01-20T21:38:49.486Z","1.0.0":"2026-02-01T10:57:48.549Z"},"bugs":{"url":"https://github.com/affinio/affinio/issues"},"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","homepage":"https://affino.dev","keywords":["tooltip","surface","headless","ui-core","accessibility","aria","popover"],"repository":{"type":"git","url":"git+https://github.com/affinio/affinio.git#main"},"description":"Headless tooltip controller powered by @affino/surface-core","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"readme":"# @affino/tooltip-core\n\nDeterministic tooltip controller powered by `@affino/surface-core`. Use it when you need hover/focus driven helpers without pulling in a full component library.\n\n## Highlights\n\n- Built on the shared surface kernel, so timers and state semantics match menus/popovers.\n- Pointer + focus orchestration with forgiving open/close delays.\n- Geometry helpers (`computePosition`) without bringing in Popper/floating-ui.\n- Pure TypeScript with zero DOM dependencies, ready for any framework adapter.\n\n## Installation\n\n```bash\npnpm add @affino/tooltip-core\n# or\nnpm install @affino/tooltip-core\n```\n\n## Quick start\n\n```ts\nimport { TooltipCore } from \"@affino/tooltip-core\"\n\nconst tooltip = new TooltipCore({\n\tid: \"field-help\",\n\topenDelay: 100,\n\tcloseDelay: 120,\n})\n\nconst triggerProps = tooltip.getTriggerProps({ describedBy: \"field-hint\" })\nconst contentProps = tooltip.getTooltipProps()\n\nconst anchorRect = triggerElement.getBoundingClientRect()\nconst tooltipRect = tooltipElement.getBoundingClientRect()\nconst position = tooltip.computePosition(anchorRect, tooltipRect)\nconst arrowProps = tooltip.getArrowProps({ anchorRect, tooltipRect, position })\n\ndocument.querySelector(\"[data-help]\")?.addEventListener(\"pointerenter\", triggerProps.onPointerEnter!)\n```\n\nSpread `triggerProps` across the element that owns the tooltip (usually a label or icon) and `contentProps` across the floating surface. The controller wires ARIA attributes, hover/focus coordination, and delayed timers so your adapter logic stays thin.\n\n## API surface\n\n### `new TooltipCore(options?, callbacks?)`\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `id` | `string` | Stable surface identifier. Auto-generated when omitted. |\n| `openDelay` | `number` | Milliseconds before opening on pointer intent (defaults to `80`). |\n| `closeDelay` | `number` | Milliseconds before closing on pointer leave (defaults to `150`). |\n| `defaultOpen` | `boolean` | Start the tooltip in the open state, useful for SSR previews. |\n\n| Callback | Payload | When |\n| --- | --- | --- |\n| `onOpen(surfaceId)` | `string` | Fired after the tooltip transitions to `open`. |\n| `onClose(surfaceId)` | `string` | Fired after closing. |\n| `onPositionChange(surfaceId, position)` | `{ left, top, placement, align }` | Fired whenever `computePosition` resolves.\n\n### Instance methods\n\n- `open(reason?)` / `close(reason?)` / `toggle()` — Imperative control for advanced flows (guided tours, analytics-driven nudges, etc.).\n- `subscribe(listener)` — Receive snapshot updates. Returns `{ unsubscribe }`.\n- `getTriggerProps(options?)` — Returns pointer/focus handlers + ARIA wiring.\n- `getTooltipProps()` — Returns attributes for the floating content node.\n- `getArrowProps(params)` — Computes CSS-friendly values for arrow elements.\n- `getDescriptionProps(options?)` — Live-region helper for verbose tooltips.\n- `computePosition(anchorRect, surfaceRect, options?)` — Runs the shared geometry helper when you need collision-aware placement.\n\n### `getTriggerProps(options?)`\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `describedBy` | `string \\| string[]` | Additional ids merged into `aria-describedby` so you can point to persistent helper text or a live region. |\n| `tabIndex` | `number` | Override the default `0` when your trigger is naturally focusable. |\n\n## Positioning helper\n\n```ts\nconst anchor = triggerElement.getBoundingClientRect()\nconst bubble = tooltipElement.getBoundingClientRect()\n\nconst { left, top } = tooltip.computePosition(anchor, bubble, {\n\tplacement: \"top\",\n\talign: \"start\",\n\tgutter: 8,\n})\n\nObject.assign(tooltipElement.style, {\n\ttransform: `translate(${left}px, ${top}px)`\n})\n```\n\nBecause everything funnels through the same kernel, tooltips and menus share identical timing and pointer semantics, making cross-surface behaviors consistent.\n\n## Arrow helper\n\n`getArrowProps` turns placement math into inline styles so your arrow element can stay dumb:\n\n```ts\nconst arrowProps = tooltip.getArrowProps({\n\tanchorRect: triggerElement.getBoundingClientRect(),\n\ttooltipRect: tooltipElement.getBoundingClientRect(),\n\tposition: tooltip.computePosition(anchorRect, tooltipRect, {\n\t\tplacement: \"bottom\",\n\t\talign: \"start\",\n\t\tgutter: 8,\n\t}),\n\toptions: { size: 10, inset: 6 },\n})\n\nObject.assign(arrowElement.dataset, {\n\tplacement: arrowProps[\"data-placement\"],\n\talign: arrowProps[\"data-align\"],\n})\nObject.assign(arrowElement.style, arrowProps.style)\n```\n\nIt exposes:\n\n- `size` — arrow box size in pixels (`10` by default).\n- `inset` — how far from the tooltip edge the arrow is allowed to roam.\n- `staticOffset` — use when your tooltip uses shadows or borders that require extra spacing between the arrow and panel.\n\nThe returned style map includes `--tooltip-arrow-size` so your CSS can stay declarative:\n\n```css\n[data-arrow] {\n\twidth: var(--tooltip-arrow-size);\n\theight: var(--tooltip-arrow-size);\n\tbackground: inherit;\n\tclip-path: polygon(50% 0, 0 100%, 100% 100%);\n}\n```\n\n## Choosing a positioning strategy\n\n`TooltipCore` intentionally stays agnostic about how you attach geometry to DOM nodes. The right strategy depends on where your trigger lives:\n\n| Layout constraint | Recommended strategy | Notes |\n| --- | --- | --- |\n| Tooltip renders inside the same scrolling container as the trigger | `position: absolute` anchored to the nearest relatively positioned parent | Cheapest option when the scroll context is predictable. Remember to re-run `computePosition` on scroll/resize events. |\n| Tooltip can teleport to `body` while the trigger sits in deeply nested scroll regions | `position: fixed` plus translating via `left/top` from `computePosition` | Works across stacking contexts and avoids jitter when transforms are involved. Requires you to clamp against the viewport since `fixed` ignores ancestor overflow. |\n| Virtualized lists or CSS transforms (`scale`, `translate`) alter the trigger rect | Use `position: fixed` and feed `viewportWidth` / `viewportHeight` overrides into `computePosition` if you render inside an iframe or scaled canvas | Forward custom viewport sizes so collision detection still works after transforms. |\n\nDecision tree:\n\n1. Does the tooltip live in a portal/teleport? If yes → start with `position: fixed` to avoid parent transforms clipping it.\n2. Are you placing it inside a scroll container that does **not** teleport? If yes → `position: absolute` tied to the container’s offset is often simpler.\n3. Are you syncing to pointer coordinates (context-style tooltips)? Use `setAnchor({ x, y, width: 0, height: 0 })` and still respect the strategy rules above.\n\n```ts\nfunction applyStyles(tooltip: HTMLElement, position: { left: number; top: number }, strategy: \"absolute\" | \"fixed\") {\n\tObject.assign(tooltip.style, {\n\t\tposition: strategy,\n\t\ttransform: `translate(${position.left}px, ${position.top}px)`,\n\t})\n}\n```\n\nPair this with a `ResizeObserver`/`IntersectionObserver` so that geometry updates whenever the trigger moves.\n\n## Live announcement helper\n\nWhen tooltips double as inline validation or status updates, use `getDescriptionProps` to mount a hidden live region and merge its id via `getTriggerProps({ describedBy: descriptionId })`:\n\n```ts\nconst descriptionProps = tooltip.getDescriptionProps({\n\tid: \"field-help-description\",\n\tpoliteness: \"assertive\",\n})\n\nObject.assign(descriptionElement, descriptionProps)\n```\n\nThe helper:\n\n- Defaults to `role=\"status\"`, `aria-live=\"polite\"`, and `aria-atomic=true`.\n- Mirrors tooltip state through `data-state` / `aria-hidden` so you can animate it independently.\n- Lets you switch to `role=\"alert\"` + `politeness=\"assertive\"` for high-priority announcements.\n\n## Accessibility pointers\n\n- **Arrow alignment:** The arrow helper already exposes `data-placement` and `data-align`. Lean on those attributes to flip CSS.\n- **Multiple descriptions:** Merge ids into `describedBy` so controls can reference both persistent helper text and ephemeral tooltip content.\n- **Reduced motion:** Honor `prefers-reduced-motion` by toggling transitions based on the `data-state` attribute the controller emits.\n","readmeFilename":"README.md"}