@deijose/nix-js 1.0.4 → 1.0.6
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/dist/lib/index.d.ts +2 -0
- package/dist/lib/nix/async.d.ts +69 -0
- package/dist/lib/nix/component.d.ts +13 -0
- package/dist/lib/nix/context.d.ts +61 -0
- package/dist/lib/nix/form.d.ts +258 -0
- package/dist/lib/nix/index.d.ts +17 -0
- package/dist/lib/nix/lifecycle.d.ts +124 -0
- package/dist/lib/nix/reactivity.d.ts +124 -0
- package/dist/lib/nix/router.d.ts +253 -0
- package/dist/lib/nix/store.d.ts +40 -0
- package/dist/lib/nix/template.d.ts +380 -0
- package/dist/lib/nix-js.cjs +6 -7
- package/dist/lib/nix-js.js +8 -1267
- package/package.json +2 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { Signal } from "./reactivity";
|
|
2
|
+
import { NixComponent } from "./lifecycle";
|
|
3
|
+
import type { NixTemplate } from "./template";
|
|
4
|
+
/**
|
|
5
|
+
* Value returned (or resolved) by a navigation guard.
|
|
6
|
+
* - `false` — cancel the navigation.
|
|
7
|
+
* - `string` — redirect to that path.
|
|
8
|
+
* - `void` / `undefined` — allow the navigation.
|
|
9
|
+
*/
|
|
10
|
+
export type NavigationGuardResult = void | undefined | false | string;
|
|
11
|
+
/**
|
|
12
|
+
* A navigation guard function.
|
|
13
|
+
*
|
|
14
|
+
* @param to Destination pathname (e.g. `"/admin"`).
|
|
15
|
+
* @param from Current pathname before the navigation.
|
|
16
|
+
* @returns `false` to cancel, a path string to redirect, or nothing to allow.
|
|
17
|
+
* May return a Promise for async guard logic.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* router.beforeEach((to, from) => {
|
|
21
|
+
* if (!auth.isLoggedIn && to !== "/login") return "/login";
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
export type NavigationGuard = (to: string, from: string) => NavigationGuardResult | Promise<NavigationGuardResult>;
|
|
25
|
+
export interface RouteRecord {
|
|
26
|
+
/**
|
|
27
|
+
* Segmento de ruta. Soporta:
|
|
28
|
+
* - Literal: "/about", "/users"
|
|
29
|
+
* - Parámetro: "/users/:id", "/posts/:slug/comments/:cid"
|
|
30
|
+
* - Wildcard: "*" (fallback global o de prefijo con children)
|
|
31
|
+
*
|
|
32
|
+
* Los paths de children se concatenan con el del padre.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* { path: "/users/:id", component: UserDetail }
|
|
36
|
+
* // Navegar a "/users/42" → params.value = { id: "42" }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* { path: "/dash", component: DashLayout, children: [
|
|
40
|
+
* { path: "/users", component: UsersPage },
|
|
41
|
+
* ]}
|
|
42
|
+
* // Genera las rutas planas: /dash, /dash/users
|
|
43
|
+
*/
|
|
44
|
+
path: string;
|
|
45
|
+
/** Factory que devuelve la vista a renderizar en este nivel */
|
|
46
|
+
component: () => NixTemplate | NixComponent;
|
|
47
|
+
/**
|
|
48
|
+
* Rutas hijas. Sus paths se unen con el del padre.
|
|
49
|
+
* El componente padre debe incluir `new RouterView(1)` para renderizarlas.
|
|
50
|
+
*/
|
|
51
|
+
children?: RouteRecord[];
|
|
52
|
+
/**
|
|
53
|
+
* Guard de nivel de ruta. Se ejecuta solo al entrar en esta ruta concreta.
|
|
54
|
+
* Misma semántica de retorno que `beforeEach`.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* { path: "/admin", component: () => new AdminPage(),
|
|
58
|
+
* beforeEnter: (to, from) => {
|
|
59
|
+
* if (!isAdmin) return "/";
|
|
60
|
+
* }}
|
|
61
|
+
*/
|
|
62
|
+
beforeEnter?: NavigationGuard;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Callback for `afterEach` hooks — receives the committed `to` and `from` paths.
|
|
66
|
+
*/
|
|
67
|
+
export type AfterEachHook = (to: string, from: string) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Result of `router.resolve(path)` — inspect what would match without navigating.
|
|
70
|
+
*/
|
|
71
|
+
export interface ResolvedRoute {
|
|
72
|
+
/** Whether the path matched any registered route. */
|
|
73
|
+
matched: boolean;
|
|
74
|
+
/** Extracted route params (empty object if no match). */
|
|
75
|
+
params: Record<string, string>;
|
|
76
|
+
/** The matched route record, or `undefined` if no match. */
|
|
77
|
+
route: RouteRecord | undefined;
|
|
78
|
+
}
|
|
79
|
+
export interface Router {
|
|
80
|
+
/** Señal con la ruta activa actual (pathname, p.ej. "/users/42") */
|
|
81
|
+
readonly current: Signal<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Señal con los parámetros dinámicos de la ruta activa (:id, :slug…).
|
|
84
|
+
* Se actualiza síncronamente con cada `navigate()`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // Ruta: "/users/:id" → navigate("/users/42")
|
|
88
|
+
* router.params.value // { id: "42" }
|
|
89
|
+
*/
|
|
90
|
+
readonly params: Signal<Record<string, string>>;
|
|
91
|
+
/**
|
|
92
|
+
* Señal con los query params de la URL (?clave=valor…).
|
|
93
|
+
* Se actualiza síncronamente con cada `navigate()`.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* router.navigate("/users?page=2&sort=name")
|
|
97
|
+
* router.query.value // { page: "2", sort: "name" }
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* router.navigate("/users", { page: 2, sort: "name" })
|
|
101
|
+
* router.query.value // { page: "2", sort: "name" }
|
|
102
|
+
*/
|
|
103
|
+
readonly query: Signal<Record<string, string>>;
|
|
104
|
+
/**
|
|
105
|
+
* Navegar a una ruta nueva (pushState + actualiza señales).
|
|
106
|
+
* Si hay guards registrados, la navegación espera a que todos pasen.
|
|
107
|
+
*
|
|
108
|
+
* @param path Ruta destino. Puede incluir query string: "/users?page=2"
|
|
109
|
+
* @param query Query params como objeto. Se mezclan con los del path.
|
|
110
|
+
* Un valor `null`/`undefined` elimina el parámetro.
|
|
111
|
+
*/
|
|
112
|
+
navigate(path: string, query?: Record<string, string | number | boolean | null | undefined>): void;
|
|
113
|
+
/**
|
|
114
|
+
* Navigate without adding an entry to the browser history.
|
|
115
|
+
* Uses `history.replaceState` instead of `pushState`.
|
|
116
|
+
* Guards still run normally.
|
|
117
|
+
*
|
|
118
|
+
* @param path Destination path. May include query string.
|
|
119
|
+
* @param query Query params as an object.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // After login — pressing "back" won't return to /login
|
|
123
|
+
* router.replace("/home");
|
|
124
|
+
*/
|
|
125
|
+
replace(path: string, query?: Record<string, string | number | boolean | null | undefined>): void;
|
|
126
|
+
/**
|
|
127
|
+
* Go back one entry in the browser history.
|
|
128
|
+
* Equivalent to `history.back()`.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* router.back();
|
|
132
|
+
*/
|
|
133
|
+
back(): void;
|
|
134
|
+
/**
|
|
135
|
+
* Go forward one entry in the browser history.
|
|
136
|
+
* Equivalent to `history.forward()`.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* router.forward();
|
|
140
|
+
*/
|
|
141
|
+
forward(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Move `delta` entries in the browser history.
|
|
144
|
+
* Negative values go back, positive go forward.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* router.go(-2); // two pages back
|
|
148
|
+
* router.go(1); // same as forward()
|
|
149
|
+
*/
|
|
150
|
+
go(delta: number): void;
|
|
151
|
+
/**
|
|
152
|
+
* Check if a path is currently active.
|
|
153
|
+
* By default performs an exact match.
|
|
154
|
+
*
|
|
155
|
+
* @param path The path to test.
|
|
156
|
+
* @param exact If `false`, matches when `current` starts with `path`.
|
|
157
|
+
* Default: `true`.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* router.isActive("/admin"); // exact match
|
|
161
|
+
* router.isActive("/admin", false); // prefix: /admin/users → true
|
|
162
|
+
*/
|
|
163
|
+
isActive(path: string, exact?: boolean): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Inspect what route would match a given path without actually navigating.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const info = router.resolve("/user/42");
|
|
169
|
+
* // { matched: true, params: { id: "42" }, route: { path: "/user/:id", ... } }
|
|
170
|
+
*/
|
|
171
|
+
resolve(path: string): ResolvedRoute;
|
|
172
|
+
/** Árbol de rutas original (tal como se pasó a createRouter) */
|
|
173
|
+
readonly routes: RouteRecord[];
|
|
174
|
+
/**
|
|
175
|
+
* Registra un guard de navegación global.
|
|
176
|
+
* Se ejecuta (en orden de registro) antes de cada navegación.
|
|
177
|
+
*
|
|
178
|
+
* Retorna una función para eliminar el guard.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* const stop = router.beforeEach((to, from) => {
|
|
182
|
+
* if (!auth && to !== "/login") return "/login";
|
|
183
|
+
* });
|
|
184
|
+
* stop(); // elimina el guard
|
|
185
|
+
*/
|
|
186
|
+
beforeEach(guard: NavigationGuard): () => void;
|
|
187
|
+
/**
|
|
188
|
+
* Register a hook that runs after every successful navigation.
|
|
189
|
+
* Useful for analytics, scroll reset, etc.
|
|
190
|
+
*
|
|
191
|
+
* Returns a function to remove the hook.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* const stop = router.afterEach((to, from) => {
|
|
195
|
+
* window.scrollTo(0, 0);
|
|
196
|
+
* });
|
|
197
|
+
* stop();
|
|
198
|
+
*/
|
|
199
|
+
afterEach(hook: AfterEachHook): () => void;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Crea el router History API y lo establece como singleton activo del módulo.
|
|
203
|
+
* Usa `history.pushState` — URLs limpias sin `#`.
|
|
204
|
+
* `RouterView` y `Link` lo consumen automáticamente — no necesitan que se los pases.
|
|
205
|
+
*
|
|
206
|
+
* @note En producción el servidor debe responder con `index.html` para cualquier
|
|
207
|
+
* ruta no-archivo. Vite dev y `vite preview` lo hacen automáticamente.
|
|
208
|
+
*/
|
|
209
|
+
export declare function createRouter(routes: RouteRecord[]): Router;
|
|
210
|
+
/**
|
|
211
|
+
* Devuelve el router activo (singleton).
|
|
212
|
+
* Útil dentro de componentes para acceder a `params` y `current` sin prop-drilling.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* class UserDetail extends NixComponent {
|
|
216
|
+
* render() {
|
|
217
|
+
* return html`<div>User: ${() => useRouter().params.value.id}</div>`;
|
|
218
|
+
* }
|
|
219
|
+
* }
|
|
220
|
+
*/
|
|
221
|
+
export declare function useRouter(): Router;
|
|
222
|
+
/**
|
|
223
|
+
* @internal — Resets the router singleton. Used by tests to avoid
|
|
224
|
+
* "A router already exists" warnings between test cases.
|
|
225
|
+
*/
|
|
226
|
+
export declare function _resetRouter(): void;
|
|
227
|
+
/**
|
|
228
|
+
* Renderiza el componente de la ruta activa en el nivel `depth`.
|
|
229
|
+
*
|
|
230
|
+
* - `new RouterView()` → nivel raíz (depth 0).
|
|
231
|
+
* - `new RouterView(1)` → primer nivel de rutas anidadas. Úsalo dentro del
|
|
232
|
+
* componente padre para que renderice el hijo correspondiente.
|
|
233
|
+
*
|
|
234
|
+
* Consume el router singleton — no requiere que se le pase el router.
|
|
235
|
+
*/
|
|
236
|
+
export declare class RouterView extends NixComponent {
|
|
237
|
+
private _depth;
|
|
238
|
+
constructor(depth?: number);
|
|
239
|
+
render(): NixTemplate;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Enlace de navegación reactivo que se estiliza como activo/inactivo según la
|
|
243
|
+
* ruta actual. Consume el router singleton — no requiere que se le pase.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* new Link("/about", "About")
|
|
247
|
+
*/
|
|
248
|
+
export declare class Link extends NixComponent {
|
|
249
|
+
private _to;
|
|
250
|
+
private _label;
|
|
251
|
+
constructor(to: string, label: string);
|
|
252
|
+
render(): NixTemplate;
|
|
253
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Signal } from "./reactivity";
|
|
2
|
+
/** Transforma cada propiedad del estado inicial en su Signal correspondiente. */
|
|
3
|
+
export type StoreSignals<T extends Record<string, unknown>> = {
|
|
4
|
+
readonly [K in keyof T]: Signal<T[K]>;
|
|
5
|
+
};
|
|
6
|
+
/** Tipo del store tal como lo ve el usuario. */
|
|
7
|
+
export type Store<T extends Record<string, unknown>, A extends Record<string, unknown> = Record<never, never>> = StoreSignals<T> & A & {
|
|
8
|
+
/**
|
|
9
|
+
* Restaura todos los signals a sus valores iniciales.
|
|
10
|
+
* Equivalente a hacer `signal.value = initialValue` en cada propiedad.
|
|
11
|
+
*/
|
|
12
|
+
$reset(): void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Crea un store reactivo global.
|
|
16
|
+
*
|
|
17
|
+
* @param initialState Objeto plano con los valores iniciales.
|
|
18
|
+
* @param actionsFactory Función que recibe los signals y retorna acciones.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Sin acciones
|
|
22
|
+
* const theme = createStore({ dark: false, fontSize: 16 });
|
|
23
|
+
* theme.dark.value = true;
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Con acciones
|
|
27
|
+
* const cart = createStore(
|
|
28
|
+
* { items: [] as string[], total: 0 },
|
|
29
|
+
* (s) => ({
|
|
30
|
+
* add: (item: string) => s.items.update(arr => [...arr, item]),
|
|
31
|
+
* clear: () => cart.$reset(),
|
|
32
|
+
* })
|
|
33
|
+
* );
|
|
34
|
+
*
|
|
35
|
+
* cart.add("Manzana");
|
|
36
|
+
* cart.items.value; // ["Manzana"]
|
|
37
|
+
* cart.clear();
|
|
38
|
+
* cart.items.value; // []
|
|
39
|
+
*/
|
|
40
|
+
export declare function createStore<T extends Record<string, unknown>, A extends Record<string, unknown> = Record<never, never>>(initialState: T, actionsFactory?: (signals: StoreSignals<T>) => A): Store<T, A>;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import type { NixComponent } from "./lifecycle";
|
|
2
|
+
export interface NixTemplate {
|
|
3
|
+
readonly __isNixTemplate: true;
|
|
4
|
+
/** Monta el template en un contenedor (uso externo / raíz). */
|
|
5
|
+
mount(container: Element | string): NixMountHandle;
|
|
6
|
+
/**
|
|
7
|
+
* @internal — Renderiza el template antes del nodo `before` (o al final
|
|
8
|
+
* de `parent` si `before` es null). Retorna una función de limpieza.
|
|
9
|
+
*/
|
|
10
|
+
_render(parent: Node, before: Node | null): () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface NixMountHandle {
|
|
13
|
+
unmount(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Contenedor para una referencia directa a un elemento DOM.
|
|
17
|
+
* Se asigna automáticamente cuando el template se monta y se limpia al
|
|
18
|
+
* desmontarse.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const inputRef = ref<HTMLInputElement>();
|
|
22
|
+
* html`<input ref=${inputRef} />`
|
|
23
|
+
* // después del mount:
|
|
24
|
+
* inputRef.el?.focus();
|
|
25
|
+
*/
|
|
26
|
+
export interface NixRef<T extends Element = Element> {
|
|
27
|
+
el: T | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Crea un objeto `NixRef` vacío.
|
|
31
|
+
* Pásalo como valor del atributo especial `ref` en un template para que
|
|
32
|
+
* Nix.js rellene automáticamente `ref.el` con el elemento real del DOM.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ref<T extends Element = Element>(): NixRef<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Toggles the visibility of an element **without unmounting it** from the DOM
|
|
37
|
+
* (sets `style.display = "none"` when hidden, restores it when visible).
|
|
38
|
+
*
|
|
39
|
+
* Use the `show` or `hide` attribute bindings inside templates — or call
|
|
40
|
+
* this helper directly for imperative use outside of templates.
|
|
41
|
+
*
|
|
42
|
+
* ### Template usage
|
|
43
|
+
* ```html
|
|
44
|
+
* <!-- show: element is visible when condition is truthy -->
|
|
45
|
+
* <div show=${() => isVisible.value}>...</div>
|
|
46
|
+
*
|
|
47
|
+
* <!-- hide: element is hidden when condition is truthy (inverse of show) -->
|
|
48
|
+
* <div hide=${() => isLoading.value}>Submit</div>
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* ### Difference from conditional rendering
|
|
52
|
+
* | | `show` / `hide` | conditional (`() => condition ? html\`…\` : null`) |
|
|
53
|
+
* |---|---|---|
|
|
54
|
+
* | DOM node kept | ✅ always | ❌ destroyed when hidden |
|
|
55
|
+
* | Lifecycle hooks | not called on toggle | called on every toggle |
|
|
56
|
+
* | Use when | hiding/showing frequently | rarely shown alternatives |
|
|
57
|
+
*
|
|
58
|
+
* ### Imperative usage (outside a template)
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { showWhen } from "@deijose/nix-js";
|
|
61
|
+
* import { effect } from "@deijose/nix-js";
|
|
62
|
+
*
|
|
63
|
+
* const el = document.getElementById("my-panel")!;
|
|
64
|
+
* // Reactively controlled:
|
|
65
|
+
* effect(() => showWhen(el, isVisible.value));
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function showWhen(el: HTMLElement, condition: boolean): void;
|
|
69
|
+
/**
|
|
70
|
+
* Resultado de `repeat()` — lista con keys para diffing eficiente.
|
|
71
|
+
* El template engine lo reconoce y solo añade/mueve/elimina los nodos
|
|
72
|
+
* que realmente cambiaron, preservando el DOM de los items estables.
|
|
73
|
+
*/
|
|
74
|
+
export interface KeyedList<T = unknown> {
|
|
75
|
+
readonly __isKeyedList: true;
|
|
76
|
+
readonly items: T[];
|
|
77
|
+
readonly keyFn: (item: T, index: number) => string | number;
|
|
78
|
+
readonly renderFn: (item: T, index: number) => NixTemplate | NixComponent;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Crea una lista con keys para diffing eficiente.
|
|
82
|
+
* Úsalo en lugar de `.map()` cuando la lista cambia frecuentemente.
|
|
83
|
+
*
|
|
84
|
+
* @param items Array reactivo de datos
|
|
85
|
+
* @param keyFn Devuelve una clave única por item (p.ej. `item => item.id`)
|
|
86
|
+
* @param renderFn Devuelve el template/componente para cada item
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ${() => repeat(
|
|
90
|
+
* users.value,
|
|
91
|
+
* u => u.id,
|
|
92
|
+
* u => html`<li>${u.name}</li>`
|
|
93
|
+
* )}
|
|
94
|
+
*/
|
|
95
|
+
export declare function repeat<T>(items: T[], keyFn: (item: T, index: number) => string | number, renderFn: (item: T, index: number) => NixTemplate | NixComponent): KeyedList<T>;
|
|
96
|
+
/**
|
|
97
|
+
* Renders `content` into `target` instead of the current position in the tree.
|
|
98
|
+
* The portal is cleaned up automatically when the parent template is unmounted.
|
|
99
|
+
*
|
|
100
|
+
* Use this to render modals, tooltips, notifications, or dropdowns outside of
|
|
101
|
+
* your component tree — typically into `document.body` — so they are not clipped
|
|
102
|
+
* by `overflow: hidden` or buried under other stacking contexts.
|
|
103
|
+
*
|
|
104
|
+
* The portal returns a `NixTemplate`, so it works as a node value anywhere in
|
|
105
|
+
* a template, including inside reactive conditionals: the portal is
|
|
106
|
+
* mounted/unmounted together with whatever controls its condition.
|
|
107
|
+
*
|
|
108
|
+
* @param content Template or component to render inside the portal.
|
|
109
|
+
* @param target CSS selector or `Element` to render into. Defaults to `document.body`.
|
|
110
|
+
*
|
|
111
|
+
* @example Reactive modal
|
|
112
|
+
* ```typescript
|
|
113
|
+
* import { signal, portal, html } from "@deijose/nix-js";
|
|
114
|
+
*
|
|
115
|
+
* const isOpen = signal(false);
|
|
116
|
+
*
|
|
117
|
+
* html`
|
|
118
|
+
* <button @click=${() => { isOpen.value = true; }}>Open</button>
|
|
119
|
+
*
|
|
120
|
+
* ${() => isOpen.value
|
|
121
|
+
* ? portal(html`
|
|
122
|
+
* <div class="overlay" @click=${() => { isOpen.value = false; }}>
|
|
123
|
+
* <div class="modal" @click.stop=${() => {}}>
|
|
124
|
+
* <h2>Hello from a portal!</h2>
|
|
125
|
+
* <button @click=${() => { isOpen.value = false; }}>Close</button>
|
|
126
|
+
* </div>
|
|
127
|
+
* </div>
|
|
128
|
+
* `)
|
|
129
|
+
* : null
|
|
130
|
+
* }
|
|
131
|
+
* `
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @example Custom target
|
|
135
|
+
* ```typescript
|
|
136
|
+
* portal(html`<div class="toast">Saved!</div>`, "#toast-root")
|
|
137
|
+
* portal(html`<Tooltip />`, document.getElementById("tooltip-layer")!)
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Opaque token created by `createPortalOutlet()`.
|
|
142
|
+
*
|
|
143
|
+
* Pass it to `portalOutlet()` to declare the DOM anchor where portals targeting
|
|
144
|
+
* this outlet will render, and to `portal(content, outlet)` as the target.
|
|
145
|
+
*
|
|
146
|
+
* @see createPortalOutlet
|
|
147
|
+
* @see portalOutlet
|
|
148
|
+
*/
|
|
149
|
+
export interface PortalOutlet {
|
|
150
|
+
readonly __isPortalOutlet: true;
|
|
151
|
+
/** @internal — resolved DOM container; set when `portalOutlet()` is mounted */
|
|
152
|
+
_container: Element | null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Creates a `PortalOutlet` token — a lightweight, typed anchor point that
|
|
156
|
+
* decouples *where* a portal renders from direct DOM access.
|
|
157
|
+
* No CSS selectors, no `document.querySelector`, no manual element references.
|
|
158
|
+
*
|
|
159
|
+
* ### Workflow
|
|
160
|
+
* 1. Create the token at module or component scope.
|
|
161
|
+
* 2. Place `${portalOutlet(outlet)}` in your layout template to declare the anchor.
|
|
162
|
+
* 3. From any child: `portal(content, outlet)` renders into that anchor.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* const modalOutlet = createPortalOutlet();
|
|
167
|
+
*
|
|
168
|
+
* // Layout:
|
|
169
|
+
* html`
|
|
170
|
+
* <main>${mainContent}</main>
|
|
171
|
+
* ${portalOutlet(modalOutlet)}
|
|
172
|
+
* `
|
|
173
|
+
*
|
|
174
|
+
* // Child (any depth):
|
|
175
|
+
* html`${() => show.value ? portal(html\`<Modal />\`, modalOutlet) : null}`
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export declare function createPortalOutlet(): PortalOutlet;
|
|
179
|
+
/**
|
|
180
|
+
* Declares the DOM anchor for a `PortalOutlet` inside a template.
|
|
181
|
+
* Creates a `<div data-nix-outlet>` at this position; portals targeting
|
|
182
|
+
* `outlet` will render their content as children of that div.
|
|
183
|
+
*
|
|
184
|
+
* The anchor's lifecycle follows its parent template — when the parent
|
|
185
|
+
* unmounts, the outlet div and any portals inside it are cleaned up.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* mount(html`
|
|
190
|
+
* <div class="app">
|
|
191
|
+
* <main>${mainContent}</main>
|
|
192
|
+
* ${portalOutlet(modalOutlet)}
|
|
193
|
+
* </div>
|
|
194
|
+
* `, document.body);
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export declare function portalOutlet(outlet: PortalOutlet): NixTemplate;
|
|
198
|
+
export declare function portal(content: NixTemplate | NixComponent, target?: Element | string | PortalOutlet | NixRef<Element>): NixTemplate;
|
|
199
|
+
/**
|
|
200
|
+
* Provides a `PortalOutlet` to descendant components via the inject system.
|
|
201
|
+
* Must be called inside `onInit()` of a `NixComponent`.
|
|
202
|
+
*
|
|
203
|
+
* Eliminates prop drilling: any descendant can call `injectOutlet()` to
|
|
204
|
+
* obtain the outlet without it being passed through every layer.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* class AppLayout extends NixComponent {
|
|
209
|
+
* private outlet = createPortalOutlet();
|
|
210
|
+
* onInit() { provideOutlet(this.outlet); }
|
|
211
|
+
* render() {
|
|
212
|
+
* return html`
|
|
213
|
+
* <main>...</main>
|
|
214
|
+
* ${portalOutlet(this.outlet)}
|
|
215
|
+
* `;
|
|
216
|
+
* }
|
|
217
|
+
* }
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export declare function provideOutlet(outlet: PortalOutlet): void;
|
|
221
|
+
/**
|
|
222
|
+
* Injects the nearest `PortalOutlet` provided by an ancestor component.
|
|
223
|
+
* Returns `undefined` if no ancestor has called `provideOutlet()`.
|
|
224
|
+
*
|
|
225
|
+
* Use `portal(content, injectOutlet())` to render into the ancestor's outlet
|
|
226
|
+
* with no CSS selectors, no `document.querySelector`, and no prop drilling.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* class ToastButton extends NixComponent {
|
|
231
|
+
* private outlet: PortalOutlet | undefined;
|
|
232
|
+
* private active = signal(false);
|
|
233
|
+
* onInit() { this.outlet = injectOutlet(); }
|
|
234
|
+
* render() {
|
|
235
|
+
* return html`
|
|
236
|
+
* <button @click=${() => { this.active.value = true; }}>Notify</button>
|
|
237
|
+
* ${() => this.active.value
|
|
238
|
+
* ? portal(html\`<div class="toast">Done!</div>\`, this.outlet)
|
|
239
|
+
* : null
|
|
240
|
+
* }
|
|
241
|
+
* `;
|
|
242
|
+
* }
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
export declare function injectOutlet(): PortalOutlet | undefined;
|
|
247
|
+
/**
|
|
248
|
+
* Fallback value for `createErrorBoundary()`:
|
|
249
|
+
* - A static `NixTemplate` or `NixComponent` — always render this on error.
|
|
250
|
+
* - A function `(err) => NixTemplate | NixComponent` — render based on the error.
|
|
251
|
+
*/
|
|
252
|
+
export type ErrorFallback = NixTemplate | NixComponent | ((err: unknown) => NixTemplate | NixComponent);
|
|
253
|
+
/**
|
|
254
|
+
* Wraps `content` in an error boundary. If any error is thrown during the
|
|
255
|
+
* **initial render** or during a **reactive update** inside `content`, the
|
|
256
|
+
* boundary automatically:
|
|
257
|
+
* 1. Tears down the broken subtree (effects, event listeners, DOM).
|
|
258
|
+
* 2. Renders `fallback` in its place — without crashing the rest of the app.
|
|
259
|
+
*
|
|
260
|
+
* Errors caught:
|
|
261
|
+
* - `onInit()` / `render()` throws in any `NixComponent` inside `content`
|
|
262
|
+
* - Throws inside `html\`\`` binding expressions during initial render
|
|
263
|
+
* - Reactive re-renders: effects created inside `content` that throw when
|
|
264
|
+
* a signal changes
|
|
265
|
+
*
|
|
266
|
+
* Not caught (same as React):
|
|
267
|
+
* - Event handler throws (wrap those with your own try/catch)
|
|
268
|
+
* - Async code (Promises, `setTimeout`, etc.)
|
|
269
|
+
* - Errors thrown inside `fallback` itself (propagate to the parent boundary)
|
|
270
|
+
*
|
|
271
|
+
* @example Basic usage
|
|
272
|
+
* ```typescript
|
|
273
|
+
* import { createErrorBoundary, html, signal } from "@deijose/nix-js";
|
|
274
|
+
*
|
|
275
|
+
* mount(
|
|
276
|
+
* createErrorBoundary(
|
|
277
|
+
* new MyWidget(),
|
|
278
|
+
* (err) => html`<div class="error">Widget failed: ${String(err)}</div>`
|
|
279
|
+
* ),
|
|
280
|
+
* "#app"
|
|
281
|
+
* );
|
|
282
|
+
* ```
|
|
283
|
+
*
|
|
284
|
+
* @example Static fallback
|
|
285
|
+
* ```typescript
|
|
286
|
+
* createErrorBoundary(
|
|
287
|
+
* html`${() => riskyValue.value}`,
|
|
288
|
+
* html`<p>Something went wrong.</p>`
|
|
289
|
+
* )
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @example Nested boundaries (inner catches first)
|
|
293
|
+
* ```typescript
|
|
294
|
+
* createErrorBoundary(
|
|
295
|
+
* html`
|
|
296
|
+
* <header>...</header>
|
|
297
|
+
* ${createErrorBoundary(new RiskyWidget(), html`<p>Widget error</p>`)}
|
|
298
|
+
* `,
|
|
299
|
+
* html`<p>App-level error</p>`
|
|
300
|
+
* )
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
export declare function createErrorBoundary(content: NixTemplate | NixComponent, fallback: ErrorFallback): NixTemplate;
|
|
304
|
+
/**
|
|
305
|
+
* Options for `transition()`. All class-name overrides are optional — by
|
|
306
|
+
* default they are derived from `name` (default `"nix"`).
|
|
307
|
+
*
|
|
308
|
+
* | phase | from class | active class | to class |
|
|
309
|
+
* |--------------|-------------------|---------------------|-----------------|
|
|
310
|
+
* | enter | `{n}-enter-from` | `{n}-enter-active` | `{n}-enter-to` |
|
|
311
|
+
* | leave | `{n}-leave-from` | `{n}-leave-active` | `{n}-leave-to` |
|
|
312
|
+
*/
|
|
313
|
+
export interface TransitionOptions {
|
|
314
|
+
/**
|
|
315
|
+
* Prefix for all generated CSS classes. Default `"nix"`.
|
|
316
|
+
* e.g. `name: "fade"` generates `.fade-enter-from`, `.fade-leave-to`, …
|
|
317
|
+
*/
|
|
318
|
+
name?: string;
|
|
319
|
+
/** Override the enter-from class individually. */
|
|
320
|
+
enterFrom?: string;
|
|
321
|
+
/** Override the enter-active class individually. */
|
|
322
|
+
enterActive?: string;
|
|
323
|
+
/** Override the enter-to class individually. */
|
|
324
|
+
enterTo?: string;
|
|
325
|
+
/** Override the leave-from class individually. */
|
|
326
|
+
leaveFrom?: string;
|
|
327
|
+
/** Override the leave-active class individually. */
|
|
328
|
+
leaveActive?: string;
|
|
329
|
+
/** Override the leave-to class individually. */
|
|
330
|
+
leaveTo?: string;
|
|
331
|
+
/**
|
|
332
|
+
* When `true` the enter transition also plays on the very first render
|
|
333
|
+
* (similar to Vue's `appear`). Default `false`.
|
|
334
|
+
*/
|
|
335
|
+
appear?: boolean;
|
|
336
|
+
/**
|
|
337
|
+
* Fallback duration in **milliseconds** used when no `transition-duration`
|
|
338
|
+
* or `animation-duration` is found on the element via `getComputedStyle`.
|
|
339
|
+
*/
|
|
340
|
+
duration?: number;
|
|
341
|
+
/** Called synchronously right before the enter classes are added. */
|
|
342
|
+
onBeforeEnter?: (el: Element) => void;
|
|
343
|
+
/** Called after the enter transition has fully completed. */
|
|
344
|
+
onAfterEnter?: (el: Element) => void;
|
|
345
|
+
/** Called synchronously right before the leave classes are added. */
|
|
346
|
+
onBeforeLeave?: (el: Element) => void;
|
|
347
|
+
/** Called after the leave transition has fully completed and the DOM is removed. */
|
|
348
|
+
onAfterLeave?: (el: Element) => void;
|
|
349
|
+
}
|
|
350
|
+
/** Content that can be wrapped with `transition()`. */
|
|
351
|
+
export type TransitionContent = NixTemplate | NixComponent | (() => NixTemplate | NixComponent | null);
|
|
352
|
+
/**
|
|
353
|
+
* Wraps `content` with CSS class-based enter / leave transitions.
|
|
354
|
+
*
|
|
355
|
+
* **Static content** (NixTemplate / NixComponent): plays the enter transition
|
|
356
|
+
* on mount (only if `appear: true`; otherwise instant), and cleans up
|
|
357
|
+
* immediately on unmount without a leave transition.
|
|
358
|
+
*
|
|
359
|
+
* **Reactive conditional** `() => Template | null`: plays the enter
|
|
360
|
+
* transition when the expression goes from `null` → value, and the leave
|
|
361
|
+
* transition when it goes from value → `null`. An in-progress leave is
|
|
362
|
+
* cancelled and the DOM is removed synchronously when new content enters.
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```css
|
|
366
|
+
* .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
|
|
367
|
+
* .fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
368
|
+
* ```
|
|
369
|
+
* ```typescript
|
|
370
|
+
* const show = signal(true);
|
|
371
|
+
*
|
|
372
|
+
* // Reactive — full enter + leave
|
|
373
|
+
* transition(() => show.value ? html`<p>Hello</p>` : null, { name: "fade" })
|
|
374
|
+
*
|
|
375
|
+
* // Static — only enter (if appear: true)
|
|
376
|
+
* transition(html`<span>Always here</span>`, { name: "slide", appear: true })
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
export declare function transition(content: TransitionContent, options?: TransitionOptions): NixTemplate;
|
|
380
|
+
export declare function html(strings: TemplateStringsArray, ...values: unknown[]): NixTemplate;
|