slot-text 0.1.2 → 0.3.1

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,18 +1,25 @@
1
1
  # slot-text
2
2
 
3
- Dependency-free text roll animation for tiny, tactile UI labels.
3
+ <p>
4
+ <a href="https://www.npmjs.com/package/slot-text"><img src="https://img.shields.io/npm/v/slot-text?color=cb3837&label=npm" alt="npm version"></a>
5
+ <img src="https://img.shields.io/badge/dependencies-zero-22c55e" alt="zero dependencies">
6
+ <img src="https://img.shields.io/bundlephobia/minzip/slot-text?color=8b5cf6&label=size" alt="bundle size">
7
+ <img src="https://img.shields.io/badge/react-✓-61dafb" alt="react">
8
+ <img src="https://img.shields.io/badge/vue-✓-42b883" alt="vue">
9
+ <img src="https://img.shields.io/badge/solid-✓-2c4f7c" alt="solid">
10
+ <img src="https://img.shields.io/badge/svelte-✓-ff3e00" alt="svelte">
11
+ <img src="https://img.shields.io/badge/typescript-✓-3178c6" alt="typescript">
12
+ </p>
4
13
 
5
- ![slot-text usage card](./assets/usage.svg)
14
+ Dependency-free text roll animation for tiny, tactile UI labels.
6
15
 
7
- ## Install
16
+ ## 📦 Install
8
17
 
9
18
  ```bash
10
19
  npm install slot-text
11
20
  ```
12
21
 
13
- ## Use
14
-
15
- ### Vanilla
22
+ ## 🚀 Quick start
16
23
 
17
24
  ```ts
18
25
  import "slot-text/style.css";
@@ -20,166 +27,143 @@ import { slotText, chromatic } from "slot-text";
20
27
 
21
28
  const label = slotText(document.querySelector("#copy")!, "Copy");
22
29
 
23
- label.set("Copied", {
24
- direction: "up",
25
- color: chromatic(),
30
+ // the classic Copy → Copied → Copy, in one call
31
+ label.flash("Copied", { enter: { color: chromatic() } });
32
+ ```
33
+
34
+ That's it — import the CSS once, attach to an element, roll text.
35
+
36
+ ## 🧩 API
37
+
38
+ | Method | What it does |
39
+ | --- | --- |
40
+ | `set(text, options?)` | Roll to new text — the text **stays** |
41
+ | `flash(text, options?)` | Roll in, then **auto-revert** to the previous text |
42
+ | `destroy()` | Clean up |
43
+
44
+ ```ts
45
+ const label = slotText(element, "Copy", options);
46
+
47
+ label.set("Copied"); // permanent change
48
+ label.set("Copy", { direction: "down" });
49
+ label.flash("Copied"); // temporary — rolls back after 1.4s
50
+ label.destroy();
51
+ ```
52
+
53
+ > 💡 **`flash` is spam-safe** — repeat clicks restart the revert timer instead
54
+ > of queuing extra rolls, and an explicit `set()` cancels any pending revert.
55
+
56
+ ```ts
57
+ label.flash("Copied", {
58
+ revertAfter: 1400, // ms before rolling back
59
+ enter: { direction: "up", color: chromatic() }, // roll-in
60
+ exit: { direction: "down" }, // roll-back
26
61
  });
27
62
  ```
28
63
 
29
- ### React
64
+ ## ⚛️ React
30
65
 
31
66
  ```tsx
32
67
  import "slot-text/style.css";
33
68
  import { SlotText } from "slot-text/react";
34
69
  import { chromatic } from "slot-text";
35
70
 
36
- export function CopyLabel({ copied }: { copied: boolean }) {
37
- return (
38
- <SlotText
39
- text={copied ? "Copied" : "Copy"}
40
- options={{
41
- direction: copied ? "up" : "down",
42
- skipUnchanged: false,
43
- color: copied ? chromatic() : undefined,
44
- }}
45
- />
46
- );
47
- }
71
+ <SlotText
72
+ text={copied ? "Copied" : "Copy"}
73
+ options={{ direction: copied ? "up" : "down", color: copied ? chromatic() : undefined }}
74
+ />
48
75
  ```
49
76
 
50
- ### Vue
77
+ ## 💚 Vue
51
78
 
52
79
  ```vue
53
80
  <script setup lang="ts">
54
81
  import "slot-text/style.css";
55
82
  import { SlotText } from "slot-text/vue";
56
- import { chromatic } from "slot-text";
57
-
58
- const options = {
59
- direction: "up",
60
- skipUnchanged: false,
61
- color: chromatic(),
62
- } as const;
63
83
  </script>
64
84
 
65
85
  <template>
66
- <SlotText text="Copied" :options="options" />
86
+ <SlotText text="Copied" :options="{ direction: 'up' }" />
67
87
  </template>
68
88
  ```
69
89
 
70
- ## API
71
-
72
- Vanilla controller:
73
-
74
- ```ts
75
- const label = slotText(element, "Copy", options);
76
-
77
- label.set("Copied");
78
- label.set("Copy", { direction: "down" });
79
- label.destroy();
80
- ```
81
-
82
- Framework components:
90
+ ## 🔷 Solid
83
91
 
84
- ```ts
85
- import { SlotText as ReactSlotText } from "slot-text/react";
86
- import { SlotText as VueSlotText } from "slot-text/vue";
87
- ```
92
+ ```tsx
93
+ import "slot-text/style.css";
94
+ import { createSignal } from "solid-js";
95
+ import { slotText } from "slot-text/solid";
88
96
 
89
- Low-level helpers:
97
+ const [label, setLabel] = createSignal("Copy");
90
98
 
91
- ```ts
92
- import {
93
- buildSlotText,
94
- animateSlotText,
95
- chromatic,
96
- } from "slot-text";
99
+ <button aria-label={label()} onClick={() => setLabel("Copied")}>
100
+ <span use:slotText={{ text: label(), options: { direction: "up" } }} />
101
+ </button>
97
102
  ```
98
103
 
99
- ## Options
104
+ ## 🧡 Svelte
100
105
 
101
- ```ts
102
- type SlotOptions = {
103
- direction?: "up" | "down";
104
- stagger?: number;
105
- duration?: number;
106
- exitOffset?: number;
107
- easing?: string;
108
- bounce?: number;
109
- color?: string | ((index: number, total: number) => string);
110
- colorFade?: number;
111
- skipUnchanged?: boolean;
112
- };
113
- ```
106
+ ```svelte
107
+ <script lang="ts">
108
+ import "slot-text/style.css";
109
+ import { slotText } from "slot-text/svelte";
114
110
 
115
- Defaults are tuned for a soft, springy roll:
111
+ let label = "Copy";
112
+ </script>
116
113
 
117
- ```ts
118
- {
119
- direction: "down",
120
- stagger: 45,
121
- duration: 300,
122
- exitOffset: 50,
123
- easing: "cubic-bezier(0.34, 1.56, 0.64, 1)",
124
- bounce: 0.6,
125
- colorFade: 280,
126
- skipUnchanged: true,
127
- }
114
+ <button aria-label={label} on:click={() => label = "Copied"}>
115
+ <span use:slotText={{ text: label, options: { direction: "up" } }}></span>
116
+ </button>
128
117
  ```
129
118
 
130
- ## Example
119
+ ## ⚙️ Options
131
120
 
132
- ```html
133
- <button>
134
- <span id="copy-label"></span>
135
- </button>
121
+ | Option | Default | Description |
122
+ | --- | --- | --- |
123
+ | `direction` | `"down"` | Roll direction: `"up"` or `"down"` |
124
+ | `stagger` | `45` | Delay between characters (ms) |
125
+ | `duration` | `300` | Per-character animation time (ms) |
126
+ | `exitOffset` | `50` | Delay before the old character exits (ms) |
127
+ | `easing` | springy bezier | CSS easing function |
128
+ | `bounce` | `0.6` | Overshoot amount |
129
+ | `color` | — | Color string, or `(index, total) => string` |
130
+ | `colorFade` | `280` | Fade back to base color (ms) |
131
+ | `skipUnchanged` | `true` | Don't re-roll identical characters |
132
+ | `interrupt` | `true` | See below 👇 |
136
133
 
137
- <script type="module">
138
- import "slot-text/style.css";
139
- import { slotText } from "slot-text";
134
+ ### 🛑 `interrupt`
140
135
 
141
- const label = slotText(document.querySelector("#copy-label"), "Copy");
136
+ - `interrupt: true` *(default)* — cuts off any roll in flight, starts fresh.
137
+ - `interrupt: false` — lets the current roll **finish**; the latest call plays
138
+ after it lands, duplicates are dropped. Ideal for spam-prone buttons.
142
139
 
143
- document.querySelector("button").addEventListener("click", () => {
144
- label.set("Copied", { direction: "up", skipUnchanged: false });
145
- window.setTimeout(() => label.set("Copy"), 1400);
146
- });
147
- </script>
148
- ```
140
+ ### 🌈 `chromatic()`
149
141
 
150
- ## Font support
142
+ Built-in rainbow color helper — pass it as `color` for a per-character hue sweep.
151
143
 
152
- Each character animates inside its own measured cell, sized with the exact
153
- font you give the element — so widths are always correct.
144
+ ## 🔤 Font support
154
145
 
155
- Works great with:
146
+ Each character animates in its own measured cell using your element's exact
147
+ font, so widths are always correct.
156
148
 
157
- - **Monospace fonts** perfect fit, every cell is identical.
158
- - **Proportional Latin / Cyrillic / Greek fonts** (Geist, Inter, SF, …)
159
- including italics and glyphs with overhang.
149
+ **Great with:** monospace fonts, proportional Latin / Cyrillic / Greek
150
+ (Geist, Inter, SF, …), italics, glyphs with overhang.
160
151
 
161
- Known tradeoffs (inherent to any per-character slot animation):
152
+ ⚠️ **Tradeoffs** (inherent to any per-character slot animation):
162
153
 
163
- - **Kerning is lost.** Each glyph is its own box, so pairs like `AV` or `To`
164
- sit slightly looser than in plain text. Invisible at UI label sizes,
165
- noticeable at large display sizes with kerning-heavy fonts.
166
- - **Ligatures won't form** `fi`, `fl`, or coding ligatures stay separate.
167
- - **Joined scripts are unsupported.** Arabic, Devanagari and other shaping
168
- scripts need contextual letterforms across the string and will render as
169
- isolated forms.
170
- - **Complex emoji split.** Single emoji are fine (surrogate pairs are
171
- handled), but ZWJ sequences (👨‍👩‍👧) and combining marks break into
172
- multiple cells.
173
- - **Very tall display/script fonts** may clip at the vertical roll mask,
174
- which is sized to `line-height: 1.3`.
154
+ - Kerning is lost pairs like `AV` sit slightly looser (invisible at label sizes).
155
+ - Ligatures won't form (`fi`, `fl`, coding ligatures).
156
+ - Joined scripts (Arabic, Devanagari) render as isolated forms.
157
+ - ZWJ emoji sequences (👨‍👩‍👧) split into cells; single emoji are fine.
158
+ - Very tall display fonts may clip at the roll mask (`line-height: 1.3`).
175
159
 
176
- In short: ideal for short Latin labels, numbers, statuses and commands — in
160
+ **In short:** ideal for short labels, numbers, statuses and commands — in
177
161
  essentially any font you'd use for those.
178
162
 
179
- ## Notes
163
+ ## 📝 Notes
180
164
 
181
- - Browser-only DOM utility.
182
- - Core API has no runtime dependencies.
183
- - React and Vue are optional peer dependencies. Plain JS users do not need them.
184
- - Works best on short labels, buttons, counters, and command text.
165
+ - Browser-only DOM utility, zero runtime dependencies.
166
+ - React, Vue, Solid, and Svelte are optional peer dependencies — plain JS users
167
+ don't need them.
185
168
  - Import the CSS once before using the animation.
169
+ - Low-level helpers also exported: `buildSlotText`, `animateSlotText`, `chromatic`.
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);
@@ -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) || Math.ceil(parseFloat(cs.fontSize) * 1.3) || 18;
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 ? `${rollTrans}, color ${colorFade}ms linear ${d}ms` : rollTrans;
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 = "") {
@@ -0,0 +1,14 @@
1
+ import { type Accessor } from "solid-js";
2
+ import { type SlotOptions } from "./index.js";
3
+ export interface SlotTextParams {
4
+ text: string;
5
+ options?: SlotOptions;
6
+ }
7
+ export declare function slotText(element: HTMLElement, accessor: Accessor<SlotTextParams>): void;
8
+ declare module "solid-js" {
9
+ namespace JSX {
10
+ interface Directives {
11
+ slotText: SlotTextParams;
12
+ }
13
+ }
14
+ }
package/dist/solid.js ADDED
@@ -0,0 +1,22 @@
1
+ import { createRenderEffect, onCleanup } from "solid-js";
2
+ import { slotText as createSlotText } from "./index.js";
3
+ export function slotText(element, accessor) {
4
+ const initial = accessor();
5
+ const controller = createSlotText(element, initial.text);
6
+ let previousText = initial.text;
7
+ let isFirstRun = true;
8
+ createRenderEffect(() => {
9
+ const next = accessor();
10
+ if (isFirstRun) {
11
+ isFirstRun = false;
12
+ return;
13
+ }
14
+ if (next.text !== previousText) {
15
+ previousText = next.text;
16
+ controller.set(next.text, next.options);
17
+ }
18
+ });
19
+ onCleanup(() => {
20
+ controller.destroy();
21
+ });
22
+ }
@@ -0,0 +1,9 @@
1
+ import { type SlotOptions } from "./index.js";
2
+ export interface SlotTextParams {
3
+ text: string;
4
+ options?: SlotOptions;
5
+ }
6
+ export declare function slotText(element: HTMLElement, params: SlotTextParams): {
7
+ update(params: SlotTextParams): void;
8
+ destroy(): void;
9
+ };
package/dist/svelte.js ADDED
@@ -0,0 +1,16 @@
1
+ import { slotText as createSlotText } from "./index.js";
2
+ export function slotText(element, params) {
3
+ const controller = createSlotText(element, params.text);
4
+ let previousText = params.text;
5
+ return {
6
+ update(params) {
7
+ if (params.text === previousText)
8
+ return;
9
+ previousText = params.text;
10
+ controller.set(params.text, params.options);
11
+ },
12
+ destroy() {
13
+ controller.destroy();
14
+ },
15
+ };
16
+ }
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.set("Copied", {
41
- direction: "up",
42
- skipUnchanged: false,
43
- color: chromatic({ from: 190 }),
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>
@@ -0,0 +1 @@
1
+ declare module "slot-text/style.css";
@@ -0,0 +1,30 @@
1
+ import { createSignal } from "solid-js";
2
+ import "slot-text/style.css";
3
+ import { chromatic } from "slot-text";
4
+ import { slotText } from "slot-text/solid";
5
+
6
+ void slotText;
7
+
8
+ export function CopyButton() {
9
+ const [copied, setCopied] = createSignal(false);
10
+
11
+ function copy() {
12
+ setCopied(true);
13
+ window.setTimeout(() => setCopied(false), 1400);
14
+ }
15
+
16
+ return (
17
+ <button type="button" aria-label={copied() ? "Copied" : "Copy"} onClick={copy}>
18
+ <span
19
+ use:slotText={{
20
+ text: copied() ? "Copied" : "Copy",
21
+ options: {
22
+ direction: copied() ? "up" : "down",
23
+ skipUnchanged: false,
24
+ color: copied() ? chromatic({ from: 190 }) : undefined,
25
+ },
26
+ }}
27
+ />
28
+ </button>
29
+ );
30
+ }
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import "slot-text/style.css";
3
+ import { chromatic } from "slot-text";
4
+ import { slotText } from "slot-text/svelte";
5
+
6
+ let copied = false;
7
+
8
+ function copy() {
9
+ copied = true;
10
+ window.setTimeout(() => {
11
+ copied = false;
12
+ }, 1400);
13
+ }
14
+ </script>
15
+
16
+ <button type="button" aria-label={copied ? "Copied" : "Copy"} on:click={copy}>
17
+ <span
18
+ use:slotText={{
19
+ text: copied ? "Copied" : "Copy",
20
+ options: {
21
+ direction: copied ? "up" : "down",
22
+ skipUnchanged: false,
23
+ color: copied ? chromatic({ from: 190 }) : undefined,
24
+ },
25
+ }}
26
+ ></span>
27
+ </button>
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2020", "DOM"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "jsx": "preserve",
10
+ "jsxImportSource": "solid-js",
11
+ "paths": {
12
+ "slot-text": ["../src/index.ts"],
13
+ "slot-text/solid": ["../src/solid.ts"],
14
+ "slot-text/svelte": ["../src/svelte.ts"]
15
+ }
16
+ },
17
+ "include": ["env.d.ts", "solid.tsx", "*.svelte", "../src/**/*.ts"]
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slot-text",
3
- "version": "0.1.2",
3
+ "version": "0.3.1",
4
4
  "description": "Dependency-free text roll animation for tiny, tactile UI labels.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "dist",
14
14
  "examples",
15
15
  "style.css",
16
+ "style.css.d.ts",
16
17
  "README.md",
17
18
  "LICENSE"
18
19
  ],
@@ -29,11 +30,24 @@
29
30
  "types": "./dist/vue.d.ts",
30
31
  "import": "./dist/vue.js"
31
32
  },
32
- "./style.css": "./style.css"
33
+ "./solid": {
34
+ "types": "./dist/solid.d.ts",
35
+ "import": "./dist/solid.js"
36
+ },
37
+ "./svelte": {
38
+ "types": "./dist/svelte.d.ts",
39
+ "import": "./dist/svelte.js"
40
+ },
41
+ "./style.css": {
42
+ "types": "./style.css.d.ts",
43
+ "default": "./style.css"
44
+ }
33
45
  },
34
46
  "types": "./dist/index.d.ts",
35
47
  "peerDependencies": {
36
48
  "react": ">=18 <20",
49
+ "solid-js": ">=1.8 <2",
50
+ "svelte": ">=4 <6",
37
51
  "vue": ">=3.4 <4"
38
52
  },
39
53
  "peerDependenciesMeta": {
@@ -42,17 +56,29 @@
42
56
  },
43
57
  "vue": {
44
58
  "optional": true
59
+ },
60
+ "solid-js": {
61
+ "optional": true
62
+ },
63
+ "svelte": {
64
+ "optional": true
45
65
  }
46
66
  },
47
67
  "scripts": {
68
+ "prepublishOnly": "npm run build",
48
69
  "build": "tsc -p tsconfig.json",
49
70
  "check": "tsc -p tsconfig.json --noEmit",
50
- "pack:check": "npm pack --dry-run"
71
+ "pack:check": "npm pack --dry-run",
72
+ "test": "vitest run"
51
73
  },
52
74
  "devDependencies": {
53
75
  "@types/react": "^19.2.17",
76
+ "happy-dom": "^20.10.2",
54
77
  "react": "^19.2.7",
78
+ "solid-js": "^1.9.13",
79
+ "svelte": "^5.56.3",
55
80
  "typescript": "^5.8.3",
81
+ "vitest": "^4.1.8",
56
82
  "vue": "^3.5.35"
57
83
  }
58
84
  }
package/style.css.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare const stylesheet: string;
2
+ export default stylesheet;