slot-text 0.1.1 → 0.2.2
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 +26 -3
- package/dist/index.d.ts +19 -2
- package/dist/index.js +37 -2
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/slotText.d.ts +7 -0
- package/dist/slotText.js +28 -3
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +1 -1
- package/examples/basic/index.html +11 -8
- package/package.json +1 -1
- package/style.css +5 -0
package/README.md
CHANGED
|
@@ -76,9 +76,23 @@ const label = slotText(element, "Copy", options);
|
|
|
76
76
|
|
|
77
77
|
label.set("Copied");
|
|
78
78
|
label.set("Copy", { direction: "down" });
|
|
79
|
+
label.flash("Copied"); // rolls in, then auto-reverts to "Copy"
|
|
79
80
|
label.destroy();
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
`flash(text, options)` shows temporary text and rolls back automatically —
|
|
84
|
+
the classic Copy → Copied → Copy button in one call. It is spam-safe: repeat
|
|
85
|
+
flashes restart the revert timer instead of queuing extra rolls, and an
|
|
86
|
+
explicit `set()` cancels any pending revert.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
label.flash("Copied", {
|
|
90
|
+
revertAfter: 1400, // ms before rolling back (default 1400)
|
|
91
|
+
enter: { direction: "up", color: chromatic() }, // roll-in options
|
|
92
|
+
exit: { direction: "down" }, // roll-back options
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
82
96
|
Framework components:
|
|
83
97
|
|
|
84
98
|
```ts
|
|
@@ -109,9 +123,15 @@ type SlotOptions = {
|
|
|
109
123
|
color?: string | ((index: number, total: number) => string);
|
|
110
124
|
colorFade?: number;
|
|
111
125
|
skipUnchanged?: boolean;
|
|
126
|
+
interrupt?: boolean;
|
|
112
127
|
};
|
|
113
128
|
```
|
|
114
129
|
|
|
130
|
+
`interrupt: true` (default) cuts off any roll still in flight and starts
|
|
131
|
+
fresh. `interrupt: false` lets the current roll finish: the latest call made
|
|
132
|
+
mid-roll plays once it lands, and calls targeting the text already displayed
|
|
133
|
+
are dropped — ideal for spam-prone triggers like buttons.
|
|
134
|
+
|
|
115
135
|
Defaults are tuned for a soft, springy roll:
|
|
116
136
|
|
|
117
137
|
```ts
|
|
@@ -124,6 +144,7 @@ Defaults are tuned for a soft, springy roll:
|
|
|
124
144
|
bounce: 0.6,
|
|
125
145
|
colorFade: 280,
|
|
126
146
|
skipUnchanged: true,
|
|
147
|
+
interrupt: true,
|
|
127
148
|
}
|
|
128
149
|
```
|
|
129
150
|
|
|
@@ -136,13 +157,15 @@ Defaults are tuned for a soft, springy roll:
|
|
|
136
157
|
|
|
137
158
|
<script type="module">
|
|
138
159
|
import "slot-text/style.css";
|
|
139
|
-
import { slotText } from "slot-text";
|
|
160
|
+
import { slotText, chromatic } from "slot-text";
|
|
140
161
|
|
|
141
162
|
const label = slotText(document.querySelector("#copy-label"), "Copy");
|
|
142
163
|
|
|
143
164
|
document.querySelector("button").addEventListener("click", () => {
|
|
144
|
-
label.
|
|
145
|
-
|
|
165
|
+
label.flash("Copied", {
|
|
166
|
+
enter: { direction: "up", skipUnchanged: false, color: chromatic() },
|
|
167
|
+
exit: { direction: "down", skipUnchanged: false },
|
|
168
|
+
});
|
|
146
169
|
});
|
|
147
170
|
</script>
|
|
148
171
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
export { animateSlotText, buildSlotText, chromatic, clearSlotText, type ChromaticOptions, type SlotOptions, } from "./slotText";
|
|
2
|
-
import { type SlotOptions } from "./slotText";
|
|
1
|
+
export { animateSlotText, buildSlotText, chromatic, clearSlotText, type ChromaticOptions, type SlotOptions, } from "./slotText.js";
|
|
2
|
+
import { type SlotOptions } from "./slotText.js";
|
|
3
|
+
export interface FlashOptions {
|
|
4
|
+
/** How long the flash text stays before rolling back, in ms (default 1400). */
|
|
5
|
+
revertAfter?: number;
|
|
6
|
+
/** Roll options for the flash text rolling in. */
|
|
7
|
+
enter?: SlotOptions;
|
|
8
|
+
/** Roll options for the original text rolling back. */
|
|
9
|
+
exit?: SlotOptions;
|
|
10
|
+
}
|
|
3
11
|
export interface SlotTextController {
|
|
4
12
|
readonly element: HTMLElement;
|
|
13
|
+
/** The text currently displayed (the flash text while a flash is showing). */
|
|
5
14
|
readonly value: string;
|
|
15
|
+
/** Roll to new text. Cancels any pending flash revert. */
|
|
6
16
|
set(text: string, options?: SlotOptions): void;
|
|
17
|
+
/**
|
|
18
|
+
* Roll to temporary text, then roll back automatically — the classic
|
|
19
|
+
* Copy → Copied → Copy button in one call. Spam-safe: repeat flashes restart
|
|
20
|
+
* the revert timer instead of queuing extra rolls.
|
|
21
|
+
*/
|
|
22
|
+
flash(text: string, options?: FlashOptions): void;
|
|
7
23
|
destroy(): void;
|
|
8
24
|
}
|
|
9
25
|
/**
|
|
@@ -13,5 +29,6 @@ export interface SlotTextController {
|
|
|
13
29
|
*
|
|
14
30
|
* const label = slotText(buttonLabel, "Copy");
|
|
15
31
|
* label.set("Copied", { direction: "up" });
|
|
32
|
+
* label.flash("Copied", { revertAfter: 1400 }); // auto-reverts to "Copy"
|
|
16
33
|
*/
|
|
17
34
|
export declare function slotText(element: HTMLElement, initialText: string, options?: SlotOptions): SlotTextController;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { animateSlotText, buildSlotText, chromatic, clearSlotText, } from "./slotText";
|
|
2
|
-
import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText";
|
|
1
|
+
export { animateSlotText, buildSlotText, chromatic, clearSlotText, } from "./slotText.js";
|
|
2
|
+
import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText.js";
|
|
3
3
|
/**
|
|
4
4
|
* Create a text-roll controller for one element.
|
|
5
5
|
*
|
|
@@ -7,9 +7,12 @@ import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText";
|
|
|
7
7
|
*
|
|
8
8
|
* const label = slotText(buttonLabel, "Copy");
|
|
9
9
|
* label.set("Copied", { direction: "up" });
|
|
10
|
+
* label.flash("Copied", { revertAfter: 1400 }); // auto-reverts to "Copy"
|
|
10
11
|
*/
|
|
11
12
|
export function slotText(element, initialText, options = {}) {
|
|
12
13
|
let value = initialText;
|
|
14
|
+
let revertTimeout;
|
|
15
|
+
let restingText;
|
|
13
16
|
buildSlotText(element, initialText);
|
|
14
17
|
return {
|
|
15
18
|
element,
|
|
@@ -17,10 +20,42 @@ export function slotText(element, initialText, options = {}) {
|
|
|
17
20
|
return value;
|
|
18
21
|
},
|
|
19
22
|
set(text, nextOptions = {}) {
|
|
23
|
+
// An explicit set wins over a pending flash revert.
|
|
24
|
+
clearTimeout(revertTimeout);
|
|
25
|
+
restingText = undefined;
|
|
20
26
|
value = text;
|
|
21
27
|
animateSlotText(element, text, { ...options, ...nextOptions });
|
|
22
28
|
},
|
|
29
|
+
flash(text, { revertAfter = 1400, enter, exit } = {}) {
|
|
30
|
+
// Capture the resting text only on the first flash of a burst, so a
|
|
31
|
+
// flash-during-flash still reverts to the original label.
|
|
32
|
+
if (restingText === undefined) {
|
|
33
|
+
restingText = value;
|
|
34
|
+
}
|
|
35
|
+
// Flashes default to non-interrupting rolls: spam-friendly, no mid-roll
|
|
36
|
+
// cutoffs. Callers can still override via `enter`/`exit`.
|
|
37
|
+
value = text;
|
|
38
|
+
animateSlotText(element, text, {
|
|
39
|
+
...options,
|
|
40
|
+
interrupt: false,
|
|
41
|
+
...enter,
|
|
42
|
+
});
|
|
43
|
+
// Restart the revert timer: one revert per burst, after the last flash.
|
|
44
|
+
clearTimeout(revertTimeout);
|
|
45
|
+
revertTimeout = window.setTimeout(() => {
|
|
46
|
+
const back = restingText;
|
|
47
|
+
restingText = undefined;
|
|
48
|
+
revertTimeout = undefined;
|
|
49
|
+
value = back;
|
|
50
|
+
animateSlotText(element, back, {
|
|
51
|
+
...options,
|
|
52
|
+
interrupt: false,
|
|
53
|
+
...exit,
|
|
54
|
+
});
|
|
55
|
+
}, revertAfter);
|
|
56
|
+
},
|
|
23
57
|
destroy() {
|
|
58
|
+
clearTimeout(revertTimeout);
|
|
24
59
|
clearSlotText(element, value);
|
|
25
60
|
},
|
|
26
61
|
};
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type HTMLAttributes } from "react";
|
|
2
|
-
import { type SlotOptions } from "./slotText";
|
|
2
|
+
import { type SlotOptions } from "./slotText.js";
|
|
3
3
|
export interface SlotTextProps extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
4
4
|
text: string;
|
|
5
5
|
options?: SlotOptions;
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement, forwardRef, useEffect, useImperativeHandle, useRef, } from "react";
|
|
2
|
-
import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText";
|
|
2
|
+
import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText.js";
|
|
3
3
|
export const SlotText = forwardRef(({ text, options, "aria-label": ariaLabel, ...props }, forwardedRef) => {
|
|
4
4
|
const elementRef = useRef(null);
|
|
5
5
|
const mountedRef = useRef(false);
|
package/dist/slotText.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export interface SlotOptions {
|
|
|
44
44
|
* uniformly instead of leaving stray letters frozen.
|
|
45
45
|
*/
|
|
46
46
|
skipUnchanged?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* true (default): a new call interrupts any roll in flight, snapping it to
|
|
49
|
+
* its target before starting fresh. false: the current roll finishes and the
|
|
50
|
+
* latest call made mid-roll plays after it lands; calls targeting the text
|
|
51
|
+
* already displayed are dropped. Ideal for spam-prone triggers like buttons.
|
|
52
|
+
*/
|
|
53
|
+
interrupt?: boolean;
|
|
47
54
|
}
|
|
48
55
|
export interface ChromaticOptions {
|
|
49
56
|
from?: number;
|
package/dist/slotText.js
CHANGED
|
@@ -20,6 +20,7 @@ const DEFAULTS = {
|
|
|
20
20
|
bounce: 0.6,
|
|
21
21
|
colorFade: 280,
|
|
22
22
|
skipUnchanged: true,
|
|
23
|
+
interrupt: true,
|
|
23
24
|
};
|
|
24
25
|
const NBSP = "\u00A0";
|
|
25
26
|
const glyph = (char) => (char === " " ? NBSP : char);
|
|
@@ -71,10 +72,20 @@ export function buildSlotText(container, text) {
|
|
|
71
72
|
container.replaceChildren(...Array.from(text, buildSlot));
|
|
72
73
|
}
|
|
73
74
|
export function animateSlotText(container, toText, options = {}) {
|
|
74
|
-
const { direction, stagger, duration, exitOffset, easing, bounce, color, colorFade, skipUnchanged, } = {
|
|
75
|
+
const { direction, stagger, duration, exitOffset, easing, bounce, color, colorFade, skipUnchanged, interrupt, } = {
|
|
75
76
|
...DEFAULTS,
|
|
76
77
|
...options,
|
|
77
78
|
};
|
|
79
|
+
// Non-interrupting mode: if a roll is already in flight, let it finish and
|
|
80
|
+
// remember this request instead. Only the latest request survives, so spam
|
|
81
|
+
// taps coalesce into a single follow-up roll once the current one lands.
|
|
82
|
+
const running = states.get(container);
|
|
83
|
+
if (running && !interrupt) {
|
|
84
|
+
if (toText !== running.target) {
|
|
85
|
+
running.pending = { text: toText, options };
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
78
89
|
// Interrupt: if a previous roll is still running, fast-forward it to its
|
|
79
90
|
// target and tear down its timers before we start fresh. This is what kills
|
|
80
91
|
// the "switch bun→npm mid-animation" glitch.
|
|
@@ -86,6 +97,10 @@ export function animateSlotText(container, toText, options = {}) {
|
|
|
86
97
|
}
|
|
87
98
|
const slots = Array.from(container.querySelectorAll(".char-slot"));
|
|
88
99
|
const fromText = slots.map((s) => s.dataset.char ?? "").join("");
|
|
100
|
+
// Non-interrupting mode also drops rolls to the text already on screen, so
|
|
101
|
+
// repeated triggers do not visibly re-roll an unchanged label.
|
|
102
|
+
if (!interrupt && fromText === toText)
|
|
103
|
+
return;
|
|
89
104
|
const maxLen = Math.max(fromText.length, toText.length);
|
|
90
105
|
// Whole-pixel slide distance = one cell height, so glyphs clip cleanly.
|
|
91
106
|
// If layout has not produced dimensions yet, fall back to line-height/font-size
|
|
@@ -98,7 +113,9 @@ export function animateSlotText(container, toText, options = {}) {
|
|
|
98
113
|
sample?.offsetHeight ||
|
|
99
114
|
container.getBoundingClientRect().height ||
|
|
100
115
|
parseFloat(cs.lineHeight) ||
|
|
101
|
-
0) ||
|
|
116
|
+
0) ||
|
|
117
|
+
Math.ceil(parseFloat(cs.fontSize) * 1.3) ||
|
|
118
|
+
18;
|
|
102
119
|
// Resting color to settle the chromatic flash back to.
|
|
103
120
|
const restColor = color ? cs.color : "";
|
|
104
121
|
// Pre-create any extra cells up front so the row never reflows mid-roll.
|
|
@@ -160,7 +177,9 @@ export function animateSlotText(container, toText, options = {}) {
|
|
|
160
177
|
const base = Math.round(staggerIndex * stagger * (1 + bounce * 0.25 * wobble(i, 2)));
|
|
161
178
|
const tilt = (bounce * 5 * wobble(i, 3)).toFixed(2);
|
|
162
179
|
const rollTrans = `transform ${d}ms ${easing}`;
|
|
163
|
-
const trans = color
|
|
180
|
+
const trans = color
|
|
181
|
+
? `${rollTrans}, color ${colorFade}ms linear ${d}ms`
|
|
182
|
+
: rollTrans;
|
|
164
183
|
const newFace = makeFace(toChar);
|
|
165
184
|
newFace.style.transformOrigin = "50% 50%";
|
|
166
185
|
newFace.style.transform = `translateY(${inStart}px) rotate(${tilt}deg)`;
|
|
@@ -228,10 +247,16 @@ export function animateSlotText(container, toText, options = {}) {
|
|
|
228
247
|
}, base + exitOffset));
|
|
229
248
|
}
|
|
230
249
|
// Safety net: snap to a pristine DOM once the slowest letter has settled.
|
|
250
|
+
// If a non-interrupting call was deferred mid-roll, replay it now — it runs
|
|
251
|
+
// as a fresh roll from this clean baseline.
|
|
231
252
|
const total = maxEnd + 80;
|
|
232
253
|
timers.push(window.setTimeout(() => {
|
|
254
|
+
const pending = state.pending;
|
|
233
255
|
states.delete(container);
|
|
234
256
|
buildSlotText(container, toText);
|
|
257
|
+
if (pending) {
|
|
258
|
+
animateSlotText(container, pending.text, pending.options);
|
|
259
|
+
}
|
|
235
260
|
}, total));
|
|
236
261
|
}
|
|
237
262
|
export function clearSlotText(container, text = "") {
|
package/dist/vue.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PropType } from "vue";
|
|
2
|
-
import { type SlotOptions } from "./slotText";
|
|
2
|
+
import { type SlotOptions } from "./slotText.js";
|
|
3
3
|
export declare const SlotText: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
4
4
|
text: {
|
|
5
5
|
type: StringConstructor;
|
package/dist/vue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent, h, onBeforeUnmount, onMounted, ref, watch, } from "vue";
|
|
2
|
-
import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText";
|
|
2
|
+
import { animateSlotText, buildSlotText, clearSlotText, } from "./slotText.js";
|
|
3
3
|
export const SlotText = defineComponent({
|
|
4
4
|
name: "SlotText",
|
|
5
5
|
inheritAttrs: false,
|
|
@@ -37,15 +37,18 @@
|
|
|
37
37
|
const label = slotText(document.querySelector("#copy-label"), "Copy");
|
|
38
38
|
|
|
39
39
|
button.addEventListener("click", () => {
|
|
40
|
-
label.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
label.flash("Copied", {
|
|
41
|
+
enter: {
|
|
42
|
+
direction: "up",
|
|
43
|
+
skipUnchanged: false,
|
|
44
|
+
color: chromatic({ from: 190 }),
|
|
45
|
+
},
|
|
46
|
+
exit: {
|
|
47
|
+
direction: "down",
|
|
48
|
+
skipUnchanged: false,
|
|
49
|
+
color: chromatic({ from: 190 }),
|
|
50
|
+
},
|
|
44
51
|
});
|
|
45
|
-
|
|
46
|
-
window.setTimeout(() => {
|
|
47
|
-
label.set("Copy", { direction: "down", skipUnchanged: false });
|
|
48
|
-
}, 1400);
|
|
49
52
|
});
|
|
50
53
|
</script>
|
|
51
54
|
</body>
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
.char-slot {
|
|
7
7
|
position: relative;
|
|
8
8
|
display: inline-flex;
|
|
9
|
+
/* Cells must never flex-shrink. When a cell's overflow stops being visible
|
|
10
|
+
(e.g. .is-resizing), its automatic minimum size drops to 0, and a
|
|
11
|
+
width-constrained line would crush those cells into overlapping slivers
|
|
12
|
+
instead of letting the row overflow like it does at rest. */
|
|
13
|
+
flex: none;
|
|
9
14
|
justify-content: center;
|
|
10
15
|
/* Clip only vertically: the roll needs a top/bottom mask, but glyph side
|
|
11
16
|
bearings, kerning overhang and the settle tilt must stay visible so
|