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/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
- // Usage: npx unicode-animations [name]
18
- // No args = cycle through all spinners
19
- // With name = show that specific spinner
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
- process.stdout.write(hide);
31
- const cleanup = () => process.stdout.write(show);
32
- process.on('SIGINT', () => { cleanup(); console.log(); process.exit(0); });
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
- console.log(`\n${bold}22 spinners available:${reset}\n`);
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
- console.log(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}`);
73
+ out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
41
74
  }
42
- console.log();
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
- console.error(`Unknown spinner: "${args[0]}"\nRun with --list to see all spinners.`);
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; // ~3.2s per spinner when cycling
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
- process.stdout.write(`${clear} ${magenta}${frame}${reset} ${bold}${name}${reset} ${dim}${s.interval}ms${reset} ${single ? '' : count}`);
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> &lt;span style={{ fontFamily: <span class="str">'monospace'</span> }}>{s.frames[frame]} {children}&lt;/span>
347
+ }
348
+
349
+ <span class="cm">// Usage: &lt;Spinner name="helix">Generating response...&lt;/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&lt;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
+ &nbsp;·&nbsp; 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>