unicode-animations 0.1.9 → 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 +69 -88
- package/dist/braille.cjs +22 -38
- package/dist/braille.d.cts +1 -1
- package/dist/braille.d.ts +1 -1
- package/dist/braille.global.js +385 -0
- package/dist/braille.js +1 -1
- package/dist/{chunk-MLXIK7E7.js → chunk-F2BWZODB.js} +22 -38
- package/dist/index.cjs +22 -38
- package/dist/index.js +1 -1
- package/package.json +33 -8
- package/scripts/demo.cjs +48 -15
- package/scripts/demo.html +512 -0
- package/scripts/postinstall.cjs +108 -56
package/scripts/demo.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const tty = require('tty');
|
|
4
6
|
|
|
5
7
|
let S;
|
|
6
8
|
try {
|
|
@@ -14,38 +16,69 @@ try {
|
|
|
14
16
|
const names = Object.keys(S);
|
|
15
17
|
const args = process.argv.slice(2);
|
|
16
18
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// --web: open browser demo
|
|
20
|
+
if (args[0] === '--web' || args[0] === '-w') {
|
|
21
|
+
const { exec } = require('child_process');
|
|
22
|
+
const demoPath = path.join(__dirname, 'demo.html');
|
|
23
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
24
|
+
exec(`${cmd} "${demoPath}"`);
|
|
25
|
+
console.log(`Opening ${demoPath}`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get a writable TTY stream — stdout if it's a TTY, otherwise /dev/tty
|
|
30
|
+
let out = process.stdout;
|
|
31
|
+
if (!out.isTTY) {
|
|
32
|
+
try {
|
|
33
|
+
const fd = fs.openSync('/dev/tty', 'w');
|
|
34
|
+
out = new tty.WriteStream(fd);
|
|
35
|
+
} catch {
|
|
36
|
+
// Fallback: no TTY available, just list and exit
|
|
37
|
+
console.log('18 spinners: ' + names.join(', '));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
20
41
|
|
|
21
42
|
const hide = '\x1B[?25l';
|
|
22
43
|
const show = '\x1B[?25h';
|
|
23
|
-
const clear = '\x1B[2K\r';
|
|
24
44
|
const bold = '\x1B[1m';
|
|
25
45
|
const dim = '\x1B[2m';
|
|
26
|
-
const cyan = '\x1B[36m';
|
|
27
46
|
const magenta = '\x1B[35m';
|
|
28
47
|
const reset = '\x1B[0m';
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
const cleanup = () =>
|
|
32
|
-
process.on('SIGINT', () => { cleanup();
|
|
49
|
+
out.write(hide);
|
|
50
|
+
const cleanup = () => { try { out.write(show); } catch {} };
|
|
51
|
+
process.on('SIGINT', () => { cleanup(); out.write('\n'); process.exit(0); });
|
|
33
52
|
process.on('exit', cleanup);
|
|
34
53
|
|
|
54
|
+
// Enable raw mode so keypresses (q, Ctrl+C, Esc) are caught immediately
|
|
55
|
+
if (process.stdin.isTTY) {
|
|
56
|
+
process.stdin.setRawMode(true);
|
|
57
|
+
process.stdin.resume();
|
|
58
|
+
process.stdin.on('data', (key) => {
|
|
59
|
+
// q, Ctrl+C, or Escape
|
|
60
|
+
if (key[0] === 0x71 || key[0] === 0x03 || key[0] === 0x1B) {
|
|
61
|
+
cleanup();
|
|
62
|
+
out.write('\n');
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
if (args[0] === '--list' || args[0] === '-l') {
|
|
36
69
|
cleanup();
|
|
37
|
-
|
|
70
|
+
out.write(`\n${bold}18 spinners available:${reset}\n\n`);
|
|
38
71
|
for (const name of names) {
|
|
39
72
|
const s = S[name];
|
|
40
|
-
|
|
73
|
+
out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
|
|
41
74
|
}
|
|
42
|
-
|
|
75
|
+
out.write('\n');
|
|
43
76
|
process.exit(0);
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
if (args[0] && !names.includes(args[0])) {
|
|
47
80
|
cleanup();
|
|
48
|
-
|
|
81
|
+
out.write(`Unknown spinner: "${args[0]}"\nRun with --list to see all spinners.\n`);
|
|
49
82
|
process.exit(1);
|
|
50
83
|
}
|
|
51
84
|
|
|
@@ -54,15 +87,15 @@ const single = !!args[0];
|
|
|
54
87
|
let i = 0;
|
|
55
88
|
let ticksOnCurrent = 0;
|
|
56
89
|
|
|
57
|
-
const TICKS_PER_SPINNER = 40;
|
|
90
|
+
const TICKS_PER_SPINNER = 40;
|
|
58
91
|
|
|
59
92
|
const timer = setInterval(() => {
|
|
60
93
|
const name = names[current];
|
|
61
94
|
const s = S[name];
|
|
62
95
|
const frame = s.frames[i % s.frames.length];
|
|
63
|
-
const count = `${dim}[${current + 1}/${names.length}]${reset}`;
|
|
96
|
+
const count = single ? '' : `${dim}[${current + 1}/${names.length}]${reset}`;
|
|
64
97
|
|
|
65
|
-
|
|
98
|
+
out.write(`\r\x1B[2K ${magenta}${frame}${reset} ${bold}${name}${reset} ${dim}${s.interval}ms${reset} ${count}`);
|
|
66
99
|
|
|
67
100
|
i++;
|
|
68
101
|
ticksOnCurrent++;
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>unicode-animations</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #131316;
|
|
10
|
+
--surface: #1e1e23;
|
|
11
|
+
--border: #2e2e36;
|
|
12
|
+
--text: #d8d8df;
|
|
13
|
+
--text-2: #a4a4b0;
|
|
14
|
+
--text-3: #6e6e80;
|
|
15
|
+
--accent: #a78bfa;
|
|
16
|
+
--mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Menlo', monospace;
|
|
17
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
18
|
+
}
|
|
19
|
+
[data-theme="light"] {
|
|
20
|
+
--bg: #f7f7fa;
|
|
21
|
+
--surface: #ffffff;
|
|
22
|
+
--border: #e8e8ec;
|
|
23
|
+
--text: #2c2c3a;
|
|
24
|
+
--text-2: #6b6b7b;
|
|
25
|
+
--text-3: #9d9daa;
|
|
26
|
+
--accent: #7c3aed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
30
|
+
body {
|
|
31
|
+
font-family: var(--sans);
|
|
32
|
+
background: var(--bg);
|
|
33
|
+
color: var(--text);
|
|
34
|
+
-webkit-font-smoothing: antialiased;
|
|
35
|
+
transition: background 0.3s, color 0.3s;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.theme-toggle {
|
|
39
|
+
position: fixed; top: 1rem; right: 1.5rem; z-index: 100;
|
|
40
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
41
|
+
border-radius: 8px; padding: 0.4rem 0.75rem;
|
|
42
|
+
cursor: pointer; font-size: 0.75rem; font-weight: 500;
|
|
43
|
+
color: var(--text-2); display: flex; align-items: center; gap: 0.4rem;
|
|
44
|
+
transition: border-color 0.2s;
|
|
45
|
+
}
|
|
46
|
+
.theme-toggle:hover { border-color: var(--text-2); }
|
|
47
|
+
|
|
48
|
+
header {
|
|
49
|
+
text-align: center;
|
|
50
|
+
padding: 4rem 2rem 1rem;
|
|
51
|
+
}
|
|
52
|
+
header h1 {
|
|
53
|
+
font-size: 2rem;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
letter-spacing: -0.03em;
|
|
56
|
+
}
|
|
57
|
+
header p {
|
|
58
|
+
margin-top: 0.4rem;
|
|
59
|
+
color: var(--text-3);
|
|
60
|
+
font-size: 0.95rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.install {
|
|
64
|
+
text-align: center;
|
|
65
|
+
margin: 1.5rem 0 3rem;
|
|
66
|
+
}
|
|
67
|
+
.install code {
|
|
68
|
+
font-family: var(--mono);
|
|
69
|
+
font-size: 0.85rem;
|
|
70
|
+
background: var(--surface);
|
|
71
|
+
border: 1px solid var(--border);
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
padding: 0.5rem 1.25rem;
|
|
74
|
+
color: var(--text-2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main {
|
|
78
|
+
max-width: 720px;
|
|
79
|
+
margin: 0 auto;
|
|
80
|
+
padding: 0 1.5rem 6rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.section-label {
|
|
84
|
+
font-size: 0.65rem;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: 0.14em;
|
|
88
|
+
color: var(--text-3);
|
|
89
|
+
margin-bottom: 1rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.grid {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: 1fr 1fr;
|
|
95
|
+
gap: 0;
|
|
96
|
+
}
|
|
97
|
+
@media (max-width: 520px) { .grid { grid-template-columns: 1fr; } }
|
|
98
|
+
|
|
99
|
+
.spinner-row {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 0.75rem;
|
|
103
|
+
padding: 0.6rem 1rem;
|
|
104
|
+
border-bottom: 1px solid var(--border);
|
|
105
|
+
transition: background 0.15s;
|
|
106
|
+
}
|
|
107
|
+
.spinner-row:hover { background: var(--surface); }
|
|
108
|
+
|
|
109
|
+
.spinner-frame {
|
|
110
|
+
font-family: var(--mono);
|
|
111
|
+
font-size: 1rem;
|
|
112
|
+
width: 5rem;
|
|
113
|
+
text-align: center;
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
color: var(--accent);
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.spinner-name {
|
|
120
|
+
font-weight: 500;
|
|
121
|
+
font-size: 0.85rem;
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.spinner-meta {
|
|
126
|
+
font-size: 0.75rem;
|
|
127
|
+
color: var(--text-3);
|
|
128
|
+
margin-left: auto;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.usage {
|
|
133
|
+
margin-top: 3rem;
|
|
134
|
+
}
|
|
135
|
+
.usage pre {
|
|
136
|
+
font-family: var(--mono);
|
|
137
|
+
font-size: 0.8rem;
|
|
138
|
+
line-height: 1.6;
|
|
139
|
+
background: var(--surface);
|
|
140
|
+
border: 1px solid var(--border);
|
|
141
|
+
border-radius: 12px;
|
|
142
|
+
padding: 1.25rem 1.5rem;
|
|
143
|
+
overflow-x: auto;
|
|
144
|
+
color: var(--text-2);
|
|
145
|
+
}
|
|
146
|
+
.usage pre .kw { color: #c084fc; }
|
|
147
|
+
.usage pre .str { color: #86efac; }
|
|
148
|
+
.usage pre .cm { color: var(--text-3); }
|
|
149
|
+
|
|
150
|
+
footer {
|
|
151
|
+
text-align: center;
|
|
152
|
+
padding: 2rem;
|
|
153
|
+
font-size: 0.75rem;
|
|
154
|
+
color: var(--text-3);
|
|
155
|
+
}
|
|
156
|
+
footer a { color: var(--accent); text-decoration: none; }
|
|
157
|
+
footer a:hover { text-decoration: underline; }
|
|
158
|
+
|
|
159
|
+
.prose {
|
|
160
|
+
font-size: 0.9rem;
|
|
161
|
+
line-height: 1.7;
|
|
162
|
+
color: var(--text-2);
|
|
163
|
+
margin-bottom: 1rem;
|
|
164
|
+
}
|
|
165
|
+
.prose code, .sub-label code, .ref-table code {
|
|
166
|
+
font-family: var(--mono);
|
|
167
|
+
font-size: 0.8rem;
|
|
168
|
+
background: var(--surface);
|
|
169
|
+
border: 1px solid var(--border);
|
|
170
|
+
border-radius: 4px;
|
|
171
|
+
padding: 0.15rem 0.4rem;
|
|
172
|
+
}
|
|
173
|
+
.ref-table code { font-size: 0.75rem; }
|
|
174
|
+
|
|
175
|
+
.doc-section {
|
|
176
|
+
margin-top: 3rem;
|
|
177
|
+
}
|
|
178
|
+
.doc-section .usage {
|
|
179
|
+
margin-top: 1rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.sub-label {
|
|
183
|
+
font-size: 0.8rem;
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
color: var(--text);
|
|
186
|
+
margin: 1.5rem 0 0.75rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.ref-table {
|
|
190
|
+
width: 100%;
|
|
191
|
+
border-collapse: collapse;
|
|
192
|
+
font-size: 0.8rem;
|
|
193
|
+
margin: 1rem 0;
|
|
194
|
+
}
|
|
195
|
+
.ref-table th {
|
|
196
|
+
text-align: left;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
font-size: 0.7rem;
|
|
199
|
+
text-transform: uppercase;
|
|
200
|
+
letter-spacing: 0.08em;
|
|
201
|
+
color: var(--text-3);
|
|
202
|
+
padding: 0.5rem 0.75rem;
|
|
203
|
+
border-bottom: 1px solid var(--border);
|
|
204
|
+
}
|
|
205
|
+
.ref-table td {
|
|
206
|
+
padding: 0.5rem 0.75rem;
|
|
207
|
+
border-bottom: 1px solid var(--border);
|
|
208
|
+
color: var(--text-2);
|
|
209
|
+
}
|
|
210
|
+
.ref-table tr:last-child td { border-bottom: none; }
|
|
211
|
+
</style>
|
|
212
|
+
</head>
|
|
213
|
+
<body>
|
|
214
|
+
|
|
215
|
+
<button class="theme-toggle" id="toggle">
|
|
216
|
+
<span id="toggleIcon">☀</span>
|
|
217
|
+
<span id="toggleLabel">Light</span>
|
|
218
|
+
</button>
|
|
219
|
+
|
|
220
|
+
<header>
|
|
221
|
+
<h1>unicode-animations</h1>
|
|
222
|
+
<p>18 braille spinner animations as raw frame data</p>
|
|
223
|
+
</header>
|
|
224
|
+
|
|
225
|
+
<div class="install">
|
|
226
|
+
<code>npm install unicode-animations</code>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<main>
|
|
230
|
+
<section>
|
|
231
|
+
<div class="section-label">All braille spinners</div>
|
|
232
|
+
<div class="grid" id="spinnerGrid"></div>
|
|
233
|
+
</section>
|
|
234
|
+
|
|
235
|
+
<section class="usage">
|
|
236
|
+
<div class="section-label" style="margin-bottom: 1rem;">Usage</div>
|
|
237
|
+
<pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
238
|
+
|
|
239
|
+
<span class="kw">const</span> { frames, interval } = spinners.braille
|
|
240
|
+
<span class="kw">let</span> i = 0
|
|
241
|
+
|
|
242
|
+
<span class="kw">const</span> timer = setInterval(() => {
|
|
243
|
+
process.stdout.write(<span class="str">`\r${frames[i++ % frames.length]} Loading...`</span>)
|
|
244
|
+
}, interval)
|
|
245
|
+
|
|
246
|
+
<span class="kw">await</span> doWork()
|
|
247
|
+
clearInterval(timer)
|
|
248
|
+
process.stdout.write(<span class="str">'\r✔ Done.\n'</span>)</pre>
|
|
249
|
+
</section>
|
|
250
|
+
|
|
251
|
+
<section class="doc-section">
|
|
252
|
+
<div class="section-label">Quick start</div>
|
|
253
|
+
<div class="usage">
|
|
254
|
+
<pre><span class="cm">// ESM</span>
|
|
255
|
+
<span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
256
|
+
|
|
257
|
+
<span class="cm">// CJS</span>
|
|
258
|
+
<span class="kw">const</span> spinners = require(<span class="str">'unicode-animations'</span>)</pre>
|
|
259
|
+
</div>
|
|
260
|
+
<p class="prose">Each spinner is a <code>{ frames: string[], interval: number }</code> object.</p>
|
|
261
|
+
</section>
|
|
262
|
+
|
|
263
|
+
<section class="doc-section">
|
|
264
|
+
<div class="section-label">Examples</div>
|
|
265
|
+
|
|
266
|
+
<div class="sub-label">CLI tool — spinner during async work</div>
|
|
267
|
+
<div class="usage">
|
|
268
|
+
<pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
269
|
+
|
|
270
|
+
<span class="kw">const</span> { frames, interval } = spinners.braille
|
|
271
|
+
<span class="kw">let</span> i = 0
|
|
272
|
+
|
|
273
|
+
<span class="kw">const</span> spinner = setInterval(() => {
|
|
274
|
+
process.stdout.write(<span class="str">`\r\x1B[2K ${frames[i++ % frames.length]} Deploying...`</span>)
|
|
275
|
+
}, interval)
|
|
276
|
+
|
|
277
|
+
<span class="kw">await</span> deploy()
|
|
278
|
+
|
|
279
|
+
clearInterval(spinner)
|
|
280
|
+
process.stdout.write(<span class="str">'\r\x1B[2K ✔ Deployed.\n'</span>)</pre>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div class="sub-label">Reusable spinner helper</div>
|
|
284
|
+
<div class="usage">
|
|
285
|
+
<pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
286
|
+
|
|
287
|
+
<span class="kw">function</span> createSpinner(msg, name = <span class="str">'braille'</span>) {
|
|
288
|
+
<span class="kw">const</span> { frames, interval } = spinners[name]
|
|
289
|
+
<span class="kw">let</span> i = 0, text = msg
|
|
290
|
+
<span class="kw">const</span> timer = setInterval(() => {
|
|
291
|
+
process.stdout.write(<span class="str">`\r\x1B[2K ${frames[i++ % frames.length]} ${text}`</span>)
|
|
292
|
+
}, interval)
|
|
293
|
+
|
|
294
|
+
<span class="kw">return</span> {
|
|
295
|
+
update(msg) { text = msg },
|
|
296
|
+
stop(msg) { clearInterval(timer); process.stdout.write(<span class="str">`\r\x1B[2K ✔ ${msg}\n`</span>) },
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
<span class="kw">const</span> s = createSpinner(<span class="str">'Connecting to database...'</span>)
|
|
301
|
+
<span class="kw">const</span> db = <span class="kw">await</span> connect()
|
|
302
|
+
s.update(<span class="str">`Running ${migrations.length} migrations...`</span>)
|
|
303
|
+
<span class="kw">await</span> db.migrate(migrations)
|
|
304
|
+
s.stop(<span class="str">'Database ready.'</span>)</pre>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="sub-label">Multi-step pipeline</div>
|
|
308
|
+
<div class="usage">
|
|
309
|
+
<pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
310
|
+
|
|
311
|
+
<span class="kw">async function</span> runWithSpinner(label, fn, name = <span class="str">'braille'</span>) {
|
|
312
|
+
<span class="kw">const</span> { frames, interval } = spinners[name]
|
|
313
|
+
<span class="kw">let</span> i = 0
|
|
314
|
+
<span class="kw">const</span> timer = setInterval(() => {
|
|
315
|
+
process.stdout.write(<span class="str">`\r\x1B[2K ${frames[i++ % frames.length]} ${label}`</span>)
|
|
316
|
+
}, interval)
|
|
317
|
+
<span class="kw">const</span> result = <span class="kw">await</span> fn()
|
|
318
|
+
clearInterval(timer)
|
|
319
|
+
process.stdout.write(<span class="str">`\r\x1B[2K ✔ ${label}\n`</span>)
|
|
320
|
+
<span class="kw">return</span> result
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
<span class="kw">await</span> runWithSpinner(<span class="str">'Linting...'</span>, lint, <span class="str">'scan'</span>)
|
|
324
|
+
<span class="kw">await</span> runWithSpinner(<span class="str">'Running tests...'</span>, test, <span class="str">'helix'</span>)
|
|
325
|
+
<span class="kw">await</span> runWithSpinner(<span class="str">'Building...'</span>, build, <span class="str">'cascade'</span>)
|
|
326
|
+
<span class="kw">await</span> runWithSpinner(<span class="str">'Publishing...'</span>, publish, <span class="str">'braille'</span>)</pre>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div class="sub-label">React component</div>
|
|
330
|
+
<div class="usage">
|
|
331
|
+
<pre><span class="kw">import</span> { useState, useEffect } <span class="kw">from</span> <span class="str">'react'</span>
|
|
332
|
+
<span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
333
|
+
|
|
334
|
+
<span class="kw">function</span> Spinner({ name = <span class="str">'braille'</span>, children }) {
|
|
335
|
+
<span class="kw">const</span> [frame, setFrame] = useState(0)
|
|
336
|
+
<span class="kw">const</span> s = spinners[name]
|
|
337
|
+
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
<span class="kw">const</span> timer = setInterval(
|
|
340
|
+
() => setFrame(f => (f + 1) % s.frames.length),
|
|
341
|
+
s.interval
|
|
342
|
+
)
|
|
343
|
+
<span class="kw">return</span> () => clearInterval(timer)
|
|
344
|
+
}, [name])
|
|
345
|
+
|
|
346
|
+
<span class="kw">return</span> <span style={{ fontFamily: <span class="str">'monospace'</span> }}>{s.frames[frame]} {children}</span>
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
<span class="cm">// Usage: <Spinner name="helix">Generating response...</Spinner></span></pre>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div class="sub-label">Browser — status indicator</div>
|
|
353
|
+
<div class="usage">
|
|
354
|
+
<pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
355
|
+
|
|
356
|
+
<span class="kw">const</span> el = document.getElementById(<span class="str">'status'</span>)
|
|
357
|
+
<span class="kw">const</span> { frames, interval } = spinners.orbit
|
|
358
|
+
<span class="kw">let</span> i = 0
|
|
359
|
+
|
|
360
|
+
<span class="kw">const</span> spinner = setInterval(() => {
|
|
361
|
+
el.textContent = <span class="str">`${frames[i++ % frames.length]} Syncing...`</span>
|
|
362
|
+
}, interval)
|
|
363
|
+
|
|
364
|
+
<span class="kw">await</span> sync()
|
|
365
|
+
clearInterval(spinner)
|
|
366
|
+
el.textContent = <span class="str">'✔ Synced'</span></pre>
|
|
367
|
+
</div>
|
|
368
|
+
</section>
|
|
369
|
+
|
|
370
|
+
<section class="doc-section">
|
|
371
|
+
<div class="section-label">All spinners</div>
|
|
372
|
+
|
|
373
|
+
<div class="sub-label">Classic braille</div>
|
|
374
|
+
<table class="ref-table">
|
|
375
|
+
<thead><tr><th>Name</th><th>Preview</th><th>Interval</th></tr></thead>
|
|
376
|
+
<tbody>
|
|
377
|
+
<tr><td><code>braille</code></td><td><code>⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏</code></td><td>80ms</td></tr>
|
|
378
|
+
<tr><td><code>braillewave</code></td><td><code>⠁⠂⠄⡀</code> → <code>⠂⠄⡀⢀</code></td><td>100ms</td></tr>
|
|
379
|
+
<tr><td><code>dna</code></td><td><code>⠋⠉⠙⠚</code> → <code>⠉⠙⠚⠒</code></td><td>80ms</td></tr>
|
|
380
|
+
</tbody>
|
|
381
|
+
</table>
|
|
382
|
+
|
|
383
|
+
<div class="sub-label">Grid animations (braille)</div>
|
|
384
|
+
<table class="ref-table">
|
|
385
|
+
<thead><tr><th>Name</th><th>Frames</th><th>Interval</th></tr></thead>
|
|
386
|
+
<tbody>
|
|
387
|
+
<tr><td><code>scan</code></td><td>10</td><td>70ms</td></tr>
|
|
388
|
+
<tr><td><code>rain</code></td><td>12</td><td>100ms</td></tr>
|
|
389
|
+
<tr><td><code>scanline</code></td><td>6</td><td>120ms</td></tr>
|
|
390
|
+
<tr><td><code>pulse</code></td><td>5</td><td>180ms</td></tr>
|
|
391
|
+
<tr><td><code>snake</code></td><td>16</td><td>80ms</td></tr>
|
|
392
|
+
<tr><td><code>sparkle</code></td><td>6</td><td>150ms</td></tr>
|
|
393
|
+
<tr><td><code>cascade</code></td><td>12</td><td>60ms</td></tr>
|
|
394
|
+
<tr><td><code>columns</code></td><td>26</td><td>60ms</td></tr>
|
|
395
|
+
<tr><td><code>orbit</code></td><td>8</td><td>100ms</td></tr>
|
|
396
|
+
<tr><td><code>breathe</code></td><td>17</td><td>100ms</td></tr>
|
|
397
|
+
<tr><td><code>waverows</code></td><td>16</td><td>90ms</td></tr>
|
|
398
|
+
<tr><td><code>checkerboard</code></td><td>4</td><td>250ms</td></tr>
|
|
399
|
+
<tr><td><code>helix</code></td><td>16</td><td>80ms</td></tr>
|
|
400
|
+
<tr><td><code>fillsweep</code></td><td>11</td><td>100ms</td></tr>
|
|
401
|
+
<tr><td><code>diagswipe</code></td><td>16</td><td>60ms</td></tr>
|
|
402
|
+
</tbody>
|
|
403
|
+
</table>
|
|
404
|
+
</section>
|
|
405
|
+
|
|
406
|
+
<section class="doc-section">
|
|
407
|
+
<div class="section-label">Custom spinners</div>
|
|
408
|
+
<p class="prose">Create your own braille spinners using the grid utilities:</p>
|
|
409
|
+
<div class="usage">
|
|
410
|
+
<pre><span class="kw">import</span> { gridToBraille, makeGrid } <span class="kw">from</span> <span class="str">'unicode-animations'</span>
|
|
411
|
+
|
|
412
|
+
<span class="cm">// Create a 4-row × 4-col grid</span>
|
|
413
|
+
<span class="kw">const</span> grid = makeGrid(4, 4)
|
|
414
|
+
grid[0][0] = <span class="kw">true</span>
|
|
415
|
+
grid[1][1] = <span class="kw">true</span>
|
|
416
|
+
grid[2][2] = <span class="kw">true</span>
|
|
417
|
+
grid[3][3] = <span class="kw">true</span>
|
|
418
|
+
|
|
419
|
+
console.log(gridToBraille(grid)) <span class="cm">// diagonal braille pattern</span></pre>
|
|
420
|
+
</div>
|
|
421
|
+
<p class="prose"><code>makeGrid(rows, cols)</code> returns a <code>boolean[][]</code>. Set cells to <code>true</code> to raise dots. <code>gridToBraille(grid)</code> converts it to a braille string (2 dot-columns per character).</p>
|
|
422
|
+
</section>
|
|
423
|
+
|
|
424
|
+
<section class="doc-section">
|
|
425
|
+
<div class="section-label">API</div>
|
|
426
|
+
|
|
427
|
+
<div class="sub-label">Spinner</div>
|
|
428
|
+
<div class="usage">
|
|
429
|
+
<pre><span class="kw">interface</span> Spinner {
|
|
430
|
+
<span class="kw">readonly</span> frames: <span class="kw">readonly</span> string[]
|
|
431
|
+
<span class="kw">readonly</span> interval: number
|
|
432
|
+
}</pre>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div class="sub-label">Exports from <code>'unicode-animations'</code></div>
|
|
436
|
+
<table class="ref-table">
|
|
437
|
+
<thead><tr><th>Export</th><th>Type</th></tr></thead>
|
|
438
|
+
<tbody>
|
|
439
|
+
<tr><td><code>default</code> / <code>spinners</code></td><td><code>Record<BrailleSpinnerName, Spinner></code></td></tr>
|
|
440
|
+
<tr><td><code>gridToBraille(grid)</code></td><td><code>(boolean[][]) => string</code></td></tr>
|
|
441
|
+
<tr><td><code>makeGrid(rows, cols)</code></td><td><code>(number, number) => boolean[][]</code></td></tr>
|
|
442
|
+
<tr><td><code>Spinner</code></td><td>TypeScript interface</td></tr>
|
|
443
|
+
<tr><td><code>BrailleSpinnerName</code></td><td>Union type of all 18 spinner names</td></tr>
|
|
444
|
+
</tbody>
|
|
445
|
+
</table>
|
|
446
|
+
<p class="prose">The subpath <code>'unicode-animations/braille'</code> re-exports everything from the main entrypoint.</p>
|
|
447
|
+
</section>
|
|
448
|
+
</main>
|
|
449
|
+
|
|
450
|
+
<footer>
|
|
451
|
+
made by <a href="https://x.com/gunnargray">Gunnar Gray</a>
|
|
452
|
+
· MIT License
|
|
453
|
+
</footer>
|
|
454
|
+
|
|
455
|
+
<script src="../dist/braille.global.js"></script>
|
|
456
|
+
<script>
|
|
457
|
+
const spinners = UnicodeAnimations.spinners;
|
|
458
|
+
|
|
459
|
+
// Build grid
|
|
460
|
+
const grid = document.getElementById('spinnerGrid');
|
|
461
|
+
const els = {};
|
|
462
|
+
|
|
463
|
+
Object.entries(spinners).forEach(([name, s]) => {
|
|
464
|
+
const row = document.createElement('div');
|
|
465
|
+
row.className = 'spinner-row';
|
|
466
|
+
|
|
467
|
+
const frame = document.createElement('span');
|
|
468
|
+
frame.className = 'spinner-frame';
|
|
469
|
+
frame.textContent = s.frames[0];
|
|
470
|
+
|
|
471
|
+
const label = document.createElement('span');
|
|
472
|
+
label.className = 'spinner-name';
|
|
473
|
+
label.textContent = name;
|
|
474
|
+
|
|
475
|
+
const meta = document.createElement('span');
|
|
476
|
+
meta.className = 'spinner-meta';
|
|
477
|
+
meta.textContent = `${s.frames.length}f · ${s.interval}ms`;
|
|
478
|
+
|
|
479
|
+
row.append(frame, label, meta);
|
|
480
|
+
grid.appendChild(row);
|
|
481
|
+
els[name] = frame;
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Animate — group by interval for efficiency
|
|
485
|
+
const byInterval = {};
|
|
486
|
+
Object.entries(spinners).forEach(([name, s]) => {
|
|
487
|
+
if (!byInterval[s.interval]) byInterval[s.interval] = [];
|
|
488
|
+
byInterval[s.interval].push({ name, frames: s.frames, i: 0 });
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
Object.entries(byInterval).forEach(([interval, group]) => {
|
|
492
|
+
setInterval(() => {
|
|
493
|
+
group.forEach(s => {
|
|
494
|
+
s.i = (s.i + 1) % s.frames.length;
|
|
495
|
+
els[s.name].textContent = s.frames[s.i];
|
|
496
|
+
});
|
|
497
|
+
}, Number(interval));
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Theme toggle
|
|
501
|
+
const toggle = document.getElementById('toggle');
|
|
502
|
+
const icon = document.getElementById('toggleIcon');
|
|
503
|
+
const label = document.getElementById('toggleLabel');
|
|
504
|
+
toggle.addEventListener('click', () => {
|
|
505
|
+
const dark = document.documentElement.dataset.theme === 'dark';
|
|
506
|
+
document.documentElement.dataset.theme = dark ? 'light' : 'dark';
|
|
507
|
+
icon.textContent = dark ? '☾' : '☀';
|
|
508
|
+
label.textContent = dark ? 'Dark' : 'Light';
|
|
509
|
+
});
|
|
510
|
+
</script>
|
|
511
|
+
</body>
|
|
512
|
+
</html>
|