spoiler-ui 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dzmitry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,576 @@
1
+ # spoiler-ui
2
+
3
+ A lightweight, zero-dependency spoiler / content-reveal element — like **Threads**, **Discord**, and **Telegram**.
4
+
5
+ Blurs hidden content behind a pill overlay. Click (or press Enter / Space) to reveal with a smooth transition.
6
+
7
+ **MIT license · zero dependencies · Vanilla JS + optional React wrapper · TypeScript types included**
8
+
9
+ <!-- SCREENSHOT: demo of all three modes (idle blurred, loading, revealed) -->
10
+
11
+ ---
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install spoiler-ui
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Three modes
22
+
23
+ | Mode | Content in DOM before reveal? | Requires server? |
24
+ |------|-------------------------------|-----------------|
25
+ | [UI only](#mode-1--ui-only) | Yes, blurred | No |
26
+ | [UI + encoding](#mode-3--ui--encoding) | Yes, as Base64 | No |
27
+ | [UI + backend](#mode-2--ui--backend) | No | Yes |
28
+
29
+ ---
30
+
31
+ ## Mode 1 — UI only
32
+
33
+ The simplest mode. Content is blurred in the DOM; revealing is purely client-side. A determined user can find the text in DevTools, but it stops casual glances.
34
+
35
+ ### Vanilla JS
36
+
37
+ ```js
38
+ import { createSpoiler, initSpoilers } from 'spoiler-ui';
39
+ import 'spoiler-ui/style';
40
+
41
+ // Programmatic
42
+ const spoiler = createSpoiler('Snape kills Dumbledore.', {
43
+ label: 'Book spoiler',
44
+ });
45
+ document.querySelector('p').appendChild(spoiler);
46
+
47
+ // Custom label, button-only reveal
48
+ const strictSpoiler = createSpoiler('He was dead the whole time.', {
49
+ label: 'Major spoiler',
50
+ revealOnClick: false, // only the pill button reveals, not the whole element
51
+ blurAmount: 10,
52
+ animationDuration: 500,
53
+ onReveal: (text) => console.log('Revealed:', text),
54
+ onError: (err) => console.error('Failed:', err),
55
+ });
56
+ document.body.appendChild(strictSpoiler);
57
+ ```
58
+
59
+ **Auto-init from HTML attributes** — no JS needed per element:
60
+
61
+ ```html
62
+ <script type="module">
63
+ import { initSpoilers } from 'spoiler-ui';
64
+ import 'spoiler-ui/style';
65
+ initSpoilers();
66
+ </script>
67
+
68
+ <!-- Basic -->
69
+ <span data-spoiler>Jon Snow is resurrected.</span>
70
+
71
+ <!-- Custom label, blur, and icon -->
72
+ <span data-spoiler="Huge spoiler" data-spoiler-blur="10" data-spoiler-icon="🔒">
73
+ The planet explodes at the end.
74
+ </span>
75
+
76
+ <!-- Extra CSS class for custom theming -->
77
+ <span data-spoiler="Spoiler" data-spoiler-class="my-theme">
78
+ The twist is revealed in chapter 12.
79
+ </span>
80
+ ```
81
+
82
+ ### React
83
+
84
+ ```jsx
85
+ import { Spoiler } from 'spoiler-ui/react';
86
+ import 'spoiler-ui/style';
87
+
88
+ export default function ReviewPost() {
89
+ return (
90
+ <p>
91
+ The film was stunning, but I did not expect{' '}
92
+ <Spoiler label="ending spoiler">
93
+ the hero to sacrifice herself in the final act
94
+ </Spoiler>
95
+ .
96
+ </p>
97
+ );
98
+ }
99
+ ```
100
+
101
+ With callbacks and a custom ref:
102
+
103
+ ```jsx
104
+ import { useRef } from 'react';
105
+ import { Spoiler } from 'spoiler-ui/react';
106
+
107
+ const ref = useRef(null);
108
+
109
+ <Spoiler
110
+ ref={ref}
111
+ label="Major spoiler"
112
+ blurAmount={10}
113
+ revealOnClick={false}
114
+ animationDuration={500}
115
+ onReveal={(text) => analytics.track('spoiler_revealed', { text })}
116
+ onError={(err) => console.error('Reveal failed:', err)}
117
+ >
118
+ The murderer was the detective all along.
119
+ </Spoiler>
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Mode 2 — UI + backend
125
+
126
+ The text is **never sent to the browser** until the user clicks reveal. On click, the component fetches the content from your server, shows a loading indicator, then reveals. A user inspecting DevTools before clicking will find nothing.
127
+
128
+ On network failure, the pill shows "Failed — retry?" and re-clicking retries the fetch automatically.
129
+
130
+ ### Vanilla JS
131
+
132
+ ```js
133
+ import { createSpoiler } from 'spoiler-ui';
134
+ import 'spoiler-ui/style';
135
+
136
+ const spoiler = createSpoiler(null, {
137
+ src: '/api/spoilers/42',
138
+ label: 'Spoiler',
139
+ onReveal: (text) => console.log('Fetched and revealed:', text),
140
+ onError: (err) => reportError(err),
141
+ });
142
+ document.querySelector('.post').appendChild(spoiler);
143
+ ```
144
+
145
+ With authentication headers:
146
+
147
+ ```js
148
+ const spoiler = createSpoiler(null, {
149
+ src: '/api/spoilers/42',
150
+ fetchOptions: {
151
+ headers: { Authorization: `Bearer ${token}` },
152
+ credentials: 'include',
153
+ },
154
+ onReveal: (text) => markAsRead(42),
155
+ });
156
+ ```
157
+
158
+ With an optional blurred placeholder shown while loading:
159
+
160
+ ```js
161
+ // 'Loading content…' is shown blurred until the fetch completes,
162
+ // then replaced with the real server response.
163
+ const spoiler = createSpoiler('Loading content\u2026', {
164
+ src: '/api/spoilers/42',
165
+ });
166
+ ```
167
+
168
+ **HTML attributes:**
169
+
170
+ ```html
171
+ <span
172
+ data-spoiler="Spoiler"
173
+ data-spoiler-src="/api/spoilers/42"
174
+ >
175
+ </span>
176
+
177
+ <!-- Server returns HTML markup -->
178
+ <span
179
+ data-spoiler="Spoiler"
180
+ data-spoiler-src="/api/spoilers/43"
181
+ data-spoiler-response-type="html"
182
+ >
183
+ </span>
184
+ ```
185
+
186
+ > **Note on `responseType: 'html'`:** The server response is injected via `innerHTML`. You are responsible for sanitising HTML before serving it. Use `responseType: 'text'` (the default) whenever the content is plain text.
187
+
188
+ ### React
189
+
190
+ ```jsx
191
+ import { Spoiler } from 'spoiler-ui/react';
192
+ import 'spoiler-ui/style';
193
+
194
+ export default function Post({ spoilerId }) {
195
+ return (
196
+ <p>
197
+ The twist is{' '}
198
+ <Spoiler
199
+ src={`/api/spoilers/${spoilerId}`}
200
+ label="Spoiler"
201
+ loadingLabel="Fetching…"
202
+ errorLabel="Load failed — retry?"
203
+ onReveal={(text) => console.log('Revealed:', text)}
204
+ onError={(err) => reportError(err)}
205
+ />
206
+ .
207
+ </p>
208
+ );
209
+ }
210
+ ```
211
+
212
+ With auth and HTML response:
213
+
214
+ ```jsx
215
+ <Spoiler
216
+ src="/api/spoilers/99"
217
+ fetchOptions={{ headers: { Authorization: `Bearer ${token}` } }}
218
+ responseType="html"
219
+ onReveal={(html) => markSpoilerSeen(99)}
220
+ onError={(err) => toast.error('Could not load spoiler')}
221
+ />
222
+ ```
223
+
224
+ **State behaviour:**
225
+
226
+ | State | Pill shows | Cursor |
227
+ |-------|-----------|--------|
228
+ | Idle | label + 👁 | pointer |
229
+ | Loading | "Loading…" + spinner | wait |
230
+ | Error | "Failed — retry?" + ⚠ | pointer |
231
+ | Revealed | *(overlay removed)* | auto |
232
+
233
+ ---
234
+
235
+ ## Mode 3 — UI + encoding
236
+
237
+ The content is Base64-encoded and stored in a `data-` attribute. The plaintext **never appears in the DOM** — it is decoded in memory only when the user clicks reveal, then injected and displayed. A DevTools inspection before clicking shows only the encoded payload, not readable text.
238
+
239
+ > **Security note:** Base64 is obscurity, not encryption. Anyone who spots the encoded attribute can decode it in seconds with `atob()`. This is meaningfully better than plaintext for casual spoiler protection, but not suitable for genuinely sensitive data. Use [Mode 2](#mode-2--ui--backend) if the content must stay secret.
240
+
241
+ ### Prepare the payload
242
+
243
+ Use the bundled `encodeBase64` utility anywhere that runs JS — a build script, a server, or the browser console:
244
+
245
+ ```js
246
+ import { encodeBase64 } from 'spoiler-ui';
247
+
248
+ const payload = encodeBase64('Darth Vader is Luke\'s father.');
249
+ // → 'RGFydGggVmFkZXIgaXMgTHVrZSdzIGZhdGhlci4='
250
+
251
+ // Full Unicode support — emoji and non-Latin scripts work fine
252
+ encodeBase64('王子 is the killer 🗡️');
253
+ ```
254
+
255
+ ### Vanilla JS
256
+
257
+ ```js
258
+ import { createSpoiler, encodeBase64 } from 'spoiler-ui';
259
+ import 'spoiler-ui/style';
260
+
261
+ const payload = encodeBase64('The real treasure was the friends we made along the way.');
262
+
263
+ const spoiler = createSpoiler(payload, {
264
+ encoded: true,
265
+ label: 'Spoiler',
266
+ onReveal: (text) => console.log('Decoded and revealed:', text),
267
+ onError: (err) => console.error('Decode failed:', err),
268
+ });
269
+ document.body.appendChild(spoiler);
270
+ ```
271
+
272
+ **What DevTools shows** (vanilla JS — React produces the same structure but as a React-managed DOM):
273
+
274
+ ```html
275
+ <!-- Before reveal — only the encoded payload is in the DOM -->
276
+ <span class="spoiler-ui" data-state="idle" data-revealed="false"
277
+ data-spoiler-payload="VGhlIHJlYWwgdHJlYXN1cmUgd2FzIHRoZSBmcmllbmRzIHdlIG1hZGUgYWxvbmcgdGhlIHdheS4=">
278
+ <span class="spoiler-ui__inner"></span>
279
+ <span class="spoiler-ui__overlay" role="button" tabindex="0" aria-label="Reveal Spoiler">
280
+ <span class="spoiler-ui__pill">Spoiler</span>
281
+ </span>
282
+ </span>
283
+
284
+ <!-- After reveal — payload attribute removed, plaintext injected -->
285
+ <span class="spoiler-ui" data-state="revealed" data-revealed="true">
286
+ <span class="spoiler-ui__inner">The real treasure was the friends we made along the way.</span>
287
+ </span>
288
+ ```
289
+
290
+ **HTML attributes:**
291
+
292
+ Encode your content and embed it in `data-spoiler-encoded`:
293
+
294
+ ```html
295
+ <span
296
+ data-spoiler="Spoiler"
297
+ data-spoiler-encoded="VGhlIHJlYWwgdHJlYXN1cmUgd2FzIHRoZSBmcmllbmRzIHdlIG1hZGUgYWxvbmcgdGhlIHdheS4="
298
+ >
299
+ </span>
300
+ ```
301
+
302
+ ```js
303
+ import { initSpoilers } from 'spoiler-ui';
304
+ initSpoilers();
305
+ ```
306
+
307
+ ### React
308
+
309
+ Pass the Base64 string as `children` and set `encoded`:
310
+
311
+ ```jsx
312
+ import { Spoiler } from 'spoiler-ui/react';
313
+ import 'spoiler-ui/style';
314
+
315
+ // Payload prepared at build time or server-side
316
+ const PAYLOAD = 'RGFydGggVmFkZXIgaXMgTHVrZSdzIGZhdGhlci4=';
317
+
318
+ export default function FilmReview() {
319
+ return (
320
+ <p>
321
+ The most iconic twist in cinema history:{' '}
322
+ <Spoiler encoded onReveal={(text) => console.log(text)}>
323
+ {PAYLOAD}
324
+ </Spoiler>
325
+ .
326
+ </p>
327
+ );
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Theming
334
+
335
+ Override CSS variables on `.spoiler-ui` or any ancestor to retheme without touching the stylesheet.
336
+
337
+ ### Stylesheet
338
+
339
+ ```css
340
+ .spoiler-ui {
341
+ --spoiler-pill-bg: #6366f1;
342
+ --spoiler-pill-color: #fff;
343
+ --spoiler-bg: rgba(99, 102, 241, 0.1);
344
+ }
345
+ ```
346
+
347
+ ### React — per-instance via `style` prop
348
+
349
+ ```jsx
350
+ <Spoiler
351
+ label="Critical"
352
+ style={{
353
+ '--spoiler-pill-bg': '#e11d48',
354
+ '--spoiler-pill-color': '#fff',
355
+ '--spoiler-bg': 'rgba(225, 29, 72, 0.08)',
356
+ }}
357
+ >
358
+ CVE-2024-1234 — remote code execution on /api/upload
359
+ </Spoiler>
360
+ ```
361
+
362
+ ### `renderPill` — full pill customisation (React)
363
+
364
+ ```jsx
365
+ <Spoiler
366
+ label="Reveal lineage"
367
+ renderPill={({ state, label }) => (
368
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
369
+ {state === 'idle' ? '🌑' : state === 'loading' ? '⏳' : '⚠️'}
370
+ <em>{label}</em>
371
+ </span>
372
+ )}
373
+ >
374
+ Luke, I am your father.
375
+ </Spoiler>
376
+ ```
377
+
378
+ ---
379
+
380
+ ## API reference
381
+
382
+ ### `createSpoiler(content, options?)`
383
+
384
+ ```js
385
+ import { createSpoiler } from 'spoiler-ui';
386
+ ```
387
+
388
+ | Parameter | Type | Description |
389
+ |-----------|------|-------------|
390
+ | `content` | `string \| Element \| null` | Content to hide. Pass `null` when using `src` or `encoded`. |
391
+ | `options.label` | `string` | Pill text. Default: `'Spoiler'` |
392
+ | `options.blurAmount` | `number` | Blur in px while hidden. Default: `6` |
393
+ | `options.revealOnClick` | `boolean` | Click anywhere on element to reveal. Default: `true` |
394
+ | `options.animationDuration` | `number` | Transition in ms. Default: `300` |
395
+ | `options.className` | `string` | Extra class on root element. Default: `''` |
396
+ | `options.src` | `string` | URL to fetch content from on reveal. |
397
+ | `options.fetchOptions` | `RequestInit` | Passed directly to `fetch()`. Default: `{}` |
398
+ | `options.responseType` | `'text' \| 'html'` | How to inject fetched/decoded content. Default: `'text'` |
399
+ | `options.encoded` | `boolean` | Treat `content` as Base64; decode on reveal. Default: `false` |
400
+ | `options.icon` | `string \| false \| null` | Override the pill icon. String replaces the default 👁; `false`/`null` hides it entirely. Default: uses `--spoiler-icon` CSS variable. |
401
+ | `options.showPill` | `boolean` | Show the pill label button. Default: `true` |
402
+ | `options.loadingLabel` | `string` | Pill text while fetching. Default: `'Loading…'` |
403
+ | `options.errorLabel` | `string` | Pill text on fetch/decode failure. Default: `'Failed — retry?'` |
404
+ | `options.onReveal` | `(content: string) => void` | Called after successful reveal with the revealed text. |
405
+ | `options.onError` | `(err: Error) => void` | Called when a fetch or Base64 decode fails. |
406
+
407
+ Returns an `HTMLElement` ready to insert into the DOM.
408
+
409
+ ---
410
+
411
+ ### `initSpoilers(scope?)`
412
+
413
+ ```js
414
+ import { initSpoilers } from 'spoiler-ui';
415
+ ```
416
+
417
+ Scans `scope` (default: `document`) for `[data-spoiler]` elements and converts them.
418
+
419
+ **Supported HTML attributes:**
420
+
421
+ | Attribute | Corresponds to |
422
+ |-----------|---------------|
423
+ | `data-spoiler` | `label` (required to activate) |
424
+ | `data-spoiler-blur` | `blurAmount` |
425
+ | `data-spoiler-duration` | `animationDuration` |
426
+ | `data-spoiler-reveal-on-click` | `revealOnClick` — set to `'false'` to restrict reveal to the pill button only |
427
+ | `data-spoiler-show-pill` | `showPill` — set to `'false'` to hide the pill label button |
428
+ | `data-spoiler-loading-label` | `loadingLabel` — pill text while fetching |
429
+ | `data-spoiler-error-label` | `errorLabel` — pill text on failure |
430
+ | `data-spoiler-src` | `src` |
431
+ | `data-spoiler-response-type` | `responseType` (`'text'` or `'html'`) |
432
+ | `data-spoiler-encoded` | Base64 payload; enables `encoded` mode |
433
+ | `data-spoiler-class` | `className` — extra CSS class on the root element |
434
+ | `data-spoiler-icon` | `icon` — `'false'` hides the icon; any other string replaces the default 👁 |
435
+
436
+ > `onReveal`, `onError`, and `fetchOptions` cannot be set via HTML attributes — use `createSpoiler()` or the React `<Spoiler>` component for callbacks and custom fetch configuration.
437
+
438
+ ---
439
+
440
+ ### `<Spoiler>` (React)
441
+
442
+ ```js
443
+ import { Spoiler } from 'spoiler-ui/react';
444
+ ```
445
+
446
+ Accepts the same options as `createSpoiler` as props, plus `children` in place of the `content` parameter.
447
+
448
+ | Prop | Type | Default |
449
+ |------|------|---------|
450
+ | `children` | `ReactNode` | — |
451
+ | `label` | `string` | `'Spoiler'` |
452
+ | `blurAmount` | `number` | `6` |
453
+ | `revealOnClick` | `boolean` | `true` |
454
+ | `animationDuration` | `number` | `300` |
455
+ | `className` | `string` | `''` |
456
+ | `src` | `string` | — |
457
+ | `fetchOptions` | `RequestInit` | `{}` |
458
+ | `responseType` | `'text' \| 'html'` | `'text'` |
459
+ | `encoded` | `boolean` | `false` |
460
+ | `icon` | `string \| false \| null` | CSS `--spoiler-icon` (👁 by default) |
461
+ | `showPill` | `boolean` | `true` |
462
+ | `loadingLabel` | `string` | `'Loading…'` |
463
+ | `errorLabel` | `string` | `'Failed — retry?'` |
464
+ | `renderPill` | `(ctx: { state, label }) => ReactNode` | Render prop for full pill customisation. |
465
+ | `onReveal` | `(content?: string) => void` | Called after successful reveal. |
466
+ | `onError` | `(err: Error) => void` | Called when a fetch or decode fails. |
467
+ | `style` | `CSSProperties & { [key: \`--${string}\`]: string }` | Inline styles merged onto root. Use to override CSS variables per instance. |
468
+ | `ref` | `RefObject<HTMLSpanElement>` | Forwarded to the root `<span>` via `forwardRef`. |
469
+
470
+ ---
471
+
472
+ ### `encodeBase64(str)` / `decodeBase64(b64)`
473
+
474
+ ```js
475
+ import { encodeBase64, decodeBase64 } from 'spoiler-ui';
476
+ ```
477
+
478
+ Full Unicode support via `TextEncoder` / `TextDecoder`. Use `encodeBase64` at build time or server-side to prepare payloads for encoded mode.
479
+
480
+ ```js
481
+ const payload = encodeBase64('Snape kills Dumbledore.');
482
+ // → 'U25hcGUga2lsbHMgRHVtYmxlZG9yZS4='
483
+
484
+ decodeBase64('U25hcGUga2lsbHMgRHVtYmxlZG9yZS4=');
485
+ // → 'Snape kills Dumbledore.'
486
+ ```
487
+
488
+ ---
489
+
490
+ ## CSS customisation
491
+
492
+ Import the stylesheet and override variables on `.spoiler-ui` or any parent:
493
+
494
+ ```js
495
+ import 'spoiler-ui/style';
496
+ ```
497
+
498
+ ```css
499
+ .spoiler-ui {
500
+ --spoiler-blur: 8px; /* blur amount while hidden */
501
+ --spoiler-duration: 400ms; /* reveal transition speed */
502
+ --spoiler-bg: rgba(0,0,0,0.2); /* overlay background */
503
+ --spoiler-pill-bg: #1a1a2e; /* pill background (idle) */
504
+ --spoiler-pill-color: #e0e0ff; /* pill text colour */
505
+ --spoiler-pill-font: inherit; /* pill font family */
506
+ --spoiler-radius: 6px; /* corner radius on overlay */
507
+ --spoiler-error-bg: #c0392b; /* pill background on error */
508
+ --spoiler-icon: '🔒'; /* icon shown before pill text */
509
+ --spoiler-pill-padding: 1px 8px;
510
+ --spoiler-pill-font-size: 0.72em;
511
+ --spoiler-pill-font-weight: 600;
512
+ --spoiler-pill-letter-spacing: 0.02em;
513
+ --spoiler-pill-gap: 4px; /* gap between icon and label */
514
+ --spoiler-pill-radius: 100px; /* pill border radius */
515
+ --spoiler-shimmer-color: rgba(255,255,255,0.5);
516
+ --spoiler-shimmer-duration: 2.4s;
517
+ }
518
+ ```
519
+
520
+ Dark mode is **not** applied automatically — override the variables in your own stylesheet:
521
+
522
+ ```css
523
+ @media (prefers-color-scheme: dark) {
524
+ .spoiler-ui {
525
+ --spoiler-bg: rgba(255, 255, 255, 0.08);
526
+ --spoiler-pill-bg: rgba(255, 255, 255, 0.18);
527
+ --spoiler-pill-color: #fff;
528
+ --spoiler-error-bg: #e74c3c;
529
+ }
530
+ }
531
+ ```
532
+
533
+ Animations are disabled automatically via `@media (prefers-reduced-motion: reduce)`.
534
+
535
+ ### Custom overlay animation
536
+
537
+ The overlay is `.spoiler-ui__overlay`. Target it directly to replace or extend the built-in shimmer:
538
+
539
+ ```css
540
+ /* Disable default shimmer */
541
+ .spoiler-ui {
542
+ --spoiler-shimmer-color: transparent;
543
+ }
544
+
545
+ /* Add your own animation */
546
+ .spoiler-ui__overlay::before {
547
+ content: '';
548
+ position: absolute;
549
+ inset: 0;
550
+ border-radius: inherit;
551
+ background-image: url('noise.png');
552
+ animation: my-particles 3s linear infinite;
553
+ }
554
+
555
+ @keyframes my-particles {
556
+ to { background-position: 200px 200px; }
557
+ }
558
+ ```
559
+
560
+ The overlay switches to `opacity: 0; pointer-events: none` on reveal — your custom animation cleans up automatically.
561
+
562
+ ---
563
+
564
+ ## Accessibility
565
+
566
+ - The reveal button has `role="button"`, `tabindex="0"`, and `aria-label="Reveal {label}"`.
567
+ - `aria-expanded="false"` is set while hidden; the attribute is removed after reveal.
568
+ - Keyboard: `Enter` or `Space` to reveal — works regardless of `revealOnClick`.
569
+ - After reveal the overlay is removed from tab order and marked `aria-hidden`.
570
+ - Focus ring shown via `:focus-visible` (no ring on mouse click).
571
+
572
+ ---
573
+
574
+ ## License
575
+
576
+ MIT
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "spoiler-ui",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight spoiler/content-reveal UI element — like Threads & Discord. Vanilla JS + optional React wrapper.",
5
+ "keywords": ["spoiler", "ui", "reveal", "blur", "discord", "threads", "component"],
6
+ "author": "Dzmitry Ihnatovich",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "types": "./spoiler.d.ts",
10
+ "main": "./spoiler.js",
11
+ "module": "./spoiler.js",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./spoiler.js",
15
+ "types": "./spoiler.d.ts"
16
+ },
17
+ "./react": {
18
+ "import": "./react.jsx",
19
+ "types": "./react.d.ts"
20
+ },
21
+ "./style": {
22
+ "import": "./spoiler.css",
23
+ "types": "./style.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "spoiler.js",
28
+ "spoiler.d.ts",
29
+ "react.jsx",
30
+ "react.d.ts",
31
+ "spoiler.css",
32
+ "style.d.ts",
33
+ "README.md"
34
+ ],
35
+ "sideEffects": [
36
+ "./spoiler.css"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "react": ">=17.0.0",
43
+ "react-dom": ">=17.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "react": { "optional": true },
47
+ "react-dom": { "optional": true }
48
+ },
49
+ "devDependencies": {
50
+ "vite": "^5.0.0"
51
+ },
52
+ "scripts": {
53
+ "dev": "vite demo",
54
+ "build": "vite build"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/guestDI/spoiler-ui"
59
+ },
60
+ "bugs": {
61
+ "url": "https://github.com/guestDI/spoiler-ui/issues"
62
+ },
63
+ "homepage": "https://github.com/guestDI/spoiler-ui#readme"
64
+ }