@deijose/nix-ionic 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/README.md ADDED
@@ -0,0 +1,759 @@
1
+ # @deijose/nix-ionic
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@deijose/nix-ionic.svg)](https://www.npmjs.com/package/@deijose/nix-ionic)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ > Ionic bridge for [Nix.js](https://nix-js-landing.vercel.app/) — routing, lifecycle hooks, and navigation powered by the official `ion-router` API.
7
+
8
+ ---
9
+
10
+ ## How it works
11
+
12
+ `@deijose/nix-ionic` bridges Nix.js components with Ionic Core's official vanilla JS routing system:
13
+
14
+ 1. Each route is registered as a **Custom Element** (`nix-page-home`, `nix-page-detail`, etc.)
15
+ 2. `ion-router` activates the correct custom element based on the URL
16
+ 3. `ion-router-outlet` manages: **view cache, page transitions, back button, iOS swipe back** — all native, zero custom code
17
+ 4. `connectedCallback` mounts the Nix component inside the custom element
18
+ 5. `ionRouteWillChange` / `ionRouteDidChange` drive the Nix lifecycle hooks
19
+
20
+ This gives you the same integration depth as `@ionic/angular` and `@ionic/react`, using only the public `ion-router` API.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @deijose/nix-ionic @deijose/nix-js @ionic/core
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Quick start
33
+
34
+ ### 1. Initialize Ionic in `main.ts`
35
+
36
+ ```typescript
37
+ import { defineCustomElements } from "@ionic/core/loader";
38
+ defineCustomElements();
39
+
40
+ import "@ionic/core/css/core.css";
41
+ import "@ionic/core/css/normalize.css";
42
+ import "@ionic/core/css/structure.css";
43
+ import "@ionic/core/css/typography.css";
44
+ import "@ionic/core/css/padding.css";
45
+ import "@ionic/core/css/flex-utils.css";
46
+ import "@ionic/core/css/display.css";
47
+ ```
48
+
49
+ ### 2. Define your routes
50
+
51
+ ```typescript
52
+ import { NixComponent, html, mount } from "@deijose/nix-js";
53
+ import { IonRouterOutlet } from "@deijose/nix-ionic";
54
+ import { HomePage } from "./pages/HomePage";
55
+ import { DetailPage } from "./pages/DetailPage";
56
+
57
+ const outlet = new IonRouterOutlet([
58
+ { path: "/", component: (ctx) => new HomePage(ctx) },
59
+ { path: "/detail/:id", component: (ctx) => new DetailPage(ctx) },
60
+ { path: "/profile", component: (ctx) => ProfilePage(ctx) },
61
+ ]);
62
+
63
+ class App extends NixComponent {
64
+ render() {
65
+ return html`<ion-app>${outlet}</ion-app>`;
66
+ }
67
+ }
68
+
69
+ mount(new App(), "#app");
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Pages
75
+
76
+ ### Class component — `IonPage`
77
+
78
+ Use `IonPage` when you need navigation lifecycle hooks.
79
+
80
+ ```typescript
81
+ import { html, signal } from "@deijose/nix-js";
82
+ import { IonPage, IonBackButton, useRouter } from "@deijose/nix-ionic";
83
+ import type { NixTemplate } from "@deijose/nix-js";
84
+ import type { PageContext } from "@deijose/nix-ionic";
85
+
86
+ export class DetailPage extends IonPage {
87
+ private post = signal<Post | null>(null);
88
+ private _id: string;
89
+
90
+ constructor({ lc, params }: PageContext) {
91
+ super(lc);
92
+ this._id = params["id"] ?? "1";
93
+ }
94
+
95
+ // Called on EVERY activation — even when returning from cached stack
96
+ override ionViewWillEnter(): void {
97
+ this._loadPost(this._id);
98
+ }
99
+
100
+ // Called when leaving the view (still in cache)
101
+ override ionViewWillLeave(): void {
102
+ // pause timers, subscriptions, etc.
103
+ }
104
+
105
+ override render(): NixTemplate {
106
+ return html`
107
+ <ion-header>
108
+ <ion-toolbar>
109
+ <ion-buttons slot="start">
110
+ ${IonBackButton()}
111
+ </ion-buttons>
112
+ <ion-title>Detail</ion-title>
113
+ </ion-toolbar>
114
+ </ion-header>
115
+ <ion-content class="ion-padding">
116
+ <p>${() => this.post.value?.title ?? ""}</p>
117
+ </ion-content>
118
+ `;
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Function component — composables
124
+
125
+ ```typescript
126
+ import { html, signal } from "@deijose/nix-js";
127
+ import { useIonViewWillEnter, useIonViewWillLeave, IonBackButton } from "@deijose/nix-ionic";
128
+ import type { NixTemplate } from "@deijose/nix-js";
129
+ import type { PageContext } from "@deijose/nix-ionic";
130
+
131
+ export function ProfilePage({ lc }: PageContext): NixTemplate {
132
+ const visits = signal(0);
133
+
134
+ useIonViewWillEnter(lc, () => {
135
+ visits.update((n) => n + 1);
136
+ });
137
+
138
+ useIonViewWillLeave(lc, () => {
139
+ console.log("leaving profile");
140
+ });
141
+
142
+ return html`
143
+ <ion-header>
144
+ <ion-toolbar>
145
+ <ion-buttons slot="start">
146
+ ${IonBackButton()}
147
+ </ion-buttons>
148
+ <ion-title>Profile</ion-title>
149
+ </ion-toolbar>
150
+ </ion-header>
151
+ <ion-content class="ion-padding">
152
+ <p>Visits: ${() => visits.value}</p>
153
+ </ion-content>
154
+ `;
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Navigation — `useRouter()`
161
+
162
+ Access the router singleton from anywhere without prop drilling:
163
+
164
+ ```typescript
165
+ import { useRouter } from "@deijose/nix-ionic";
166
+
167
+ const router = useRouter();
168
+
169
+ // Navigate forward
170
+ router.navigate("/detail/42");
171
+
172
+ // Navigate back
173
+ router.back();
174
+
175
+ // Replace current view (no history entry)
176
+ router.replace("/home");
177
+
178
+ // Reactive signals
179
+ router.canGoBack.value // boolean — true when back stack exists
180
+ router.params.value // { id: "42" } for /detail/:id
181
+ router.path.value // current pathname
182
+ ```
183
+
184
+ ### In a class component
185
+
186
+ ```typescript
187
+ override render(): NixTemplate {
188
+ const router = useRouter(); // safe to call inside render()
189
+
190
+ return html`
191
+ <ion-button @click=${() => router.navigate("/profile")}>
192
+ Go to Profile
193
+ </ion-button>
194
+ `;
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ## `IonBackButton()`
201
+
202
+ A wrapper around `<ion-back-button>` that intercepts the click before Ionic's internal router processes it, calling `router.back()` directly. Automatically hidden on the root page.
203
+
204
+ ```typescript
205
+ import { IonBackButton } from "@deijose/nix-ionic";
206
+
207
+ // In any template — no arguments needed
208
+ html`
209
+ <ion-buttons slot="start">
210
+ ${IonBackButton()}
211
+ </ion-buttons>
212
+ `
213
+
214
+ // Optional default href (navigates here if no back stack)
215
+ ${IonBackButton("/")}
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Lifecycle hooks
221
+
222
+ All hooks are optional. For class components, implement the methods directly. For function components, use the composables.
223
+
224
+ | Hook | When it fires |
225
+ |---|---|
226
+ | `ionViewWillEnter` | Before the view becomes visible (every activation) |
227
+ | `ionViewDidEnter` | After the view is fully visible |
228
+ | `ionViewWillLeave` | Before the view is hidden (stays in cache) |
229
+ | `ionViewDidLeave` | After the view is hidden |
230
+
231
+ ### Key difference from `onMount` / `onInit`
232
+
233
+ `onMount` and `onInit` (from Nix.js) only fire **once** when the component is first created. Ionic caches views in the stack — when the user returns to a cached view, `onMount` does NOT run again.
234
+
235
+ Use `ionViewWillEnter` for anything that needs to refresh on every visit (data fetching, resetting state, restarting timers):
236
+
237
+ ```typescript
238
+ // ❌ Only runs once — misses subsequent visits
239
+ override onMount() {
240
+ this._fetchData();
241
+ }
242
+
243
+ // ✅ Runs on every activation
244
+ override ionViewWillEnter() {
245
+ this._fetchData();
246
+ }
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Route guards
252
+
253
+ ```typescript
254
+ new IonRouterOutlet([
255
+ { path: "/", component: (ctx) => new HomePage(ctx) },
256
+ {
257
+ path: "/admin",
258
+ component: (ctx) => new AdminPage(ctx),
259
+ beforeEnter: (to, from) => {
260
+ if (!isLoggedIn()) return "/login"; // redirect
261
+ if (!isAdmin()) return false; // cancel navigation
262
+ // return void or undefined to allow
263
+ },
264
+ },
265
+ ]);
266
+ ```
267
+
268
+ | Return value | Effect |
269
+ |---|---|
270
+ | `void` / `undefined` | Allow navigation |
271
+ | `false` | Cancel — stay on current view |
272
+ | `"string"` | Redirect to that path |
273
+
274
+ ---
275
+
276
+ ## `PageContext`
277
+
278
+ Every route factory receives a `PageContext`:
279
+
280
+ ```typescript
281
+ interface PageContext {
282
+ lc: PageLifecycle; // navigation lifecycle signals
283
+ params: Record<string,string>; // /detail/:id → { id: "42" }
284
+ }
285
+ ```
286
+
287
+ ---
288
+
289
+ ## API Reference
290
+
291
+ ### `IonRouterOutlet`
292
+
293
+ ```typescript
294
+ new IonRouterOutlet(routes: RouteDefinition[])
295
+ ```
296
+
297
+ Mounts `ion-router` + `ion-router-outlet` in the DOM. Registers a custom element per route. Initialize once in your app entry point.
298
+
299
+ ### `useRouter()`
300
+
301
+ ```typescript
302
+ useRouter(): RouterStore
303
+ ```
304
+
305
+ Returns the active router store. Must be called after `IonRouterOutlet` is instantiated.
306
+
307
+ ### `IonBackButton(defaultHref?)`
308
+
309
+ ```typescript
310
+ IonBackButton(defaultHref?: string): NixTemplate
311
+ ```
312
+
313
+ Back button that works with the Nix router. Hidden when `canGoBack` is false.
314
+
315
+ ### `IonPage`
316
+
317
+ Abstract class. Extend for pages that need lifecycle hooks.
318
+
319
+ ```typescript
320
+ abstract class IonPage extends NixComponent {
321
+ constructor(lc: PageLifecycle)
322
+ ionViewWillEnter?(): void
323
+ ionViewDidEnter?(): void
324
+ ionViewWillLeave?(): void
325
+ ionViewDidLeave?(): void
326
+ abstract render(): NixTemplate
327
+ }
328
+ ```
329
+
330
+ ### Composables
331
+
332
+ ```typescript
333
+ useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void
334
+ useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void
335
+ useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void
336
+ useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Comparison with other frameworks
342
+
343
+ | Feature | `@ionic/angular` | `@ionic/react` | `@deijose/nix-ionic` |
344
+ |---|---|---|---|
345
+ | Router integration | Angular Router | React Router | `ion-router` (vanilla) |
346
+ | View cache | ✅ | ✅ | ✅ native |
347
+ | Page transitions | ✅ | ✅ | ✅ native |
348
+ | iOS swipe back | ✅ | ✅ | ✅ native |
349
+ | `ion-back-button` | native | wrapper | wrapper |
350
+ | Lifecycle hooks | directive | hooks | `IonPage` / composables |
351
+ | Navigation API | `NavController` | `useHistory` | `useRouter()` |
352
+
353
+
354
+ ## Project setup
355
+
356
+ ### Prerequisites
357
+
358
+ | Tool | Version | Install |
359
+ |---|---|---|
360
+ | Node.js | ≥ 18 | [nodejs.org](https://nodejs.org) |
361
+ | npm | ≥ 9 | included with Node |
362
+ | Capacitor CLI | latest | `npm i -g @capacitor/cli` |
363
+ | Android Studio | latest | [developer.android.com](https://developer.android.com/studio) |
364
+ | Xcode | ≥ 14 | Mac App Store |
365
+
366
+ ---
367
+
368
+ ## Create a new project
369
+
370
+ ```bash
371
+ # 1. Scaffold a Vite + TypeScript project
372
+ npm create vite@latest my-app -- --template vanilla-ts
373
+ cd my-app
374
+
375
+ # 2. Install dependencies
376
+ npm install
377
+
378
+ # 3. Install Nix.js + Ionic + nix-ionic
379
+ npm install @deijose/nix-js @ionic/core @deijose/nix-ionic
380
+
381
+ # 4. Install Capacitor (for native iOS / Android)
382
+ npm install @capacitor/core @capacitor/cli
383
+ npm install @capacitor/android @capacitor/ios
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Recommended folder structure
389
+
390
+ ```
391
+ my-app/
392
+ ├── android/ ← generated by Capacitor (do not edit manually)
393
+ ├── ios/ ← generated by Capacitor (do not edit manually)
394
+ ├── public/
395
+ │ └── favicon.ico
396
+ ├── src/
397
+ │ ├── ionic-nix/ ← copy from @deijose/nix-ionic if customizing
398
+ │ │ ├── IonRouterOutlet.ts
399
+ │ │ ├── lifecycle.ts
400
+ │ │ └── index.ts
401
+ │ │
402
+ │ ├── pages/ ← one file per screen
403
+ │ │ ├── HomePage.ts
404
+ │ │ ├── DetailPage.ts
405
+ │ │ └── ProfilePage.ts
406
+ │ │
407
+ │ ├── components/ ← reusable UI components (not pages)
408
+ │ │ ├── PostCard.ts
409
+ │ │ └── Avatar.ts
410
+ │ │
411
+ │ ├── stores/ ← global state via Nix.js createStore()
412
+ │ │ ├── auth.ts
413
+ │ │ └── cart.ts
414
+ │ │
415
+ │ ├── services/ ← API calls, business logic
416
+ │ │ ├── api.ts
417
+ │ │ └── storage.ts
418
+ │ │
419
+ │ ├── style.css ← global styles + Ionic CSS imports
420
+ │ └── main.ts ← app entry point
421
+
422
+ ├── index.html
423
+ ├── capacitor.config.ts
424
+ ├── package.json
425
+ ├── tsconfig.json
426
+ └── vite.config.ts
427
+ ```
428
+
429
+ ### `main.ts` — entry point
430
+
431
+ ```typescript
432
+ // ── Ionic loader ─────────────────────────────────────────────────────────────
433
+ import { defineCustomElements } from "@ionic/core/loader";
434
+ defineCustomElements();
435
+
436
+ // ── Ionic CSS (order matters) ─────────────────────────────────────────────────
437
+ import "@ionic/core/css/core.css";
438
+ import "@ionic/core/css/normalize.css";
439
+ import "@ionic/core/css/structure.css";
440
+ import "@ionic/core/css/typography.css";
441
+ import "@ionic/core/css/padding.css";
442
+ import "@ionic/core/css/flex-utils.css";
443
+ import "@ionic/core/css/display.css";
444
+
445
+ // ── App styles ────────────────────────────────────────────────────────────────
446
+ import "./style.css";
447
+
448
+ // ── App ───────────────────────────────────────────────────────────────────────
449
+ import { NixComponent, html, mount } from "@deijose/nix-js";
450
+ import type { NixTemplate } from "@deijose/nix-js";
451
+ import { IonRouterOutlet } from "@deijose/nix-ionic";
452
+
453
+ import { HomePage } from "./pages/HomePage";
454
+ import { DetailPage } from "./pages/DetailPage";
455
+ import { ProfilePage } from "./pages/ProfilePage";
456
+
457
+ const outlet = new IonRouterOutlet([
458
+ { path: "/", component: (ctx) => new HomePage(ctx) },
459
+ { path: "/detail/:id", component: (ctx) => new DetailPage(ctx) },
460
+ { path: "/profile", component: (ctx) => ProfilePage(ctx) },
461
+ ]);
462
+
463
+ class App extends NixComponent {
464
+ override render(): NixTemplate {
465
+ return html`<ion-app>${outlet}</ion-app>`;
466
+ }
467
+ }
468
+
469
+ mount(new App(), "#app");
470
+ ```
471
+
472
+ ### `index.html` — root HTML
473
+
474
+ ```html
475
+ <!DOCTYPE html>
476
+ <html lang="en" dir="ltr">
477
+ <head>
478
+ <meta charset="UTF-8" />
479
+ <meta
480
+ name="viewport"
481
+ content="viewport-fit=cover, width=device-width, initial-scale=1.0,
482
+ minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
483
+ />
484
+ <title>My App</title>
485
+ </head>
486
+ <body>
487
+ <div id="app"></div>
488
+ <script type="module" src="/src/main.ts"></script>
489
+ </body>
490
+ </html>
491
+ ```
492
+
493
+ ### `capacitor.config.ts`
494
+
495
+ ```typescript
496
+ import type { CapacitorConfig } from "@capacitor/cli";
497
+
498
+ const config: CapacitorConfig = {
499
+ appId: "com.example.myapp",
500
+ appName: "My App",
501
+ webDir: "dist",
502
+ };
503
+
504
+ export default config;
505
+ ```
506
+
507
+ ### `vite.config.ts`
508
+
509
+ ```typescript
510
+ import { defineConfig } from "vite";
511
+
512
+ export default defineConfig({
513
+ optimizeDeps: {
514
+ exclude: ["@ionic/core"],
515
+ },
516
+ build: {
517
+ rollupOptions: {
518
+ output: { manualChunks: undefined },
519
+ },
520
+ },
521
+ });
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Development commands
527
+
528
+ ### Web
529
+
530
+ ```bash
531
+ # Start dev server (hot reload)
532
+ npm run dev
533
+
534
+ # Type check
535
+ npx tsc --noEmit
536
+
537
+ # Production build
538
+ npm run build
539
+
540
+ # Preview production build
541
+ npm run preview
542
+ ```
543
+
544
+ ---
545
+
546
+ ## Android
547
+
548
+ ### First-time setup
549
+
550
+ ```bash
551
+ # 1. Build the web app
552
+ npm run build
553
+
554
+ # 2. Initialize Capacitor (only once)
555
+ npx cap init "My App" "com.example.myapp" --web-dir dist
556
+
557
+ # 3. Add Android platform
558
+ npx cap add android
559
+
560
+ # 4. Sync web assets to native project
561
+ npx cap sync android
562
+ ```
563
+
564
+ ### Daily workflow
565
+
566
+ ```bash
567
+ # After any change to web code:
568
+ npm run build
569
+ npx cap sync android
570
+
571
+ # Open in Android Studio (run / debug from there)
572
+ npx cap open android
573
+
574
+ # Or run directly on a connected device / emulator
575
+ npx cap run android
576
+ ```
577
+
578
+ ### Live reload on Android (dev)
579
+
580
+ ```bash
581
+ # Start dev server first
582
+ npm run dev
583
+
584
+ # In a second terminal — live reload on device
585
+ npx cap run android --livereload --external
586
+ ```
587
+
588
+ > **Note:** device and computer must be on the same Wi-Fi network for live reload.
589
+
590
+ ---
591
+
592
+ ## iOS
593
+
594
+ > Requires macOS + Xcode.
595
+
596
+ ### First-time setup
597
+
598
+ ```bash
599
+ # 1. Build the web app
600
+ npm run build
601
+
602
+ # 2. Add iOS platform
603
+ npx cap add ios
604
+
605
+ # 3. Sync
606
+ npx cap sync ios
607
+ ```
608
+
609
+ ### Daily workflow
610
+
611
+ ```bash
612
+ npm run build
613
+ npx cap sync ios
614
+
615
+ # Open in Xcode (run / debug from there)
616
+ npx cap open ios
617
+
618
+ # Or run on simulator
619
+ npx cap run ios
620
+ ```
621
+
622
+ ### Live reload on iOS (dev)
623
+
624
+ ```bash
625
+ npm run dev
626
+ npx cap run ios --livereload --external
627
+ ```
628
+
629
+ ---
630
+
631
+ ## Capacitor plugins
632
+
633
+ Add any official Capacitor plugin the same way:
634
+
635
+ ```bash
636
+ # Camera
637
+ npm install @capacitor/camera
638
+ npx cap sync
639
+
640
+ # Filesystem
641
+ npm install @capacitor/filesystem
642
+ npx cap sync
643
+
644
+ # Push notifications
645
+ npm install @capacitor/push-notifications
646
+ npx cap sync
647
+ ```
648
+
649
+ Then use them in your services:
650
+
651
+ ```typescript
652
+ // src/services/camera.ts
653
+ import { Camera, CameraResultType } from "@capacitor/camera";
654
+
655
+ export async function takePhoto(): Promise<string> {
656
+ const photo = await Camera.getPhoto({
657
+ quality: 90,
658
+ allowEditing: false,
659
+ resultType: CameraResultType.DataUrl,
660
+ });
661
+ return photo.dataUrl ?? "";
662
+ }
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Page template
668
+
669
+ Copy this as a starting point for any new page:
670
+
671
+ ```typescript
672
+ // src/pages/MyPage.ts
673
+ import { html, signal } from "@deijose/nix-js";
674
+ import type { NixTemplate } from "@deijose/nix-js";
675
+ import { IonPage, IonBackButton, useRouter } from "@deijose/nix-ionic";
676
+ import type { PageContext } from "@deijose/nix-ionic";
677
+
678
+ export class MyPage extends IonPage {
679
+ private data = signal<string | null>(null);
680
+
681
+ constructor({ lc, params }: PageContext) {
682
+ super(lc);
683
+ // params contains dynamic route segments
684
+ // e.g. for /my/:id → params["id"]
685
+ }
686
+
687
+ // Runs on EVERY visit (initial + returning from stack)
688
+ override ionViewWillEnter(): void {
689
+ this._load();
690
+ }
691
+
692
+ // Runs when navigating away (view stays cached)
693
+ override ionViewWillLeave(): void {
694
+ // pause subscriptions, timers, etc.
695
+ }
696
+
697
+ private async _load(): Promise<void> {
698
+ // fetch data
699
+ }
700
+
701
+ override render(): NixTemplate {
702
+ const router = useRouter();
703
+
704
+ return html`
705
+ <ion-header>
706
+ <ion-toolbar>
707
+ <ion-buttons slot="start">
708
+ ${IonBackButton()}
709
+ </ion-buttons>
710
+ <ion-title>My Page</ion-title>
711
+ </ion-toolbar>
712
+ </ion-header>
713
+
714
+ <ion-content class="ion-padding">
715
+ <p>${() => this.data.value ?? "Loading..."}</p>
716
+
717
+ <ion-button @click=${() => router.navigate("/other")}>
718
+ Go somewhere
719
+ </ion-button>
720
+ </ion-content>
721
+ `;
722
+ }
723
+ }
724
+ ```
725
+
726
+ Register it in `main.ts`:
727
+
728
+ ```typescript
729
+ { path: "/my/:id", component: (ctx) => new MyPage(ctx) },
730
+ ```
731
+
732
+ ---
733
+
734
+ ## Build for production
735
+
736
+ ```bash
737
+ # Web PWA
738
+ npm run build
739
+ # Output in dist/ — deploy to any static host (Vercel, Netlify, etc.)
740
+
741
+ # Android APK / AAB
742
+ npm run build
743
+ npx cap sync android
744
+ npx cap open android
745
+ # In Android Studio: Build → Generate Signed Bundle/APK
746
+
747
+ # iOS IPA
748
+ npm run build
749
+ npx cap sync ios
750
+ npx cap open ios
751
+ # In Xcode: Product → Archive
752
+ ```
753
+
754
+
755
+ ## License
756
+
757
+ [MIT](https://opensource.org/licenses/MIT)
758
+
759
+ ---
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ionic-nix/IonRouterOutlet.ts
3
+ *
4
+ * Bridge entre Nix.js e ion-router (API pública oficial para vanilla JS).
5
+ *
6
+ * Arquitectura:
7
+ * 1. Por cada ruta registramos un Custom Element (nix-page-home, etc.)
8
+ * 2. ion-router activa el custom element según la URL
9
+ * 3. ion-router-outlet gestiona: caché, animaciones, back button, swipe back
10
+ * 4. connectedCallback del custom element monta el componente Nix adentro
11
+ * 5. ionRouteWillChange / ionRouteDidChange disparan el ciclo de vida Nix
12
+ */
13
+ import { NixComponent } from "@deijose/nix-js";
14
+ import type { NixTemplate, Signal } from "@deijose/nix-js";
15
+ import { type PageLifecycle } from "./lifecycle";
16
+ interface RouterStore {
17
+ navigate: (path: string) => void;
18
+ replace: (path: string) => void;
19
+ back: () => void;
20
+ canGoBack: Signal<boolean>;
21
+ params: Signal<Record<string, string>>;
22
+ path: Signal<string>;
23
+ }
24
+ export declare function useRouter(): RouterStore;
25
+ export declare function IonBackButton(defaultHref?: string): NixTemplate;
26
+ export interface PageContext {
27
+ lc: PageLifecycle;
28
+ params: Record<string, string>;
29
+ }
30
+ export interface RouteDefinition {
31
+ path: string;
32
+ component: (ctx: PageContext) => NixComponent | NixTemplate;
33
+ beforeEnter?: (to: string, from: string) => boolean | string | void;
34
+ }
35
+ export declare class IonRouterOutlet extends NixComponent {
36
+ private routes;
37
+ private ionRouterEl;
38
+ private _currentPath;
39
+ private _canGoBack;
40
+ private _params;
41
+ constructor(routes: RouteDefinition[]);
42
+ onInit(): void;
43
+ onMount(): () => void;
44
+ navigate(path: string): void;
45
+ replace(path: string): void;
46
+ back(): void;
47
+ private _handlePopState;
48
+ private _handleWillChange;
49
+ private _handleDidChange;
50
+ private _matchRoute;
51
+ private _extractParams;
52
+ private _pathToTag;
53
+ private _registerCustomElements;
54
+ render(): NixTemplate;
55
+ onUnmount(): void;
56
+ }
57
+ export {};
@@ -0,0 +1,2 @@
1
+ export { IonPage, createPageLifecycle, useIonViewWillEnter, useIonViewDidEnter, useIonViewWillLeave, useIonViewDidLeave, type PageLifecycle, } from "./lifecycle";
2
+ export { IonRouterOutlet, IonBackButton, useRouter, type RouteDefinition, type PageContext, } from "./IonRouterOutlet";
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});let e=require("@deijose/nix-js");function t(){return{willEnter:(0,e.signal)(0),didEnter:(0,e.signal)(0),willLeave:(0,e.signal)(0),didLeave:(0,e.signal)(0)}}var n=class extends e.NixComponent{__lc;constructor(e){super(),this.__lc=e}onInit(){let t=this.__lc;this.ionViewWillEnter&&(0,e.watch)(t.willEnter,this.ionViewWillEnter.bind(this)),this.ionViewDidEnter&&(0,e.watch)(t.didEnter,this.ionViewDidEnter.bind(this)),this.ionViewWillLeave&&(0,e.watch)(t.willLeave,this.ionViewWillLeave.bind(this)),this.ionViewDidLeave&&(0,e.watch)(t.didLeave,this.ionViewDidLeave.bind(this))}};function r(t,n){(0,e.watch)(t.willEnter,n)}function i(t,n){(0,e.watch)(t.didEnter,n)}function a(t,n){(0,e.watch)(t.willLeave,n)}function o(t,n){(0,e.watch)(t.didLeave,n)}var s=null;function c(){if(!s)throw Error("[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet");return s}function l(e){return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-back-button");i.setAttribute("default-href",e??"/");let a=t=>{t.stopPropagation(),t.preventDefault(),s?.canGoBack.value?s.back():e&&s?.navigate(e)};return i.addEventListener("click",a),t.insertBefore(i,n),()=>{i.removeEventListener("click",a),i.remove()}}}}var u=new Map,d=class extends e.NixComponent{routes;ionRouterEl=null;_currentPath=(0,e.signal)(location.pathname);_canGoBack=(0,e.signal)(!1);_params=(0,e.signal)({});constructor(e){super(),this.routes=e,s={navigate:e=>this.navigate(e),replace:e=>this.replace(e),back:()=>this.back(),canGoBack:this._canGoBack,params:this._params,path:this._currentPath},this._registerCustomElements()}onInit(){window.addEventListener("popstate",this._handlePopState)}onMount(){this.ionRouterEl=document.querySelector("ion-router");let e=e=>{let{to:t,from:n}=e.detail;this._handleWillChange(t,n??"")},t=e=>{let{to:t}=e.detail;this._handleDidChange(t)},n=e=>{e.detail.register(100,e=>{this._canGoBack.value?this.back():e()})};return window.addEventListener("ionRouteWillChange",e),window.addEventListener("ionRouteDidChange",t),document.addEventListener("ionBackButton",n),()=>{window.removeEventListener("popstate",this._handlePopState),window.removeEventListener("ionRouteWillChange",e),window.removeEventListener("ionRouteDidChange",t),document.removeEventListener("ionBackButton",n),s=null}}navigate(e){e!==location.pathname&&this.ionRouterEl?.push(e,"forward")}replace(e){this.ionRouterEl?.push(e,"root")}back(){this.ionRouterEl?.back()}_handlePopState=()=>{this._handleWillChange(location.pathname,this._currentPath.peek())};_handleWillChange(e,t){let n=this._matchRoute(e);if(!n)return;if(n.beforeEnter){let i=n.beforeEnter(e,t);if(!1===i)return void this.back();if("string"==typeof i)return void this.navigate(i)}this._currentPath.value=e,this._params.value=this._extractParams(n.path,e);let i=this._pathToTag(t);u.get(i)?.willLeave.update(e=>e+1)}_handleDidChange(e){let t=document.querySelector("ion-router-outlet");this._canGoBack.value=t?.canGoBack?.()??!1,u.forEach((t,n)=>{n!==this._pathToTag(e)&&t.didLeave.update(e=>e+1)});let n=this._pathToTag(e),i=u.get(n),a=document.querySelector(n);i&&a?._isCached&&(i.willEnter.update(e=>e+1),requestAnimationFrame(()=>i.didEnter.update(e=>e+1)))}_matchRoute(e){let t=this.routes.find(t=>t.path===e);if(t)return t;for(let t of this.routes)if(RegExp(`^${t.path.replace(/:[^/]+/g,"([^/]+)")}$`).test(e))return t;return this.routes.find(e=>"*"===e.path)}_extractParams(e,t){let n={},i=e.split("/"),a=t.split("/");for(let e=0;e<i.length;e++)i[e].startsWith(":")&&(n[i[e].slice(1)]=a[e]??"");return n}_pathToTag(e){return e&&"/"!==e?`nix-page-${e.replace(/\/:?[^/]+/g,e=>"-"+e.replace(/\//g,"").replace(/:/g,"")).replace(/^\//,"").replace(/\//g,"-")}`:"nix-page-home"}_registerCustomElements(){for(let e of this.routes){if("*"===e.path)continue;let n=this._pathToTag(e.path),i=e,a=this;customElements.get(n)||customElements.define(n,class extends HTMLElement{_cleanup=null;connectedCallback(){let e=a._extractParams(i.path,location.pathname),r=t();u.set(n,r),this.classList.add("ion-page");let o=i.component({lc:r,params:e});if("render"in o&&"function"==typeof o.render){let e=o;e.onInit?.();let t=e.render()._render(this,null),n=e.onMount?.();this._cleanup=()=>{e.onUnmount?.(),"function"==typeof n&&n(),t()}}else this._cleanup=o._render(this,null);r.willEnter.update(e=>e+1),requestAnimationFrame(()=>{r.didEnter.update(e=>e+1),this._isCached=!0})}disconnectedCallback(){let e=u.get(n);e&&(e.willLeave.update(e=>e+1),e.didLeave.update(e=>e+1)),this._cleanup?.(),this._cleanup=null,u.delete(n)}})}}render(){let e=this;return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-router");i.setAttribute("use-hash","false"),i.innerHTML=e.routes.filter(e=>"*"!==e.path).map(t=>`<ion-route url="${t.path}" component="${e._pathToTag(t.path)}"></ion-route>`).join("");let a=document.createElement("ion-router-outlet");return t.insertBefore(i,n),t.insertBefore(a,n),e.ionRouterEl=i,()=>{i.remove(),a.remove()}}}}onUnmount(){u.clear(),s=null}};exports.IonBackButton=l,exports.IonPage=n,exports.IonRouterOutlet=d,exports.createPageLifecycle=t,exports.useIonViewDidEnter=i,exports.useIonViewDidLeave=o,exports.useIonViewWillEnter=r,exports.useIonViewWillLeave=a,exports.useRouter=c;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nix-ionic.cjs","names":[],"sources":["../../src/lifecycle.ts","../../src/IonRouterOutlet.ts"],"sourcesContent":["/**\n * ionic-nix/lifecycle.ts\n *\n * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:\n * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave\n *\n * Cómo funciona (sin provide/inject):\n * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.\n * 2. Lo pasa directamente al factory de la ruta como argumento.\n * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.\n * 4. IonPage/composables registran watchers sobre las señales del lc.\n * 5. Cuando el router navega, incrementa las señales → watchers se disparan.\n */\n\nimport { signal, watch } from \"@deijose/nix-js\";\nimport type { Signal } from \"@deijose/nix-js\";\nimport { NixComponent } from \"@deijose/nix-js\";\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageLifecycle {\n willEnter: Signal<number>;\n didEnter: Signal<number>;\n willLeave: Signal<number>;\n didLeave: Signal<number>;\n}\n\n/** Crea un nuevo PageLifecycle con señales en 0. */\nexport function createPageLifecycle(): PageLifecycle {\n return {\n willEnter: signal(0),\n didEnter: signal(0),\n willLeave: signal(0),\n didLeave: signal(0),\n };\n}\n\n// --------------------------------------------------------------------------\n// IonPage — clase base para páginas con hooks de navegación\n// --------------------------------------------------------------------------\n//\n// Uso:\n// class HomePage extends IonPage {\n// constructor(lc: PageLifecycle) { super(lc); }\n//\n// ionViewWillEnter() { /* fetch de datos frescos */ }\n// render() { return html`...`; }\n// }\n\nexport abstract class IonPage extends NixComponent {\n private __lc: PageLifecycle;\n\n constructor(lc: PageLifecycle) {\n super();\n this.__lc = lc;\n }\n\n override onInit(): void {\n const lc = this.__lc;\n // watch no corre en init (immediate: false), así que ionViewWillEnter\n // solo se llama cuando el outlet incrementa la señal, no al construir.\n if (this.ionViewWillEnter) watch(lc.willEnter, this.ionViewWillEnter.bind(this));\n if (this.ionViewDidEnter) watch(lc.didEnter, this.ionViewDidEnter.bind(this));\n if (this.ionViewWillLeave) watch(lc.willLeave, this.ionViewWillLeave.bind(this));\n if (this.ionViewDidLeave) watch(lc.didLeave, this.ionViewDidLeave.bind(this));\n }\n\n ionViewWillEnter?(): void;\n ionViewDidEnter?(): void;\n ionViewWillLeave?(): void;\n ionViewDidLeave?(): void;\n}\n\n// --------------------------------------------------------------------------\n// Composables para function components\n// --------------------------------------------------------------------------\n//\n// Uso:\n// function ProfilePage(lc: PageLifecycle): NixTemplate {\n// useIonViewWillEnter(lc, () => { /* fetch */ });\n// return html`...`;\n// }\n\nexport function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willEnter, fn);\n}\n\nexport function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didEnter, fn);\n}\n\nexport function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willLeave, fn);\n}\n\nexport function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didLeave, fn);\n}","/**\n * ionic-nix/IonRouterOutlet.ts\n *\n * Bridge entre Nix.js e ion-router (API pública oficial para vanilla JS).\n *\n * Arquitectura:\n * 1. Por cada ruta registramos un Custom Element (nix-page-home, etc.)\n * 2. ion-router activa el custom element según la URL\n * 3. ion-router-outlet gestiona: caché, animaciones, back button, swipe back\n * 4. connectedCallback del custom element monta el componente Nix adentro\n * 5. ionRouteWillChange / ionRouteDidChange disparan el ciclo de vida Nix\n */\n\nimport { NixComponent, signal } from \"@deijose/nix-js\";\nimport type { NixTemplate, Signal } from \"@deijose/nix-js\";\nimport { createPageLifecycle, type PageLifecycle } from \"./lifecycle\";\n\n// --------------------------------------------------------------------------\n// Router store singleton\n// --------------------------------------------------------------------------\n\ninterface RouterStore {\n navigate: (path: string) => void;\n replace: (path: string) => void;\n back: () => void;\n canGoBack: Signal<boolean>;\n params: Signal<Record<string, string>>;\n path: Signal<string>;\n}\n\nlet _router: RouterStore | null = null;\n\nexport function useRouter(): RouterStore {\n if (!_router) throw new Error(\"[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet\");\n return _router;\n}\n\n// --------------------------------------------------------------------------\n// IonBackButton — intercepta el click antes del shadow DOM\n// --------------------------------------------------------------------------\n\nexport function IonBackButton(defaultHref?: string): NixTemplate {\n return {\n __isNixTemplate: true as const,\n mount(container) {\n const el = typeof container === \"string\" ? document.querySelector(container)! : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const btn = document.createElement(\"ion-back-button\") as HTMLElement;\n btn.setAttribute(\"default-href\", defaultHref ?? \"/\");\n const handler = (e: Event) => {\n e.stopPropagation();\n e.preventDefault();\n if (_router?.canGoBack.value) _router.back();\n else if (defaultHref) _router?.navigate(defaultHref);\n };\n btn.addEventListener(\"click\", handler);\n parent.insertBefore(btn, before);\n return () => {\n btn.removeEventListener(\"click\", handler);\n btn.remove();\n };\n },\n };\n}\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageContext {\n lc: PageLifecycle;\n params: Record<string, string>;\n}\n\nexport interface RouteDefinition {\n path: string;\n component: (ctx: PageContext) => NixComponent | NixTemplate;\n beforeEnter?: (to: string, from: string) => boolean | string | void;\n}\n\n// --------------------------------------------------------------------------\n// Mapa global de lifecycles activos por tag\n// --------------------------------------------------------------------------\n\nconst _lifecycleRegistry = new Map<string, PageLifecycle>();\n\n// --------------------------------------------------------------------------\n// IonRouterOutlet\n// --------------------------------------------------------------------------\n\nexport class IonRouterOutlet extends NixComponent {\n private routes: RouteDefinition[];\n private ionRouterEl: HTMLElement | null = null;\n\n private _currentPath = signal(location.pathname);\n private _canGoBack = signal(false);\n private _params = signal<Record<string, string>>({});\n\n constructor(routes: RouteDefinition[]) {\n super();\n this.routes = routes;\n\n // Store disponible desde el constructor — antes que cualquier\n // connectedCallback de custom element\n _router = {\n navigate: (path) => this.navigate(path),\n replace: (path) => this.replace(path),\n back: () => this.back(),\n canGoBack: this._canGoBack,\n params: this._params,\n path: this._currentPath,\n };\n\n this._registerCustomElements();\n }\n\n override onInit(): void {\n window.addEventListener(\"popstate\", this._handlePopState);\n }\n\n override onMount(): () => void {\n this.ionRouterEl = document.querySelector(\"ion-router\");\n\n const onWillChange = (ev: Event) => {\n const { to, from } = (ev as CustomEvent).detail as { to: string; from: string };\n this._handleWillChange(to, from ?? \"\");\n };\n const onDidChange = (ev: Event) => {\n const { to } = (ev as CustomEvent).detail as { to: string };\n this._handleDidChange(to);\n };\n const onIonBack = (ev: Event) => {\n (ev as CustomEvent).detail.register(100, (next: () => void) => {\n if (this._canGoBack.value) this.back(); else next();\n });\n };\n\n window.addEventListener(\"ionRouteWillChange\", onWillChange);\n window.addEventListener(\"ionRouteDidChange\", onDidChange);\n document.addEventListener(\"ionBackButton\", onIonBack);\n\n return () => {\n window.removeEventListener(\"popstate\", this._handlePopState);\n window.removeEventListener(\"ionRouteWillChange\", onWillChange);\n window.removeEventListener(\"ionRouteDidChange\", onDidChange);\n document.removeEventListener(\"ionBackButton\", onIonBack);\n _router = null;\n };\n }\n\n // -------------------------------------------------------------------------\n // API pública\n // -------------------------------------------------------------------------\n\n navigate(path: string): void {\n if (path === location.pathname) return;\n (this.ionRouterEl as any)?.push(path, \"forward\");\n }\n\n replace(path: string): void {\n (this.ionRouterEl as any)?.push(path, \"root\");\n }\n\n back(): void {\n (this.ionRouterEl as any)?.back();\n }\n\n // -------------------------------------------------------------------------\n // Handlers privados\n // -------------------------------------------------------------------------\n\n private _handlePopState = (): void => {\n // El browser navegó con sus propios botones — sincronizar\n this._handleWillChange(location.pathname, this._currentPath.peek());\n };\n\n private _handleWillChange(to: string, from: string): void {\n const route = this._matchRoute(to);\n if (!route) return;\n\n // Guards\n if (route.beforeEnter) {\n const result = route.beforeEnter(to, from);\n if (result === false) { this.back(); return; }\n if (typeof result === \"string\") { this.navigate(result); return; }\n }\n\n this._currentPath.value = to;\n this._params.value = this._extractParams(route.path, to);\n\n // willLeave en la vista que sale (ya tiene lc registrado)\n const fromTag = this._pathToTag(from);\n _lifecycleRegistry.get(fromTag)?.willLeave.update((n) => n + 1);\n }\n\n private _handleDidChange(to: string): void {\n const outlet = document.querySelector(\"ion-router-outlet\") as any;\n this._canGoBack.value = outlet?.canGoBack?.() ?? false;\n\n // didLeave en la vista que salió\n _lifecycleRegistry.forEach((lc, tag) => {\n if (tag !== this._pathToTag(to)) lc.didLeave.update((n) => n + 1);\n });\n\n // Para vistas CACHEADAS (ya existían en el DOM, connectedCallback no se llama de nuevo)\n // disparamos willEnter/didEnter aquí.\n // Para vistas NUEVAS, connectedCallback ya lo hizo — disparar de nuevo haría doble.\n // Distinguimos: si el tag ya estaba en el registry ANTES de este didChange,\n // es una vista cacheada. Usamos un flag en el custom element.\n const toTag = this._pathToTag(to);\n const toLc = _lifecycleRegistry.get(toTag);\n const toEl = document.querySelector(toTag) as any;\n if (toLc && toEl?._isCached) {\n toLc.willEnter.update((n) => n + 1);\n requestAnimationFrame(() => toLc.didEnter.update((n) => n + 1));\n }\n }\n\n // -------------------------------------------------------------------------\n // Helpers\n // -------------------------------------------------------------------------\n\n private _matchRoute(path: string): RouteDefinition | undefined {\n const exact = this.routes.find((r) => r.path === path);\n if (exact) return exact;\n for (const route of this.routes) {\n const re = new RegExp(`^${route.path.replace(/:[^/]+/g, \"([^/]+)\")}$`);\n if (re.test(path)) return route;\n }\n return this.routes.find((r) => r.path === \"*\");\n }\n\n private _extractParams(routePath: string, realPath: string): Record<string, string> {\n const params: Record<string, string> = {};\n const rP = routePath.split(\"/\");\n const uP = realPath.split(\"/\");\n for (let i = 0; i < rP.length; i++) {\n if (rP[i].startsWith(\":\")) params[rP[i].slice(1)] = uP[i] ?? \"\";\n }\n return params;\n }\n\n private _pathToTag(path: string): string {\n if (!path || path === \"/\") return \"nix-page-home\";\n const clean = path\n .replace(/\\/:?[^/]+/g, (m) => \"-\" + m.replace(/\\//g, \"\").replace(/:/g, \"\"))\n .replace(/^\\//, \"\")\n .replace(/\\//g, \"-\");\n return `nix-page-${clean}`;\n }\n\n // -------------------------------------------------------------------------\n // Registro de Custom Elements\n // -------------------------------------------------------------------------\n\n private _registerCustomElements(): void {\n for (const route of this.routes) {\n if (route.path === \"*\") continue;\n\n const tag = this._pathToTag(route.path);\n const routeDef = route;\n const self = this;\n\n if (customElements.get(tag)) continue;\n\n customElements.define(tag, class extends HTMLElement {\n private _cleanup: (() => void) | null = null;\n\n connectedCallback(): void {\n const params = self._extractParams(routeDef.path, location.pathname);\n const lc = createPageLifecycle();\n\n _lifecycleRegistry.set(tag, lc);\n this.classList.add(\"ion-page\");\n\n const pageNode = routeDef.component({ lc, params });\n\n if (\"render\" in pageNode && typeof (pageNode as NixComponent).render === \"function\") {\n const comp = pageNode as NixComponent;\n comp.onInit?.();\n const renderCleanup = comp.render()._render(this, null);\n const mountRet = comp.onMount?.();\n this._cleanup = () => {\n comp.onUnmount?.();\n if (typeof mountRet === \"function\") mountRet();\n renderCleanup();\n };\n } else {\n this._cleanup = (pageNode as NixTemplate)._render(this, null);\n }\n\n // Disparar willEnter/didEnter aquí — el lc ya está registrado\n // y el componente ya está inicializado con sus watchers\n lc.willEnter.update((n) => n + 1);\n requestAnimationFrame(() => {\n lc.didEnter.update((n) => n + 1);\n // Marcar como cacheado para que ionRouteDidChange pueda\n // disparar willEnter en visitas subsiguientes\n (this as any)._isCached = true;\n });\n }\n\n disconnectedCallback(): void {\n // Disparar willLeave/didLeave al salir\n const lc = _lifecycleRegistry.get(tag);\n if (lc) {\n lc.willLeave.update((n) => n + 1);\n lc.didLeave.update((n) => n + 1);\n }\n this._cleanup?.();\n this._cleanup = null;\n _lifecycleRegistry.delete(tag);\n }\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Render — crea ion-router + ion-router-outlet como elementos DOM reales\n // -------------------------------------------------------------------------\n\n override render(): NixTemplate {\n const self = this;\n return {\n __isNixTemplate: true as const,\n mount(container: Element | string): { unmount(): void } {\n const el = typeof container === \"string\"\n ? (document.querySelector(container) as Element)\n : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n // ion-router con las rutas declarativas\n const routerEl = document.createElement(\"ion-router\");\n routerEl.setAttribute(\"use-hash\", \"false\");\n routerEl.innerHTML = self.routes\n .filter((r) => r.path !== \"*\")\n .map((r) => `<ion-route url=\"${r.path}\" component=\"${self._pathToTag(r.path)}\"></ion-route>`)\n .join(\"\");\n\n const outletEl = document.createElement(\"ion-router-outlet\");\n\n parent.insertBefore(routerEl, before);\n parent.insertBefore(outletEl, before);\n\n self.ionRouterEl = routerEl;\n\n return () => { routerEl.remove(); outletEl.remove(); };\n },\n };\n }\n\n override onUnmount(): void {\n _lifecycleRegistry.clear();\n _router = null;\n }\n}"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ import{NixComponent as e,signal as t,watch as n}from"@deijose/nix-js";function r(){return{willEnter:t(0),didEnter:t(0),willLeave:t(0),didLeave:t(0)}}var i=class extends e{__lc;constructor(e){super(),this.__lc=e}onInit(){let e=this.__lc;this.ionViewWillEnter&&n(e.willEnter,this.ionViewWillEnter.bind(this)),this.ionViewDidEnter&&n(e.didEnter,this.ionViewDidEnter.bind(this)),this.ionViewWillLeave&&n(e.willLeave,this.ionViewWillLeave.bind(this)),this.ionViewDidLeave&&n(e.didLeave,this.ionViewDidLeave.bind(this))}};function a(e,t){n(e.willEnter,t)}function o(e,t){n(e.didEnter,t)}function s(e,t){n(e.willLeave,t)}function c(e,t){n(e.didLeave,t)}var l=null;function u(){if(!l)throw Error("[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet");return l}function d(e){return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-back-button");i.setAttribute("default-href",e??"/");let a=t=>{t.stopPropagation(),t.preventDefault(),l?.canGoBack.value?l.back():e&&l?.navigate(e)};return i.addEventListener("click",a),t.insertBefore(i,n),()=>{i.removeEventListener("click",a),i.remove()}}}}var f=new Map,p=class extends e{routes;ionRouterEl=null;_currentPath=t(location.pathname);_canGoBack=t(!1);_params=t({});constructor(e){super(),this.routes=e,l={navigate:e=>this.navigate(e),replace:e=>this.replace(e),back:()=>this.back(),canGoBack:this._canGoBack,params:this._params,path:this._currentPath},this._registerCustomElements()}onInit(){window.addEventListener("popstate",this._handlePopState)}onMount(){this.ionRouterEl=document.querySelector("ion-router");let e=e=>{let{to:t,from:n}=e.detail;this._handleWillChange(t,n??"")},t=e=>{let{to:t}=e.detail;this._handleDidChange(t)},n=e=>{e.detail.register(100,e=>{this._canGoBack.value?this.back():e()})};return window.addEventListener("ionRouteWillChange",e),window.addEventListener("ionRouteDidChange",t),document.addEventListener("ionBackButton",n),()=>{window.removeEventListener("popstate",this._handlePopState),window.removeEventListener("ionRouteWillChange",e),window.removeEventListener("ionRouteDidChange",t),document.removeEventListener("ionBackButton",n),l=null}}navigate(e){e!==location.pathname&&this.ionRouterEl?.push(e,"forward")}replace(e){this.ionRouterEl?.push(e,"root")}back(){this.ionRouterEl?.back()}_handlePopState=()=>{this._handleWillChange(location.pathname,this._currentPath.peek())};_handleWillChange(e,t){let n=this._matchRoute(e);if(!n)return;if(n.beforeEnter){let i=n.beforeEnter(e,t);if(!1===i)return void this.back();if("string"==typeof i)return void this.navigate(i)}this._currentPath.value=e,this._params.value=this._extractParams(n.path,e);let i=this._pathToTag(t);f.get(i)?.willLeave.update(e=>e+1)}_handleDidChange(e){let t=document.querySelector("ion-router-outlet");this._canGoBack.value=t?.canGoBack?.()??!1,f.forEach((t,n)=>{n!==this._pathToTag(e)&&t.didLeave.update(e=>e+1)});let n=this._pathToTag(e),i=f.get(n),a=document.querySelector(n);i&&a?._isCached&&(i.willEnter.update(e=>e+1),requestAnimationFrame(()=>i.didEnter.update(e=>e+1)))}_matchRoute(e){let t=this.routes.find(t=>t.path===e);if(t)return t;for(let t of this.routes)if(RegExp(`^${t.path.replace(/:[^/]+/g,"([^/]+)")}$`).test(e))return t;return this.routes.find(e=>"*"===e.path)}_extractParams(e,t){let n={},i=e.split("/"),a=t.split("/");for(let e=0;e<i.length;e++)i[e].startsWith(":")&&(n[i[e].slice(1)]=a[e]??"");return n}_pathToTag(e){return e&&"/"!==e?`nix-page-${e.replace(/\/:?[^/]+/g,e=>"-"+e.replace(/\//g,"").replace(/:/g,"")).replace(/^\//,"").replace(/\//g,"-")}`:"nix-page-home"}_registerCustomElements(){for(let e of this.routes){if("*"===e.path)continue;let t=this._pathToTag(e.path),n=e,i=this;customElements.get(t)||customElements.define(t,class extends HTMLElement{_cleanup=null;connectedCallback(){let e=i._extractParams(n.path,location.pathname),a=r();f.set(t,a),this.classList.add("ion-page");let o=n.component({lc:a,params:e});if("render"in o&&"function"==typeof o.render){let e=o;e.onInit?.();let t=e.render()._render(this,null),n=e.onMount?.();this._cleanup=()=>{e.onUnmount?.(),"function"==typeof n&&n(),t()}}else this._cleanup=o._render(this,null);a.willEnter.update(e=>e+1),requestAnimationFrame(()=>{a.didEnter.update(e=>e+1),this._isCached=!0})}disconnectedCallback(){let e=f.get(t);e&&(e.willLeave.update(e=>e+1),e.didLeave.update(e=>e+1)),this._cleanup?.(),this._cleanup=null,f.delete(t)}})}}render(){let e=this;return{__isNixTemplate:!0,mount(e){let t="string"==typeof e?document.querySelector(e):e;return{unmount:this._render(t,null)}},_render(t,n){let i=document.createElement("ion-router");i.setAttribute("use-hash","false"),i.innerHTML=e.routes.filter(e=>"*"!==e.path).map(t=>`<ion-route url="${t.path}" component="${e._pathToTag(t.path)}"></ion-route>`).join("");let a=document.createElement("ion-router-outlet");return t.insertBefore(i,n),t.insertBefore(a,n),e.ionRouterEl=i,()=>{i.remove(),a.remove()}}}}onUnmount(){f.clear(),l=null}};export{d as IonBackButton,i as IonPage,p as IonRouterOutlet,r as createPageLifecycle,o as useIonViewDidEnter,c as useIonViewDidLeave,a as useIonViewWillEnter,s as useIonViewWillLeave,u as useRouter};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nix-ionic.js","names":[],"sources":["../../src/lifecycle.ts","../../src/IonRouterOutlet.ts"],"sourcesContent":["/**\n * ionic-nix/lifecycle.ts\n *\n * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:\n * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave\n *\n * Cómo funciona (sin provide/inject):\n * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.\n * 2. Lo pasa directamente al factory de la ruta como argumento.\n * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.\n * 4. IonPage/composables registran watchers sobre las señales del lc.\n * 5. Cuando el router navega, incrementa las señales → watchers se disparan.\n */\n\nimport { signal, watch } from \"@deijose/nix-js\";\nimport type { Signal } from \"@deijose/nix-js\";\nimport { NixComponent } from \"@deijose/nix-js\";\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageLifecycle {\n willEnter: Signal<number>;\n didEnter: Signal<number>;\n willLeave: Signal<number>;\n didLeave: Signal<number>;\n}\n\n/** Crea un nuevo PageLifecycle con señales en 0. */\nexport function createPageLifecycle(): PageLifecycle {\n return {\n willEnter: signal(0),\n didEnter: signal(0),\n willLeave: signal(0),\n didLeave: signal(0),\n };\n}\n\n// --------------------------------------------------------------------------\n// IonPage — clase base para páginas con hooks de navegación\n// --------------------------------------------------------------------------\n//\n// Uso:\n// class HomePage extends IonPage {\n// constructor(lc: PageLifecycle) { super(lc); }\n//\n// ionViewWillEnter() { /* fetch de datos frescos */ }\n// render() { return html`...`; }\n// }\n\nexport abstract class IonPage extends NixComponent {\n private __lc: PageLifecycle;\n\n constructor(lc: PageLifecycle) {\n super();\n this.__lc = lc;\n }\n\n override onInit(): void {\n const lc = this.__lc;\n // watch no corre en init (immediate: false), así que ionViewWillEnter\n // solo se llama cuando el outlet incrementa la señal, no al construir.\n if (this.ionViewWillEnter) watch(lc.willEnter, this.ionViewWillEnter.bind(this));\n if (this.ionViewDidEnter) watch(lc.didEnter, this.ionViewDidEnter.bind(this));\n if (this.ionViewWillLeave) watch(lc.willLeave, this.ionViewWillLeave.bind(this));\n if (this.ionViewDidLeave) watch(lc.didLeave, this.ionViewDidLeave.bind(this));\n }\n\n ionViewWillEnter?(): void;\n ionViewDidEnter?(): void;\n ionViewWillLeave?(): void;\n ionViewDidLeave?(): void;\n}\n\n// --------------------------------------------------------------------------\n// Composables para function components\n// --------------------------------------------------------------------------\n//\n// Uso:\n// function ProfilePage(lc: PageLifecycle): NixTemplate {\n// useIonViewWillEnter(lc, () => { /* fetch */ });\n// return html`...`;\n// }\n\nexport function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willEnter, fn);\n}\n\nexport function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didEnter, fn);\n}\n\nexport function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.willLeave, fn);\n}\n\nexport function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void {\n watch(lc.didLeave, fn);\n}","/**\n * ionic-nix/IonRouterOutlet.ts\n *\n * Bridge entre Nix.js e ion-router (API pública oficial para vanilla JS).\n *\n * Arquitectura:\n * 1. Por cada ruta registramos un Custom Element (nix-page-home, etc.)\n * 2. ion-router activa el custom element según la URL\n * 3. ion-router-outlet gestiona: caché, animaciones, back button, swipe back\n * 4. connectedCallback del custom element monta el componente Nix adentro\n * 5. ionRouteWillChange / ionRouteDidChange disparan el ciclo de vida Nix\n */\n\nimport { NixComponent, signal } from \"@deijose/nix-js\";\nimport type { NixTemplate, Signal } from \"@deijose/nix-js\";\nimport { createPageLifecycle, type PageLifecycle } from \"./lifecycle\";\n\n// --------------------------------------------------------------------------\n// Router store singleton\n// --------------------------------------------------------------------------\n\ninterface RouterStore {\n navigate: (path: string) => void;\n replace: (path: string) => void;\n back: () => void;\n canGoBack: Signal<boolean>;\n params: Signal<Record<string, string>>;\n path: Signal<string>;\n}\n\nlet _router: RouterStore | null = null;\n\nexport function useRouter(): RouterStore {\n if (!_router) throw new Error(\"[nix-ionic] useRouter() llamado antes de montar IonRouterOutlet\");\n return _router;\n}\n\n// --------------------------------------------------------------------------\n// IonBackButton — intercepta el click antes del shadow DOM\n// --------------------------------------------------------------------------\n\nexport function IonBackButton(defaultHref?: string): NixTemplate {\n return {\n __isNixTemplate: true as const,\n mount(container) {\n const el = typeof container === \"string\" ? document.querySelector(container)! : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n const btn = document.createElement(\"ion-back-button\") as HTMLElement;\n btn.setAttribute(\"default-href\", defaultHref ?? \"/\");\n const handler = (e: Event) => {\n e.stopPropagation();\n e.preventDefault();\n if (_router?.canGoBack.value) _router.back();\n else if (defaultHref) _router?.navigate(defaultHref);\n };\n btn.addEventListener(\"click\", handler);\n parent.insertBefore(btn, before);\n return () => {\n btn.removeEventListener(\"click\", handler);\n btn.remove();\n };\n },\n };\n}\n\n// --------------------------------------------------------------------------\n// Tipos públicos\n// --------------------------------------------------------------------------\n\nexport interface PageContext {\n lc: PageLifecycle;\n params: Record<string, string>;\n}\n\nexport interface RouteDefinition {\n path: string;\n component: (ctx: PageContext) => NixComponent | NixTemplate;\n beforeEnter?: (to: string, from: string) => boolean | string | void;\n}\n\n// --------------------------------------------------------------------------\n// Mapa global de lifecycles activos por tag\n// --------------------------------------------------------------------------\n\nconst _lifecycleRegistry = new Map<string, PageLifecycle>();\n\n// --------------------------------------------------------------------------\n// IonRouterOutlet\n// --------------------------------------------------------------------------\n\nexport class IonRouterOutlet extends NixComponent {\n private routes: RouteDefinition[];\n private ionRouterEl: HTMLElement | null = null;\n\n private _currentPath = signal(location.pathname);\n private _canGoBack = signal(false);\n private _params = signal<Record<string, string>>({});\n\n constructor(routes: RouteDefinition[]) {\n super();\n this.routes = routes;\n\n // Store disponible desde el constructor — antes que cualquier\n // connectedCallback de custom element\n _router = {\n navigate: (path) => this.navigate(path),\n replace: (path) => this.replace(path),\n back: () => this.back(),\n canGoBack: this._canGoBack,\n params: this._params,\n path: this._currentPath,\n };\n\n this._registerCustomElements();\n }\n\n override onInit(): void {\n window.addEventListener(\"popstate\", this._handlePopState);\n }\n\n override onMount(): () => void {\n this.ionRouterEl = document.querySelector(\"ion-router\");\n\n const onWillChange = (ev: Event) => {\n const { to, from } = (ev as CustomEvent).detail as { to: string; from: string };\n this._handleWillChange(to, from ?? \"\");\n };\n const onDidChange = (ev: Event) => {\n const { to } = (ev as CustomEvent).detail as { to: string };\n this._handleDidChange(to);\n };\n const onIonBack = (ev: Event) => {\n (ev as CustomEvent).detail.register(100, (next: () => void) => {\n if (this._canGoBack.value) this.back(); else next();\n });\n };\n\n window.addEventListener(\"ionRouteWillChange\", onWillChange);\n window.addEventListener(\"ionRouteDidChange\", onDidChange);\n document.addEventListener(\"ionBackButton\", onIonBack);\n\n return () => {\n window.removeEventListener(\"popstate\", this._handlePopState);\n window.removeEventListener(\"ionRouteWillChange\", onWillChange);\n window.removeEventListener(\"ionRouteDidChange\", onDidChange);\n document.removeEventListener(\"ionBackButton\", onIonBack);\n _router = null;\n };\n }\n\n // -------------------------------------------------------------------------\n // API pública\n // -------------------------------------------------------------------------\n\n navigate(path: string): void {\n if (path === location.pathname) return;\n (this.ionRouterEl as any)?.push(path, \"forward\");\n }\n\n replace(path: string): void {\n (this.ionRouterEl as any)?.push(path, \"root\");\n }\n\n back(): void {\n (this.ionRouterEl as any)?.back();\n }\n\n // -------------------------------------------------------------------------\n // Handlers privados\n // -------------------------------------------------------------------------\n\n private _handlePopState = (): void => {\n // El browser navegó con sus propios botones — sincronizar\n this._handleWillChange(location.pathname, this._currentPath.peek());\n };\n\n private _handleWillChange(to: string, from: string): void {\n const route = this._matchRoute(to);\n if (!route) return;\n\n // Guards\n if (route.beforeEnter) {\n const result = route.beforeEnter(to, from);\n if (result === false) { this.back(); return; }\n if (typeof result === \"string\") { this.navigate(result); return; }\n }\n\n this._currentPath.value = to;\n this._params.value = this._extractParams(route.path, to);\n\n // willLeave en la vista que sale (ya tiene lc registrado)\n const fromTag = this._pathToTag(from);\n _lifecycleRegistry.get(fromTag)?.willLeave.update((n) => n + 1);\n }\n\n private _handleDidChange(to: string): void {\n const outlet = document.querySelector(\"ion-router-outlet\") as any;\n this._canGoBack.value = outlet?.canGoBack?.() ?? false;\n\n // didLeave en la vista que salió\n _lifecycleRegistry.forEach((lc, tag) => {\n if (tag !== this._pathToTag(to)) lc.didLeave.update((n) => n + 1);\n });\n\n // Para vistas CACHEADAS (ya existían en el DOM, connectedCallback no se llama de nuevo)\n // disparamos willEnter/didEnter aquí.\n // Para vistas NUEVAS, connectedCallback ya lo hizo — disparar de nuevo haría doble.\n // Distinguimos: si el tag ya estaba en el registry ANTES de este didChange,\n // es una vista cacheada. Usamos un flag en el custom element.\n const toTag = this._pathToTag(to);\n const toLc = _lifecycleRegistry.get(toTag);\n const toEl = document.querySelector(toTag) as any;\n if (toLc && toEl?._isCached) {\n toLc.willEnter.update((n) => n + 1);\n requestAnimationFrame(() => toLc.didEnter.update((n) => n + 1));\n }\n }\n\n // -------------------------------------------------------------------------\n // Helpers\n // -------------------------------------------------------------------------\n\n private _matchRoute(path: string): RouteDefinition | undefined {\n const exact = this.routes.find((r) => r.path === path);\n if (exact) return exact;\n for (const route of this.routes) {\n const re = new RegExp(`^${route.path.replace(/:[^/]+/g, \"([^/]+)\")}$`);\n if (re.test(path)) return route;\n }\n return this.routes.find((r) => r.path === \"*\");\n }\n\n private _extractParams(routePath: string, realPath: string): Record<string, string> {\n const params: Record<string, string> = {};\n const rP = routePath.split(\"/\");\n const uP = realPath.split(\"/\");\n for (let i = 0; i < rP.length; i++) {\n if (rP[i].startsWith(\":\")) params[rP[i].slice(1)] = uP[i] ?? \"\";\n }\n return params;\n }\n\n private _pathToTag(path: string): string {\n if (!path || path === \"/\") return \"nix-page-home\";\n const clean = path\n .replace(/\\/:?[^/]+/g, (m) => \"-\" + m.replace(/\\//g, \"\").replace(/:/g, \"\"))\n .replace(/^\\//, \"\")\n .replace(/\\//g, \"-\");\n return `nix-page-${clean}`;\n }\n\n // -------------------------------------------------------------------------\n // Registro de Custom Elements\n // -------------------------------------------------------------------------\n\n private _registerCustomElements(): void {\n for (const route of this.routes) {\n if (route.path === \"*\") continue;\n\n const tag = this._pathToTag(route.path);\n const routeDef = route;\n const self = this;\n\n if (customElements.get(tag)) continue;\n\n customElements.define(tag, class extends HTMLElement {\n private _cleanup: (() => void) | null = null;\n\n connectedCallback(): void {\n const params = self._extractParams(routeDef.path, location.pathname);\n const lc = createPageLifecycle();\n\n _lifecycleRegistry.set(tag, lc);\n this.classList.add(\"ion-page\");\n\n const pageNode = routeDef.component({ lc, params });\n\n if (\"render\" in pageNode && typeof (pageNode as NixComponent).render === \"function\") {\n const comp = pageNode as NixComponent;\n comp.onInit?.();\n const renderCleanup = comp.render()._render(this, null);\n const mountRet = comp.onMount?.();\n this._cleanup = () => {\n comp.onUnmount?.();\n if (typeof mountRet === \"function\") mountRet();\n renderCleanup();\n };\n } else {\n this._cleanup = (pageNode as NixTemplate)._render(this, null);\n }\n\n // Disparar willEnter/didEnter aquí — el lc ya está registrado\n // y el componente ya está inicializado con sus watchers\n lc.willEnter.update((n) => n + 1);\n requestAnimationFrame(() => {\n lc.didEnter.update((n) => n + 1);\n // Marcar como cacheado para que ionRouteDidChange pueda\n // disparar willEnter en visitas subsiguientes\n (this as any)._isCached = true;\n });\n }\n\n disconnectedCallback(): void {\n // Disparar willLeave/didLeave al salir\n const lc = _lifecycleRegistry.get(tag);\n if (lc) {\n lc.willLeave.update((n) => n + 1);\n lc.didLeave.update((n) => n + 1);\n }\n this._cleanup?.();\n this._cleanup = null;\n _lifecycleRegistry.delete(tag);\n }\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Render — crea ion-router + ion-router-outlet como elementos DOM reales\n // -------------------------------------------------------------------------\n\n override render(): NixTemplate {\n const self = this;\n return {\n __isNixTemplate: true as const,\n mount(container: Element | string): { unmount(): void } {\n const el = typeof container === \"string\"\n ? (document.querySelector(container) as Element)\n : container;\n const cleanup = this._render(el, null);\n return { unmount: cleanup };\n },\n _render(parent: Node, before: Node | null): () => void {\n // ion-router con las rutas declarativas\n const routerEl = document.createElement(\"ion-router\");\n routerEl.setAttribute(\"use-hash\", \"false\");\n routerEl.innerHTML = self.routes\n .filter((r) => r.path !== \"*\")\n .map((r) => `<ion-route url=\"${r.path}\" component=\"${self._pathToTag(r.path)}\"></ion-route>`)\n .join(\"\");\n\n const outletEl = document.createElement(\"ion-router-outlet\");\n\n parent.insertBefore(routerEl, before);\n parent.insertBefore(outletEl, before);\n\n self.ionRouterEl = routerEl;\n\n return () => { routerEl.remove(); outletEl.remove(); };\n },\n };\n }\n\n override onUnmount(): void {\n _lifecycleRegistry.clear();\n _router = null;\n }\n}"],"mappings":";;AA8BA,SAAgB,IAAqC;AACnD,QAAO;EACL,WAAW,EAAO,EAAE;EACpB,UAAW,EAAO,EAAE;EACpB,WAAW,EAAO,EAAE;EACpB,UAAW,EAAO,EAAE;EACrB;;AAeH,IAAsB,IAAtB,cAAsC,EAAa;CACjD;CAEA,YAAY,GAAmB;AAE7B,EADA,OAAO,EACP,KAAK,OAAO;;CAGd,SAAwB;EACtB,IAAM,IAAK,KAAK;AAMhB,EAHI,KAAK,oBAAkB,EAAM,EAAG,WAAW,KAAK,iBAAiB,KAAK,KAAK,CAAC,EAC5E,KAAK,mBAAkB,EAAM,EAAG,UAAW,KAAK,gBAAgB,KAAK,KAAK,CAAC,EAC3E,KAAK,oBAAkB,EAAM,EAAG,WAAW,KAAK,iBAAiB,KAAK,KAAK,CAAC,EAC5E,KAAK,mBAAkB,EAAM,EAAG,UAAW,KAAK,gBAAgB,KAAK,KAAK,CAAC;;;AAmBnF,SAAgB,EAAoB,GAAmB,GAAsB;AAC3E,GAAM,EAAG,WAAW,EAAG;;AAGzB,SAAgB,EAAmB,GAAmB,GAAsB;AAC1E,GAAM,EAAG,UAAU,EAAG;;AAGxB,SAAgB,EAAoB,GAAmB,GAAsB;AAC3E,GAAM,EAAG,WAAW,EAAG;;AAGzB,SAAgB,EAAmB,GAAmB,GAAsB;AAC1E,GAAM,EAAG,UAAU,EAAG;;;;ACpExB,IAAI,IAA8B;AAElC,SAAgB,IAAyB;AACvC,KAAI,CAAC,EAAS,OAAU,MAAM,kEAAkE;AAChG,QAAO;;AAOT,SAAgB,EAAc,GAAmC;AAC/D,QAAO;EACL,iBAAiB;EACjB,MAAM,GAAW;GACf,IAAM,IAAK,OAAO,KAAc,WAAW,SAAS,cAAc,EAAU,GAAI;AAEhF,UAAO,EAAE,SADO,KAAK,QAAQ,GAAI,KAAK,EACX;;EAE7B,QAAQ,GAAc,GAAiC;GACrD,IAAM,IAAM,SAAS,cAAc,kBAAkB;AACrD,KAAI,aAAa,gBAAgB,KAAe,IAAI;GACpD,IAAM,KAAW,MAAa;AAG5B,IAFA,EAAE,iBAAiB,EACnB,EAAE,gBAAgB,EACd,GAAS,UAAU,QAAO,EAAQ,MAAM,GACnC,KAAa,GAAS,SAAS,EAAY;;AAItD,UAFA,EAAI,iBAAiB,SAAS,EAAQ,EACtC,EAAO,aAAa,GAAK,EAAO,QACnB;AAEX,IADA,EAAI,oBAAoB,SAAS,EAAQ,EACzC,EAAI,QAAQ;;;EAGjB;;AAsBH,IAAM,oBAAqB,IAAI,KAA4B,EAM9C,IAAb,cAAqC,EAAa;CAChD;CACA,cAA0C;CAE1C,eAAuB,EAAO,SAAS,SAAS;CAChD,aAAuB,EAAO,GAAM;CACpC,UAAuB,EAA+B,EAAE,CAAC;CAEzD,YAAY,GAA2B;AAerC,EAdA,OAAO,EACP,KAAK,SAAS,GAId,IAAU;GACR,WAAY,MAAS,KAAK,SAAS,EAAK;GACxC,UAAY,MAAS,KAAK,QAAQ,EAAK;GACvC,YAAqB,KAAK,MAAM;GAChC,WAAW,KAAK;GAChB,QAAW,KAAK;GAChB,MAAW,KAAK;GACjB,EAED,KAAK,yBAAyB;;CAGhC,SAAwB;AACtB,SAAO,iBAAiB,YAAY,KAAK,gBAAgB;;CAG3D,UAA+B;AAC7B,OAAK,cAAc,SAAS,cAAc,aAAa;EAEvD,IAAM,KAAgB,MAAc;GAClC,IAAM,EAAE,OAAI,YAAU,EAAmB;AACzC,QAAK,kBAAkB,GAAI,KAAQ,GAAG;KAElC,KAAe,MAAc;GACjC,IAAM,EAAE,UAAQ,EAAmB;AACnC,QAAK,iBAAiB,EAAG;KAErB,KAAa,MAAc;AAC9B,KAAmB,OAAO,SAAS,MAAM,MAAqB;AAC7D,IAAI,KAAK,WAAW,QAAO,KAAK,MAAM,GAAO,GAAM;KACnD;;AAOJ,SAJA,OAAO,iBAAiB,sBAAsB,EAAa,EAC3D,OAAO,iBAAiB,qBAAsB,EAAY,EAC1D,SAAS,iBAAiB,iBAAoB,EAAU,QAE3C;AAKX,GAJA,OAAO,oBAAoB,YAAqB,KAAK,gBAAgB,EACrE,OAAO,oBAAoB,sBAAsB,EAAa,EAC9D,OAAO,oBAAoB,qBAAsB,EAAY,EAC7D,SAAS,oBAAoB,iBAAoB,EAAU,EAC3D,IAAU;;;CAQd,SAAS,GAAoB;AACvB,QAAS,SAAS,YACrB,KAAK,aAAqB,KAAK,GAAM,UAAU;;CAGlD,QAAQ,GAAoB;AACzB,OAAK,aAAqB,KAAK,GAAM,OAAO;;CAG/C,OAAa;AACV,OAAK,aAAqB,MAAM;;CAOnC,wBAAsC;AAEpC,OAAK,kBAAkB,SAAS,UAAU,KAAK,aAAa,MAAM,CAAC;;CAGrE,kBAA0B,GAAY,GAAoB;EACxD,IAAM,IAAQ,KAAK,YAAY,EAAG;AAClC,MAAI,CAAC,EAAO;AAGZ,MAAI,EAAM,aAAa;GACrB,IAAM,IAAS,EAAM,YAAY,GAAI,EAAK;AAC1C,OAAI,MAAW,IAAO;AAAE,SAAK,MAAM;AAAE;;AACrC,OAAI,OAAO,KAAW,UAAU;AAAE,SAAK,SAAS,EAAO;AAAE;;;AAI3D,EADA,KAAK,aAAa,QAAQ,GAC1B,KAAK,QAAQ,QAAa,KAAK,eAAe,EAAM,MAAM,EAAG;EAG7D,IAAM,IAAU,KAAK,WAAW,EAAK;AACrC,IAAmB,IAAI,EAAQ,EAAE,UAAU,QAAQ,MAAM,IAAI,EAAE;;CAGjE,iBAAyB,GAAkB;EACzC,IAAM,IAAS,SAAS,cAAc,oBAAoB;AAI1D,EAHA,KAAK,WAAW,QAAQ,GAAQ,aAAa,IAAI,IAGjD,EAAmB,SAAS,GAAI,MAAQ;AACtC,GAAI,MAAQ,KAAK,WAAW,EAAG,IAAE,EAAG,SAAS,QAAQ,MAAM,IAAI,EAAE;IACjE;EAOF,IAAM,IAAS,KAAK,WAAW,EAAG,EAC5B,IAAS,EAAmB,IAAI,EAAM,EACtC,IAAS,SAAS,cAAc,EAAM;AAC5C,EAAI,KAAQ,GAAM,cAChB,EAAK,UAAU,QAAQ,MAAM,IAAI,EAAE,EACnC,4BAA4B,EAAK,SAAS,QAAQ,MAAM,IAAI,EAAE,CAAC;;CAQnE,YAAoB,GAA2C;EAC7D,IAAM,IAAQ,KAAK,OAAO,MAAM,MAAM,EAAE,SAAS,EAAK;AACtD,MAAI,EAAO,QAAO;AAClB,OAAK,IAAM,KAAS,KAAK,OAEvB,KADe,OAAO,IAAI,EAAM,KAAK,QAAQ,WAAW,UAAU,CAAC,GAAG,CAC/D,KAAK,EAAK,CAAE,QAAO;AAE5B,SAAO,KAAK,OAAO,MAAM,MAAM,EAAE,SAAS,IAAI;;CAGhD,eAAuB,GAAmB,GAA0C;EAClF,IAAM,IAAiC,EAAE,EACnC,IAAK,EAAU,MAAM,IAAI,EACzB,IAAK,EAAS,MAAM,IAAI;AAC9B,OAAK,IAAI,IAAI,GAAG,IAAI,EAAG,QAAQ,IAC7B,CAAI,EAAG,GAAG,WAAW,IAAI,KAAE,EAAO,EAAG,GAAG,MAAM,EAAE,IAAI,EAAG,MAAM;AAE/D,SAAO;;CAGT,WAAmB,GAAsB;AAMvC,SALI,CAAC,KAAQ,MAAS,MAAY,kBAK3B,YAJO,EACX,QAAQ,eAAe,MAAM,MAAM,EAAE,QAAQ,OAAO,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,CAC1E,QAAQ,OAAO,GAAG,CAClB,QAAQ,OAAO,IAAI;;CAQxB,0BAAwC;AACtC,OAAK,IAAM,KAAS,KAAK,QAAQ;AAC/B,OAAI,EAAM,SAAS,IAAK;GAExB,IAAM,IAAW,KAAK,WAAW,EAAM,KAAK,EACtC,IAAW,GACX,IAAW;AAEb,kBAAe,IAAI,EAAI,IAE3B,eAAe,OAAO,GAAK,cAAc,YAAY;IACnD,WAAwC;IAExC,oBAA0B;KACxB,IAAM,IAAS,EAAK,eAAe,EAAS,MAAM,SAAS,SAAS,EAC9D,IAAS,GAAqB;AAGpC,KADA,EAAmB,IAAI,GAAK,EAAG,EAC/B,KAAK,UAAU,IAAI,WAAW;KAE9B,IAAM,IAAW,EAAS,UAAU;MAAE;MAAI;MAAQ,CAAC;AAEnD,SAAI,YAAY,KAAY,OAAQ,EAA0B,UAAW,YAAY;MACnF,IAAM,IAAO;AACb,QAAK,UAAU;MACf,IAAM,IAAgB,EAAK,QAAQ,CAAC,QAAQ,MAAM,KAAK,EACjD,IAAgB,EAAK,WAAW;AACtC,WAAK,iBAAiB;AAGpB,OAFA,EAAK,aAAa,EACd,OAAO,KAAa,cAAY,GAAU,EAC9C,GAAe;;WAGjB,MAAK,WAAY,EAAyB,QAAQ,MAAM,KAAK;AAM/D,KADA,EAAG,UAAU,QAAQ,MAAM,IAAI,EAAE,EACjC,4BAA4B;AAIzB,MAHD,EAAG,SAAS,QAAQ,MAAM,IAAI,EAAE,EAG/B,KAAa,YAAY;OAC1B;;IAGJ,uBAA6B;KAE3B,IAAM,IAAK,EAAmB,IAAI,EAAI;AAOtC,KANI,MACF,EAAG,UAAU,QAAQ,MAAM,IAAI,EAAE,EACjC,EAAG,SAAS,QAAQ,MAAM,IAAI,EAAE,GAElC,KAAK,YAAY,EACjB,KAAK,WAAW,MAChB,EAAmB,OAAO,EAAI;;KAEhC;;;CAQN,SAA+B;EAC7B,IAAM,IAAO;AACb,SAAO;GACL,iBAAiB;GACjB,MAAM,GAAkD;IACtD,IAAM,IAAK,OAAO,KAAc,WAC3B,SAAS,cAAc,EAAU,GAClC;AAEJ,WAAO,EAAE,SADO,KAAK,QAAQ,GAAI,KAAK,EACX;;GAE7B,QAAQ,GAAc,GAAiC;IAErD,IAAM,IAAW,SAAS,cAAc,aAAa;AAErD,IADA,EAAS,aAAa,YAAY,QAAQ,EAC1C,EAAS,YAAY,EAAK,OACvB,QAAQ,MAAM,EAAE,SAAS,IAAI,CAC7B,KAAK,MAAM,mBAAmB,EAAE,KAAK,eAAe,EAAK,WAAW,EAAE,KAAK,CAAC,gBAAgB,CAC5F,KAAK,GAAG;IAEX,IAAM,IAAW,SAAS,cAAc,oBAAoB;AAO5D,WALA,EAAO,aAAa,GAAU,EAAO,EACrC,EAAO,aAAa,GAAU,EAAO,EAErC,EAAK,cAAc,SAEN;AAAqB,KAAnB,EAAS,QAAQ,EAAE,EAAS,QAAQ;;;GAEtD;;CAGH,YAA2B;AAEzB,EADA,EAAmB,OAAO,EAC1B,IAAU"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ionic-nix/lifecycle.ts
3
+ *
4
+ * Sistema de ciclo de vida de navegación, análogo a los hooks de Ionic:
5
+ * ionViewWillEnter / ionViewDidEnter / ionViewWillLeave / ionViewDidLeave
6
+ *
7
+ * Cómo funciona (sin provide/inject):
8
+ * 1. IonRouterOutlet crea un `PageLifecycle` por cada ruta.
9
+ * 2. Lo pasa directamente al factory de la ruta como argumento.
10
+ * 3. El factory llama a `new MiPagina(lc)` o `MiPagina(lc)`.
11
+ * 4. IonPage/composables registran watchers sobre las señales del lc.
12
+ * 5. Cuando el router navega, incrementa las señales → watchers se disparan.
13
+ */
14
+ import type { Signal } from "@deijose/nix-js";
15
+ import { NixComponent } from "@deijose/nix-js";
16
+ export interface PageLifecycle {
17
+ willEnter: Signal<number>;
18
+ didEnter: Signal<number>;
19
+ willLeave: Signal<number>;
20
+ didLeave: Signal<number>;
21
+ }
22
+ /** Crea un nuevo PageLifecycle con señales en 0. */
23
+ export declare function createPageLifecycle(): PageLifecycle;
24
+ export declare abstract class IonPage extends NixComponent {
25
+ private __lc;
26
+ constructor(lc: PageLifecycle);
27
+ onInit(): void;
28
+ ionViewWillEnter?(): void;
29
+ ionViewDidEnter?(): void;
30
+ ionViewWillLeave?(): void;
31
+ ionViewDidLeave?(): void;
32
+ }
33
+ export declare function useIonViewWillEnter(lc: PageLifecycle, fn: () => void): void;
34
+ export declare function useIonViewDidEnter(lc: PageLifecycle, fn: () => void): void;
35
+ export declare function useIonViewWillLeave(lc: PageLifecycle, fn: () => void): void;
36
+ export declare function useIonViewDidLeave(lc: PageLifecycle, fn: () => void): void;
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@deijose/nix-ionic",
3
+ "version": "0.1.0",
4
+ "description": "Ionic lifecycle & router bridge for Nix.js",
5
+ "license": "MIT",
6
+ "author": "Deiver Vasquez",
7
+ "type": "module",
8
+ "homepage": "https://github.com/DeijoseDevelop/nix-ionic.git",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/DeijoseDevelop/nix-ionic.git"
12
+ },
13
+ "main": "./dist/lib/nix-ionic.cjs",
14
+ "module": "./dist/lib/nix-ionic.js",
15
+ "types": "./dist/lib/index.d.ts",
16
+ "keywords": [
17
+ "ionic",
18
+ "nix-js",
19
+ "bridge",
20
+ "routing",
21
+ "navigation",
22
+ "lifecycle",
23
+ "capacitor",
24
+ "mobile",
25
+ "native",
26
+ "ios",
27
+ "android",
28
+ "web-components",
29
+ "reactive",
30
+ "signals",
31
+ "typescript",
32
+ "micro-framework",
33
+ "ui",
34
+ "frontend"
35
+ ],
36
+ "exports": {
37
+ ".": {
38
+ "import": {
39
+ "types": "./dist/lib/index.d.ts",
40
+ "default": "./dist/lib/nix-ionic.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/lib/index.d.ts",
44
+ "default": "./dist/lib/nix-ionic.cjs"
45
+ }
46
+ }
47
+ },
48
+ "files": [
49
+ "dist",
50
+ "README.md"
51
+ ],
52
+ "sideEffects": false,
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "scripts": {
57
+ "dev": "vite",
58
+ "build": "tsc && vite build",
59
+ "preview": "vite preview",
60
+ "build:lib": "vite build --config vite.lib.config.ts && tsc --project tsconfig.lib.json && terser dist/lib/nix-ionic.js -c -m -o dist/lib/nix-ionic.js && terser dist/lib/nix-ionic.cjs -c -m -o dist/lib/nix-ionic.cjs",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run"
63
+ },
64
+ "peerDependencies": {
65
+ "@deijose/nix-js": ">=1.7.7"
66
+ },
67
+ "devDependencies": {
68
+ "@deijose/nix-js": "^1.7.7",
69
+ "typescript": "~5.9.3",
70
+ "vite": "^8.0.0",
71
+ "vite-plugin-dts": "^4.5.4",
72
+ "happy-dom": "^20.8.3",
73
+ "terser": "^5.46.0"
74
+ }
75
+ }