slot-text 0.2.2 → 0.3.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 CHANGED
@@ -1,208 +1,191 @@
1
1
  # slot-text
2
2
 
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>
13
+
3
14
  Dependency-free text roll animation for tiny, tactile UI labels.
4
15
 
5
- ![slot-text usage card](./assets/usage.svg)
16
+ ## 📦 Install
17
+
18
+ Choose your preferred package manager:
6
19
 
7
- ## Install
20
+ **npm**
8
21
 
9
22
  ```bash
10
23
  npm install slot-text
11
24
  ```
12
25
 
13
- ## Use
26
+ **pnpm**
14
27
 
15
- ### Vanilla
28
+ ```bash
29
+ pnpm add slot-text
30
+ ```
16
31
 
17
- ```ts
18
- import "slot-text/style.css";
19
- import { slotText, chromatic } from "slot-text";
32
+ **Bun**
20
33
 
21
- const label = slotText(document.querySelector("#copy")!, "Copy");
22
-
23
- label.set("Copied", {
24
- direction: "up",
25
- color: chromatic(),
26
- });
34
+ ```bash
35
+ bun add slot-text
27
36
  ```
28
37
 
29
- ### React
38
+ **Yarn**
30
39
 
31
- ```tsx
32
- import "slot-text/style.css";
33
- import { SlotText } from "slot-text/react";
34
- import { chromatic } from "slot-text";
35
-
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
- }
40
+ ```bash
41
+ yarn add slot-text
48
42
  ```
49
43
 
50
- ### Vue
44
+ ## 🚀 Quick start
51
45
 
52
- ```vue
53
- <script setup lang="ts">
46
+ ```ts
54
47
  import "slot-text/style.css";
55
- import { SlotText } from "slot-text/vue";
56
- import { chromatic } from "slot-text";
48
+ import { slotText, chromatic } from "slot-text";
57
49
 
58
- const options = {
59
- direction: "up",
60
- skipUnchanged: false,
61
- color: chromatic(),
62
- } as const;
63
- </script>
50
+ const label = slotText(document.querySelector("#copy")!, "Copy");
64
51
 
65
- <template>
66
- <SlotText text="Copied" :options="options" />
67
- </template>
52
+ // the classic Copy → Copied → Copy, in one call
53
+ label.flash("Copied", { enter: { color: chromatic() } });
68
54
  ```
69
55
 
70
- ## API
56
+ That's it — import the CSS once, attach to an element, roll text.
57
+
58
+ ## 🧩 API
71
59
 
72
- Vanilla controller:
60
+ | Method | What it does |
61
+ | --- | --- |
62
+ | `set(text, options?)` | Roll to new text — the text **stays** |
63
+ | `flash(text, options?)` | Roll in, then **auto-revert** to the previous text |
64
+ | `destroy()` | Clean up |
73
65
 
74
66
  ```ts
75
67
  const label = slotText(element, "Copy", options);
76
68
 
77
- label.set("Copied");
69
+ label.set("Copied"); // permanent change
78
70
  label.set("Copy", { direction: "down" });
79
- label.flash("Copied"); // rolls in, then auto-reverts to "Copy"
71
+ label.flash("Copied"); // temporary rolls back after 1.4s
80
72
  label.destroy();
81
73
  ```
82
74
 
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.
75
+ > 💡 **`flash` is spam-safe** repeat clicks restart the revert timer instead
76
+ > of queuing extra rolls, and an explicit `set()` cancels any pending revert.
87
77
 
88
78
  ```ts
89
79
  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
80
+ revertAfter: 1400, // ms before rolling back
81
+ enter: { direction: "up", color: chromatic() }, // roll-in
82
+ exit: { direction: "down" }, // roll-back
93
83
  });
94
84
  ```
95
85
 
96
- Framework components:
86
+ ## ⚛️ React
97
87
 
98
- ```ts
99
- import { SlotText as ReactSlotText } from "slot-text/react";
100
- import { SlotText as VueSlotText } from "slot-text/vue";
88
+ ```tsx
89
+ import "slot-text/style.css";
90
+ import { SlotText } from "slot-text/react";
91
+ import { chromatic } from "slot-text";
92
+
93
+ <SlotText
94
+ text={copied ? "Copied" : "Copy"}
95
+ options={{ direction: copied ? "up" : "down", color: copied ? chromatic() : undefined }}
96
+ />
101
97
  ```
102
98
 
103
- Low-level helpers:
99
+ ## 💚 Vue
104
100
 
105
- ```ts
106
- import {
107
- buildSlotText,
108
- animateSlotText,
109
- chromatic,
110
- } from "slot-text";
101
+ ```vue
102
+ <script setup lang="ts">
103
+ import "slot-text/style.css";
104
+ import { SlotText } from "slot-text/vue";
105
+ </script>
106
+
107
+ <template>
108
+ <SlotText text="Copied" :options="{ direction: 'up' }" />
109
+ </template>
111
110
  ```
112
111
 
113
- ## Options
112
+ ## 🔷 Solid
114
113
 
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
- };
114
+ ```tsx
115
+ import "slot-text/style.css";
116
+ import { createSignal } from "solid-js";
117
+ import { slotText } from "slot-text/solid";
118
+
119
+ const [label, setLabel] = createSignal("Copy");
120
+
121
+ <button aria-label={label()} onClick={() => setLabel("Copied")}>
122
+ <span use:slotText={{ text: label(), options: { direction: "up" } }} />
123
+ </button>
128
124
  ```
129
125
 
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.
126
+ ## 🧡 Svelte
134
127
 
135
- Defaults are tuned for a soft, springy roll:
128
+ ```svelte
129
+ <script lang="ts">
130
+ import "slot-text/style.css";
131
+ import { slotText } from "slot-text/svelte";
136
132
 
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
- }
133
+ let label = "Copy";
134
+ </script>
135
+
136
+ <button aria-label={label} on:click={() => label = "Copied"}>
137
+ <span use:slotText={{ text: label, options: { direction: "up" } }}></span>
138
+ </button>
149
139
  ```
150
140
 
151
- ## Example
141
+ ## ⚙️ Options
152
142
 
153
- ```html
154
- <button>
155
- <span id="copy-label"></span>
156
- </button>
143
+ | Option | Default | Description |
144
+ | --- | --- | --- |
145
+ | `direction` | `"down"` | Roll direction: `"up"` or `"down"` |
146
+ | `stagger` | `45` | Delay between characters (ms) |
147
+ | `duration` | `300` | Per-character animation time (ms) |
148
+ | `exitOffset` | `50` | Delay before the old character exits (ms) |
149
+ | `easing` | springy bezier | CSS easing function |
150
+ | `bounce` | `0.6` | Overshoot amount |
151
+ | `color` | — | Color string, or `(index, total) => string` |
152
+ | `colorFade` | `280` | Fade back to base color (ms) |
153
+ | `skipUnchanged` | `true` | Don't re-roll identical characters |
154
+ | `interrupt` | `true` | See below 👇 |
157
155
 
158
- <script type="module">
159
- import "slot-text/style.css";
160
- import { slotText, chromatic } from "slot-text";
156
+ ### 🛑 `interrupt`
161
157
 
162
- const label = slotText(document.querySelector("#copy-label"), "Copy");
158
+ - `interrupt: true` *(default)* — cuts off any roll in flight, starts fresh.
159
+ - `interrupt: false` — lets the current roll **finish**; the latest call plays
160
+ after it lands, duplicates are dropped. Ideal for spam-prone buttons.
163
161
 
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
- ```
162
+ ### 🌈 `chromatic()`
172
163
 
173
- ## Font support
164
+ Built-in rainbow color helper — pass it as `color` for a per-character hue sweep.
174
165
 
175
- Each character animates inside its own measured cell, sized with the exact
176
- font you give the element — so widths are always correct.
166
+ ## 🔤 Font support
177
167
 
178
- Works great with:
168
+ Each character animates in its own measured cell using your element's exact
169
+ font, so widths are always correct.
179
170
 
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.
171
+ **Great with:** monospace fonts, proportional Latin / Cyrillic / Greek
172
+ (Geist, Inter, SF, …), italics, glyphs with overhang.
183
173
 
184
- Known tradeoffs (inherent to any per-character slot animation):
174
+ ⚠️ **Tradeoffs** (inherent to any per-character slot animation):
185
175
 
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`.
176
+ - Kerning is lost pairs like `AV` sit slightly looser (invisible at label sizes).
177
+ - Ligatures won't form (`fi`, `fl`, coding ligatures).
178
+ - Joined scripts (Arabic, Devanagari) render as isolated forms.
179
+ - ZWJ emoji sequences (👨‍👩‍👧) split into cells; single emoji are fine.
180
+ - Very tall display fonts may clip at the roll mask (`line-height: 1.3`).
198
181
 
199
- In short: ideal for short Latin labels, numbers, statuses and commands — in
182
+ **In short:** ideal for short labels, numbers, statuses and commands — in
200
183
  essentially any font you'd use for those.
201
184
 
202
- ## Notes
185
+ ## 📝 Notes
203
186
 
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.
187
+ - Browser-only DOM utility, zero runtime dependencies.
188
+ - React, Vue, Solid, and Svelte are optional peer dependencies — plain JS users
189
+ don't need them.
208
190
  - Import the CSS once before using the animation.
191
+ - Low-level helpers also exported: `buildSlotText`, `animateSlotText`, `chromatic`.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { animateSlotText, buildSlotText, chromatic, clearSlotText, } from "./slotText.js";
2
- import { animateSlotText, buildSlotText, 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
  *
@@ -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.2",
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 CHANGED
@@ -1,6 +1,7 @@
1
1
  .slot-text {
2
2
  display: inline-flex;
3
3
  white-space: pre;
4
+ align-items: baseline;
4
5
  }
5
6
 
6
7
  .char-slot {
@@ -12,6 +13,7 @@
12
13
  instead of letting the row overflow like it does at rest. */
13
14
  flex: none;
14
15
  justify-content: center;
16
+ align-items: baseline;
15
17
  /* Clip only vertically: the roll needs a top/bottom mask, but glyph side
16
18
  bearings, kerning overhang and the settle tilt must stay visible so
17
19
  letters never look cropped. (overflow: hidden is the legacy fallback.) */
@@ -19,7 +21,6 @@
19
21
  overflow-x: visible;
20
22
  overflow-y: clip;
21
23
  line-height: 1.3;
22
- vertical-align: bottom;
23
24
  }
24
25
 
25
26
  /* Cells appearing from or collapsing to empty change width drastically, so
package/style.css.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare const stylesheet: string;
2
+ export default stylesheet;