@mateosuarezdev/flash 0.0.1
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.md +32 -0
- package/README.md +945 -0
- package/dist/debug.cjs +11 -0
- package/dist/debug.d.ts +195 -0
- package/dist/debug.js +28 -0
- package/dist/index.cjs +11 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.js +1335 -0
- package/dist/jsx-dev-runtime-BfTCjShK.js +65 -0
- package/dist/jsx-dev-runtime-D4XANMVW.cjs +11 -0
- package/dist/jsx-dev-runtime.cjs +11 -0
- package/dist/jsx-dev-runtime.d.ts +90 -0
- package/dist/jsx-dev-runtime.js +17 -0
- package/dist/jsx-runtime.cjs +11 -0
- package/dist/jsx-runtime.d.ts +90 -0
- package/dist/jsx-runtime.js +17 -0
- package/dist/server.cjs +27 -0
- package/dist/server.d.ts +60 -0
- package/dist/server.js +224 -0
- package/dist/utils.cjs +11 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +18 -0
- package/package.json +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
# ⚡ Flash
|
|
2
|
+
|
|
3
|
+
> Fine-grained reactive JSX framework with zero VDOM overhead
|
|
4
|
+
|
|
5
|
+
Flash is a lightweight, performant JSX framework built on Preact Signals for fine-grained reactivity. Write familiar JSX, get blazing fast updates with direct DOM manipulation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Client-Side
|
|
12
|
+
- ⚡ **Fine-grained reactivity** - Powered by Preact Signals, updates only what changed
|
|
13
|
+
- 🎯 **No Virtual DOM** - Direct DOM manipulation for maximum performance
|
|
14
|
+
- 🎭 **Flexible animations** - Use any library (Framer Motion, GSAP, etc.) or vanilla CSS/classes
|
|
15
|
+
- 🎪 **DOM resurrection** - Automatic animation reversal for rapid toggling
|
|
16
|
+
- 🔑 **Keyed list rendering** - SolidJS-style efficient list updates (insert/update/delete/move)
|
|
17
|
+
- 🎨 **View Transitions API** - Smooth page transitions out of the box
|
|
18
|
+
- 🏎️ **Frame scheduler** - Prevent layout thrashing with read/update/render phases
|
|
19
|
+
- 🎬 **FLIP animations** - Built-in utilities for performant layout animations
|
|
20
|
+
- 🪄 **Auto-animate** - Automatic layout animations (in progress)
|
|
21
|
+
- 🪝 **First-class exit animations** - `onBeforeExit` pauses unmounting (save data, animations, etc.)
|
|
22
|
+
- 🌳 **Context API** - Share state across component trees
|
|
23
|
+
- 🧭 **Built-in Router** - File-based routing with lazy loading (work in progress)
|
|
24
|
+
- 📦 **Tiny bundle size** - No compiler required, minimal runtime
|
|
25
|
+
- 🔄 **Reactive props** - Props can be signals for automatic updates
|
|
26
|
+
|
|
27
|
+
### Server-Side
|
|
28
|
+
- 🌐 **Server-Side Rendering** - Three rendering strategies (sync, async, streaming)
|
|
29
|
+
- ⚡ **Parallel async resolution** - Resolves async components efficiently
|
|
30
|
+
- 📦 **Pre-rendering & caching** - Built-in static site generation
|
|
31
|
+
- 🌊 **Progressive enhancement** - Stream HTML for better perceived performance
|
|
32
|
+
- 🔒 **Automatic XSS protection** - HTML escaping by default
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @mateosuarezdev/flash @preact/signals-core
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Configure your `tsconfig.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"compilerOptions": {
|
|
47
|
+
"jsx": "react-jsx",
|
|
48
|
+
"jsxImportSource": "@mateosuarezdev/flash/runtime"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { render, onMount } from "@mateosuarezdev/flash";
|
|
59
|
+
import { signal } from "@preact/signals-core";
|
|
60
|
+
|
|
61
|
+
const count = signal(0);
|
|
62
|
+
|
|
63
|
+
function Counter() {
|
|
64
|
+
onMount(() => {
|
|
65
|
+
console.log("Counter mounted!");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<h1>Count: {() => count.value}</h1>
|
|
71
|
+
<button onClick={() => count.value++}>Increment</button>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render(<Counter />, document.getElementById("app"));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Core Concepts
|
|
82
|
+
|
|
83
|
+
### Reactive Boundaries
|
|
84
|
+
|
|
85
|
+
Wrap expressions in functions to create reactive boundaries that update automatically when signals change:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { signal } from "@preact/signals-core";
|
|
89
|
+
|
|
90
|
+
const name = signal("World");
|
|
91
|
+
|
|
92
|
+
function Greeting() {
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
{/* This updates when name changes */}
|
|
96
|
+
<h1>Hello {() => name.value}!</h1>
|
|
97
|
+
|
|
98
|
+
{/* Conditional rendering */}
|
|
99
|
+
{() =>
|
|
100
|
+
name.value === "World" ? (
|
|
101
|
+
<p>Welcome!</p>
|
|
102
|
+
) : (
|
|
103
|
+
<p>Hello, {() => name.value}!</p>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Reactive Props
|
|
112
|
+
|
|
113
|
+
Props can be functions that automatically update:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const isDark = signal(false)
|
|
117
|
+
|
|
118
|
+
<button
|
|
119
|
+
className={() => isDark.value ? 'dark' : 'light'}
|
|
120
|
+
disabled={() => !isDark.value}
|
|
121
|
+
>
|
|
122
|
+
Toggle
|
|
123
|
+
</button>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Keyed Lists
|
|
127
|
+
|
|
128
|
+
Use the `key` prop for efficient list rendering:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
const items = signal([
|
|
132
|
+
{ id: 1, name: "Apple" },
|
|
133
|
+
{ id: 2, name: "Banana" },
|
|
134
|
+
{ id: 3, name: "Cherry" },
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
function List() {
|
|
138
|
+
return (
|
|
139
|
+
<ul>
|
|
140
|
+
{() => items.value.map((item) => <li key={item.id}>{item.name}</li>)}
|
|
141
|
+
</ul>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Flash efficiently handles:
|
|
147
|
+
|
|
148
|
+
- ✅ **Inserts** - New items are rendered and inserted
|
|
149
|
+
- ✅ **Removals** - Deleted items are unmounted and removed
|
|
150
|
+
- ✅ **Reordering** - DOM nodes are moved to match new order
|
|
151
|
+
- ✅ **Updates** - Existing items are reused (no re-render)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Lifecycle Hooks
|
|
156
|
+
|
|
157
|
+
### onMount
|
|
158
|
+
|
|
159
|
+
Runs after the component is mounted to the DOM:
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
function Component() {
|
|
163
|
+
let ref: HTMLDivElement;
|
|
164
|
+
|
|
165
|
+
onMount(() => {
|
|
166
|
+
console.log("Element:", ref);
|
|
167
|
+
// Fetch data, start animations, etc.
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return <div ref={(el) => (ref = el)}>Content</div>;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### onUnmount
|
|
175
|
+
|
|
176
|
+
Runs when the component is removed from the DOM:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
function Component() {
|
|
180
|
+
onUnmount(() => {
|
|
181
|
+
console.log("Cleaning up...");
|
|
182
|
+
// Cancel subscriptions, clear timers, etc.
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return <div>Content</div>;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### onBeforeExit
|
|
190
|
+
|
|
191
|
+
Runs before unmounting - a **first-class feature**, not a workaround. You can pause the entire unmount process to:
|
|
192
|
+
- Play exit animations
|
|
193
|
+
- Save form data or state
|
|
194
|
+
- Confirm user actions
|
|
195
|
+
- Clean up async operations
|
|
196
|
+
- Anything you need before removal
|
|
197
|
+
|
|
198
|
+
The unmount tree is held until your async callback completes:
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { animate } from "framer-motion";
|
|
202
|
+
|
|
203
|
+
function FadeBox() {
|
|
204
|
+
let ref: HTMLElement;
|
|
205
|
+
|
|
206
|
+
onBeforeExit(async (token) => {
|
|
207
|
+
// Play exit animation
|
|
208
|
+
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 });
|
|
209
|
+
|
|
210
|
+
// Handle cancellation (e.g., rapid toggling)
|
|
211
|
+
token.onCancel(() => {
|
|
212
|
+
animation.stop();
|
|
213
|
+
animate(ref, { opacity: 1 }, { duration: 0.3 });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await animation.finished;
|
|
217
|
+
// Component won't unmount until animation completes
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return <div ref={(el) => (ref = el)}>Fading content</div>;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Save data before unmounting:**
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
function Form() {
|
|
228
|
+
const formData = signal({ name: "", email: "" });
|
|
229
|
+
|
|
230
|
+
onBeforeExit(async () => {
|
|
231
|
+
// Save to localStorage or API
|
|
232
|
+
await saveFormData(formData.value);
|
|
233
|
+
console.log("Data saved before unmount!");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return <form>...</form>;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Cancellation Example:**
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
const show = signal(true)
|
|
244
|
+
|
|
245
|
+
// Rapid toggling: true → false → true
|
|
246
|
+
// Flash will cancel the exit animation and reuse the DOM!
|
|
247
|
+
<button onClick={() => show.value = !show.value}>
|
|
248
|
+
Toggle
|
|
249
|
+
</button>
|
|
250
|
+
|
|
251
|
+
{() => show.value && <FadeBox />}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Context API
|
|
257
|
+
|
|
258
|
+
Share state across component trees without prop drilling:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import { createContext, useContext } from "@mateosuarezdev/flash";
|
|
262
|
+
|
|
263
|
+
const ThemeContext = createContext({ theme: "light" });
|
|
264
|
+
|
|
265
|
+
function App() {
|
|
266
|
+
ThemeContext.provide({ theme: "dark" });
|
|
267
|
+
|
|
268
|
+
return <Child />;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function Child() {
|
|
272
|
+
const theme = useContext(ThemeContext);
|
|
273
|
+
console.log(theme); // { theme: 'dark' }
|
|
274
|
+
|
|
275
|
+
return <div>Theme: {theme.theme}</div>;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Router (Work in Progress)
|
|
282
|
+
|
|
283
|
+
Flash includes a built-in router with reactive pathname tracking and custom URL change events:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import { Router, pathname, push, replace, back, onUrlChange } from '@mateosuarezdev/flash/router'
|
|
287
|
+
|
|
288
|
+
function App() {
|
|
289
|
+
return (
|
|
290
|
+
<Router>
|
|
291
|
+
<nav>
|
|
292
|
+
<a href="/" onClick={(e) => { e.preventDefault(); push('/') }}>Home</a>
|
|
293
|
+
<a href="/about" onClick={(e) => { e.preventDefault(); push('/about') }}>About</a>
|
|
294
|
+
</nav>
|
|
295
|
+
|
|
296
|
+
{/* Reactive routing based on pathname signal */}
|
|
297
|
+
{() => {
|
|
298
|
+
switch (pathname.value) {
|
|
299
|
+
case '/':
|
|
300
|
+
return <Home />
|
|
301
|
+
case '/about':
|
|
302
|
+
return <About />
|
|
303
|
+
default:
|
|
304
|
+
return <NotFound />
|
|
305
|
+
}
|
|
306
|
+
}}
|
|
307
|
+
</Router>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Core Router Features
|
|
313
|
+
|
|
314
|
+
**Reactive Pathname Tracking:**
|
|
315
|
+
```tsx
|
|
316
|
+
import { pathname } from '@mateosuarezdev/flash/router'
|
|
317
|
+
|
|
318
|
+
// pathname is a signal that updates on navigation
|
|
319
|
+
function NavBar() {
|
|
320
|
+
return (
|
|
321
|
+
<nav>
|
|
322
|
+
<a
|
|
323
|
+
class={() => pathname.value === '/' ? 'active' : ''}
|
|
324
|
+
href="/"
|
|
325
|
+
>
|
|
326
|
+
Home
|
|
327
|
+
</a>
|
|
328
|
+
</nav>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Programmatic Navigation:**
|
|
334
|
+
```tsx
|
|
335
|
+
import { push, replace, back } from '@mateosuarezdev/flash/router'
|
|
336
|
+
|
|
337
|
+
// Navigate to a new route
|
|
338
|
+
push('/dashboard')
|
|
339
|
+
|
|
340
|
+
// Replace current route (no history entry)
|
|
341
|
+
replace('/login')
|
|
342
|
+
|
|
343
|
+
// Go back in history
|
|
344
|
+
back()
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**URL Change Events:**
|
|
348
|
+
```tsx
|
|
349
|
+
import { onUrlChange } from '@mateosuarezdev/flash/router'
|
|
350
|
+
|
|
351
|
+
function Component() {
|
|
352
|
+
onUrlChange((event) => {
|
|
353
|
+
if (event) {
|
|
354
|
+
console.log('Navigation:', event.action) // 'pushState' | 'replaceState' | 'popstate' | 'beforeunload'
|
|
355
|
+
console.log('From:', event.oldURL?.pathname)
|
|
356
|
+
console.log('To:', event.newURL?.pathname)
|
|
357
|
+
|
|
358
|
+
// Prevent navigation by calling event.preventDefault()
|
|
359
|
+
// Great for unsaved changes warnings
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
return <div>Content</div>
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Custom UrlChangeEvent:**
|
|
368
|
+
|
|
369
|
+
The router automatically intercepts and dispatches custom `urlchangeevent` for all navigation:
|
|
370
|
+
- `pushState` - New history entry
|
|
371
|
+
- `replaceState` - Replace current entry
|
|
372
|
+
- `popstate` - Back/forward navigation
|
|
373
|
+
- `beforeunload` - Page close/reload
|
|
374
|
+
|
|
375
|
+
Events can be prevented to block navigation (useful for form guards, unsaved changes, etc.)
|
|
376
|
+
|
|
377
|
+
**Current Features:**
|
|
378
|
+
- ✅ Reactive pathname signal
|
|
379
|
+
- ✅ Programmatic navigation (push, replace, back)
|
|
380
|
+
- ✅ Custom URL change events with prevention
|
|
381
|
+
- ✅ History state tracking
|
|
382
|
+
- ✅ Lifecycle integration (auto-cleanup with onUnmount)
|
|
383
|
+
|
|
384
|
+
**Planned Features:**
|
|
385
|
+
- 📁 File-based routing with automatic route generation
|
|
386
|
+
- 🔀 Lazy loading and code splitting helpers
|
|
387
|
+
- 🎨 Integrated View Transitions API support
|
|
388
|
+
- 📱 Nested routes and layouts
|
|
389
|
+
- 🎯 Route guards and middleware
|
|
390
|
+
- 🔍 Path parameter extraction
|
|
391
|
+
- 🔗 Link component with active state
|
|
392
|
+
|
|
393
|
+
**Note:** The router is currently in active development. The current implementation provides low-level primitives for building routing solutions!
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Animations & Performance
|
|
398
|
+
|
|
399
|
+
Flash is designed to be a **batteries-included framework** with powerful animation and performance utilities built right in.
|
|
400
|
+
|
|
401
|
+
### Flexible Animation Options
|
|
402
|
+
|
|
403
|
+
Flash gives you **complete freedom** to animate however you want:
|
|
404
|
+
|
|
405
|
+
#### 1. Use Any Animation Library
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
import { animate } from 'framer-motion'
|
|
409
|
+
import { animate as animateJsAnimate } from 'animejs'
|
|
410
|
+
|
|
411
|
+
function Component() {
|
|
412
|
+
let ref: HTMLElement
|
|
413
|
+
|
|
414
|
+
onMount(() => {
|
|
415
|
+
// Framer Motion
|
|
416
|
+
animate(ref, { x: 100 }, { duration: 0.3 })
|
|
417
|
+
|
|
418
|
+
// Anime.js, GSAP, Motion One, or any library!
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
onBeforeExit(async (token) => {
|
|
422
|
+
const animation = animate(ref, { opacity: 0 }, { duration: 0.3 })
|
|
423
|
+
|
|
424
|
+
token.onCancel(() => {
|
|
425
|
+
animation.stop()
|
|
426
|
+
animate(ref, { opacity: 1 }, { duration: 0.3 })
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
await animation.finished
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
return <div ref={(el) => ref = el}>Animated</div>
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### 2. Vanilla CSS Transitions
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
function Component() {
|
|
440
|
+
let ref: HTMLElement
|
|
441
|
+
|
|
442
|
+
onMount(() => {
|
|
443
|
+
ref.style.transition = 'opacity 300ms'
|
|
444
|
+
ref.style.opacity = '0'
|
|
445
|
+
setTimeout(() => ref.style.opacity = '1', 10)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
onBeforeExit(async () => {
|
|
449
|
+
ref.style.opacity = '0'
|
|
450
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return <div ref={(el) => ref = el}>CSS Animated</div>
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
#### 3. Toggle CSS Classes
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
function Component() {
|
|
461
|
+
let ref: HTMLElement
|
|
462
|
+
|
|
463
|
+
onMount(() => {
|
|
464
|
+
requestAnimationFrame(() => {
|
|
465
|
+
ref.classList.add('enter-active')
|
|
466
|
+
setTimeout(() => ref.classList.remove('enter-active'), 300)
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
onBeforeExit(async () => {
|
|
471
|
+
ref.classList.add('exit-active')
|
|
472
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
return <div ref={(el) => ref = el} class="animated">Content</div>
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Built-in Performance Utilities
|
|
480
|
+
|
|
481
|
+
#### Frame Scheduler (Prevent Layout Thrashing)
|
|
482
|
+
|
|
483
|
+
Inspired by Framer Motion's frame loop, Flash includes a high-performance scheduler that prevents layout thrashing by separating read/update/render phases:
|
|
484
|
+
|
|
485
|
+
```tsx
|
|
486
|
+
import { frame } from '@mateosuarezdev/flash'
|
|
487
|
+
|
|
488
|
+
// Simple usage
|
|
489
|
+
frame.read(() => {
|
|
490
|
+
const height = element.offsetHeight // DOM reads
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
frame.update(() => {
|
|
494
|
+
position += velocity // Calculations
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
frame.render(() => {
|
|
498
|
+
element.style.transform = `translateY(${position}px)` // DOM writes
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
// Chained operations with type-safe data flow
|
|
502
|
+
frame.chain({
|
|
503
|
+
read: () => element.offsetHeight,
|
|
504
|
+
update: (height) => height * 2,
|
|
505
|
+
render: (doubled) => element.style.height = `${doubled}px`
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
// Keep-alive for continuous animations
|
|
509
|
+
const animate = frame.render(() => {
|
|
510
|
+
element.style.transform = `rotate(${rotation}deg)`
|
|
511
|
+
}, true) // true = runs every frame
|
|
512
|
+
|
|
513
|
+
// Cancel when done
|
|
514
|
+
frame.cancel(animate)
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
#### FLIP Animations
|
|
518
|
+
|
|
519
|
+
Built-in FLIP (First, Last, Invert, Play) utilities for performant layout animations:
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
import { flip, flipGroup } from '@mateosuarezdev/flash'
|
|
523
|
+
|
|
524
|
+
// Animate a single element
|
|
525
|
+
flip(element, () => {
|
|
526
|
+
// Make DOM changes
|
|
527
|
+
element.classList.add('expanded')
|
|
528
|
+
element.style.width = '400px'
|
|
529
|
+
}, {
|
|
530
|
+
duration: 300,
|
|
531
|
+
easing: 'ease-out-cubic'
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// Animate list reordering
|
|
535
|
+
const items = document.querySelectorAll('.item')
|
|
536
|
+
|
|
537
|
+
flipGroup(items, () => {
|
|
538
|
+
// Reorder items
|
|
539
|
+
container.appendChild(items[2])
|
|
540
|
+
}, {
|
|
541
|
+
duration: 400,
|
|
542
|
+
easing: 'ease-out-cubic'
|
|
543
|
+
})
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### Auto-Animate (Work in Progress)
|
|
547
|
+
|
|
548
|
+
Automatically animate layout changes (inspired by Framer Motion's layout animations):
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
<div autoanimate>
|
|
552
|
+
{/* Children automatically animate when added/removed/reordered */}
|
|
553
|
+
{() => items.value.map(item => (
|
|
554
|
+
<div key={item.id}>{item.name}</div>
|
|
555
|
+
))}
|
|
556
|
+
</div>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**Note:** Auto-animate is currently in development and will provide automatic FLIP animations for layout changes without manual setup.
|
|
560
|
+
|
|
561
|
+
### View Transitions API
|
|
562
|
+
|
|
563
|
+
Built-in support for the browser's View Transitions API:
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
import { startViewTransition } from '@mateosuarezdev/flash'
|
|
567
|
+
|
|
568
|
+
const expanded = signal(false)
|
|
569
|
+
|
|
570
|
+
<button onClick={() => {
|
|
571
|
+
startViewTransition(() => {
|
|
572
|
+
expanded.value = !expanded.value
|
|
573
|
+
})
|
|
574
|
+
}}>
|
|
575
|
+
Toggle
|
|
576
|
+
</button>
|
|
577
|
+
|
|
578
|
+
<div
|
|
579
|
+
className={() => expanded.value ? 'expanded' : 'collapsed'}
|
|
580
|
+
viewTransitionName="container"
|
|
581
|
+
>
|
|
582
|
+
Content
|
|
583
|
+
</div>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Performance Best Practices
|
|
587
|
+
|
|
588
|
+
**Use the Frame Scheduler:**
|
|
589
|
+
```tsx
|
|
590
|
+
// ❌ Bad: Layout thrashing
|
|
591
|
+
const height = element.offsetHeight // Read
|
|
592
|
+
element.style.height = `${height * 2}px` // Write
|
|
593
|
+
const width = element.offsetWidth // Read (forces reflow!)
|
|
594
|
+
element.style.width = `${width * 2}px` // Write
|
|
595
|
+
|
|
596
|
+
// ✅ Good: Batched reads and writes
|
|
597
|
+
frame.chain({
|
|
598
|
+
read: () => ({
|
|
599
|
+
height: element.offsetHeight,
|
|
600
|
+
width: element.offsetWidth
|
|
601
|
+
}),
|
|
602
|
+
render: ({ height, width }) => {
|
|
603
|
+
element.style.height = `${height * 2}px`
|
|
604
|
+
element.style.width = `${width * 2}px`
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Use FLIP for Layout Changes:**
|
|
610
|
+
```tsx
|
|
611
|
+
// ❌ Bad: Animating layout properties directly
|
|
612
|
+
element.animate({ width: '400px', height: '300px' }, { duration: 300 })
|
|
613
|
+
|
|
614
|
+
// ✅ Good: Use FLIP to transform instead
|
|
615
|
+
flip(element, () => {
|
|
616
|
+
element.style.width = '400px'
|
|
617
|
+
element.style.height = '300px'
|
|
618
|
+
}, { duration: 300 })
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Advanced Examples
|
|
624
|
+
|
|
625
|
+
### Counter with Computed Values
|
|
626
|
+
|
|
627
|
+
```tsx
|
|
628
|
+
import { signal, computed } from "@preact/signals-core";
|
|
629
|
+
|
|
630
|
+
const count = signal(0);
|
|
631
|
+
const double = computed(() => count.value * 2);
|
|
632
|
+
|
|
633
|
+
function Counter() {
|
|
634
|
+
return (
|
|
635
|
+
<div>
|
|
636
|
+
<p>Count: {() => count.value}</p>
|
|
637
|
+
<p>Double: {() => double.value}</p>
|
|
638
|
+
<button onClick={() => count.value++}>Increment</button>
|
|
639
|
+
</div>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Dynamic List with Add/Remove
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
import { signal } from "@preact/signals-core";
|
|
648
|
+
|
|
649
|
+
const items = signal([
|
|
650
|
+
{ id: 1, name: "Task 1" },
|
|
651
|
+
{ id: 2, name: "Task 2" },
|
|
652
|
+
]);
|
|
653
|
+
|
|
654
|
+
let nextId = 3;
|
|
655
|
+
|
|
656
|
+
function TodoList() {
|
|
657
|
+
const addItem = () => {
|
|
658
|
+
items.value = [...items.value, { id: nextId++, name: `Task ${nextId}` }];
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const removeItem = (id: number) => {
|
|
662
|
+
items.value = items.value.filter((item) => item.id !== id);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
return (
|
|
666
|
+
<div>
|
|
667
|
+
<button onClick={addItem}>Add Task</button>
|
|
668
|
+
<ul>
|
|
669
|
+
{() =>
|
|
670
|
+
items.value.map((item) => (
|
|
671
|
+
<li key={item.id}>
|
|
672
|
+
{item.name}
|
|
673
|
+
<button onClick={() => removeItem(item.id)}>Delete</button>
|
|
674
|
+
</li>
|
|
675
|
+
))
|
|
676
|
+
}
|
|
677
|
+
</ul>
|
|
678
|
+
</div>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Nested Reactive Updates
|
|
684
|
+
|
|
685
|
+
```tsx
|
|
686
|
+
const user = signal({ name: "John", age: 25 });
|
|
687
|
+
|
|
688
|
+
function Profile() {
|
|
689
|
+
return (
|
|
690
|
+
<div>
|
|
691
|
+
<h1>{() => user.value.name}</h1>
|
|
692
|
+
<p>Age: {() => user.value.age}</p>
|
|
693
|
+
<button
|
|
694
|
+
onClick={() => {
|
|
695
|
+
user.value = { ...user.value, age: user.value.age + 1 };
|
|
696
|
+
}}
|
|
697
|
+
>
|
|
698
|
+
Birthday
|
|
699
|
+
</button>
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Conditional Rendering with Animations
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
import { animate } from "framer-motion";
|
|
709
|
+
|
|
710
|
+
const show = signal(true);
|
|
711
|
+
|
|
712
|
+
function AnimatedBox() {
|
|
713
|
+
let ref: HTMLElement;
|
|
714
|
+
|
|
715
|
+
onMount(() => {
|
|
716
|
+
animate(ref, { opacity: [0, 1], y: [-20, 0] }, { duration: 0.3 });
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
onBeforeExit(async (token) => {
|
|
720
|
+
const animation = animate(ref, { opacity: 0, y: -20 }, { duration: 0.3 });
|
|
721
|
+
|
|
722
|
+
token.onCancel(() => {
|
|
723
|
+
animation.stop();
|
|
724
|
+
animate(ref, { opacity: 1, y: 0 }, { duration: 0.3 });
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
await animation.finished;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
return <div ref={(el) => (ref = el)}>Animated content</div>;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function App() {
|
|
734
|
+
return (
|
|
735
|
+
<div>
|
|
736
|
+
<button onClick={() => (show.value = !show.value)}>Toggle</button>
|
|
737
|
+
{() => show.value && <AnimatedBox />}
|
|
738
|
+
</div>
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## Server-Side Rendering
|
|
746
|
+
|
|
747
|
+
Flash provides three rendering strategies for different use cases:
|
|
748
|
+
|
|
749
|
+
### renderToString() - Synchronous
|
|
750
|
+
|
|
751
|
+
Fast synchronous rendering for static content:
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
import { renderToString } from '@mateosuarezdev/flash/server'
|
|
755
|
+
|
|
756
|
+
const html = renderToString(<App />)
|
|
757
|
+
// Returns: Complete HTML string (no async support)
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### renderToStringAsync() - Complete HTML
|
|
761
|
+
|
|
762
|
+
Waits for all async components, perfect for SEO and pre-rendering:
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import { renderToStringAsync } from '@mateosuarezdev/flash/server'
|
|
766
|
+
|
|
767
|
+
const html = await renderToStringAsync(<App />)
|
|
768
|
+
// Returns: Complete HTML with all async resolved
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### renderToStream() - Progressive Enhancement
|
|
772
|
+
|
|
773
|
+
Stream HTML for better perceived performance:
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
import { renderToStream } from '@mateosuarezdev/flash/server'
|
|
777
|
+
|
|
778
|
+
const stream = renderToStream(<App />)
|
|
779
|
+
|
|
780
|
+
for await (const chunk of stream) {
|
|
781
|
+
response.write(chunk)
|
|
782
|
+
}
|
|
783
|
+
// Streams: Initial HTML + progressive updates
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Pre-rendering & Caching
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
import { prerenderer } from '@mateosuarezdev/flash/server/prerender'
|
|
790
|
+
|
|
791
|
+
// Save pre-rendered HTML
|
|
792
|
+
await prerenderer.save('/', html)
|
|
793
|
+
|
|
794
|
+
// Load from cache
|
|
795
|
+
const cached = await prerenderer.load('/')
|
|
796
|
+
if (cached) return new Response(cached)
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Learn more:** Check out the [Server Architecture Guide](./docs/server-architecture.md) for detailed information about:
|
|
800
|
+
- Rendering strategies comparison
|
|
801
|
+
- Async component resolution
|
|
802
|
+
- Streaming architecture
|
|
803
|
+
- Caching and pre-rendering
|
|
804
|
+
- Security best practices
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## API Reference
|
|
809
|
+
|
|
810
|
+
### Core
|
|
811
|
+
|
|
812
|
+
- `render(element, container)` - Mount your app to the DOM
|
|
813
|
+
- `Fragment` - Render multiple children without a wrapper
|
|
814
|
+
|
|
815
|
+
### Lifecycle
|
|
816
|
+
|
|
817
|
+
- `onMount(callback)` - Run after component mounts
|
|
818
|
+
- `onUnmount(callback)` - Run when component unmounts
|
|
819
|
+
- `onBeforeExit(callback)` - Run before unmounting (pauses unmount tree for animations, data saving, etc.)
|
|
820
|
+
|
|
821
|
+
### Context
|
|
822
|
+
|
|
823
|
+
- `createContext(defaultValue)` - Create a context
|
|
824
|
+
- `useContext(context)` - Consume context value
|
|
825
|
+
|
|
826
|
+
### Animations & Performance
|
|
827
|
+
|
|
828
|
+
- `startViewTransition(callback)` - Trigger View Transition API
|
|
829
|
+
- `frame.read(callback)` - Schedule DOM reads (measurements)
|
|
830
|
+
- `frame.update(callback)` - Schedule calculations
|
|
831
|
+
- `frame.render(callback)` - Schedule DOM writes (mutations)
|
|
832
|
+
- `frame.chain({ read, update, render })` - Chain operations with data flow
|
|
833
|
+
- `flip(element, applyChanges, options)` - FLIP animation for single element
|
|
834
|
+
- `flipGroup(elements, applyChanges, options)` - FLIP animation for groups
|
|
835
|
+
- `flipMove(element, newParent, options)` - Animate element to new container
|
|
836
|
+
- `autoAnimate(element, options)` - Enable auto layout animations (WIP)
|
|
837
|
+
|
|
838
|
+
### Special Props
|
|
839
|
+
|
|
840
|
+
- `ref={(el) => ...}` - Get reference to DOM element
|
|
841
|
+
- `key={value}` - Unique identifier for list items
|
|
842
|
+
- `viewTransitionName={name}` - Named view transition target
|
|
843
|
+
- `autoanimate={true}` - Enable auto-layout animations (WIP)
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## Performance Tips
|
|
848
|
+
|
|
849
|
+
1. **Use signals at module level** for shared state
|
|
850
|
+
2. **Wrap dynamic expressions in functions** `{() => signal.value}` not `{signal.value}`
|
|
851
|
+
3. **Always use keys** for list items
|
|
852
|
+
4. **Minimize reactive boundaries** - only wrap what needs to update
|
|
853
|
+
5. **Use computed signals** for derived state
|
|
854
|
+
6. **Use `frame` for DOM operations** - Prevent layout thrashing by batching reads/writes
|
|
855
|
+
7. **Use FLIP for layout animations** - Animate transforms instead of layout properties
|
|
856
|
+
8. **Leverage DOM resurrection** - Flash automatically reuses DOM for rapid toggles
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## Comparison to Other Frameworks
|
|
861
|
+
|
|
862
|
+
| Feature | Flash | React | SolidJS | Vue |
|
|
863
|
+
| ------------------------ | ------- | ----- | ------- | ------- |
|
|
864
|
+
| Reactivity | Signals | VDOM | Signals | Proxies |
|
|
865
|
+
| Bundle Size | ~10KB | ~40KB | ~7KB | ~30KB |
|
|
866
|
+
| Fine-grained Updates | ✅ | ❌ | ✅ | ✅ |
|
|
867
|
+
| Keyed Lists | ✅ | ✅ | ✅ | ✅ |
|
|
868
|
+
| Built-in FLIP Utils | ✅ | ❌ | ❌ | ❌ |
|
|
869
|
+
| Frame Scheduler | ✅ | ❌ | ❌ | ❌ |
|
|
870
|
+
| DOM Resurrection | ✅ | ❌ | ❌ | ❌ |
|
|
871
|
+
| Animation Flexibility | Any lib | Any lib | Any lib | Any lib |
|
|
872
|
+
| SSR Support | ✅ | ✅ | ✅ | ✅ |
|
|
873
|
+
| SSR Streaming | ✅ | ✅ | ✅ | ✅ |
|
|
874
|
+
| No Compiler | ✅ | ✅ | ❌ | ❌ |
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## FAQ
|
|
879
|
+
|
|
880
|
+
**Q: Do I need a compiler?**
|
|
881
|
+
A: No! Flash works with standard JSX transformation. Just configure `jsxImportSource`.
|
|
882
|
+
|
|
883
|
+
**Q: Can I use TypeScript?**
|
|
884
|
+
A: Yes! Flash is written in TypeScript with full type support.
|
|
885
|
+
|
|
886
|
+
**Q: How does reactivity work?**
|
|
887
|
+
A: Flash uses Preact Signals. When you wrap an expression in a function `{() => signal.value}`, Flash creates a reactive boundary that auto-updates when the signal changes.
|
|
888
|
+
|
|
889
|
+
**Q: What about SSR?**
|
|
890
|
+
A: Yes! Flash has full SSR support with three rendering strategies (sync, async, streaming). See the [Server Architecture Guide](./docs/server-architecture.md).
|
|
891
|
+
|
|
892
|
+
**Q: Why functions for reactive values?**
|
|
893
|
+
A: Functions create clear boundaries for reactivity and work without a compiler. It's explicit and simple.
|
|
894
|
+
|
|
895
|
+
**Q: What is DOM resurrection?**
|
|
896
|
+
A: When a component is exiting (playing exit animation) but gets toggled back on, Flash cancels the animation and reuses the existing DOM instead of creating a new one. This provides smooth animation reversals without any setup.
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
## Examples
|
|
901
|
+
|
|
902
|
+
Check out the [examples](./src/main.tsx) directory for more demos:
|
|
903
|
+
|
|
904
|
+
- Basic counter and computed values
|
|
905
|
+
- Enter/exit animations with cancellation
|
|
906
|
+
- View Transitions API integration
|
|
907
|
+
- Keyed list rendering with add/remove
|
|
908
|
+
- Context API usage
|
|
909
|
+
- Reactive props and class names
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
## Architecture
|
|
914
|
+
|
|
915
|
+
Want to understand how Flash works under the hood?
|
|
916
|
+
|
|
917
|
+
**[Client Architecture Guide](./docs/architecture.md)** - Deep dive into:
|
|
918
|
+
- JSX transformation flow
|
|
919
|
+
- VNode types and rendering pipeline
|
|
920
|
+
- Reactivity system internals
|
|
921
|
+
- Content replacement strategies (resurrection, text optimization)
|
|
922
|
+
- Keyed list reconciliation algorithm
|
|
923
|
+
|
|
924
|
+
**[Server Architecture Guide](./docs/server-architecture.md)** - Deep dive into:
|
|
925
|
+
- Three rendering strategies (sync, async, streaming)
|
|
926
|
+
- Async component resolution (parallel execution)
|
|
927
|
+
- Streaming architecture and progressive enhancement
|
|
928
|
+
- Pre-rendering and caching system
|
|
929
|
+
- Security best practices (XSS protection)
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## Contributing
|
|
934
|
+
|
|
935
|
+
Flash is in active development. Contributions are welcome!
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## License
|
|
940
|
+
|
|
941
|
+
MIT © Mateo Suarez
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
**⚡ Built with Flash - Fine-grained reactivity meets familiar JSX**
|