kitzo 1.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 ADDED
@@ -0,0 +1,168 @@
1
+ # zero-kitzo
2
+
3
+ [![npm](https://img.shields.io/npm/v/kitzo)](https://www.npmjs.com/package/kitzo)
4
+
5
+ ### A lightweight tool
6
+
7
+ Current features
8
+
9
+ - Copy on click
10
+ - Tooltip on mouseover
11
+ - Ripple effect on mousedown
12
+ - Debounce function
13
+
14
+ #### Install
15
+
16
+ ```bash
17
+ npm i kitzo
18
+ ```
19
+
20
+ > or
21
+
22
+ ```javascript
23
+ <script src="https://cdn.jsdelivr.net/npm/kitzo@1.0.0/dist/kitzo.umd.min.js"></script>
24
+ ```
25
+
26
+ ---
27
+
28
+ #### Quick usage overview
29
+
30
+ | [NPM](#npm-usage) | [CDN](#cdn-usage) |
31
+ | -------------------------------- | ------------------ |
32
+ | [`kitzoCopy()`](#copy-api-1) | `kitzo.copy()` |
33
+ | [`kitzoTooltip()`](#tooltip-api) | `kitzo.tooltip()` |
34
+ | [`kitzoRipple()`](#ripple-api) | `kitzo.ripple()` |
35
+ | [`kitzoDebounce()`](#debounce) | `kitzo.debounce()` |
36
+
37
+ #### NPM usage
38
+
39
+ ```javascript
40
+ import { kitzoCopy, kitzoTooltip, kitzoRipple } from 'kitzo';
41
+ ```
42
+
43
+ > Use a modern build tool. **vite** - recommended
44
+
45
+ ##### Copy API:
46
+
47
+ ```javascript
48
+ kitzoCopy(selector | element, {
49
+ doc: string,
50
+ event: 'click' | 'dblclick' | 'contextmenu' | 'mouseup' | 'touchend',
51
+ });
52
+ ```
53
+
54
+ > Instantly adds click-to-copy functionality to buttons, reliably and with fallback.
55
+
56
+ ##### Tooltip API:
57
+
58
+ ```javascript
59
+ kitzoTooltip(selectors | element | NodeList, {
60
+ tooltip: string,
61
+ direction: 'top' | 'right' | 'bottom' | 'left',
62
+ arrow: 'on' | 'off',
63
+ offset: number,
64
+ customClass: string,
65
+ style: {},
66
+ });
67
+ ```
68
+
69
+ > Attach minimal tooltips to buttons for clean, helpful hover hints.
70
+
71
+ ##### Ripple API:
72
+
73
+ ```javascript
74
+ kitzoRipple(selectors | element | NodeList, {
75
+ opacity: number,
76
+ duration: number,
77
+ color: string,
78
+ size: number | null,
79
+ });
80
+ ```
81
+
82
+ > Adds a lightweight, clean ripple effect to your buttons on click.
83
+
84
+ ##### Debounce API:
85
+
86
+ ```javascript
87
+ kitzoDebounce(callback, delayInMilliseconds);
88
+ ```
89
+
90
+ ```javascript
91
+ // Log only after typing stops for 500ms
92
+ const logSearch = kitzoDebounce((text) => {
93
+ console.log("Searching for:", text);
94
+ }, 500);
95
+
96
+ // Attach to input
97
+ document.querySelector("#search").addEventListener("input", (e) => {
98
+ logSearch(e.target.value);
99
+ });
100
+ ```
101
+
102
+ > Debounce on every call of function.
103
+
104
+ ---
105
+
106
+ #### CDN usage
107
+
108
+ ```html
109
+ <script src="https://cdn.jsdelivr.net/npm/kitzo@1.0.0/dist/kitzo.umd.min.js"></script>
110
+ ```
111
+
112
+ > Attach this script tag in the html head tag and you are good to go.
113
+
114
+ ```javascript
115
+ kitzo.copy();
116
+ kitzo.tooltip();
117
+ kitzo.ripple();
118
+ ```
119
+
120
+ ##### Copy API:
121
+
122
+ ```javascript
123
+ kitzo.copy(selectors | element, {
124
+ doc: string,
125
+ event: 'click' | 'dblclick' | 'contextmenu' | 'mouseup' | 'touchend',
126
+ });
127
+ ```
128
+
129
+ ##### Tooltip API:
130
+
131
+ ```javascript
132
+ kitzo.tooltip(selectors | element | NodeList, {
133
+ tooltip: string,
134
+ direction: 'top' | 'right' | 'bottom' | 'left',
135
+ arrow: 'on' | 'off',
136
+ offset: number,
137
+ customClass: string,
138
+ style: {},
139
+ });
140
+ ```
141
+
142
+ ##### Ripple API:
143
+
144
+ ```javascript
145
+ kitzo.ripple(selectors | element | NodeList, {
146
+ opacity: number,
147
+ duration: number,
148
+ color: string,
149
+ size: number | null,
150
+ });
151
+ ```
152
+
153
+ ##### Debounce API:
154
+
155
+ ```javascript
156
+ kitzo.debounce(callback, delayInMilliseconds);
157
+ ```
158
+ ```javascript
159
+ // Log only after typing stops for 500ms
160
+ const logSearch = kitzo.debounce((text) => {
161
+ console.log("Searching for:", text);
162
+ }, 500);
163
+
164
+ // Attach to input
165
+ document.querySelector("#search").addEventListener("input", (e) => {
166
+ logSearch(e.target.value);
167
+ });
168
+ ```
@@ -0,0 +1,454 @@
1
+ //! Helper functions
2
+ function getButtons(element) {
3
+ if (typeof element === 'string') {
4
+ return document.querySelectorAll(element);
5
+ }
6
+ if (element instanceof Element) {
7
+ return [element];
8
+ }
9
+ if (element instanceof NodeList || element instanceof HTMLCollection) {
10
+ return element;
11
+ }
12
+ }
13
+
14
+ // Add style tags
15
+ let tooltipStyleAdded = false;
16
+ let rippleStyleAdded = false;
17
+
18
+ function addStyleTag(styles) {
19
+ const style = document.createElement('style');
20
+ style.innerHTML = styles;
21
+ document.head.appendChild(style);
22
+ }
23
+
24
+ function addStyleTagToHtmlHead(type, styles) {
25
+ if (type === 'tooltip' && !tooltipStyleAdded) {
26
+ addStyleTag(styles);
27
+ tooltipStyleAdded = true;
28
+ }
29
+ if (type === 'ripple' && !rippleStyleAdded) {
30
+ addStyleTag(styles);
31
+ rippleStyleAdded = true;
32
+ }
33
+ }
34
+
35
+ //! Copy function
36
+ function legecyCopy(docs) {
37
+ try {
38
+ const textarea = document.createElement('textarea');
39
+ textarea.value = docs;
40
+ document.body.appendChild(textarea);
41
+ textarea.select();
42
+ document.execCommand('copy');
43
+ document.body.removeChild(textarea);
44
+ } catch (error) {
45
+ alert('Couldn’t copy automatically. Please copy manually.');
46
+ console.error(error);
47
+ }
48
+ }
49
+
50
+ async function copyText(docs) {
51
+ if (navigator.clipboard && navigator.clipboard.writeText) {
52
+ try {
53
+ await navigator.clipboard.writeText(docs);
54
+ } catch (error) {
55
+ legecyCopy(docs);
56
+ console.error(error);
57
+ }
58
+ } else {
59
+ legecyCopy(docs);
60
+ }
61
+ }
62
+
63
+ const copyConfigMap = new WeakMap();
64
+ const allowedEvents = ['click', 'dblclick', 'contextmenu', 'mouseup', 'touchend'];
65
+ const attachedEvents = new Set();
66
+
67
+ function copy(element, config = {}) {
68
+ config = Object.assign(
69
+ {
70
+ doc: '',
71
+ event: 'click',
72
+ },
73
+ config
74
+ );
75
+
76
+ const { doc, event } = config;
77
+
78
+ if (!element) {
79
+ console.error('A button element/selector is expected');
80
+ return;
81
+ }
82
+
83
+ if (!doc) {
84
+ console.error('doc cannot be empty');
85
+ return;
86
+ }
87
+
88
+ if (typeof doc !== 'string') {
89
+ console.error('Doc should be in string format');
90
+ return;
91
+ }
92
+
93
+ if (typeof event !== 'string') {
94
+ console.error('Only strings are allowed as events');
95
+ return;
96
+ }
97
+
98
+ if (!event.trim()) {
99
+ console.error('event cannot be empty');
100
+ return;
101
+ }
102
+
103
+ const allButtons = getButtons(element);
104
+ if (!allButtons) {
105
+ console.error('No elements found for zeroCopy');
106
+ return;
107
+ }
108
+
109
+ if (!allowedEvents.includes(event)) {
110
+ console.warn(`[zeroCopy] "${event}" is not allowed. Defaulting to "click".`);
111
+ }
112
+
113
+ const safeEvent = allowedEvents.includes(event) ? event : 'click';
114
+
115
+ allButtons.forEach((btn) => {
116
+ btn.setAttribute('data-zero-copy', 'true');
117
+
118
+ copyConfigMap.set(btn, {
119
+ doc,
120
+ event: safeEvent,
121
+ });
122
+ });
123
+
124
+ if (!attachedEvents.has(safeEvent)) {
125
+ document.addEventListener(safeEvent, (e) => {
126
+ const btn = e.target.closest('[data-zero-copy]');
127
+ if (!btn) return;
128
+
129
+ const { doc, event } = copyConfigMap.get(btn);
130
+ if (event && event === safeEvent) {
131
+ copyText(doc);
132
+ }
133
+ });
134
+ attachedEvents.add(safeEvent);
135
+ }
136
+ }
137
+
138
+ function debounce(fn, delay = 300) {
139
+ let timer;
140
+
141
+ return (...args) => {
142
+ clearTimeout(timer);
143
+ timer = setTimeout(() => fn(...args), delay);
144
+ };
145
+ }
146
+
147
+ function rippleStyles() {
148
+ return `.zero-ripple {
149
+ position: relative;
150
+ overflow: hidden;
151
+ }
152
+
153
+ .zero-ripples {
154
+ display: block;
155
+ position: absolute;
156
+ top: 0;
157
+ left: 0;
158
+ transform: translate(-50%, -50%);
159
+ width: 0;
160
+ height: 0;
161
+ background-color: var(--ripples-color);
162
+ z-index: 5;
163
+ border-radius: 50%;
164
+ opacity: 1;
165
+ pointer-events: none;
166
+ }
167
+
168
+ .zero-ripples.expand {
169
+ animation: expand-ripple var(--ripples-duration) linear forwards;
170
+ }
171
+
172
+ @keyframes expand-ripple {
173
+ 0% {
174
+ width: 0;
175
+ height: 0;
176
+ opacity: var(--ripples-opacity);
177
+ }
178
+ 100% {
179
+ width: var(--ripples-size);
180
+ height: var(--ripples-size);
181
+ opacity: 0;
182
+ }
183
+ }`;
184
+ }
185
+
186
+ //! Ripple effect
187
+ let rippleListenerAdded = false;
188
+
189
+ function ripple(element, config = {}) {
190
+ if (!element) {
191
+ console.error('A button element/selector is expected');
192
+ return;
193
+ }
194
+
195
+ addStyleTagToHtmlHead('ripple', rippleStyles());
196
+
197
+ config = Object.assign(
198
+ {
199
+ opacity: 0.5,
200
+ duration: 1,
201
+ color: 'white',
202
+ size: null,
203
+ },
204
+ config
205
+ );
206
+
207
+ const { opacity, color, duration, size } = config;
208
+
209
+ const allButtons = getButtons(element);
210
+ if (!allButtons) {
211
+ console.error('No elements found for zeroRipple');
212
+ return;
213
+ }
214
+ allButtons.forEach((btn) => {
215
+ btn.classList.add('zero-ripple');
216
+ btn.setAttribute('data-zero-ripple', 'true');
217
+ });
218
+
219
+ if (!rippleListenerAdded) {
220
+ document.addEventListener('mousedown', (e) => {
221
+ const btn = e.target.closest('[data-zero-ripple]');
222
+ if (btn) {
223
+ const span = document.createElement('span');
224
+ span.className = 'zero-ripples';
225
+ btn.appendChild(span);
226
+
227
+ const { left, top, width } = btn.getBoundingClientRect();
228
+ span.style.left = e.clientX - left + 'px';
229
+ span.style.top = e.clientY - top + 'px';
230
+
231
+ btn.style.setProperty('--ripples-opacity', opacity);
232
+ btn.style.setProperty('--ripples-duration', duration + 's');
233
+ btn.style.setProperty('--ripples-size', `${size || width * 2}px`);
234
+ btn.style.setProperty('--ripples-color', color);
235
+
236
+ span.classList.add('expand');
237
+
238
+ span.addEventListener('animationend', () => span.remove());
239
+ }
240
+ });
241
+
242
+ rippleListenerAdded = true;
243
+ }
244
+ }
245
+
246
+ function tooltipStyles() {
247
+ return `:root {
248
+ --tooltip-bg-clr: hsl(0, 0%, 10%);
249
+ --tooltip-text-clr: hsl(0, 0%, 90%);
250
+ }
251
+
252
+ @media (prefers-color-scheme: dark) {
253
+ :root {
254
+ --tooltip-bg-clr: hsl(0, 0%, 95%);
255
+ --tooltip-text-clr: hsl(0, 0%, 10%);
256
+ }
257
+ }
258
+
259
+ .zero-tooltip {
260
+ --tooltip-arrow-clr: var(--tooltip-bg-clr);
261
+
262
+ box-sizing: border-box;
263
+ font-family: inherit;
264
+ text-align: center;
265
+
266
+ position: fixed;
267
+ top: 0;
268
+ left: 0;
269
+ z-index: 999999;
270
+
271
+ background-color: var(--tooltip-bg-clr);
272
+ color: var(--tooltip-text-clr);
273
+ padding-block: 0.325rem;
274
+ padding-inline: 0.625rem;
275
+ border-radius: 4px;
276
+ box-shadow: 0 2px 6px hsla(235, 0%, 0%, 0.25);
277
+
278
+ opacity: 0 !important;
279
+
280
+ transition: opacity 200ms;
281
+ transition-delay: 100ms;
282
+ pointer-events: none;
283
+ }
284
+
285
+ .zero-tooltip.show {
286
+ opacity: 1 !important;
287
+ }
288
+
289
+ .zero-tooltip-top::before,
290
+ .zero-tooltip-right::before,
291
+ .zero-tooltip-bottom::before,
292
+ .zero-tooltip-left::before {
293
+
294
+ content: '';
295
+ position: absolute;
296
+ border: 6px solid;
297
+ }
298
+
299
+ .zero-tooltip-top::before {
300
+ top: calc(100% - 1px);
301
+ left: 50%;
302
+ translate: -50% 0;
303
+ border-color: var(--tooltip-arrow-clr) transparent transparent transparent;
304
+ }
305
+
306
+ .zero-tooltip-right::before {
307
+ top: 50%;
308
+ right: calc(100% - 1px);
309
+ translate: 0 -50%;
310
+ border-color: transparent var(--tooltip-arrow-clr) transparent transparent;
311
+ }
312
+
313
+ .zero-tooltip-bottom::before {
314
+ bottom: calc(100% - 1px);
315
+ left: 50%;
316
+ translate: -50% 0;
317
+ border-color: transparent transparent var(--tooltip-arrow-clr) transparent;
318
+ }
319
+
320
+ .zero-tooltip-left::before {
321
+ left: calc(100% - 1px);
322
+ top: 50%;
323
+ translate: 0 -50%;
324
+ border-color: transparent transparent transparent var(--tooltip-arrow-clr);
325
+ }`;
326
+ }
327
+
328
+ //! Tooltip
329
+ let tooltipDiv;
330
+ let tooltipListenerAdded = false;
331
+ const tooltipConfigMap = new WeakMap();
332
+
333
+ function tooltip(element, config = {}) {
334
+ if (window.matchMedia('(pointer: coarse)').matches) return;
335
+
336
+ if (!element) {
337
+ console.error('A button element/selector is expected');
338
+ return;
339
+ }
340
+
341
+ addStyleTagToHtmlHead('tooltip', tooltipStyles());
342
+
343
+ config = Object.assign(
344
+ {
345
+ tooltip: 'Tool tip',
346
+ direction: 'bottom',
347
+ arrow: 'off',
348
+ offset: 10,
349
+ customClass: '',
350
+ style: {},
351
+ },
352
+ config
353
+ );
354
+
355
+ const allButtons = getButtons(element);
356
+ if (!allButtons) {
357
+ console.error('No elements found for zeroTooltip');
358
+ return;
359
+ }
360
+
361
+ const disAllowedStyles = ['top', 'left', 'right', 'bottom', 'position', 'zIndex', 'opacity', 'transform', 'translate', 'scale', 'rotate', 'perspective'];
362
+ for (const key of disAllowedStyles) {
363
+ if (key in config.style) {
364
+ console.warn(`[zeroTooltip] "${key}" style is managed internally and will be ignored.`);
365
+ delete config.style[key];
366
+ }
367
+ }
368
+
369
+ allButtons.forEach((btn) => {
370
+ btn.setAttribute('data-zero-tooltip', true);
371
+ tooltipConfigMap.set(btn, config);
372
+ });
373
+
374
+ if (!tooltipDiv) {
375
+ tooltipDiv = document.createElement('div');
376
+ tooltipDiv.style.opacity = '0';
377
+ document.body.appendChild(tooltipDiv);
378
+ }
379
+
380
+ function getPosition(btn, dir, offset) {
381
+ const { width, height, top, left } = btn.getBoundingClientRect();
382
+ let posX;
383
+ let posY;
384
+
385
+ if (dir === 'top') {
386
+ posX = left + width / 2 - tooltipDiv.offsetWidth / 2;
387
+ posY = top - tooltipDiv.offsetHeight - offset;
388
+ return { posX, posY };
389
+ }
390
+
391
+ if (dir === 'right') {
392
+ posX = left + width + offset;
393
+ posY = top + height / 2 - tooltipDiv.offsetHeight / 2;
394
+ return { posX, posY };
395
+ }
396
+
397
+ if (dir === 'bottom') {
398
+ posX = left + width / 2 - tooltipDiv.offsetWidth / 2;
399
+ posY = top + height + offset;
400
+ return { posX, posY };
401
+ }
402
+
403
+ if (dir === 'left') {
404
+ posX = left - tooltipDiv.offsetWidth - offset;
405
+ posY = top + height / 2 - tooltipDiv.offsetHeight / 2;
406
+ return { posX, posY };
407
+ }
408
+ }
409
+
410
+ if (!tooltipListenerAdded) {
411
+ document.addEventListener('mouseover', (e) => {
412
+ const btn = e.target.closest('[data-zero-tooltip]');
413
+ if (btn) {
414
+ const { tooltip, direction, offset, customClass, style, arrow } = tooltipConfigMap.get(btn);
415
+
416
+ tooltipDiv.removeAttribute('style');
417
+ Object.assign(tooltipDiv.style, {
418
+ position: 'fixed',
419
+ top: '0px',
420
+ left: '0px',
421
+ zIndex: '999999',
422
+ ...style,
423
+ });
424
+
425
+ const isArrowOn = arrow === 'on';
426
+ tooltipDiv.textContent = tooltip;
427
+ tooltipDiv.className = `zero-tooltip ${isArrowOn ? `zero-tooltip-${direction}` : ''} ${customClass.trim() ? customClass : ''}`;
428
+
429
+ if (isArrowOn) {
430
+ const color = getComputedStyle(tooltipDiv).backgroundColor;
431
+ tooltipDiv.style.setProperty('--tooltip-arrow-clr', color);
432
+ }
433
+
434
+ const { posX, posY } = getPosition(btn, direction, offset);
435
+ tooltipDiv.style.transform = `translate(${posX}px, ${posY}px)`;
436
+
437
+ requestAnimationFrame(() => {
438
+ tooltipDiv.classList.add('show');
439
+ });
440
+ }
441
+ });
442
+
443
+ document.addEventListener('mouseout', (e) => {
444
+ const btn = e.target.closest('[data-zero-tooltip]');
445
+ if (btn) {
446
+ tooltipDiv.classList.remove('show');
447
+ }
448
+ });
449
+
450
+ tooltipListenerAdded = true;
451
+ }
452
+ }
453
+
454
+ export { copy, debounce, ripple, tooltip };
@@ -0,0 +1,465 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.kitzo = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ //! Helper functions
8
+ function getButtons(element) {
9
+ if (typeof element === 'string') {
10
+ return document.querySelectorAll(element);
11
+ }
12
+ if (element instanceof Element) {
13
+ return [element];
14
+ }
15
+ if (element instanceof NodeList || element instanceof HTMLCollection) {
16
+ return element;
17
+ }
18
+ }
19
+
20
+ // Add style tags
21
+ let tooltipStyleAdded = false;
22
+ let rippleStyleAdded = false;
23
+
24
+ function addStyleTag(styles) {
25
+ const style = document.createElement('style');
26
+ style.innerHTML = styles;
27
+ document.head.appendChild(style);
28
+ }
29
+
30
+ function addStyleTagToHtmlHead(type, styles) {
31
+ if (type === 'tooltip' && !tooltipStyleAdded) {
32
+ addStyleTag(styles);
33
+ tooltipStyleAdded = true;
34
+ }
35
+ if (type === 'ripple' && !rippleStyleAdded) {
36
+ addStyleTag(styles);
37
+ rippleStyleAdded = true;
38
+ }
39
+ }
40
+
41
+ //! Copy function
42
+ function legecyCopy(docs) {
43
+ try {
44
+ const textarea = document.createElement('textarea');
45
+ textarea.value = docs;
46
+ document.body.appendChild(textarea);
47
+ textarea.select();
48
+ document.execCommand('copy');
49
+ document.body.removeChild(textarea);
50
+ } catch (error) {
51
+ alert('Couldn’t copy automatically. Please copy manually.');
52
+ console.error(error);
53
+ }
54
+ }
55
+
56
+ async function copyText(docs) {
57
+ if (navigator.clipboard && navigator.clipboard.writeText) {
58
+ try {
59
+ await navigator.clipboard.writeText(docs);
60
+ } catch (error) {
61
+ legecyCopy(docs);
62
+ console.error(error);
63
+ }
64
+ } else {
65
+ legecyCopy(docs);
66
+ }
67
+ }
68
+
69
+ const copyConfigMap = new WeakMap();
70
+ const allowedEvents = ['click', 'dblclick', 'contextmenu', 'mouseup', 'touchend'];
71
+ const attachedEvents = new Set();
72
+
73
+ function copy(element, config = {}) {
74
+ config = Object.assign(
75
+ {
76
+ doc: '',
77
+ event: 'click',
78
+ },
79
+ config
80
+ );
81
+
82
+ const { doc, event } = config;
83
+
84
+ if (!element) {
85
+ console.error('A button element/selector is expected');
86
+ return;
87
+ }
88
+
89
+ if (!doc) {
90
+ console.error('doc cannot be empty');
91
+ return;
92
+ }
93
+
94
+ if (typeof doc !== 'string') {
95
+ console.error('Doc should be in string format');
96
+ return;
97
+ }
98
+
99
+ if (typeof event !== 'string') {
100
+ console.error('Only strings are allowed as events');
101
+ return;
102
+ }
103
+
104
+ if (!event.trim()) {
105
+ console.error('event cannot be empty');
106
+ return;
107
+ }
108
+
109
+ const allButtons = getButtons(element);
110
+ if (!allButtons) {
111
+ console.error('No elements found for zeroCopy');
112
+ return;
113
+ }
114
+
115
+ if (!allowedEvents.includes(event)) {
116
+ console.warn(`[zeroCopy] "${event}" is not allowed. Defaulting to "click".`);
117
+ }
118
+
119
+ const safeEvent = allowedEvents.includes(event) ? event : 'click';
120
+
121
+ allButtons.forEach((btn) => {
122
+ btn.setAttribute('data-zero-copy', 'true');
123
+
124
+ copyConfigMap.set(btn, {
125
+ doc,
126
+ event: safeEvent,
127
+ });
128
+ });
129
+
130
+ if (!attachedEvents.has(safeEvent)) {
131
+ document.addEventListener(safeEvent, (e) => {
132
+ const btn = e.target.closest('[data-zero-copy]');
133
+ if (!btn) return;
134
+
135
+ const { doc, event } = copyConfigMap.get(btn);
136
+ if (event && event === safeEvent) {
137
+ copyText(doc);
138
+ }
139
+ });
140
+ attachedEvents.add(safeEvent);
141
+ }
142
+ }
143
+
144
+ function debounce(fn, delay = 300) {
145
+ let timer;
146
+
147
+ return (...args) => {
148
+ clearTimeout(timer);
149
+ timer = setTimeout(() => fn(...args), delay);
150
+ };
151
+ }
152
+
153
+ function rippleStyles() {
154
+ return `.zero-ripple {
155
+ position: relative;
156
+ overflow: hidden;
157
+ }
158
+
159
+ .zero-ripples {
160
+ display: block;
161
+ position: absolute;
162
+ top: 0;
163
+ left: 0;
164
+ transform: translate(-50%, -50%);
165
+ width: 0;
166
+ height: 0;
167
+ background-color: var(--ripples-color);
168
+ z-index: 5;
169
+ border-radius: 50%;
170
+ opacity: 1;
171
+ pointer-events: none;
172
+ }
173
+
174
+ .zero-ripples.expand {
175
+ animation: expand-ripple var(--ripples-duration) linear forwards;
176
+ }
177
+
178
+ @keyframes expand-ripple {
179
+ 0% {
180
+ width: 0;
181
+ height: 0;
182
+ opacity: var(--ripples-opacity);
183
+ }
184
+ 100% {
185
+ width: var(--ripples-size);
186
+ height: var(--ripples-size);
187
+ opacity: 0;
188
+ }
189
+ }`;
190
+ }
191
+
192
+ //! Ripple effect
193
+ let rippleListenerAdded = false;
194
+
195
+ function ripple(element, config = {}) {
196
+ if (!element) {
197
+ console.error('A button element/selector is expected');
198
+ return;
199
+ }
200
+
201
+ addStyleTagToHtmlHead('ripple', rippleStyles());
202
+
203
+ config = Object.assign(
204
+ {
205
+ opacity: 0.5,
206
+ duration: 1,
207
+ color: 'white',
208
+ size: null,
209
+ },
210
+ config
211
+ );
212
+
213
+ const { opacity, color, duration, size } = config;
214
+
215
+ const allButtons = getButtons(element);
216
+ if (!allButtons) {
217
+ console.error('No elements found for zeroRipple');
218
+ return;
219
+ }
220
+ allButtons.forEach((btn) => {
221
+ btn.classList.add('zero-ripple');
222
+ btn.setAttribute('data-zero-ripple', 'true');
223
+ });
224
+
225
+ if (!rippleListenerAdded) {
226
+ document.addEventListener('mousedown', (e) => {
227
+ const btn = e.target.closest('[data-zero-ripple]');
228
+ if (btn) {
229
+ const span = document.createElement('span');
230
+ span.className = 'zero-ripples';
231
+ btn.appendChild(span);
232
+
233
+ const { left, top, width } = btn.getBoundingClientRect();
234
+ span.style.left = e.clientX - left + 'px';
235
+ span.style.top = e.clientY - top + 'px';
236
+
237
+ btn.style.setProperty('--ripples-opacity', opacity);
238
+ btn.style.setProperty('--ripples-duration', duration + 's');
239
+ btn.style.setProperty('--ripples-size', `${size || width * 2}px`);
240
+ btn.style.setProperty('--ripples-color', color);
241
+
242
+ span.classList.add('expand');
243
+
244
+ span.addEventListener('animationend', () => span.remove());
245
+ }
246
+ });
247
+
248
+ rippleListenerAdded = true;
249
+ }
250
+ }
251
+
252
+ function tooltipStyles() {
253
+ return `:root {
254
+ --tooltip-bg-clr: hsl(0, 0%, 10%);
255
+ --tooltip-text-clr: hsl(0, 0%, 90%);
256
+ }
257
+
258
+ @media (prefers-color-scheme: dark) {
259
+ :root {
260
+ --tooltip-bg-clr: hsl(0, 0%, 95%);
261
+ --tooltip-text-clr: hsl(0, 0%, 10%);
262
+ }
263
+ }
264
+
265
+ .zero-tooltip {
266
+ --tooltip-arrow-clr: var(--tooltip-bg-clr);
267
+
268
+ box-sizing: border-box;
269
+ font-family: inherit;
270
+ text-align: center;
271
+
272
+ position: fixed;
273
+ top: 0;
274
+ left: 0;
275
+ z-index: 999999;
276
+
277
+ background-color: var(--tooltip-bg-clr);
278
+ color: var(--tooltip-text-clr);
279
+ padding-block: 0.325rem;
280
+ padding-inline: 0.625rem;
281
+ border-radius: 4px;
282
+ box-shadow: 0 2px 6px hsla(235, 0%, 0%, 0.25);
283
+
284
+ opacity: 0 !important;
285
+
286
+ transition: opacity 200ms;
287
+ transition-delay: 100ms;
288
+ pointer-events: none;
289
+ }
290
+
291
+ .zero-tooltip.show {
292
+ opacity: 1 !important;
293
+ }
294
+
295
+ .zero-tooltip-top::before,
296
+ .zero-tooltip-right::before,
297
+ .zero-tooltip-bottom::before,
298
+ .zero-tooltip-left::before {
299
+
300
+ content: '';
301
+ position: absolute;
302
+ border: 6px solid;
303
+ }
304
+
305
+ .zero-tooltip-top::before {
306
+ top: calc(100% - 1px);
307
+ left: 50%;
308
+ translate: -50% 0;
309
+ border-color: var(--tooltip-arrow-clr) transparent transparent transparent;
310
+ }
311
+
312
+ .zero-tooltip-right::before {
313
+ top: 50%;
314
+ right: calc(100% - 1px);
315
+ translate: 0 -50%;
316
+ border-color: transparent var(--tooltip-arrow-clr) transparent transparent;
317
+ }
318
+
319
+ .zero-tooltip-bottom::before {
320
+ bottom: calc(100% - 1px);
321
+ left: 50%;
322
+ translate: -50% 0;
323
+ border-color: transparent transparent var(--tooltip-arrow-clr) transparent;
324
+ }
325
+
326
+ .zero-tooltip-left::before {
327
+ left: calc(100% - 1px);
328
+ top: 50%;
329
+ translate: 0 -50%;
330
+ border-color: transparent transparent transparent var(--tooltip-arrow-clr);
331
+ }`;
332
+ }
333
+
334
+ //! Tooltip
335
+ let tooltipDiv;
336
+ let tooltipListenerAdded = false;
337
+ const tooltipConfigMap = new WeakMap();
338
+
339
+ function tooltip(element, config = {}) {
340
+ if (window.matchMedia('(pointer: coarse)').matches) return;
341
+
342
+ if (!element) {
343
+ console.error('A button element/selector is expected');
344
+ return;
345
+ }
346
+
347
+ addStyleTagToHtmlHead('tooltip', tooltipStyles());
348
+
349
+ config = Object.assign(
350
+ {
351
+ tooltip: 'Tool tip',
352
+ direction: 'bottom',
353
+ arrow: 'off',
354
+ offset: 10,
355
+ customClass: '',
356
+ style: {},
357
+ },
358
+ config
359
+ );
360
+
361
+ const allButtons = getButtons(element);
362
+ if (!allButtons) {
363
+ console.error('No elements found for zeroTooltip');
364
+ return;
365
+ }
366
+
367
+ const disAllowedStyles = ['top', 'left', 'right', 'bottom', 'position', 'zIndex', 'opacity', 'transform', 'translate', 'scale', 'rotate', 'perspective'];
368
+ for (const key of disAllowedStyles) {
369
+ if (key in config.style) {
370
+ console.warn(`[zeroTooltip] "${key}" style is managed internally and will be ignored.`);
371
+ delete config.style[key];
372
+ }
373
+ }
374
+
375
+ allButtons.forEach((btn) => {
376
+ btn.setAttribute('data-zero-tooltip', true);
377
+ tooltipConfigMap.set(btn, config);
378
+ });
379
+
380
+ if (!tooltipDiv) {
381
+ tooltipDiv = document.createElement('div');
382
+ tooltipDiv.style.opacity = '0';
383
+ document.body.appendChild(tooltipDiv);
384
+ }
385
+
386
+ function getPosition(btn, dir, offset) {
387
+ const { width, height, top, left } = btn.getBoundingClientRect();
388
+ let posX;
389
+ let posY;
390
+
391
+ if (dir === 'top') {
392
+ posX = left + width / 2 - tooltipDiv.offsetWidth / 2;
393
+ posY = top - tooltipDiv.offsetHeight - offset;
394
+ return { posX, posY };
395
+ }
396
+
397
+ if (dir === 'right') {
398
+ posX = left + width + offset;
399
+ posY = top + height / 2 - tooltipDiv.offsetHeight / 2;
400
+ return { posX, posY };
401
+ }
402
+
403
+ if (dir === 'bottom') {
404
+ posX = left + width / 2 - tooltipDiv.offsetWidth / 2;
405
+ posY = top + height + offset;
406
+ return { posX, posY };
407
+ }
408
+
409
+ if (dir === 'left') {
410
+ posX = left - tooltipDiv.offsetWidth - offset;
411
+ posY = top + height / 2 - tooltipDiv.offsetHeight / 2;
412
+ return { posX, posY };
413
+ }
414
+ }
415
+
416
+ if (!tooltipListenerAdded) {
417
+ document.addEventListener('mouseover', (e) => {
418
+ const btn = e.target.closest('[data-zero-tooltip]');
419
+ if (btn) {
420
+ const { tooltip, direction, offset, customClass, style, arrow } = tooltipConfigMap.get(btn);
421
+
422
+ tooltipDiv.removeAttribute('style');
423
+ Object.assign(tooltipDiv.style, {
424
+ position: 'fixed',
425
+ top: '0px',
426
+ left: '0px',
427
+ zIndex: '999999',
428
+ ...style,
429
+ });
430
+
431
+ const isArrowOn = arrow === 'on';
432
+ tooltipDiv.textContent = tooltip;
433
+ tooltipDiv.className = `zero-tooltip ${isArrowOn ? `zero-tooltip-${direction}` : ''} ${customClass.trim() ? customClass : ''}`;
434
+
435
+ if (isArrowOn) {
436
+ const color = getComputedStyle(tooltipDiv).backgroundColor;
437
+ tooltipDiv.style.setProperty('--tooltip-arrow-clr', color);
438
+ }
439
+
440
+ const { posX, posY } = getPosition(btn, direction, offset);
441
+ tooltipDiv.style.transform = `translate(${posX}px, ${posY}px)`;
442
+
443
+ requestAnimationFrame(() => {
444
+ tooltipDiv.classList.add('show');
445
+ });
446
+ }
447
+ });
448
+
449
+ document.addEventListener('mouseout', (e) => {
450
+ const btn = e.target.closest('[data-zero-tooltip]');
451
+ if (btn) {
452
+ tooltipDiv.classList.remove('show');
453
+ }
454
+ });
455
+
456
+ tooltipListenerAdded = true;
457
+ }
458
+ }
459
+
460
+ exports.kitzoCopy = copy;
461
+ exports.kitzoDebounce = debounce;
462
+ exports.kitzoRipple = ripple;
463
+ exports.kitzoTooltip = tooltip;
464
+
465
+ }));
@@ -0,0 +1,80 @@
1
+ export function kitzoTooltip(
2
+ element: string | Element | NodeListOf<Element> | HTMLCollection,
3
+ config?: {
4
+ /**
5
+ * The tooltip text to display (default: "Tool tip")
6
+ */
7
+ tooltip?: string;
8
+
9
+ /**
10
+ * Direction where tooltip appears: 'top', 'right', 'bottom', or 'left' (default: 'bottom')
11
+ */
12
+ direction?: 'top' | 'right' | 'bottom' | 'left';
13
+
14
+ /**
15
+ * Show an arrow pointing to the target ('on' or 'off', default: 'off')
16
+ */
17
+ arrow?: 'on' | 'off';
18
+
19
+ /**
20
+ * Distance in pixels between the tooltip and the target element (default: 10)
21
+ */
22
+ offset?: number;
23
+
24
+ /**
25
+ * Optional custom class to add to the tooltip (for styling)
26
+ */
27
+ customClass?: string;
28
+
29
+ /**
30
+ * Inline styles to apply (excluding top, left, right, bottom, position, zIndex, opacity, transform, translate, scale, rotate, perspective)
31
+ */
32
+ style?: Partial<CSSStyleDeclaration>;
33
+ }
34
+ ): void;
35
+
36
+ export function kitzoRipple(
37
+ element: string | Element | NodeListOf<Element> | HTMLCollection,
38
+ config?: {
39
+ /**
40
+ * Ripple opacity (0 to 1). Default: 0.5
41
+ */
42
+ opacity?: number;
43
+ /**
44
+ * Animation duration in seconds ('s'). Default: 1
45
+ */
46
+ duration?: number;
47
+ /**
48
+ * Ripple color (CSS color). Default: 'white'
49
+ */
50
+ color?: string;
51
+ /**
52
+ * Ripple size in pixels ('px'). If null, auto-scales based on button size. Default: null
53
+ */
54
+ size?: number | null;
55
+ }
56
+ ): void;
57
+
58
+ export function kitzoCopy(
59
+ element: string | Element | NodeListOf<Element>,
60
+ config: {
61
+ /**
62
+ * The text to be copied to the clipboard.
63
+ * Must be a non-empty string.
64
+ */
65
+ doc?: string;
66
+
67
+ /**
68
+ * The DOM event that triggers the copy action.
69
+ * Only the following events are allowed:
70
+ * - 'click' (default)
71
+ * - 'dblclick'
72
+ * - 'contextmenu'
73
+ * - 'mouseup'
74
+ * - 'touchend'
75
+ */
76
+ event?: 'click' | 'dblclick' | 'contextmenu' | 'mouseup' | 'touchend';
77
+ }
78
+ ): void;
79
+
80
+ export function debounce<Args extends any[]>(fn: (...args: Args) => any, delay?: number): (...args: Args) => void;
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "kitzo",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight JavaScript UI micro-library.",
5
+ "type": "module",
6
+ "main": "./dist/kitzo.umd.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/kitzo.esm.js",
10
+ "require": "./dist/kitzo.umd.js",
11
+ "types": "./dist/kitzo.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "dev": "vite",
19
+ "build": "rollup -c",
20
+ "preview": "vite preview"
21
+ },
22
+ "keywords": [
23
+ "tooltip",
24
+ "ripple",
25
+ "copy-button",
26
+ "micro-library",
27
+ "modular",
28
+ "ui",
29
+ "javascript",
30
+ "kitzo"
31
+ ],
32
+ "author": "Riyad",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/riyad-96/kitzo"
37
+ },
38
+ "homepage": "https://github.com/riyad-96/kitzo#readme",
39
+ "devDependencies": {
40
+ "rollup": "^4.46.2",
41
+ "vite": "^7.0.4"
42
+ }
43
+ }