dynaura 0.0.0-stage → 0.1.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 dynaura
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 CHANGED
@@ -1,3 +1,423 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # dynaura
4
+
5
+ **Dynamic Island toasts for the web.**<br />
6
+ One pill that spring-morphs between states, swipes away, and stays out of your way.
7
+
8
+ [![npm version](https://img.shields.io/npm/v/dynaura?color=15140f&label=npm)](https://www.npmjs.com/package/dynaura)
9
+ [![license](https://img.shields.io/badge/license-MIT-15140f)](./LICENSE)
10
+ [![types](https://img.shields.io/badge/types-included-3178c6)](#typescript)
11
+ [![dependencies](https://img.shields.io/badge/dependencies-0-30d158)](./package.json)
12
+
13
+ [**Documentation**](https://dynaura.ashutoshswamy.in) &nbsp;·&nbsp; [GitHub](https://github.com/ashutoshswamy/dynaura) &nbsp;·&nbsp; [Report a bug](https://github.com/ashutoshswamy/dynaura/issues)
14
+
15
+ </div>
16
+
17
+ <br />
18
+
19
+ ```tsx
20
+ dynaura.success({ title: "Saved", description: "Your changes are live." });
21
+ ```
22
+
23
+ That is the whole setup: mount one `<Toaster />`, call `dynaura` from anywhere.
24
+
25
+ ## Contents
26
+
27
+ [Why dynaura](#why-dynaura) · [Install](#install) · [Quick start](#quick-start) · [Recipes](#recipes) · [API](#api) · [Gestures](#gestures) · [Theming](#theming) · [Accessibility](#accessibility) · [Browser support](#browser-support) · [Troubleshooting](#troubleshooting) · [Development](#development) · [Author](#author)
28
+
29
+ ## Why dynaura
30
+
31
+ | | |
32
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | **One island, real motion** | A single pill morphs between compact and expanded using a CSS `linear()` spring. No animation library. |
34
+ | **Rich cards** | `variant: "card"` turns the toast into a header tab plus any custom body, joined by a curved notch. Flight passes, ride trackers, receipts. |
35
+ | **Live activities** | Sticky toasts with a running timer or an animated waveform. |
36
+ | **Progress that feels alive** | A value bar and rolling digits, updated in place with `dynaura.update`. |
37
+ | **Built for touch** | Swipe to dismiss, tap or long-press to pin, hover to pause. |
38
+ | **Themeable** | Dark, light, system, or your own colors. Everything is a CSS variable. |
39
+ | **Any framework** | React, Vue, Svelte, or plain JS, all on one shared core. |
40
+ | **Light and accessible** | Zero dependencies, typed, `aria-live`, and `prefers-reduced-motion` respected. |
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ npm install dynaura
46
+ ```
47
+
48
+ ## Quick start
49
+
50
+ Mount **one** `Toaster`, import the stylesheet once, then call `dynaura` from anywhere.
51
+
52
+ ### React
53
+
54
+ ```tsx
55
+ import { Toaster, dynaura } from "dynaura";
56
+ import "dynaura/styles.css";
57
+
58
+ export default function App() {
59
+ return (
60
+ <>
61
+ <Toaster />
62
+ <button
63
+ onClick={() =>
64
+ dynaura.success({
65
+ title: "Saved",
66
+ description: "Your changes are live.",
67
+ })
68
+ }
69
+ >
70
+ Save
71
+ </button>
72
+ </>
73
+ );
74
+ }
75
+ ```
76
+
77
+ Works with Next.js App Router: the bundle ships with `"use client"`, so render `<Toaster />` from a client component or your root layout.
78
+
79
+ <details>
80
+ <summary><b>Vue</b></summary>
81
+
82
+ ```vue
83
+ <script setup>
84
+ import { Toaster, dynaura } from "dynaura/vue";
85
+ import "dynaura/styles.css";
86
+ </script>
87
+
88
+ <template>
89
+ <Toaster position="top" theme="system" />
90
+ <button @click="dynaura.success('Saved')">Save</button>
91
+ </template>
92
+ ```
93
+
94
+ </details>
95
+
96
+ <details>
97
+ <summary><b>Svelte</b></summary>
98
+
99
+ ```svelte
100
+ <script>
101
+ import { toaster, dynaura } from "dynaura/svelte";
102
+ import "dynaura/styles.css";
103
+ </script>
104
+
105
+ <div use:toaster={{ position: "top", theme: "system" }} />
106
+ <button on:click={() => dynaura.success("Saved")}>Save</button>
107
+ ```
108
+
109
+ </details>
110
+
111
+ <details>
112
+ <summary><b>Vanilla JS</b></summary>
113
+
114
+ ```js
115
+ import { createToaster, dynaura } from "dynaura/vanilla";
116
+ import "dynaura/styles.css";
117
+
118
+ const toaster = createToaster({ position: "top" });
119
+ dynaura.info("Hello");
120
+ // toaster.destroy() to unmount
121
+ ```
122
+
123
+ </details>
124
+
125
+ ## Recipes
126
+
127
+ ### Success, error and actions
128
+
129
+ ```ts
130
+ dynaura.error({
131
+ title: "Payment failed",
132
+ description: "Card ending 4242 was declined.",
133
+ button: { title: "Retry", onClick: retry },
134
+ });
135
+ ```
136
+
137
+ The island expands to show the description and button, then collapses before it dismisses. A `button` click dismisses the toast.
138
+
139
+ ### Promises
140
+
141
+ ```ts
142
+ dynaura.promise(save(), {
143
+ loading: "Saving…",
144
+ success: (data) => ({ title: "Saved", description: `Version ${data.v}` }),
145
+ error: { title: "Failed", description: "Check your connection." },
146
+ });
147
+ ```
148
+
149
+ The same island morphs from loading to success or error. `success` and `error` can be strings, option objects, or functions of the result. The original promise is returned.
150
+
151
+ ### Progress
152
+
153
+ ```ts
154
+ const id = dynaura.loading({ title: "Uploading", progress: 0 });
155
+
156
+ onProgress((p) => dynaura.update(id, { progress: p })); // 0 to 1
157
+
158
+ dynaura.update(id, {
159
+ type: "success",
160
+ title: "Upload complete",
161
+ progress: undefined, // clear the bar
162
+ duration: 4000,
163
+ confetti: true,
164
+ });
165
+ ```
166
+
167
+ Frequent `progress` updates do not restart the toast.
168
+
169
+ ### Live activities
170
+
171
+ ```ts
172
+ dynaura.live({
173
+ title: "Recording voice memo",
174
+ button: { title: "Stop", onClick: stop },
175
+ });
176
+
177
+ dynaura.live({ title: "Now playing", kind: "wave" }); // animated waveform instead of a timer
178
+ ```
179
+
180
+ ### Rich cards
181
+
182
+ ```tsx
183
+ dynaura.info({
184
+ title: "Boarding soon",
185
+ trailing: "40 min",
186
+ variant: "card",
187
+ duration: 9000,
188
+ description: <BoardingPass />, // any React node (or a DOM Node in Vue, Svelte and vanilla)
189
+ button: { title: "Show boarding pass", onClick: openPass },
190
+ });
191
+ ```
192
+
193
+ Card content inherits the toast colors through `var(--dynaura-fg)`, `var(--dynaura-muted)` and `var(--state)`, so it follows any theme. It looks best with `theme="light"`. The header tab follows `position`: centered for `top` and `bottom`, flush to the edge for `-left` and `-right`.
194
+
195
+ ### Queue
196
+
197
+ ```ts
198
+ // show toasts one after another instead of replacing the current one
199
+ <Toaster queue />
200
+
201
+ // or per toast
202
+ dynaura.info({ title: "Invite sent to Maya", queue: true, duration: 1600 });
203
+ ```
204
+
205
+ ### Custom icons and trailing slots
206
+
207
+ ```tsx
208
+ import { CalendarDaysIcon } from "@heroicons/react/24/outline";
209
+
210
+ dynaura.info({
211
+ title: "Design sync starts soon",
212
+ icon: <CalendarDaysIcon width={16} height={16} />, // replaces the built-in icon
213
+ trailing: "5 min", // small right-side slot
214
+ });
215
+ ```
216
+
217
+ ## API
218
+
219
+ ### `dynaura`
220
+
221
+ | Method | Description |
222
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
223
+ | `success(opts)` `error(opts)` `warning(opts)` `info(opts)` | Show a toast. `opts` is a string (title only) or an options object. Returns the toast id. |
224
+ | `loading(opts)` | Sticky toast with a spinner. |
225
+ | `live(opts)` | Live activity: sticky, with a running timer (default) or a waveform (`kind: "wave"`). |
226
+ | `promise(promise, { loading, success, error })` | Morphs loading into success or error. Returns the original promise. |
227
+ | `update(id, patch)` | Patch a toast in place, including `type`. Also works on queued toasts. |
228
+ | `dismiss(id?)` | Dismiss the current toast (the next queued one shows) or a specific id. |
229
+ | `clear()` | Dismiss everything, including the queue. |
230
+
231
+ ### Toast options
232
+
233
+ | Option | Type | Default | Description |
234
+ | ------------- | ----------------------------- | ----------------- | ----------------------------------------------------------------------- |
235
+ | `title` | `string` | required | Main text. |
236
+ | `description` | `string \| Node \| ReactNode` | | Shown when the island expands. |
237
+ | `icon` | same | built-in | Replaces the leading icon. |
238
+ | `trailing` | same | | Small right-side slot, e.g. `"2 min"`. |
239
+ | `duration` | `number \| null` | `4000` | Milliseconds. `null` is sticky. `loading` and `live` default to `null`. |
240
+ | `button` | `{ title, onClick }` | | Action button. The toast dismisses after the click. |
241
+ | `progress` | `number` (0 to 1) | | Value bar plus rolling-digit percent. |
242
+ | `confetti` | `boolean` | `false` | Confetti burst when shown. Skipped under reduced motion. |
243
+ | `kind` | `"timer" \| "wave"` | `"timer"` | Flavor of a `live` toast. |
244
+ | `variant` | `"default" \| "card"` | `"default"` | `"card"`: header tab plus a full-bleed custom body. |
245
+ | `queue` | `boolean` | Toaster's `queue` | Per-toast override of the Toaster option. |
246
+
247
+ > Strings are always inserted as text, never as HTML.
248
+ > React nodes work in the React adapter. Vue, Svelte and vanilla accept strings or DOM nodes.
249
+
250
+ ### `<Toaster />` and `createToaster()` options
251
+
252
+ | Option | Type | Default | Description |
253
+ | ---------- | ------------------------------------------------------------------------------ | -------- | -------------------------------------------------------- |
254
+ | `position` | `"top"` `"top-left"` `"top-right"` `"bottom"` `"bottom-left"` `"bottom-right"` | `"top"` | Where the island sits. |
255
+ | `offset` | `number` | `12` | Pixels from the screen edge. Safe-area insets are added. |
256
+ | `theme` | `"dark" \| "light" \| "system" \| CustomTheme` | `"dark"` | See [Theming](#theming). |
257
+ | `queue` | `boolean` | `false` | Show toasts one after another. |
258
+ | `timer` | `boolean` | `true` | Thin time-remaining bar on timed toasts. |
259
+ | `haptics` | `boolean` | `true` | `navigator.vibrate` on show. A no-op where unsupported. |
260
+
261
+ Mount **one** Toaster per page.
262
+
263
+ ### Entry points
264
+
265
+ | Import | Use |
266
+ | -------------------- | ------------------------------------ |
267
+ | `dynaura` | React: `Toaster`, `dynaura` |
268
+ | `dynaura/vue` | Vue 3: `Toaster`, `dynaura` |
269
+ | `dynaura/svelte` | Svelte: `toaster` action, `dynaura` |
270
+ | `dynaura/vanilla` | Plain JS: `createToaster`, `dynaura` |
271
+ | `dynaura/styles.css` | The stylesheet. Import it once. |
272
+
273
+ ### TypeScript
274
+
275
+ Types ship with the package: `ToastOptions`, `ToastType`, `ToasterProps`, `CustomTheme`, `Position` and `Content`.
276
+
277
+ ```ts
278
+ import type { ToastOptions, CustomTheme } from "dynaura";
279
+ ```
280
+
281
+ ## Gestures
282
+
283
+ | Gesture | Result |
284
+ | -------------------------------- | ----------------------------------------------------------------------- |
285
+ | **Hover** (mouse) | Expands and pauses the timer. Leaving resumes with the exact time left. |
286
+ | **Tap** | Pins an expandable toast open and paused. Tap outside to unpin. |
287
+ | **Long-press** | Pins, with haptic feedback. |
288
+ | **Swipe** toward the screen edge | Dismisses. A short swipe snaps back. |
289
+
290
+ ## Theming
291
+
292
+ ```tsx
293
+ <Toaster theme="light" />
294
+ <Toaster theme="system" /> {/* follows the OS */}
295
+ <Toaster theme={{ background: "#2b1055", foreground: "#ffffff", success: "#7cffcb" }} />
296
+ ```
297
+
298
+ `CustomTheme` keys take any CSS color, and all are optional: `background`, `foreground`, `muted`, `ring`, `button`, `success`, `error`, `warning`, `info`, `loading`, `live`. Missing keys fall back to the dark theme.
299
+
300
+ Everything is a CSS variable, so you can also override styles directly:
301
+
302
+ ```css
303
+ .dynaura-root {
304
+ --dynaura-font: "Inter", system-ui, sans-serif;
305
+ --dynaura-duration: 500ms; /* morph speed */
306
+ --dynaura-success: #00c853;
307
+ }
308
+ ```
309
+
310
+ ### Font
311
+
312
+ The default font is [Bricolage Grotesque](https://fonts.google.com/specimen/Bricolage+Grotesque). `dynaura/styles.css` loads it from Google Fonts with an `@import` on its first line. To self-host or skip the request, copy the stylesheet without that line and set `--dynaura-font`.
313
+
314
+ ## Accessibility
315
+
316
+ - The island has `role="status"` and `aria-live="polite"`.
317
+ - Action buttons are keyboard focusable, with a visible focus ring.
318
+ - Under `prefers-reduced-motion: reduce`, motion is cut to near-instant. Loading spinners keep spinning.
319
+ - Rolling digits expose their value through `aria-label`.
320
+ - Text is rendered as text. Toast strings can never inject HTML.
321
+
322
+ ## Browser support
323
+
324
+ Modern evergreen browsers. The morph uses CSS `linear()` easing (Chrome 113+, Safari 17.2+, Firefox 112+) and `color-mix()`. Older browsers fall back to a bounce curve. Without `color-mix()` support, tinted backgrounds fall back to a neutral tint.
325
+
326
+ Tested in Chrome and WebKit with Playwright. Firefox has not been verified yet.
327
+
328
+ ## Troubleshooting
329
+
330
+ <details>
331
+ <summary><b>Nothing appears</b></summary>
332
+
333
+ Check that `<Toaster />` is mounted once and that `dynaura/styles.css` is imported. Without the stylesheet the island renders unstyled.
334
+
335
+ </details>
336
+
337
+ <details>
338
+ <summary><b>The font is wrong, or I need to avoid Google Fonts</b></summary>
339
+
340
+ The stylesheet's first line is an `@import` for Bricolage Grotesque. Copy the stylesheet without it, or set `--dynaura-font` to your own font. This is also the fix for a strict Content Security Policy.
341
+
342
+ </details>
343
+
344
+ <details>
345
+ <summary><b>Can I have more than one Toaster?</b></summary>
346
+
347
+ No. State is a single global, so mount one Toaster per page.
348
+
349
+ </details>
350
+
351
+ <details>
352
+ <summary><b>Does it work with SSR?</b></summary>
353
+
354
+ Yes. The Toaster renders nothing on the server and mounts on the client. Calling `dynaura.*` on the server only sets state and shows nothing.
355
+
356
+ </details>
357
+
358
+ <details>
359
+ <summary><b>The toast sits under the notch or home indicator on iOS</b></summary>
360
+
361
+ Add `viewport-fit=cover` to your viewport meta tag. dynaura adds `env(safe-area-inset-*)` to the offset.
362
+
363
+ </details>
364
+
365
+ <details>
366
+ <summary><b>My description is a React component but it does not show in Vue or Svelte</b></summary>
367
+
368
+ React nodes only work in the React adapter. In Vue, Svelte and vanilla, pass a string or a DOM `Node`.
369
+
370
+ </details>
371
+
372
+ ## Development
373
+
374
+ ```sh
375
+ git clone https://github.com/ashutoshswamy/dynaura.git
376
+ cd dynaura
377
+ npm install
378
+ npm run dev # local test playground (not the docs site): imports src/ directly
379
+ npm test # vitest + jsdom
380
+ npm run build # tsup, outputs dist/
381
+ ```
382
+
383
+ The playground at `http://localhost:5173` exercises every feature: themes (with live color pickers for custom), positions, queue, the timer bar, rich cards, and copyable code for each example. The code shown there comes from the files that run, so it cannot drift.
384
+
385
+ <details>
386
+ <summary><b>How it works</b></summary>
387
+
388
+ - **One island.** A single DOM element holds the current toast. A new toast morphs it instead of stacking, unless `queue` is on.
389
+ - **Size morph.** The core measures the compact row and the expanded content, then transitions `width`, `height` and `border-radius` between them with a CSS `linear()` spring.
390
+ - **Card variant.** The header tab and body are drawn as one `clip-path`. The core recomputes the outline every frame while the size transitions.
391
+ - **Timers.** Remaining time is tracked, so hover, drag and pin pause and resume exactly. The timer bar is a CSS animation that pauses with them.
392
+ - **Adapters.** Vue, Svelte and vanilla are thin wrappers over `createToaster()`. React adds portals so `description`, `icon` and `trailing` can be React nodes.
393
+
394
+ </details>
395
+
396
+ <details>
397
+ <summary><b>Adding an adapter</b></summary>
398
+
399
+ Call `createToaster(options)` on mount and `.destroy()` on unmount. Forward option changes with `.set()`. If the framework has its own node type, pass a `render(el, node)` option: the core calls it with `node = null` to unmount.
400
+
401
+ </details>
402
+
403
+ <details>
404
+ <summary><b>Releasing</b></summary>
405
+
406
+ ```sh
407
+ npm version patch # or minor / major
408
+ npm publish # prepublishOnly runs typecheck, build and tests first
409
+ ```
410
+
411
+ </details>
412
+
413
+ ## Author
414
+
415
+ Built and maintained by **Ashutosh Swamy**.
416
+
417
+ [Portfolio](https://ashutoshswamy.in) &nbsp;·&nbsp; [GitHub](https://github.com/ashutoshswamy) &nbsp;·&nbsp; [LinkedIn](https://www.linkedin.com/in/ashutoshswamy) &nbsp;·&nbsp; [X](https://x.com/ashutoshswamy_)
418
+
419
+ If dynaura is useful to you, a star on [GitHub](https://github.com/ashutoshswamy/dynaura) helps others find it.
420
+
421
+ ## License
422
+
423
+ [MIT](./LICENSE) © 2026 dynaura