tolltop 1.0.1 → 3.0.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.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # tolltop
2
2
 
3
- One-attribute tooltips. Popover API + CSS Anchor Positioning. Auto-inverts color based on page background.
3
+ Tiny, dependency-free tooltips with smart edge-aware positioning. One attribute to add a
4
+ tooltip, one call to configure them all, no build step, and it works in every browser
5
+ released since mid-2023 (Chrome/Edge 114+, Firefox 114+, Safari 16.5+).
4
6
 
5
7
  ## Install
6
8
 
@@ -10,33 +12,65 @@ npm install tolltop
10
12
 
11
13
  ## Usage
12
14
 
13
- ### Script tag (CDN)
14
-
15
15
  ```html
16
- <script src="https://cdn.jsdelivr.net/npm/tolltop"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/tolltop/tolltop.js"></script>
17
+
18
+ <!-- mark any element with the attribute -->
19
+ <button data-tooltip="Save your work">Save</button>
17
20
  ```
18
21
 
19
- ### ES module
22
+ Tooltips show on hover and on keyboard focus, and hide on `Esc`. The script injects its own
23
+ styles, so loading `tolltop.css` is optional. Link it (before the script) only if you want
24
+ to edit the CSS directly:
20
25
 
21
- ```js
22
- import 'tolltop'
26
+ ```html
27
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tolltop/tolltop.css">
28
+ <script src="https://cdn.jsdelivr.net/npm/tolltop/tolltop.js"></script>
23
29
  ```
24
30
 
25
- ### Then just add the attribute
31
+ ## Configuration
26
32
 
27
- ```html
28
- <button data-tooltip="Save your work">Save</button>
33
+ Style and behavior are global. Call `tolltop()` once with any subset of options (it merges
34
+ into the current config and applies immediately):
35
+
36
+ ```js
37
+ tolltop({
38
+ bg: '#18181b',
39
+ color: '#e4e4e7',
40
+ radius: 6, // number = px, or any CSS length string
41
+ fontSize: 12, // same
42
+ padding: '6px 9px',
43
+ maxWidth: 240, // px
44
+ placement: 'auto', // 'auto' | 'top' | 'bottom'
45
+ gap: 8, // px between the trigger and the tooltip
46
+ edge: 24, // px min gap from each viewport side
47
+ });
29
48
  ```
30
49
 
31
- That's it. No init, no config, no dependencies.
50
+ | Option | Example | Default |
51
+ | ------------ | ---------------- | ---------- |
52
+ | `bg` | `'#1e3a5f'` | `#18181b` |
53
+ | `color` | `'#fff'` | `#e4e4e7` |
54
+ | `radius` | `10` or `'10px'` | `6` |
55
+ | `fontSize` | `14` or `'.9rem'`| `12` |
56
+ | `padding` | `'8px 12px'` | `6px 9px` |
57
+ | `maxWidth` | `320` | `240` |
58
+ | `placement` | `'top'` | `'auto'` |
59
+ | `gap` | `10` | `8` |
60
+ | `edge` | `16` | `24` |
61
+
62
+ Calling `tolltop()` with no arguments returns the current config. The only per-element
63
+ attribute is `data-tooltip="text"`, which marks an element and supplies its text.
32
64
 
33
- ## What it does
65
+ ## Positioning
34
66
 
35
- - **Positions** tooltips above the trigger using CSS Anchor Positioning, flips to bottom when clipped
36
- - **300ms delay** before showing no accidental flashes
37
- - **Auto-detects** background luminance and picks light or dark tooltip style
38
- - **Event delegation** works on dynamically added elements
39
- - **Escape** key dismisses
67
+ - Shows above the trigger by default.
68
+ - Flips below if there isn't room above. Set `placement` to force a side (it still flips if
69
+ the forced side won't fit).
70
+ - Centers horizontally on the trigger, then shifts left or right just enough to stay on
71
+ screen, leaving `edge` px of room on each side (the vertical scrollbar is excluded).
72
+ - Long text is capped at the available width and wraps instead of running off screen.
73
+ - The arrow stays centered on the trigger even after the box shifts, and points toward it.
40
74
 
41
75
  ## License
42
76
 
package/package.json CHANGED
@@ -1,24 +1,25 @@
1
1
  {
2
2
  "name": "tolltop",
3
- "version": "1.0.1",
4
- "description": "One-attribute tooltips. Popover API + CSS Anchor Positioning. Auto-inverts color based on page background.",
5
- "main": "index.js",
6
- "types": "index.d.ts",
3
+ "version": "3.0.0",
4
+ "description": "Tiny dependency-free tooltips with smart edge-aware positioning. One attribute, no build, works in browsers back to mid-2023.",
5
+ "main": "tolltop.js",
6
+ "style": "tolltop.css",
7
7
  "files": [
8
- "index.js",
9
- "index.d.ts"
8
+ "tolltop.js",
9
+ "tolltop.css"
10
10
  ],
11
11
  "keywords": [
12
12
  "tooltip",
13
- "popover",
14
- "anchor-positioning",
15
- "css",
13
+ "tooltips",
14
+ "vanilla",
15
+ "no-dependencies",
16
+ "positioning",
16
17
  "lightweight"
17
18
  ],
18
19
  "author": "David Miranda",
19
20
  "license": "MIT",
20
21
  "repository": {
21
22
  "type": "git",
22
- "url": "https://github.com/davidmiranda/tolltop"
23
+ "url": "git+https://github.com/panphora/tolltop.git"
23
24
  }
24
25
  }
package/tolltop.css ADDED
@@ -0,0 +1,60 @@
1
+ .tolltop {
2
+ --tt-bg: #18181b;
3
+ --tt-color: #e4e4e7;
4
+ --tt-radius: 6px;
5
+ --tt-font-size: 12px;
6
+ --tt-padding: 6px 9px;
7
+ --tt-arrow: 6px;
8
+ --tt-arrow-x: 50%;
9
+
10
+ position: fixed;
11
+ top: 0;
12
+ left: 0;
13
+ z-index: 2147483647;
14
+ box-sizing: border-box;
15
+ margin: 0;
16
+ width: max-content;
17
+ max-width: 240px;
18
+ padding: var(--tt-padding);
19
+ border-radius: var(--tt-radius);
20
+ background: var(--tt-bg);
21
+ color: var(--tt-color);
22
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
23
+ font-size: var(--tt-font-size);
24
+ font-weight: 500;
25
+ line-height: 1.4;
26
+ text-align: center;
27
+ white-space: normal;
28
+ overflow-wrap: break-word;
29
+ pointer-events: none;
30
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
31
+ opacity: 0;
32
+ visibility: hidden;
33
+ }
34
+
35
+ .tolltop[data-show] {
36
+ opacity: 1;
37
+ visibility: visible;
38
+ }
39
+
40
+ .tolltop::after {
41
+ content: "";
42
+ position: absolute;
43
+ left: var(--tt-arrow-x);
44
+ transform: translateX(-50%);
45
+ width: 0;
46
+ height: 0;
47
+ border: var(--tt-arrow) solid transparent;
48
+ }
49
+
50
+ .tolltop[data-placement="top"]::after {
51
+ top: 100%;
52
+ border-bottom-width: 0;
53
+ border-top-color: var(--tt-bg);
54
+ }
55
+
56
+ .tolltop[data-placement="bottom"]::after {
57
+ bottom: 100%;
58
+ border-top-width: 0;
59
+ border-bottom-color: var(--tt-bg);
60
+ }
package/tolltop.js ADDED
@@ -0,0 +1,188 @@
1
+ (() => {
2
+ 'use strict';
3
+ if (window.__tolltop) return;
4
+ window.__tolltop = true;
5
+
6
+ const VEDGE = 8;
7
+ const TIP_ID = 'tolltop-tip';
8
+
9
+ // Baseline styles, injected only if tolltop.css isn't already on the page.
10
+ // Keep in sync with tolltop.css.
11
+ const CSS = `.tolltop{--tt-bg:#18181b;--tt-color:#e4e4e7;--tt-radius:6px;--tt-font-size:12px;--tt-padding:6px 9px;--tt-arrow:6px;--tt-arrow-x:50%;position:fixed;top:0;left:0;z-index:2147483647;box-sizing:border-box;margin:0;width:max-content;max-width:240px;padding:var(--tt-padding);border-radius:var(--tt-radius);background:var(--tt-bg);color:var(--tt-color);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:var(--tt-font-size);font-weight:500;line-height:1.4;text-align:center;white-space:normal;overflow-wrap:break-word;pointer-events:none;box-shadow:0 2px 12px rgba(0,0,0,.3);opacity:0;visibility:hidden}.tolltop[data-show]{opacity:1;visibility:visible}.tolltop::after{content:"";position:absolute;left:var(--tt-arrow-x);transform:translateX(-50%);width:0;height:0;border:var(--tt-arrow) solid transparent}.tolltop[data-placement="top"]::after{top:100%;border-bottom-width:0;border-top-color:var(--tt-bg)}.tolltop[data-placement="bottom"]::after{bottom:100%;border-top-width:0;border-bottom-color:var(--tt-bg)}`;
12
+
13
+ const cfg = {
14
+ bg: null,
15
+ color: null,
16
+ radius: null,
17
+ fontSize: null,
18
+ padding: null,
19
+ maxWidth: 240,
20
+ placement: 'auto',
21
+ gap: 8,
22
+ edge: 24,
23
+ };
24
+
25
+ let tip = null;
26
+ let active = null;
27
+ let activePrevAria = null;
28
+
29
+ const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
30
+ const isNumStr = (v) => /^-?\d*\.?\d+$/.test(String(v).trim());
31
+ const toLen = (v) =>
32
+ v == null || v === '' ? null : typeof v === 'number' ? v + 'px' : isNumStr(v) ? v.trim() + 'px' : v.trim();
33
+ const numOr = (v, fallback) => (isFinite(Number(v)) ? Number(v) : fallback);
34
+
35
+ function setVar(name, value) {
36
+ if (value == null || value === '') tip.style.removeProperty(name);
37
+ else tip.style.setProperty(name, value);
38
+ }
39
+
40
+ function applyConfig() {
41
+ if (!tip) return;
42
+ setVar('--tt-bg', cfg.bg);
43
+ setVar('--tt-color', cfg.color);
44
+ setVar('--tt-radius', toLen(cfg.radius));
45
+ setVar('--tt-font-size', toLen(cfg.fontSize));
46
+ setVar('--tt-padding', cfg.padding);
47
+ }
48
+
49
+ function ensureTip() {
50
+ if (tip) return tip;
51
+ tip = document.createElement('div');
52
+ tip.className = 'tolltop';
53
+ tip.id = TIP_ID;
54
+ tip.setAttribute('role', 'tooltip');
55
+ document.body.appendChild(tip);
56
+ if (getComputedStyle(tip).position !== 'fixed') {
57
+ const style = document.createElement('style');
58
+ style.setAttribute('data-tolltop', '');
59
+ style.textContent = CSS;
60
+ document.head.insertBefore(style, document.head.firstChild);
61
+ }
62
+ applyConfig();
63
+ return tip;
64
+ }
65
+
66
+ function position(el) {
67
+ const rect = el.getBoundingClientRect();
68
+ // clientWidth/Height exclude the scrollbar; innerWidth would not.
69
+ const vw = document.documentElement.clientWidth;
70
+ const vh = document.documentElement.clientHeight;
71
+ const gap = Math.max(0, numOr(cfg.gap, 8));
72
+ const edge = Math.max(0, numOr(cfg.edge, 24));
73
+
74
+ const maxW = Math.min(numOr(cfg.maxWidth, 240), vw - edge * 2);
75
+ tip.style.maxWidth = maxW + 'px';
76
+
77
+ const t = tip.getBoundingClientRect();
78
+
79
+ const pref = cfg.placement;
80
+ const topY = rect.top - t.height - gap;
81
+ const botY = rect.bottom + gap;
82
+ const topFits = topY >= VEDGE;
83
+ const botFits = botY + t.height <= vh - VEDGE;
84
+ let placement;
85
+ if (pref === 'bottom') placement = botFits || !topFits ? 'bottom' : 'top';
86
+ else if (pref === 'top') placement = topFits || !botFits ? 'top' : 'bottom';
87
+ else placement = topFits ? 'top' : 'bottom';
88
+ // When the tip is taller than the viewport, pin to the top edge so its top stays visible.
89
+ const y =
90
+ t.height >= vh - VEDGE * 2 ? VEDGE : clamp(placement === 'top' ? topY : botY, VEDGE, vh - t.height - VEDGE);
91
+
92
+ const centerX = rect.left + rect.width / 2;
93
+ const x = clamp(centerX - t.width / 2, edge, vw - t.width - edge);
94
+
95
+ const radius = parseFloat(getComputedStyle(tip).borderTopLeftRadius) || 6;
96
+ const arrow = parseFloat(getComputedStyle(tip).getPropertyValue('--tt-arrow')) || 6;
97
+ const arrowX = clamp(centerX - x, radius + arrow, t.width - radius - arrow);
98
+
99
+ tip.style.left = x + 'px';
100
+ tip.style.top = y + 'px';
101
+ tip.style.setProperty('--tt-arrow-x', arrowX + 'px');
102
+ tip.setAttribute('data-placement', placement);
103
+ tip.setAttribute('data-show', '');
104
+ }
105
+
106
+ function restoreAria() {
107
+ if (!active) return;
108
+ if (activePrevAria == null) active.removeAttribute('aria-describedby');
109
+ else active.setAttribute('aria-describedby', activePrevAria);
110
+ activePrevAria = null;
111
+ }
112
+
113
+ function show(el) {
114
+ const text = el.getAttribute('data-tooltip');
115
+ if (!text) return;
116
+ ensureTip();
117
+ if (active !== el) {
118
+ if (active) restoreAria();
119
+ active = el;
120
+ activePrevAria = el.getAttribute('aria-describedby');
121
+ const ids = activePrevAria ? activePrevAria.split(/\s+/) : [];
122
+ if (ids.indexOf(TIP_ID) === -1) ids.push(TIP_ID);
123
+ el.setAttribute('aria-describedby', ids.join(' '));
124
+ }
125
+ tip.textContent = text;
126
+ position(el);
127
+ }
128
+
129
+ function hide() {
130
+ if (!active) return;
131
+ restoreAria();
132
+ active = null;
133
+ if (tip) tip.removeAttribute('data-show');
134
+ }
135
+
136
+ function reposition() {
137
+ if (!active) return;
138
+ // Hide if the trigger has been removed or scrolled out of view (a fixed tip
139
+ // isn't clipped by an ancestor's overflow, so it would otherwise float free).
140
+ if (!active.isConnected) {
141
+ hide();
142
+ return;
143
+ }
144
+ const r = active.getBoundingClientRect();
145
+ const vw = document.documentElement.clientWidth;
146
+ const vh = document.documentElement.clientHeight;
147
+ if (r.bottom < 0 || r.top > vh || r.right < 0 || r.left > vw) {
148
+ hide();
149
+ return;
150
+ }
151
+ position(active);
152
+ }
153
+
154
+ function trigger(node) {
155
+ return node && node.closest ? node.closest('[data-tooltip]') : null;
156
+ }
157
+
158
+ document.addEventListener('pointerover', (e) => {
159
+ const el = trigger(e.target);
160
+ if (el && el !== active) show(el);
161
+ });
162
+ document.addEventListener('pointerout', (e) => {
163
+ if (active && (!e.relatedTarget || !active.contains(e.relatedTarget))) hide();
164
+ });
165
+ document.addEventListener('focusin', (e) => {
166
+ const el = trigger(e.target);
167
+ if (el) show(el);
168
+ });
169
+ document.addEventListener('focusout', hide);
170
+ document.addEventListener('keydown', (e) => {
171
+ if (e.key === 'Escape') hide();
172
+ });
173
+ window.addEventListener('scroll', reposition, true);
174
+ window.addEventListener('resize', reposition);
175
+
176
+ window.tolltop = function (opts) {
177
+ if (opts && typeof opts === 'object') {
178
+ for (const k in opts) {
179
+ if (Object.prototype.hasOwnProperty.call(cfg, k)) cfg[k] = opts[k];
180
+ }
181
+ applyConfig();
182
+ if (active) position(active);
183
+ }
184
+ const out = {};
185
+ for (const k in cfg) out[k] = cfg[k];
186
+ return out;
187
+ };
188
+ })();
package/index.d.ts DELETED
@@ -1 +0,0 @@
1
- declare module 'tolltop' {}
package/index.js DELETED
@@ -1 +0,0 @@
1
- (()=>{const s=document.createElement('style');s.textContent=`[popover].tt{position:fixed;position-anchor:--tt;position-area:top;position-try-fallbacks:flip-block;inset:auto;margin:0 0 8px;padding:5px 9px;font:500 12px/1.4 ui-monospace,monospace;border-radius:5px;border:none;max-width:240px;pointer-events:none;z-index:2147483647;opacity:0;transition:opacity .1s ease}[popover].tt:popover-open{opacity:1}@starting-style{[popover].tt:popover-open{opacity:0}}[popover].tt.tt-light{background:#fff;color:#18181b;box-shadow:0 2px 10px rgba(0,0,0,.18),0 0 0 1px rgba(0,0,0,.06)}[popover].tt.tt-dark{background:#18181b;color:#e4e4e7;box-shadow:0 2px 10px rgba(0,0,0,.45),0 0 0 1px rgba(255,255,255,.06)}`;document.head.appendChild(s);const D=300;let tip=null,timer=null,anchor=null;function ct(el){let n=el;while(n&&n!==document){if(n.nodeType===1&&n.hasAttribute&&n.hasAttribute('data-tooltip'))return n;n=n.parentNode}return null}function init(){if(tip)return tip;tip=document.createElement('div');tip.classList.add('tt');tip.setAttribute('popover','manual');document.body.appendChild(tip);return tip}function isDark(el){let n=el;while(n&&n!==document){const bg=getComputedStyle(n).backgroundColor;const m=bg.match(/\d+/g);if(m&&(m.length<4||+m[3]>0)){const[r,g,b]=m.map(Number);return r*.299+g*.587+b*.114<128}n=n.parentElement}return true}function show(el){const text=el.getAttribute('data-tooltip');if(!text)return;el.style.anchorName='--tt';const t=init();t.textContent=text;t.classList.toggle('tt-light',isDark(el));t.classList.toggle('tt-dark',!isDark(el));t.showPopover();anchor=el}function hide(){clearTimeout(timer);if(tip)try{tip.hidePopover()}catch{}if(anchor){anchor.style.anchorName='';anchor=null}}document.addEventListener('pointerenter',e=>{const el=ct(e.target);if(!el)return;hide();timer=setTimeout(()=>show(el),D)},true);document.addEventListener('pointerleave',e=>{if(ct(e.target))hide()},true);document.addEventListener('keydown',e=>{if(e.key==='Escape')hide()})})();