slot-text 0.2.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,189 +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.flash("Copied"); // rolls in, then auto-reverts to "Copy"
80
- label.destroy();
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.
90
+ ## 🔷 Solid
87
91
 
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
-
96
- Framework components:
97
-
98
- ```ts
99
- import { SlotText as ReactSlotText } from "slot-text/react";
100
- import { SlotText as VueSlotText } from "slot-text/vue";
101
- ```
92
+ ```tsx
93
+ import "slot-text/style.css";
94
+ import { createSignal } from "solid-js";
95
+ import { slotText } from "slot-text/solid";
102
96
 
103
- Low-level helpers:
97
+ const [label, setLabel] = createSignal("Copy");
104
98
 
105
- ```ts
106
- import {
107
- buildSlotText,
108
- animateSlotText,
109
- chromatic,
110
- } from "slot-text";
99
+ <button aria-label={label()} onClick={() => setLabel("Copied")}>
100
+ <span use:slotText={{ text: label(), options: { direction: "up" } }} />
101
+ </button>
111
102
  ```
112
103
 
113
- ## Options
104
+ ## 🧡 Svelte
114
105
 
115
- ```ts
116
- type SlotOptions = {
117
- direction?: "up" | "down";
118
- stagger?: number;
119
- duration?: number;
120
- exitOffset?: number;
121
- easing?: string;
122
- bounce?: number;
123
- color?: string | ((index: number, total: number) => string);
124
- colorFade?: number;
125
- skipUnchanged?: boolean;
126
- interrupt?: boolean;
127
- };
128
- ```
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.
106
+ ```svelte
107
+ <script lang="ts">
108
+ import "slot-text/style.css";
109
+ import { slotText } from "slot-text/svelte";
134
110
 
135
- Defaults are tuned for a soft, springy roll:
111
+ let label = "Copy";
112
+ </script>
136
113
 
137
- ```ts
138
- {
139
- direction: "down",
140
- stagger: 45,
141
- duration: 300,
142
- exitOffset: 50,
143
- easing: "cubic-bezier(0.34, 1.56, 0.64, 1)",
144
- bounce: 0.6,
145
- colorFade: 280,
146
- skipUnchanged: true,
147
- interrupt: true,
148
- }
114
+ <button aria-label={label} on:click={() => label = "Copied"}>
115
+ <span use:slotText={{ text: label, options: { direction: "up" } }}></span>
116
+ </button>
149
117
  ```
150
118
 
151
- ## Example
119
+ ## ⚙️ Options
152
120
 
153
- ```html
154
- <button>
155
- <span id="copy-label"></span>
156
- </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 👇 |
157
133
 
158
- <script type="module">
159
- import "slot-text/style.css";
160
- import { slotText, chromatic } from "slot-text";
134
+ ### 🛑 `interrupt`
161
135
 
162
- 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.
163
139
 
164
- document.querySelector("button").addEventListener("click", () => {
165
- label.flash("Copied", {
166
- enter: { direction: "up", skipUnchanged: false, color: chromatic() },
167
- exit: { direction: "down", skipUnchanged: false },
168
- });
169
- });
170
- </script>
171
- ```
140
+ ### 🌈 `chromatic()`
172
141
 
173
- ## Font support
142
+ Built-in rainbow color helper — pass it as `color` for a per-character hue sweep.
174
143
 
175
- Each character animates inside its own measured cell, sized with the exact
176
- font you give the element — so widths are always correct.
144
+ ## 🔤 Font support
177
145
 
178
- Works great with:
146
+ Each character animates in its own measured cell using your element's exact
147
+ font, so widths are always correct.
179
148
 
180
- - **Monospace fonts** perfect fit, every cell is identical.
181
- - **Proportional Latin / Cyrillic / Greek fonts** (Geist, Inter, SF, …)
182
- including italics and glyphs with overhang.
149
+ **Great with:** monospace fonts, proportional Latin / Cyrillic / Greek
150
+ (Geist, Inter, SF, …), italics, glyphs with overhang.
183
151
 
184
- Known tradeoffs (inherent to any per-character slot animation):
152
+ ⚠️ **Tradeoffs** (inherent to any per-character slot animation):
185
153
 
186
- - **Kerning is lost.** Each glyph is its own box, so pairs like `AV` or `To`
187
- sit slightly looser than in plain text. Invisible at UI label sizes,
188
- noticeable at large display sizes with kerning-heavy fonts.
189
- - **Ligatures won't form** `fi`, `fl`, or coding ligatures stay separate.
190
- - **Joined scripts are unsupported.** Arabic, Devanagari and other shaping
191
- scripts need contextual letterforms across the string and will render as
192
- isolated forms.
193
- - **Complex emoji split.** Single emoji are fine (surrogate pairs are
194
- handled), but ZWJ sequences (👨‍👩‍👧) and combining marks break into
195
- multiple cells.
196
- - **Very tall display/script fonts** may clip at the vertical roll mask,
197
- 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`).
198
159
 
199
- In short: ideal for short Latin labels, numbers, statuses and commands — in
160
+ **In short:** ideal for short labels, numbers, statuses and commands — in
200
161
  essentially any font you'd use for those.
201
162
 
202
- ## Notes
163
+ ## 📝 Notes
203
164
 
204
- - Browser-only DOM utility.
205
- - Core API has no runtime dependencies.
206
- - React and Vue are optional peer dependencies. Plain JS users do not need them.
207
- - 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.
208
168
  - Import the CSS once before using the animation.
169
+ - Low-level helpers also exported: `buildSlotText`, `animateSlotText`, `chromatic`.
@@ -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
+ }
@@ -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.2.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;