@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 +759 -0
- package/dist/IonRouterOutlet.d.ts +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/lib/nix-ionic.cjs +1 -0
- package/dist/lib/nix-ionic.cjs.map +1 -0
- package/dist/lib/nix-ionic.js +1 -0
- package/dist/lib/nix-ionic.js.map +1 -0
- package/dist/lifecycle.d.ts +36 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
# @deijose/nix-ionic
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@deijose/nix-ionic)
|
|
4
|
+
[](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 {};
|
package/dist/index.d.ts
ADDED
|
@@ -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":"oGA8BA,SAAgB,GAAqC,CACnD,MAAO,CACL,WAAA,EAAA,EAAA,QAAkB,EAAE,CACpB,UAAA,EAAA,EAAA,QAAkB,EAAE,CACpB,WAAA,EAAA,EAAA,QAAkB,EAAE,CACpB,UAAA,EAAA,EAAA,QAAkB,EAAE,CACrB,CAeH,IAAsB,EAAtB,cAAsC,EAAA,YAAa,CACjD,KAEA,YAAY,EAAmB,CAC7B,OAAO,CACP,KAAK,KAAO,EAGd,QAAwB,CACtB,IAAM,EAAK,KAAK,KAGZ,KAAK,mBAAkB,EAAA,EAAA,OAAM,EAAG,UAAW,KAAK,iBAAiB,KAAK,KAAK,CAAC,CAC5E,KAAK,kBAAkB,EAAA,EAAA,OAAM,EAAG,SAAW,KAAK,gBAAgB,KAAK,KAAK,CAAC,CAC3E,KAAK,mBAAkB,EAAA,EAAA,OAAM,EAAG,UAAW,KAAK,iBAAiB,KAAK,KAAK,CAAC,CAC5E,KAAK,kBAAkB,EAAA,EAAA,OAAM,EAAG,SAAW,KAAK,gBAAgB,KAAK,KAAK,CAAC,GAmBnF,SAAgB,EAAoB,EAAmB,EAAsB,EAC3E,EAAA,EAAA,OAAM,EAAG,UAAW,EAAG,CAGzB,SAAgB,EAAmB,EAAmB,EAAsB,EAC1E,EAAA,EAAA,OAAM,EAAG,SAAU,EAAG,CAGxB,SAAgB,EAAoB,EAAmB,EAAsB,EAC3E,EAAA,EAAA,OAAM,EAAG,UAAW,EAAG,CAGzB,SAAgB,EAAmB,EAAmB,EAAsB,EAC1E,EAAA,EAAA,OAAM,EAAG,SAAU,EAAG,CCpExB,IAAI,EAA8B,KAElC,SAAgB,GAAyB,CACvC,GAAI,CAAC,EAAS,MAAU,MAAM,kEAAkE,CAChG,OAAO,EAOT,SAAgB,EAAc,EAAmC,CAC/D,MAAO,CACL,gBAAiB,GACjB,MAAM,EAAW,CACf,IAAM,EAAK,OAAO,GAAc,SAAW,SAAS,cAAc,EAAU,CAAI,EAEhF,MAAO,CAAE,QADO,KAAK,QAAQ,EAAI,KAAK,CACX,EAE7B,QAAQ,EAAc,EAAiC,CACrD,IAAM,EAAM,SAAS,cAAc,kBAAkB,CACrD,EAAI,aAAa,eAAgB,GAAe,IAAI,CACpD,IAAM,EAAW,GAAa,CAC5B,EAAE,iBAAiB,CACnB,EAAE,gBAAgB,CACd,GAAS,UAAU,MAAO,EAAQ,MAAM,CACnC,GAAa,GAAS,SAAS,EAAY,EAItD,OAFA,EAAI,iBAAiB,QAAS,EAAQ,CACtC,EAAO,aAAa,EAAK,EAAO,KACnB,CACX,EAAI,oBAAoB,QAAS,EAAQ,CACzC,EAAI,QAAQ,GAGjB,CAsBH,IAAM,EAAqB,IAAI,IAMlB,EAAb,cAAqC,EAAA,YAAa,CAChD,OACA,YAA0C,KAE1C,cAAA,EAAA,EAAA,QAA8B,SAAS,SAAS,CAChD,YAAA,EAAA,EAAA,QAA8B,GAAM,CACpC,SAAA,EAAA,EAAA,QAAsD,EAAE,CAAC,CAEzD,YAAY,EAA2B,CACrC,OAAO,CACP,KAAK,OAAS,EAId,EAAU,CACR,SAAY,GAAS,KAAK,SAAS,EAAK,CACxC,QAAY,GAAS,KAAK,QAAQ,EAAK,CACvC,SAAqB,KAAK,MAAM,CAChC,UAAW,KAAK,WAChB,OAAW,KAAK,QAChB,KAAW,KAAK,aACjB,CAED,KAAK,yBAAyB,CAGhC,QAAwB,CACtB,OAAO,iBAAiB,WAAY,KAAK,gBAAgB,CAG3D,SAA+B,CAC7B,KAAK,YAAc,SAAS,cAAc,aAAa,CAEvD,IAAM,EAAgB,GAAc,CAClC,GAAM,CAAE,KAAI,QAAU,EAAmB,OACzC,KAAK,kBAAkB,EAAI,GAAQ,GAAG,EAElC,EAAe,GAAc,CACjC,GAAM,CAAE,MAAQ,EAAmB,OACnC,KAAK,iBAAiB,EAAG,EAErB,EAAa,GAAc,CAC9B,EAAmB,OAAO,SAAS,IAAM,GAAqB,CACzD,KAAK,WAAW,MAAO,KAAK,MAAM,CAAO,GAAM,EACnD,EAOJ,OAJA,OAAO,iBAAiB,qBAAsB,EAAa,CAC3D,OAAO,iBAAiB,oBAAsB,EAAY,CAC1D,SAAS,iBAAiB,gBAAoB,EAAU,KAE3C,CACX,OAAO,oBAAoB,WAAqB,KAAK,gBAAgB,CACrE,OAAO,oBAAoB,qBAAsB,EAAa,CAC9D,OAAO,oBAAoB,oBAAsB,EAAY,CAC7D,SAAS,oBAAoB,gBAAoB,EAAU,CAC3D,EAAU,MAQd,SAAS,EAAoB,CACvB,IAAS,SAAS,UACrB,KAAK,aAAqB,KAAK,EAAM,UAAU,CAGlD,QAAQ,EAAoB,CACzB,KAAK,aAAqB,KAAK,EAAM,OAAO,CAG/C,MAAa,CACV,KAAK,aAAqB,MAAM,CAOnC,oBAAsC,CAEpC,KAAK,kBAAkB,SAAS,SAAU,KAAK,aAAa,MAAM,CAAC,EAGrE,kBAA0B,EAAY,EAAoB,CACxD,IAAM,EAAQ,KAAK,YAAY,EAAG,CAClC,GAAI,CAAC,EAAO,OAGZ,GAAI,EAAM,YAAa,CACrB,IAAM,EAAS,EAAM,YAAY,EAAI,EAAK,CAC1C,GAAI,IAAW,GAAO,CAAE,KAAK,MAAM,CAAE,OACrC,GAAI,OAAO,GAAW,SAAU,CAAE,KAAK,SAAS,EAAO,CAAE,QAG3D,KAAK,aAAa,MAAQ,EAC1B,KAAK,QAAQ,MAAa,KAAK,eAAe,EAAM,KAAM,EAAG,CAG7D,IAAM,EAAU,KAAK,WAAW,EAAK,CACrC,EAAmB,IAAI,EAAQ,EAAE,UAAU,OAAQ,GAAM,EAAI,EAAE,CAGjE,iBAAyB,EAAkB,CACzC,IAAM,EAAS,SAAS,cAAc,oBAAoB,CAC1D,KAAK,WAAW,MAAQ,GAAQ,aAAa,EAAI,GAGjD,EAAmB,SAAS,EAAI,IAAQ,CAClC,IAAQ,KAAK,WAAW,EAAG,EAAE,EAAG,SAAS,OAAQ,GAAM,EAAI,EAAE,EACjE,CAOF,IAAM,EAAS,KAAK,WAAW,EAAG,CAC5B,EAAS,EAAmB,IAAI,EAAM,CACtC,EAAS,SAAS,cAAc,EAAM,CACxC,GAAQ,GAAM,YAChB,EAAK,UAAU,OAAQ,GAAM,EAAI,EAAE,CACnC,0BAA4B,EAAK,SAAS,OAAQ,GAAM,EAAI,EAAE,CAAC,EAQnE,YAAoB,EAA2C,CAC7D,IAAM,EAAQ,KAAK,OAAO,KAAM,GAAM,EAAE,OAAS,EAAK,CACtD,GAAI,EAAO,OAAO,EAClB,IAAK,IAAM,KAAS,KAAK,OAEvB,GADe,OAAO,IAAI,EAAM,KAAK,QAAQ,UAAW,UAAU,CAAC,GAAG,CAC/D,KAAK,EAAK,CAAE,OAAO,EAE5B,OAAO,KAAK,OAAO,KAAM,GAAM,EAAE,OAAS,IAAI,CAGhD,eAAuB,EAAmB,EAA0C,CAClF,IAAM,EAAiC,EAAE,CACnC,EAAK,EAAU,MAAM,IAAI,CACzB,EAAK,EAAS,MAAM,IAAI,CAC9B,IAAK,IAAI,EAAI,EAAG,EAAI,EAAG,OAAQ,IACzB,EAAG,GAAG,WAAW,IAAI,GAAE,EAAO,EAAG,GAAG,MAAM,EAAE,EAAI,EAAG,IAAM,IAE/D,OAAO,EAGT,WAAmB,EAAsB,CAMvC,MALI,CAAC,GAAQ,IAAS,IAAY,gBAK3B,YAJO,EACX,QAAQ,aAAe,GAAM,IAAM,EAAE,QAAQ,MAAO,GAAG,CAAC,QAAQ,KAAM,GAAG,CAAC,CAC1E,QAAQ,MAAO,GAAG,CAClB,QAAQ,MAAO,IAAI,GAQxB,yBAAwC,CACtC,IAAK,IAAM,KAAS,KAAK,OAAQ,CAC/B,GAAI,EAAM,OAAS,IAAK,SAExB,IAAM,EAAW,KAAK,WAAW,EAAM,KAAK,CACtC,EAAW,EACX,EAAW,KAEb,eAAe,IAAI,EAAI,EAE3B,eAAe,OAAO,EAAK,cAAc,WAAY,CACnD,SAAwC,KAExC,mBAA0B,CACxB,IAAM,EAAS,EAAK,eAAe,EAAS,KAAM,SAAS,SAAS,CAC9D,EAAS,GAAqB,CAEpC,EAAmB,IAAI,EAAK,EAAG,CAC/B,KAAK,UAAU,IAAI,WAAW,CAE9B,IAAM,EAAW,EAAS,UAAU,CAAE,KAAI,SAAQ,CAAC,CAEnD,GAAI,WAAY,GAAY,OAAQ,EAA0B,QAAW,WAAY,CACnF,IAAM,EAAO,EACb,EAAK,UAAU,CACf,IAAM,EAAgB,EAAK,QAAQ,CAAC,QAAQ,KAAM,KAAK,CACjD,EAAgB,EAAK,WAAW,CACtC,KAAK,aAAiB,CACpB,EAAK,aAAa,CACd,OAAO,GAAa,YAAY,GAAU,CAC9C,GAAe,OAGjB,KAAK,SAAY,EAAyB,QAAQ,KAAM,KAAK,CAK/D,EAAG,UAAU,OAAQ,GAAM,EAAI,EAAE,CACjC,0BAA4B,CAC1B,EAAG,SAAS,OAAQ,GAAM,EAAI,EAAE,CAG/B,KAAa,UAAY,IAC1B,CAGJ,sBAA6B,CAE3B,IAAM,EAAK,EAAmB,IAAI,EAAI,CAClC,IACF,EAAG,UAAU,OAAQ,GAAM,EAAI,EAAE,CACjC,EAAG,SAAS,OAAQ,GAAM,EAAI,EAAE,EAElC,KAAK,YAAY,CACjB,KAAK,SAAW,KAChB,EAAmB,OAAO,EAAI,GAEhC,EAQN,QAA+B,CAC7B,IAAM,EAAO,KACb,MAAO,CACL,gBAAiB,GACjB,MAAM,EAAkD,CACtD,IAAM,EAAK,OAAO,GAAc,SAC3B,SAAS,cAAc,EAAU,CAClC,EAEJ,MAAO,CAAE,QADO,KAAK,QAAQ,EAAI,KAAK,CACX,EAE7B,QAAQ,EAAc,EAAiC,CAErD,IAAM,EAAW,SAAS,cAAc,aAAa,CACrD,EAAS,aAAa,WAAY,QAAQ,CAC1C,EAAS,UAAY,EAAK,OACvB,OAAQ,GAAM,EAAE,OAAS,IAAI,CAC7B,IAAK,GAAM,mBAAmB,EAAE,KAAK,eAAe,EAAK,WAAW,EAAE,KAAK,CAAC,gBAAgB,CAC5F,KAAK,GAAG,CAEX,IAAM,EAAW,SAAS,cAAc,oBAAoB,CAO5D,OALA,EAAO,aAAa,EAAU,EAAO,CACrC,EAAO,aAAa,EAAU,EAAO,CAErC,EAAK,YAAc,MAEN,CAAE,EAAS,QAAQ,CAAE,EAAS,QAAQ,GAEtD,CAGH,WAA2B,CACzB,EAAmB,OAAO,CAC1B,EAAU"}
|
|
@@ -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":"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"}
|
|
@@ -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
|
+
}
|