melina 1.1.4 → 1.3.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/GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Melina.js Developer Guide
2
2
 
3
- **A comprehensive guide to building applications with Melina.js**
3
+ **A lightweight, islands-architecture framework for Bun**
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -9,16 +9,12 @@
9
9
  3. [Quick Start](#quick-start)
10
10
  4. [Server Components](#server-components)
11
11
  5. [Client Components (Islands)](#client-components-islands)
12
- 6. [Routing](#routing)
12
+ 6. [The Router](#the-router)
13
13
  7. [Layouts](#layouts)
14
14
  8. [State Persistence](#state-persistence)
15
- 9. [View Transitions](#view-transitions)
16
- 10. [Cross-Island Communication](#cross-island-communication)
17
- 11. [API Routes](#api-routes)
18
- 12. [Styling](#styling)
19
- 13. [Best Practices & Gotchas](#best-practices--gotchas)
20
- 14. [API Reference](#api-reference)
21
- 15. [Troubleshooting](#troubleshooting)
15
+ 9. [Cross-Island Communication](#cross-island-communication)
16
+ 10. [Best Practices & Gotchas](#best-practices--gotchas)
17
+ 11. [API Reference](#api-reference)
22
18
 
23
19
  ---
24
20
 
@@ -35,9 +31,7 @@ Melina.js is a Next.js-compatible framework with a simpler architecture, built s
35
31
  - ✅ Nested layouts with automatic composition
36
32
  - ✅ Islands architecture for optimal performance
37
33
  - ✅ SPA-like navigation with partial page swaps
38
- - ✅ High-fidelity state preservation (Hangar architecture)
39
- - ✅ Native View Transitions API support
40
- - ✅ CSS animations that persist across navigation
34
+ - ✅ CSS animations that don't reset on navigation
41
35
  - ✅ Streaming HTML responses
42
36
 
43
37
  ---
@@ -46,10 +40,10 @@ Melina.js is a Next.js-compatible framework with a simpler architecture, built s
46
40
 
47
41
  ### The Two Graphs
48
42
 
49
- Melina.js separates your app into two distinct execution contexts:
43
+ Melina.js separates your app into two distinct "graphs":
50
44
 
51
- 1. **Server Graph** Layouts and pages that render on the server (Bun)
52
- 2. **Client Graph** Interactive components (islands) that run in the browser
45
+ 1. **Server Graph** - Layouts and pages that render on the server
46
+ 2. **Client Graph** - Interactive components (islands) that run in the browser
53
47
 
54
48
  ```
55
49
  ┌─────────────────────────────────────────────────────────────┐
@@ -72,28 +66,9 @@ Melina.js separates your app into two distinct execution contexts:
72
66
  └─────────────────────────────────────────────────────────────┘
73
67
  ```
74
68
 
75
- ### The Two Zones
76
-
77
- Your layout creates two distinct zones:
78
-
79
- 1. **Persistent Zone** — Elements outside `#melina-page-content` (header, footer, sidebars)
80
- 2. **Swappable Zone** — The `#melina-page-content` container
81
-
82
- ```tsx
83
- <body>
84
- <header>...</header> {/* PERSISTENT: Never unmounts */}
85
-
86
- <main id="melina-page-content">
87
- {children} {/* SWAPPABLE: Replaced on navigation */}
88
- </main>
89
-
90
- <footer>...</footer> {/* PERSISTENT: Never unmounts */}
91
- </body>
92
- ```
93
-
94
69
  ### Partial Page Swaps
95
70
 
96
- When you navigate between pages, Melina.js doesn't replace the entire page. Instead:
71
+ When you navigate between pages, Melina.js doesn't replace the entire `<body>`. Instead:
97
72
 
98
73
  1. Fetches the new page HTML
99
74
  2. Finds `#melina-page-content` in both current and new page
@@ -107,7 +82,7 @@ When you navigate between pages, Melina.js doesn't replace the entire page. Inst
107
82
  ### Installation
108
83
 
109
84
  ```bash
110
- bun add melina
85
+ bun add @anthropic/melina.js
111
86
  ```
112
87
 
113
88
  ### Create Your App Structure
@@ -140,40 +115,34 @@ Server Components are the default. They render on the server and send HTML to th
140
115
 
141
116
  ```tsx
142
117
  // app/page.tsx - Server Component (default)
143
- export default async function HomePage() {
144
- const data = await fetchFromDatabase(); // Can use async!
145
-
146
- return (
147
- <div>
148
- <h1>Welcome</h1>
149
- <p>Data: {data}</p>
150
- </div>
151
- );
118
+ export default function HomePage() {
119
+ const data = await fetchFromDatabase(); // Can use async!
120
+
121
+ return (
122
+ <div>
123
+ <h1>Welcome</h1>
124
+ <p>Data: {data}</p>
125
+ </div>
126
+ );
152
127
  }
153
128
  ```
154
129
 
155
- ### What Server Components CAN Do
156
-
130
+ ### What Server Components CAN do:
157
131
  - ✅ Async/await directly in the component
158
132
  - ✅ Access databases, file systems, environment variables
159
133
  - ✅ Import other Server Components
160
134
  - ✅ Import Client Components (they'll become islands)
161
135
 
162
- ### What Server Components CANNOT Do
163
-
136
+ ### What Server Components CANNOT do:
164
137
  - ❌ Use `useState`, `useEffect`, or other hooks
165
138
  - ❌ Use browser APIs (`window`, `localStorage`)
166
- - ❌ Handle click events or other interactive behaviors
139
+ - ❌ Handle click events
167
140
 
168
141
  ---
169
142
 
170
143
  ## Client Components (Islands)
171
144
 
172
- Add `'use client'` at the top of your file to make it interactive. Melina automatically handles the transformation!
173
-
174
- ### Auto-Wrapping (Default)
175
-
176
- Just export your component normally — Melina transforms it during SSR:
145
+ Add `'use client'` at the top of your file to make it interactive:
177
146
 
178
147
  ```tsx
179
148
  // app/components/Counter.tsx
@@ -182,39 +151,14 @@ Just export your component normally — Melina transforms it during SSR:
182
151
  import { useState } from 'react';
183
152
 
184
153
  export function Counter({ initialCount = 0 }) {
185
- const [count, setCount] = useState(initialCount);
186
-
187
- return (
188
- <button onClick={() => setCount(c => c + 1)}>
189
- Count: {count}
190
- </button>
191
- );
192
- }
193
- ```
194
-
195
- That's it! No extra imports or wrappers required. The framework:
196
-
197
- 1. Detects `'use client'` directive
198
- 2. Automatically transforms exports to island wrappers during SSR
199
- 3. Renders the actual component on the client
200
-
201
- ### Manual Wrapping (Advanced)
202
-
203
- For more control, you can manually wrap with `island()`:
204
-
205
- ```tsx
206
- 'use client';
207
-
208
- import { useState } from 'react';
209
- import { island } from 'melina/island';
210
-
211
- function CounterImpl({ initialCount = 0 }) {
212
- const [count, setCount] = useState(initialCount);
213
- return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
154
+ const [count, setCount] = useState(initialCount);
155
+
156
+ return (
157
+ <button onClick={() => setCount(c => c + 1)}>
158
+ Count: {count}
159
+ </button>
160
+ );
214
161
  }
215
-
216
- // Explicit island wrapper
217
- export const Counter = island(CounterImpl, 'Counter');
218
162
  ```
219
163
 
220
164
  ### Using Islands in Server Components
@@ -226,32 +170,30 @@ Just import and use them:
226
170
  import { Counter } from './components/Counter';
227
171
 
228
172
  export default function HomePage() {
229
- return (
230
- <div>
231
- <h1>My Page</h1>
232
- <Counter initialCount={10} /> {/* Island! */}
233
- </div>
234
- );
173
+ return (
174
+ <div>
175
+ <h1>My Page</h1>
176
+ <Counter initialCount={10} /> {/* Island! */}
177
+ </div>
178
+ );
235
179
  }
236
180
  ```
237
181
 
238
182
  The framework automatically:
239
-
240
- 1. Renders an island placeholder on the server (for SEO/initial paint)
241
- 2. Adds `data-melina-island` markers with serialized props
183
+ 1. Renders the component on the server (for SEO/initial paint)
184
+ 2. Adds `data-melina-island` markers
242
185
  3. Hydrates on the client with React
243
186
 
244
187
  ---
245
188
 
246
- ## Routing
189
+ ## The Router
247
190
 
248
191
  ### File-Based Routes
249
192
 
250
193
  ```
251
194
  app/page.tsx → /
252
195
  app/about/page.tsx → /about
253
- app/blog/[slug]/page.tsx → /blog/:slug (dynamic)
254
- app/docs/[...path]/page.tsx → /docs/* (catch-all)
196
+ app/blog/[id]/page.tsx → /blog/:id (dynamic)
255
197
  ```
256
198
 
257
199
  ### The `<Link>` Component
@@ -259,37 +201,58 @@ app/docs/[...path]/page.tsx → /docs/* (catch-all)
259
201
  Use `<Link>` for SPA-style navigation:
260
202
 
261
203
  ```tsx
262
- import { Link } from 'melina/Link';
204
+ import { Link } from '@anthropic/melina.js';
263
205
 
264
206
  export default function Nav() {
265
- return (
266
- <nav>
267
- <Link href="/">Home</Link>
268
- <Link href="/about">About</Link>
269
- <Link href="/blog/my-post">Blog Post</Link>
270
- </nav>
271
- );
207
+ return (
208
+ <nav>
209
+ <Link href="/">Home</Link>
210
+ <Link href="/about">About</Link>
211
+ </nav>
212
+ );
272
213
  }
273
214
  ```
274
215
 
275
- ### Route Precedence
276
-
277
- | Priority | Type | Example | Description |
278
- |----------|------|---------|-------------|
279
- | 1 | Static | `/blog/my-post` | Exact match |
280
- | 2 | Dynamic | `/blog/[slug]` | Single segment |
281
- | 3 | Catch-all | `/blog/[...slug]` | Multiple segments |
282
-
283
216
  ### Navigation Lifecycle
284
217
 
285
218
  When a user clicks a link:
286
219
 
287
220
  1. **Fetch**: Router fetches the new page HTML
288
221
  2. **Parse**: Parses it into a DOM tree
289
- 3. **Transition**: Triggers View Transition (if supported)
290
- 4. **Swap**: Only replaces `#melina-page-content`
291
- 5. **Hydrate**: Finds and hydrates new islands
292
- 6. **Event**: Dispatches `melina:navigated`
222
+ 3. **Swap**: Only replaces `#melina-page-content`
223
+ 4. **Hydrate**: Finds new islands and hydrates them
224
+
225
+ **Elements outside `#melina-page-content` persist automatically!**
226
+
227
+ ### ⚡ What Happens to Islands During Navigation
228
+
229
+ Each island is its own **independent React root**. Here's what happens:
230
+
231
+ | Island Location | On Navigation | State |
232
+ |-----------------|---------------|-------|
233
+ | In Layout (header/footer) | **Persists** - DOM untouched | ✅ Preserved |
234
+ | In Page | **Destroyed** - innerHTML replaced | ❌ Lost |
235
+ | Same island on new page | New React root created | Fresh state |
236
+
237
+ **Example:**
238
+ ```
239
+ Navigate from /home to /about:
240
+
241
+ ┌─ Layout ────────────────────────────────────────┐
242
+ │ <SearchBar /> ← React Root #1 (STAYS) │
243
+ │ state preserved! ✅ │
244
+ ├─────────────────────────────────────────────────┤
245
+ │ <main id="melina-page-content"> │
246
+ │ /home: <Counter /> ← Root #2 (DESTROYED) ❌ │
247
+ │ /about: <ContactForm /> ← Root #3 (NEW) 🆕 │
248
+ │ </main> │
249
+ ├─────────────────────────────────────────────────┤
250
+ │ <JobTracker /> ← React Root #4 (STAYS) │
251
+ │ state preserved! ✅ │
252
+ └─────────────────────────────────────────────────┘
253
+ ```
254
+
255
+ **Key insight:** Even if both pages use `<Counter />`, they are **different React roots**. State doesn't transfer.
293
256
 
294
257
  ---
295
258
 
@@ -305,24 +268,24 @@ import { SearchBar } from './components/SearchBar';
305
268
  import { JobTracker } from './components/JobTracker';
306
269
 
307
270
  export default function RootLayout({ children }) {
308
- return (
309
- <html>
310
- <body>
311
- <header>
312
- <nav>...</nav>
313
- <SearchBar /> {/* Persists across navigation! */}
314
- </header>
315
-
316
- {/* CRITICAL: Only this part swaps on navigation */}
317
- <main id="melina-page-content">
318
- {children}
319
- </main>
320
-
321
- <footer>...</footer>
322
- <JobTracker /> {/* Persists too! */}
323
- </body>
324
- </html>
325
- );
271
+ return (
272
+ <html>
273
+ <body>
274
+ <header>
275
+ <nav>...</nav>
276
+ <SearchBar /> {/* Persists across navigation! */}
277
+ </header>
278
+
279
+ {/* Only this part swaps on navigation */}
280
+ <main id="melina-page-content">
281
+ {children}
282
+ </main>
283
+
284
+ <footer>...</footer>
285
+ <JobTracker /> {/* Persists too! */}
286
+ </body>
287
+ </html>
288
+ );
326
289
  }
327
290
  ```
328
291
 
@@ -336,20 +299,6 @@ app/
336
299
  └── page.tsx # Dashboard page
337
300
  ```
338
301
 
339
- ```tsx
340
- // app/dashboard/layout.tsx
341
- export default function DashboardLayout({ children }) {
342
- return (
343
- <div className="dashboard">
344
- <Sidebar />
345
- <div id="melina-page-content">
346
- {children}
347
- </div>
348
- </div>
349
- );
350
- }
351
- ```
352
-
353
302
  ---
354
303
 
355
304
  ## State Persistence
@@ -359,25 +308,25 @@ export default function DashboardLayout({ children }) {
359
308
  Islands in the layout (outside `#melina-page-content`) are never destroyed during navigation:
360
309
 
361
310
  ```tsx
311
+ // This SearchBar's React state survives navigation!
362
312
  <header>
363
- <SearchBar /> {/* React state survives navigation */}
313
+ <SearchBar />
364
314
  </header>
365
315
 
366
316
  <main id="melina-page-content">
367
- {children} {/* Only this changes */}
317
+ {children} {/* Only this changes */}
368
318
  </main>
369
319
  ```
370
320
 
371
321
  ### ⚠️ The Props Desynchronization Trade-off
372
322
 
373
- **Important!** When navigating:
323
+ **This is important!** When navigating:
374
324
 
375
325
  1. Server renders new page with new props
376
- 2. Router keeps existing layout (to preserve state)
377
- 3. **Persistent islands keep their OLD props!**
326
+ 2. Router keeps old layout (to preserve state)
327
+ 3. **Old islands keep their OLD props!**
378
328
 
379
329
  **Example:**
380
-
381
330
  ```tsx
382
331
  // layout.tsx
383
332
  <Header activeTab={pathname} /> // ❌ Props won't update!
@@ -385,10 +334,10 @@ Islands in the layout (outside `#melina-page-content`) are never destroyed durin
385
334
 
386
335
  If you're on `/home` and navigate to `/about`:
387
336
  - Server sends `activeTab="about"`
388
- - Router discards that, keeps old Header
337
+ - Router throws that away, keeps old Header
389
338
  - Header still shows `activeTab="home"` 😱
390
339
 
391
- ### Solution: Read from URL
340
+ ### Solution: Use URL State
392
341
 
393
342
  Persistent islands should read from URL, not props:
394
343
 
@@ -396,165 +345,25 @@ Persistent islands should read from URL, not props:
396
345
  // ✅ CORRECT - Read from URL
397
346
  'use client';
398
347
 
399
- import { useState, useEffect } from 'react';
400
- import { island } from 'melina/island';
401
-
402
- function HeaderImpl() {
403
- const [activeTab, setActiveTab] = useState('/');
404
-
405
- useEffect(() => {
406
- // Read current path
407
- setActiveTab(window.location.pathname);
348
+ export function Header() {
349
+ const [activeTab, setActiveTab] = useState('/');
408
350
 
409
- // Update on navigation
410
- const handleNav = () => setActiveTab(window.location.pathname);
411
- window.addEventListener('melina:navigated', handleNav);
412
- return () => window.removeEventListener('melina:navigated', handleNav);
413
- }, []);
414
-
415
- return (
416
- <nav>
417
- <Link href="/" className={activeTab === '/' ? 'active' : ''}>
418
- Home
419
- </Link>
420
- <Link href="/about" className={activeTab === '/about' ? 'active' : ''}>
421
- About
422
- </Link>
423
- </nav>
424
- );
425
- }
426
-
427
- export const Header = island(HeaderImpl, 'Header');
428
- ```
429
-
430
- ### Persisting State for Page Islands
431
-
432
- Page islands (inside `#melina-page-content`) lose state on navigation. Options:
433
-
434
- **Option 1: Move to Layout**
435
- ```tsx
436
- // If the island should persist, put it in the layout
437
- <JobTracker /> // In layout = always persists
438
- ```
439
-
440
- **Option 2: Use localStorage**
441
- ```tsx
442
- function CounterImpl({ initialCount = 0 }) {
443
- const [count, setCount] = useState(() => {
444
- const saved = localStorage.getItem('counter');
445
- return saved ? parseInt(saved) : initialCount;
446
- });
447
-
448
- useEffect(() => {
449
- localStorage.setItem('counter', String(count));
450
- }, [count]);
451
-
452
- return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
453
- }
454
- ```
455
-
456
- **Option 3: Use URL State**
457
- ```tsx
458
- function FilterPanel() {
459
- const [filter, setFilter] = useState(() => {
460
- return new URLSearchParams(window.location.search).get('filter') || 'all';
461
- });
462
-
463
- const updateFilter = (newFilter) => {
464
- const url = new URL(window.location.href);
465
- url.searchParams.set('filter', newFilter);
466
- window.history.replaceState({}, '', url);
467
- setFilter(newFilter);
468
- };
469
-
470
- return (
471
- <select value={filter} onChange={e => updateFilter(e.target.value)}>
472
- <option value="all">All</option>
473
- <option value="active">Active</option>
474
- </select>
475
- );
476
- }
477
- ```
478
-
479
- ---
480
-
481
- ## View Transitions
482
-
483
- Melina.js leverages the browser-native [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for smooth, morphing animations between pages.
484
-
485
- ### Basic Setup
486
-
487
- ```css
488
- /* app/globals.css */
489
-
490
- /* Give elements a shared identity */
491
- .album-cover {
492
- view-transition-name: album-cover;
493
- }
494
-
495
- /* Customize the transition */
496
- ::view-transition-old(album-cover),
497
- ::view-transition-new(album-cover) {
498
- animation-duration: 0.3s;
499
- animation-timing-function: ease-in-out;
500
- }
501
- ```
502
-
503
- ### Dynamic Transition Names
504
-
505
- Use inline styles for dynamic transition names:
506
-
507
- ```tsx
508
- // List view
509
- <div
510
- className="album-thumbnail"
511
- style={{ viewTransitionName: `album-${album.id}` }}
512
- >
513
- <img src={album.cover} />
514
- </div>
515
-
516
- // Detail view
517
- <div
518
- className="album-hero"
519
- style={{ viewTransitionName: `album-${params.id}` }}
520
- >
521
- <img src={album.cover} />
522
- </div>
523
- ```
524
-
525
- ### Single-Instance Morphing
526
-
527
- Layout-level islands can transform between states:
528
-
529
- ```tsx
530
- // MusicPlayer that morphs between mini and expanded
531
- function MusicPlayerImpl() {
532
- const [path, setPath] = useState('/');
533
-
534
- useEffect(() => {
535
- setPath(window.location.pathname);
536
- const handler = () => setPath(window.location.pathname);
537
- window.addEventListener('melina:navigated', handler);
538
- return () => window.removeEventListener('melina:navigated', handler);
539
- }, []);
540
-
541
- const isExpanded = path.startsWith('/album/');
542
-
543
- return (
544
- <div
545
- className={isExpanded ? 'player-expanded' : 'player-mini'}
546
- style={{ viewTransitionName: 'music-player' }}
547
- >
548
- {isExpanded ? <ExpandedView /> : <MiniView />}
549
- </div>
550
- );
551
- }
552
- ```
553
-
554
- ```css
555
- ::view-transition-old(music-player),
556
- ::view-transition-new(music-player) {
557
- animation-duration: 0.4s;
351
+ useEffect(() => {
352
+ // Read current path
353
+ setActiveTab(window.location.pathname);
354
+
355
+ // Update on navigation
356
+ const handleNav = () => setActiveTab(window.location.pathname);
357
+ window.addEventListener('melina:navigated', handleNav);
358
+ return () => window.removeEventListener('melina:navigated', handleNav);
359
+ }, []);
360
+
361
+ return (
362
+ <nav>
363
+ <Link href="/" className={activeTab === '/' ? 'active' : ''}>Home</Link>
364
+ <Link href="/about" className={activeTab === '/about' ? 'active' : ''}>About</Link>
365
+ </nav>
366
+ );
558
367
  }
559
368
  ```
560
369
 
@@ -562,212 +371,103 @@ function MusicPlayerImpl() {
562
371
 
563
372
  ## Cross-Island Communication
564
373
 
565
- Islands are independent React roots. They can't share state directly via React context.
374
+ Islands are independent React roots. They can't share state directly.
566
375
 
567
- ### Option 1: Custom Events
376
+ ### Option 1: localStorage + Events
568
377
 
569
378
  ```tsx
570
379
  // Island A: Emit event
571
380
  function submitJob(name) {
572
- const job = { id: Date.now(), name };
573
- window.dispatchEvent(new CustomEvent('job:created', { detail: job }));
381
+ const job = { id: Date.now(), name };
382
+ localStorage.setItem('lastJob', JSON.stringify(job));
383
+ window.dispatchEvent(new CustomEvent('job:created', { detail: job }));
574
384
  }
575
385
 
576
386
  // Island B: Listen for events
577
387
  useEffect(() => {
578
- const handler = (e) => setJobs(prev => [...prev, e.detail]);
579
- window.addEventListener('job:created', handler);
580
- return () => window.removeEventListener('job:created', handler);
388
+ const handler = (e) => setJobs(prev => [...prev, e.detail]);
389
+ window.addEventListener('job:created', handler);
390
+ return () => window.removeEventListener('job:created', handler);
581
391
  }, []);
582
392
  ```
583
393
 
584
394
  ### Option 2: Event Bus
585
395
 
586
396
  ```tsx
587
- // lib/eventBus.ts
397
+ // eventBus.ts
588
398
  export const eventBus = {
589
- emit(event: string, data: any) {
590
- window.dispatchEvent(new CustomEvent(`app:${event}`, { detail: data }));
591
- },
592
-
593
- on(event: string, callback: (data: any) => void) {
594
- const handler = (e: CustomEvent) => callback(e.detail);
595
- window.addEventListener(`app:${event}`, handler);
596
- return () => window.removeEventListener(`app:${event}`, handler);
597
- }
399
+ emit(event: string, data: any) {
400
+ window.dispatchEvent(new CustomEvent(`app:${event}`, { detail: data }));
401
+ },
402
+
403
+ on(event: string, callback: (data: any) => void) {
404
+ const handler = (e: CustomEvent) => callback(e.detail);
405
+ window.addEventListener(`app:${event}`, handler);
406
+ return () => window.removeEventListener(`app:${event}`, handler);
407
+ }
598
408
  };
599
-
600
- // Usage
601
- eventBus.emit('user:login', { id: 1, name: 'Alice' });
602
- eventBus.on('user:login', (user) => console.log('Logged in:', user));
603
409
  ```
604
410
 
605
- ### Option 3: localStorage + Storage Events
411
+ ### Option 3: URL State
606
412
 
607
- ```tsx
608
- // Island A: Write
609
- localStorage.setItem('user', JSON.stringify(user));
610
- window.dispatchEvent(new Event('storage'));
611
-
612
- // Island B: Listen
613
- const [user, setUser] = useState(() =>
614
- JSON.parse(localStorage.getItem('user') || 'null')
615
- );
616
-
617
- useEffect(() => {
618
- const handler = () => {
619
- setUser(JSON.parse(localStorage.getItem('user') || 'null'));
620
- };
621
- window.addEventListener('storage', handler);
622
- return () => window.removeEventListener('storage', handler);
623
- }, []);
624
- ```
625
-
626
- ---
627
-
628
- ## API Routes
629
-
630
- ### Basic API Route
631
-
632
- ```tsx
633
- // app/api/hello/route.ts
634
- export async function GET(req: Request) {
635
- return Response.json({
636
- message: 'Hello from API!',
637
- serverTime: new Date().toISOString()
638
- });
639
- }
640
-
641
- export async function POST(req: Request) {
642
- const body = await req.json();
643
- // Process data...
644
- return Response.json({ success: true });
645
- }
646
- ```
647
-
648
- ### Full Server Capabilities
649
-
650
- ```tsx
651
- // app/api/data/route.ts
652
- import { readFileSync } from 'fs';
653
- import { db } from '../../../lib/database';
654
-
655
- export async function GET() {
656
- // Read from file system
657
- const config = readFileSync('./config.json', 'utf-8');
658
-
659
- // Query database
660
- const users = await db.query('SELECT * FROM users');
661
-
662
- return Response.json({ config: JSON.parse(config), users });
663
- }
664
- ```
665
-
666
- ### Dynamic API Routes
667
-
668
- ```tsx
669
- // app/api/users/[id]/route.ts
670
- export async function GET(req: Request, { params }) {
671
- const user = await db.findUser(params.id);
672
-
673
- if (!user) {
674
- return Response.json({ error: 'Not found' }, { status: 404 });
675
- }
676
-
677
- return Response.json(user);
678
- }
679
- ```
680
-
681
- ---
682
-
683
- ## Styling
684
-
685
- ### Global CSS
413
+ For state that should survive refresh:
686
414
 
687
415
  ```tsx
688
- // app/layout.tsx
689
- import './globals.css';
690
-
691
- export default function Layout({ children }) {
692
- return (
693
- <html>
694
- <head>
695
- {/* CSS is automatically injected */}
696
- </head>
697
- <body>...</body>
698
- </html>
699
- );
700
- }
701
- ```
702
-
703
- ### Tailwind CSS v4
416
+ // Read from URL
417
+ const searchParams = new URLSearchParams(window.location.search);
418
+ const filter = searchParams.get('filter');
704
419
 
705
- Melina.js has built-in Tailwind v4 support with CSS-first configuration:
706
-
707
- ```css
708
- /* app/globals.css */
709
- @import "tailwindcss";
710
-
711
- /* Custom theme via CSS */
712
- @theme {
713
- --color-primary: #667eea;
714
- --color-accent: #764ba2;
420
+ // Update URL
421
+ function setFilter(value) {
422
+ const url = new URL(window.location.href);
423
+ url.searchParams.set('filter', value);
424
+ window.history.replaceState({}, '', url);
715
425
  }
716
426
  ```
717
427
 
718
- No `tailwind.config.js` needed!
719
-
720
428
  ---
721
429
 
722
430
  ## Best Practices & Gotchas
723
431
 
724
432
  ### ✅ Do's
725
433
 
726
- 1. **Keep layouts lean** Only put truly persistent elements there
727
- 2. **Use URL state for persistent islands** Don't rely on server props
728
- 3. **Use `#melina-page-content`** The router needs this container
729
- 4. **Listen for `melina:navigated`** Keep persistent islands synced
730
- 5. **Use `island()` wrapper** — Required for client components
434
+ 1. **Keep layouts lean** - Only put truly persistent elements there
435
+ 2. **Use URL state for persistent islands** - Don't rely on server props
436
+ 3. **Use `#melina-page-content`** - The router needs this container
437
+ 4. **Listen for `melina:navigated`** - Keep persistent islands synced
731
438
 
732
439
  ### ❌ Don'ts
733
440
 
734
- 1. **Don't pass dynamic props to persistent islands** They won't update
735
- 2. **Don't use global CSS transitions on body** They'll restart on swap
736
- 3. **Don't use React context across islands** They're separate roots
737
- 4. **Don't forget cleanup** — Remove event listeners in useEffect cleanup
441
+ 1. **Don't pass dynamic props to persistent islands** - They won't update
442
+ 2. **Don't use global CSS transitions on body** - They'll restart on swap
443
+ 3. **Don't forget `display: contents`** - If island wrapper breaks layout
738
444
 
739
445
  ### Common Patterns
740
446
 
741
447
  ```tsx
742
448
  // ✅ Good: Persistent island reads URL
743
- function NavTabsImpl() {
744
- const [path, setPath] = useState('/');
745
-
746
- useEffect(() => {
747
- setPath(window.location.pathname);
748
- const handler = () => setPath(window.location.pathname);
749
- window.addEventListener('melina:navigated', handler);
750
- return () => window.removeEventListener('melina:navigated', handler);
751
- }, []);
752
-
753
- return <nav>...</nav>;
449
+ function NavTabs() {
450
+ const [path, setPath] = useState('/');
451
+ useEffect(() => {
452
+ setPath(window.location.pathname);
453
+ window.addEventListener('melina:navigated', () => {
454
+ setPath(window.location.pathname);
455
+ });
456
+ }, []);
457
+ return <nav>...</nav>;
754
458
  }
755
459
 
756
- export const NavTabs = island(NavTabsImpl, 'NavTabs');
757
-
758
460
  // ✅ Good: Cross-page data via localStorage
759
- function JobTrackerImpl() {
760
- const [jobs, setJobs] = useState(() => {
761
- return JSON.parse(localStorage.getItem('jobs') || '[]');
762
- });
763
- // ...
461
+ function JobTracker() {
462
+ const [jobs, setJobs] = useState(() => {
463
+ return JSON.parse(localStorage.getItem('jobs') || '[]');
464
+ });
465
+ // ...
764
466
  }
765
467
 
766
- export const JobTracker = island(JobTrackerImpl, 'JobTracker');
767
-
768
468
  // ❌ Bad: Relying on props in persistent island
769
- function HeaderImpl({ activeTab }) { // Won't update on navigation!
770
- return <nav className={activeTab}>...</nav>;
469
+ function Header({ activeTab }) { // Won't update on navigation!
470
+ return <nav className={activeTab}>...</nav>;
771
471
  }
772
472
  ```
773
473
 
@@ -782,118 +482,32 @@ melina init <project-name> # Create new project
782
482
  melina start # Start dev server
783
483
  ```
784
484
 
785
- ### Core Imports
786
-
787
- ```tsx
788
- // Programmatic API
789
- import { start, createApp, serve, createAppRouter } from 'melina';
790
-
791
- // Navigation component
792
- import { Link } from 'melina/Link';
793
-
794
- // Manual island wrapper (optional - auto-wrapping is default)
795
- import { island } from 'melina/island';
796
- ```
797
-
798
- ### Programmatic API
799
-
800
- #### `start(options?)` — Quickstart Function
801
-
802
- The easiest way to run Melina from your own script:
485
+ ### Components
803
486
 
804
487
  ```tsx
805
- import { start } from 'melina';
488
+ import { Link } from '@anthropic/melina.js';
806
489
 
807
- // Basic usage
808
- await start();
809
-
810
- // With options
811
- await start({
812
- appDir: './app', // Default: './app'
813
- port: 8080, // Default: 3000
814
- defaultTitle: 'My App', // Default: 'Melina App'
815
- globalCss: './app/styles.css',
816
- });
817
- ```
818
-
819
- #### `createAppRouter(options)` — Router Factory
820
-
821
- Creates a request handler with file-based routing:
822
-
823
- ```tsx
824
- import { serve, createAppRouter } from 'melina';
825
-
826
- const router = createAppRouter({
827
- appDir: './app',
828
- defaultTitle: 'My App',
829
- globalCss: './app/globals.css',
830
- });
831
-
832
- // Use with custom middleware
833
- serve(async (req, measure) => {
834
- // Custom logic before routing
835
- if (req.url.endsWith('/health')) {
836
- return new Response('OK');
837
- }
838
-
839
- return router(req, measure);
840
- }, { port: 3000 });
841
- ```
842
-
843
- #### `createApp(options)` — Alias for `createAppRouter`
844
-
845
- ```tsx
846
- import { createApp } from 'melina';
847
- const app = createApp({ appDir: './app' });
490
+ <Link href="/path">Text</Link>
848
491
  ```
849
492
 
850
- #### `serve(handler, options)` — Low-Level Server
493
+ ### Events
851
494
 
852
- ```tsx
853
- import { serve } from 'melina';
854
-
855
- serve(async (req, measure) => {
856
- // Your custom handler
857
- return new Response('Hello');
858
- }, {
859
- port: 3000,
860
- // Or unix socket for production
861
- unix: '/tmp/melina.sock'
495
+ ```js
496
+ // Fired after SPA navigation completes
497
+ window.addEventListener('melina:navigated', () => {
498
+ console.log('Navigation complete!');
862
499
  });
863
500
  ```
864
501
 
865
- ### The `island()` Function
866
-
867
- Manual island wrapper (usually not needed with auto-wrapping):
502
+ ### Server Functions
868
503
 
869
504
  ```tsx
870
- import { island } from 'melina/island';
505
+ import { serve, createAppRouter } from '@anthropic/melina.js';
871
506
 
872
- // Basic usage
873
- export const Counter = island(CounterImpl, 'Counter');
874
- ```
875
-
876
- ### The `<Link>` Component
877
-
878
- ```tsx
879
- import { Link } from 'melina/Link';
880
-
881
- <Link href="/path">Text</Link>
882
- <Link href="/path" className="nav-link">Styled Link</Link>
883
- ```
884
-
885
- ### Navigation Events
886
-
887
- ```javascript
888
- // Fired when navigation starts
889
- window.addEventListener('melina:navigation-start', (e) => {
890
- console.log('From:', e.detail.from, 'To:', e.detail.to);
891
- });
892
-
893
- // Fired after SPA navigation completes
894
- window.addEventListener('melina:navigated', () => {
895
- console.log('Navigation complete!');
896
- });
507
+ serve(createAppRouter({
508
+ appDir: './app',
509
+ defaultTitle: 'My App',
510
+ }), { port: 3000 });
897
511
  ```
898
512
 
899
513
  ---
@@ -904,8 +518,6 @@ window.addEventListener('melina:navigated', () => {
904
518
 
905
519
  You're probably relying on server props. See [Props Desynchronization](#️-the-props-desynchronization-trade-off).
906
520
 
907
- **Solution:** Read from URL + listen for `melina:navigated`
908
-
909
521
  ### "CSS animations restart on navigation"
910
522
 
911
523
  Make sure your animated elements are **outside** `#melina-page-content`.
@@ -914,37 +526,13 @@ Make sure your animated elements are **outside** `#melina-page-content`.
914
526
 
915
527
  Islands are separate React roots. Use localStorage/events. See [Cross-Island Communication](#cross-island-communication).
916
528
 
917
- ### "View Transitions aren't working"
918
-
919
- 1. Ensure browser supports View Transitions API (Chrome 111+)
920
- 2. Check that `view-transition-name` values match between pages
921
- 3. Ensure no duplicate `view-transition-name` on the same page
922
-
923
- ### "EADDRINUSE error on restart"
924
-
925
- The Unix socket wasn't cleaned up. Melina handles this automatically, but if it persists:
926
-
927
- ```bash
928
- rm /tmp/melina.sock
929
- ```
930
-
931
- ### "Missing 'app' directory"
932
-
933
- Make sure you have an `app/` directory with at least a `page.tsx` or `layout.tsx`:
934
-
935
- ```bash
936
- mkdir -p app
937
- echo 'export default () => <h1>Hello!</h1>;' > app/page.tsx
938
- ```
939
-
940
529
  ---
941
530
 
942
531
  ## Version History
943
532
 
944
- - **v1.0.0** Hangar architecture, View Transitions, high-fidelity persistence
945
- - **v0.2.0** Partial page swap architecture, persistent islands
946
- - **v0.1.0** — Initial release with file-based routing
533
+ - **v0.2.0** - Partial page swap architecture, persistent islands
534
+ - **v0.1.0** - Initial release with file-based routing
947
535
 
948
536
  ---
949
537
 
950
- Built with ❤️ using [Bun](https://bun.sh) 🧅
538
+ Built with ❤️ using Bun