ilha 0.9.5 → 0.10.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 CHANGED
@@ -21,12 +21,14 @@ import ilha, { html } from "ilha";
21
21
 
22
22
  const Counter = ilha
23
23
  .state("count", 0)
24
- .on("button@click", ({ state }) => state.count(state.count() + 1))
24
+ .action("increment", (_, { state }) => {
25
+ state.count((count) => count + 1);
26
+ })
25
27
  .render(
26
- ({ state }) => html`
28
+ ({ state, action }) => html`
27
29
  <div>
28
30
  <p>Count: ${state.count()}</p>
29
- <button>Increment</button>
31
+ <button onclick=${action.increment}>Increment</button>
30
32
  </div>
31
33
  `,
32
34
  );
@@ -42,15 +44,45 @@ Counter.mount(document.getElementById("app"));
42
44
 
43
45
  ## Core Concepts
44
46
 
45
- Islands are **self-contained reactive components** that know how to render themselves to an HTML string (SSR) and mount themselves into the DOM (client). You build an island using a fluent builder chain: declare inputs, state, events, effects, then call `.render()` to get a callable `Island` object.
47
+ Islands are **self-contained reactive components** that know how to render themselves to an HTML string (SSR) and mount themselves into the DOM (client). Create one directly with `ilha(renderFn)`, or use the fluent builder when it needs local capabilities.
46
48
 
47
49
  State is managed with signals — when a signal changes, only the affected island re-renders using a minimal DOM morph. No virtual DOM diffing, no framework overhead.
48
50
 
51
+ ### Choose the smallest component form
52
+
53
+ Ilha has two runtime modes with three authoring forms:
54
+
55
+ | Form | Use it when |
56
+ | ----------------------------------------- | -------------------------------------------------------------------------------- |
57
+ | `const View = () => JSX` | You need reusable markup inside another island |
58
+ | `const View = ilha(() => JSX)` | The component needs its own reactive scope, lifecycle, mount, or hydration |
59
+ | `ilha.state(...).action(...).render(...)` | The island needs local state, derived values, actions, input, or lifecycle hooks |
60
+
61
+ Start with a plain component:
62
+
63
+ ```tsx
64
+ const Status = () => <p>Ready</p>;
65
+ ```
66
+
67
+ Promote it without changing its markup when it needs an island boundary:
68
+
69
+ ```tsx
70
+ const Status = ilha(() => <p>Ready</p>);
71
+ ```
72
+
73
+ Expand it into the builder when it needs local capabilities:
74
+
75
+ ```tsx
76
+ const Status = ilha.state("message", "Ready").render(({ state }) => <p>{state.message()}</p>);
77
+ ```
78
+
79
+ Both `ilha(() => JSX)` and the builder return a complete `Island`. A plain function remains transparent: its rendering, events, and cleanup belong to the containing island.
80
+
49
81
  ---
50
82
 
51
83
  ## Builder API
52
84
 
53
- Every island starts from the `ilha` builder object (or `ilha.input<T>()` / `ilha.input(schema)` if you need typed props).
85
+ Use the `ilha` builder when an island needs capabilities such as typed input, state, derived values, actions, effects, or lifecycle hooks.
54
86
 
55
87
  ### `ilha.input<T>()` / `ilha.input(schema)`
56
88
 
@@ -99,7 +131,7 @@ state.count(); // → 0 (read)
99
131
  state.count(5); // → sets to 5 (write)
100
132
  ```
101
133
 
102
- Inside `html\`\``, you can interpolate signal accessors directly **without calling them** — `ilha` detects signal accessors and calls them for you, also applying HTML escaping:
134
+ Inside `html\`\``, you can interpolate signal accessors directly **without calling them** —`ilha` detects signal accessors and calls them for you, also applying HTML escaping:
103
135
 
104
136
  ```ts
105
137
  html`<p>${state.count}</p>`; // same as html`<p>${state.count()}</p>`
@@ -129,15 +161,37 @@ Read with `derived.name()` like state. Each accessor also exposes `loading`, `va
129
161
 
130
162
  ---
131
163
 
164
+ ### `.action(key, fn)`
165
+
166
+ Declares a reusable synchronous or asynchronous operation. Define actions before consumers, then call them from lowercase native event handlers:
167
+
168
+ ```tsx
169
+ const Counter = ilha
170
+ .state("count", 0)
171
+ .action("increment", (amount: number, { state }) => {
172
+ state.count((count) => count + amount);
173
+ return state.count();
174
+ })
175
+ .render(({ state, action }) => (
176
+ <button onclick={() => action.increment(1)}>{state.count()}</button>
177
+ ));
178
+ ```
179
+
180
+ Async actions expose reactive `pending`, `data`, and `error` properties. Action callbacks receive state, derived values, input, host, and an abort signal.
181
+
182
+ ---
183
+
132
184
  ### `.on(selector, handler)`
133
185
 
134
- Attaches a delegated event listener. The selector string uses the format `"cssSelector@eventName"`. Omit the selector part to target the island host itself.
186
+ Prefer lowercase native event props for handlers owned by one rendered element. Use `.on()` when you need a CSS selector, an island-host listener, the full handler context, or combined modifiers.
187
+
188
+ The selector string uses the format `"cssSelector@eventName"`. Omit the selector part to target the island host itself.
135
189
 
136
190
  ```ts
137
191
  ilha
138
192
  .state("count", 0)
139
- .on("@click", ({ state }) => state.count(state.count() + 1)) // host click
140
- .on("button.inc@click", ({ state }) => state.count(state.count() + 1)) // child click
193
+ .on("@click", ({ state }) => state.count((count) => count + 1)) // host click
194
+ .on("button.inc@click", ({ state }) => state.count((count) => count + 1)) // child click
141
195
  .on("input@input", ({ state, event }) => {
142
196
  state.query((event.target as HTMLInputElement).value);
143
197
  })
@@ -301,7 +355,7 @@ ilha
301
355
  .state("count", 0)
302
356
  .on("@click", ({ state }) => {
303
357
  if (state.count() > 5) throw new Error("too many clicks");
304
- state.count(state.count() + 1);
358
+ state.count((count) => count + 1);
305
359
  })
306
360
  .onError(({ error, source }) => {
307
361
  console.error(`[${source}] ${error.message}`);
@@ -388,12 +442,12 @@ ilha.css(styles).render(() => `<div class="card">…</div>`);
388
442
 
389
443
  ### Composing Islands
390
444
 
391
- Child islands are interpolated directly inside a parent's html&grave;&grave; template. During SSR the child's HTML is rendered inline; during client mount the child is activated independently inside its own host element.
445
+ Child islands are interpolated directly inside a parent's html template. During SSR the child's HTML is rendered inline; during client mount the child is activated independently inside its own host element.
392
446
 
393
447
  ```ts
394
- const Icon = ilha.render(() => `<svg>…</svg>`);
448
+ const Icon = ilha(() => `<svg>…</svg>`);
395
449
 
396
- const Card = ilha.render(
450
+ const Card = ilha(
397
451
  () => html`
398
452
  <div class="card">
399
453
  ${Icon}
@@ -403,21 +457,19 @@ const Card = ilha.render(
403
457
  );
404
458
  ```
405
459
 
406
- **Passing props** — call the child island with a props object:
460
+ **Passing props** — use JSX props when composing a child island:
407
461
 
408
- ```ts
462
+ ```tsx
409
463
  const Badge = ilha
410
464
  .input(z.object({ label: z.string(), color: z.string().default("teal") }))
411
- .render(({ input }) => html`<span style="background:${input.color}">${input.label}</span>`);
465
+ .render(({ input }) => <span style={{ background: input.color }}>{input.label}</span>);
412
466
 
413
- const Card = ilha.render(
414
- () => html`
415
- <div>
416
- ${Badge({ label: "New", color: "coral" })}
417
- <p>Content</p>
418
- </div>
419
- `,
420
- );
467
+ const Card = ilha(() => (
468
+ <div>
469
+ <Badge label="New" color="coral" />
470
+ <p>Content</p>
471
+ </div>
472
+ ));
421
473
  ```
422
474
 
423
475
  Props (including JSX `children` and function callbacks) stay on the **live slot map** during parent render/mount. `data-ilha-props` only carries JSON-safe scalars for hydration hints — never rely on it for children or handlers. Nested islands under layout shells (`defineLayout` / `wrapLayout`) use the same path as page-level composition.
@@ -425,7 +477,7 @@ Props (including JSX `children` and function callbacks) stay on the **live slot
425
477
  **Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
426
478
 
427
479
  ```ts
428
- const List = ilha.render(
480
+ const List = ilha(
429
481
  () =>
430
482
  html`<ul>
431
483
  ${items.map((item) => html`<li>${Item.key(item.id)({ name: item.name })}</li>`)}
@@ -456,25 +508,32 @@ The `leave` transition is awaited before cleanup runs.
456
508
 
457
509
  ---
458
510
 
459
- ### `.render(fn)`
511
+ ### `.render(fn)` and `ilha(fn)`
460
512
 
461
- Finalises the builder and returns an `Island`. The render function receives `{ state, derived, input }` and must return a string or `RawHtml`.
513
+ `.render()` finalises a configured builder and returns an `Island`. The render function receives `{ state, derived, action, input }` and returns a string or `RawHtml`.
462
514
 
463
515
  ```ts
464
- const MyIsland = ilha.state("x", 1).render(({ state, input }) => html`<p>${state.x}</p>`);
516
+ const MyIsland = ilha.state("x", 1).render(({ state }) => html`<p>${state.x()}</p>`);
517
+ ```
518
+
519
+ For an island without builder configuration, `ilha(fn)` is shorthand for `ilha.render(fn)`:
520
+
521
+ ```tsx
522
+ const StaticIsland = ilha(() => <p>Hello</p>);
523
+ const Label = ilha<{ label: string }>(({ input }) => <p>{input.label}</p>);
465
524
  ```
466
525
 
467
526
  ---
468
527
 
469
528
  ## Island Interface
470
529
 
471
- Every island produced by `.render()` exposes:
530
+ Every island produced by `.render()` or `ilha(fn)` exposes:
472
531
 
473
- ### `island(props?)` / `island.toString(props?)`
532
+ ### `island.toString(props?)`
474
533
 
475
- Render the island to an HTML string synchronously. `island.toString()` is always synchronous. If `.derived()` entries have async functions, they render in `loading: true` state when called synchronously.
534
+ Render the island to an HTML string synchronously. If `.derived()` entries have async functions, they render in `loading: true` state.
476
535
 
477
- Calling `island(props)` returns a `string` (or `Promise<string>` when derived values are async and awaited).
536
+ Use `await island(props)` instead when asynchronous derived values must settle before rendering. Always include `await` when using the callable form so a possible `Promise<string>` is never mistaken for a string.
478
537
 
479
538
  ```ts
480
539
  MyIsland.toString(); // always sync
@@ -580,8 +639,8 @@ import ilha, { signal, html } from "ilha";
580
639
 
581
640
  const username = signal("anonymous");
582
641
 
583
- const Header = ilha.render(() => html`<header>Hi, ${username()}!</header>`);
584
- const Footer = ilha.render(() => html`<footer>Logged in as ${username()}</footer>`);
642
+ const Header = ilha(() => html`<header>Hi, ${username()}!</header>`);
643
+ const Footer = ilha(() => html`<footer>Logged in as ${username()}</footer>`);
585
644
 
586
645
  // Both islands re-render when `username` changes from anywhere.
587
646
  username("alice");
@@ -792,11 +851,13 @@ import ilha from "ilha";
792
851
 
793
852
  const Counter = ilha
794
853
  .state("count", 0)
795
- .on("button@click", ({ state }) => state.count(state.count() + 1))
796
- .render(({ state }) => (
854
+ .action("increment", (_, { state }) => {
855
+ state.count((count) => count + 1);
856
+ })
857
+ .render(({ state, action }) => (
797
858
  <div>
798
859
  <p>Count: {state.count}</p>
799
- <button>Increment</button>
860
+ <button onclick={action.increment}>Increment</button>
800
861
  </div>
801
862
  ));
802
863
  ```
@@ -814,25 +875,25 @@ Interpolated children follow the same rules as the `html` tag — strings are es
814
875
  | `bind:*` | Two-way bindings, same as in `html` templates — pass a signal accessor: `<input bind:value={state.name} />` |
815
876
  | `key` | Keys a child island for reorder-safe rendering (same as `.key()`). Keys must be non-empty and must not contain `:` |
816
877
 
817
- For safety, `on*` attributes (e.g. `onclick`) and `srcdoc` are stripped — attach event listeners with `.on()` instead — and URL attributes (`href`, `src`, `action`, …) with unsafe schemes like `javascript:` are dropped.
878
+ Lowercase native event props such as `onclick={handler}` attach with `addEventListener` during island mount; executable handlers never become inline SSR attributes. `srcdoc` is stripped, and URL attributes (`href`, `src`, `action`, …) with unsafe schemes such as `javascript:` are dropped.
818
879
 
819
880
  ### Islands as components
820
881
 
821
- Islands are plain functions, so they compose as JSX components — props and keys work as you'd expect:
882
+ Islands are callable JSX components, so props and keys work as expected. Unlike transparent plain components, each island keeps its own reactive scope and lifecycle:
822
883
 
823
884
  ```tsx
824
885
  const Badge = ilha
825
886
  .input<{ label: string }>()
826
887
  .render(({ input }) => <span class="badge">{input.label}</span>);
827
888
 
828
- const Card = ilha.render(() => (
889
+ const Card = ilha(() => (
829
890
  <div class="card">
830
891
  <Badge label="New" />
831
892
  <p>Card content</p>
832
893
  </div>
833
894
  ));
834
895
 
835
- const List = ilha.render(() => (
896
+ const List = ilha(() => (
836
897
  <ul>
837
898
  {items.map((item) => (
838
899
  <li>
@@ -56,9 +56,21 @@ interface IslandCall {
56
56
  props: Record<string, unknown> | undefined;
57
57
  key: string | undefined;
58
58
  }
59
- interface MarkedSignalAccessor<T> {
59
+ type NativeEventModifier = "abortable" | "once" | "capture" | "passive";
60
+ interface NativeEventContext {
61
+ readonly signal: AbortSignal;
62
+ }
63
+ type NativeEventHandler<E extends Event = Event> = (event: E, context: NativeEventContext) => unknown;
64
+ type NonFunctionValue<T> = T extends ((...args: any[]) => any) ? never : T;
65
+ type SignalSetter<T> = NonFunctionValue<T> | ((previous: T) => T);
66
+ declare const SIGNAL_WRITER_TYPE: unique symbol;
67
+ /** @internal Type-level write target carried by signal accessors. */
68
+ interface SignalWriter<T> {
69
+ readonly [SIGNAL_WRITER_TYPE]?: (value: T) => void;
70
+ }
71
+ interface MarkedSignalAccessor<T> extends SignalWriter<T> {
60
72
  (): T;
61
- (...args: [value: T]): void;
73
+ (...args: [value: SignalSetter<T>]): void;
62
74
  select<S>(selector: (state: T) => S): MarkedSignalAccessor<S>;
63
75
  [SIGNAL_ACCESSOR]: true;
64
76
  }
@@ -67,7 +79,7 @@ declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (str
67
79
  declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
68
80
  type ContextSignal<T> = {
69
81
  (): T;
70
- (value: T): void;
82
+ (value: SignalSetter<T>): void;
71
83
  };
72
84
  declare function ilhaContextFn<T>(key: string, initial: T): ContextSignal<T>;
73
85
  /**
@@ -149,11 +161,11 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
149
161
  key: string;
150
162
  fn: DerivedFn<TInput, TStateMap, unknown>;
151
163
  }
152
- type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedAccessor<TDerivedMap[K]> };
164
+ type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedAccessor<TDerivedMap[K]>; };
153
165
  type ExternalSignal<T = unknown> = SignalAccessor<T>;
154
166
  type SignalAccessor<T> = MarkedSignalAccessor<T>;
155
167
  type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
156
- type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]> };
168
+ type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]>; };
157
169
  interface HydratableOptions {
158
170
  name: string;
159
171
  as?: string;
@@ -187,14 +199,32 @@ interface KeyedIsland<TInput> {
187
199
  [ISLAND_CALL]: true;
188
200
  }
189
201
  type AnyIsland = Island<any, any>;
190
- type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> = {
202
+ type AnyActionFn = (props: any, ctx: any) => unknown;
203
+ type ActionMap = Record<string, AnyActionFn>;
204
+ type ActionCall<P> = [P] extends [undefined] ? () => void : unknown extends P ? () => void : (props: P) => void;
205
+ type ActionAccessor<P = undefined, R = void> = ActionCall<P> & {
206
+ readonly pending: boolean;
207
+ readonly data: Awaited<R> | undefined;
208
+ readonly error: Error | undefined;
209
+ };
210
+ type IslandActions<TActionMap extends ActionMap> = { readonly [K in keyof TActionMap]: TActionMap[K] extends ((props: infer P, ctx: any) => infer R) ? ActionAccessor<P, R> : never; };
211
+ type ActionContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
191
212
  state: IslandState<TStateMap>;
192
213
  derived: IslandDerived<TDerivedMap>;
193
214
  input: TInput;
215
+ host: Element;
216
+ signal: AbortSignal;
194
217
  };
195
- type EffectContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
218
+ type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TActionMap extends ActionMap = Record<never, never>> = {
196
219
  state: IslandState<TStateMap>;
197
220
  derived: IslandDerived<TDerivedMap>;
221
+ action: IslandActions<TActionMap>;
222
+ input: TInput;
223
+ };
224
+ type EffectContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> = {
225
+ state: IslandState<TStateMap>;
226
+ derived: IslandDerived<TDerivedMap>;
227
+ action: IslandActions<TActionMap>;
198
228
  input: TInput;
199
229
  host: Element;
200
230
  /**
@@ -205,16 +235,18 @@ type EffectContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMa
205
235
  */
206
236
  signal: AbortSignal;
207
237
  };
208
- type OnMountContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
238
+ type OnMountContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> = {
209
239
  state: IslandState<TStateMap>;
210
240
  derived: IslandDerived<TDerivedMap>;
241
+ action: IslandActions<TActionMap>;
211
242
  input: TInput;
212
243
  host: Element;
213
244
  hydrated: boolean;
214
245
  };
215
- type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
246
+ type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> = {
216
247
  state: IslandState<TStateMap>;
217
248
  derived: IslandDerived<TDerivedMap>;
249
+ action: IslandActions<TActionMap>;
218
250
  input: TInput;
219
251
  host: Element;
220
252
  target: Element;
@@ -231,9 +263,10 @@ type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedM
231
263
  };
232
264
  type HTMLEventFor<E extends string> = E extends keyof HTMLElementEventMap ? HTMLElementEventMap[E] : Event;
233
265
  type HTMLTargetFor<E extends string> = E extends keyof HTMLElementEventMap ? NonNullable<HTMLElementEventMap[E]["target"]> extends Element ? NonNullable<HTMLElementEventMap[E]["target"]> : Element : Element;
234
- type HandlerContextFor<TInput, TStateMap extends Record<string, unknown>, TEventName extends string, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
266
+ type HandlerContextFor<TInput, TStateMap extends Record<string, unknown>, TEventName extends string, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> = {
235
267
  state: IslandState<TStateMap>;
236
268
  derived: IslandDerived<TDerivedMap>;
269
+ action: IslandActions<TActionMap>;
237
270
  input: TInput;
238
271
  host: Element;
239
272
  target: HTMLTargetFor<TEventName>;
@@ -250,20 +283,24 @@ interface StateEntry<TInput> {
250
283
  key: string;
251
284
  init: StateInit<TInput, unknown>;
252
285
  }
253
- interface OnEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> {
286
+ interface ActionEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
287
+ key: string;
288
+ fn: (props: unknown, ctx: ActionContext<TInput, TStateMap, TDerivedMap>) => unknown;
289
+ }
290
+ interface OnEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> {
254
291
  selector: string;
255
292
  event: string;
256
293
  options: AddEventListenerOptions;
257
294
  abortable: boolean;
258
- handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>;
295
+ handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap, TActionMap>) => void | Promise<void>;
259
296
  }
260
- interface EffectEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
261
- fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void;
297
+ interface EffectEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TActionMap extends ActionMap = Record<never, never>> {
298
+ fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap, TActionMap>) => (() => void) | void;
262
299
  }
263
300
  /** Where the error originated. `"on"` covers sync throws and async rejections
264
301
  * from `.on()` handlers; `"effect"` covers sync throws from `.effect()` runs
265
302
  * (async work spawned inside an effect is not awaited by the runtime). */
266
- type ErrorSource = "on" | "effect" | "mount" | "transition";
303
+ type ErrorSource = "on" | "effect" | "mount" | "transition" | "action";
267
304
  /**
268
305
  * Register a global error handler invoked when any island reports an error
269
306
  * (from .on, .effect, .onMount, or transitions) and has no local .onError()
@@ -271,19 +308,20 @@ type ErrorSource = "on" | "effect" | "mount" | "transition";
271
308
  * are handled locally and do not reach the global sink.
272
309
  */
273
310
  declare function onUncaughtError(fn: (error: Error, source: ErrorSource) => void): () => void;
274
- type ErrorContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
311
+ type ErrorContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> = {
275
312
  error: Error;
276
313
  source: ErrorSource;
277
314
  state: IslandState<TStateMap>;
278
315
  derived: IslandDerived<TDerivedMap>;
316
+ action: IslandActions<TActionMap>;
279
317
  input: TInput;
280
318
  host: Element;
281
319
  };
282
- interface OnErrorEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
283
- fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap>) => void;
320
+ interface OnErrorEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TActionMap extends ActionMap = Record<never, never>> {
321
+ fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap, TActionMap>) => void;
284
322
  }
285
- interface OnMountEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
286
- fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void;
323
+ interface OnMountEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TActionMap extends ActionMap = Record<never, never>> {
324
+ fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap, TActionMap>) => (() => void) | void;
287
325
  }
288
326
  interface TransitionOptions {
289
327
  enter?: (host: Element) => Promise<void> | void;
@@ -296,48 +334,58 @@ interface MountOptions {
296
334
  interface MountResult {
297
335
  unmount: () => void | Promise<void>;
298
336
  }
299
- interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
337
+ interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TActionMap extends ActionMap = Record<never, never>> {
300
338
  schema: StandardSchemaV1 | null;
301
339
  /** Shallow defaults merged before props (POJO `.input({ ... })` only). */
302
340
  defaultInput: Record<string, unknown> | null;
303
341
  states: StateEntry<TInput>[];
304
342
  deriveds: DerivedEntry<TInput, TStateMap>[];
305
- ons: OnEntry<TInput, TStateMap, TDerivedMap>[];
306
- effects: EffectEntry<TInput, TStateMap, TDerivedMap>[];
307
- onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap>[];
308
- onErrors: OnErrorEntry<TInput, TStateMap, TDerivedMap>[];
343
+ actions: ActionEntry<TInput, TStateMap, TDerivedMap>[];
344
+ ons: OnEntry<TInput, TStateMap, TDerivedMap, TActionMap>[];
345
+ effects: EffectEntry<TInput, TStateMap, TDerivedMap, TActionMap>[];
346
+ onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap, TActionMap>[];
347
+ onErrors: OnErrorEntry<TInput, TStateMap, TDerivedMap, TActionMap>[];
309
348
  transition: TransitionOptions | null;
310
349
  css: string | null;
311
350
  /** Slot wrapper tag when this island is embedded in a parent (default div). */
312
351
  as: string | null;
313
352
  }
314
- declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> {
315
- readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>;
316
- constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>);
317
- input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<never, never>, Record<never, never>>;
318
- input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<never, never>, Record<never, never>>;
319
- input<T extends Record<string, unknown>>(defaults: T): IlhaBuilder<T, Record<never, never>, Record<never, never>>;
320
- as<Tag extends string>(tag: Tag): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
321
- state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap>;
322
- derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>>;
323
- on<S extends string>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
324
- effect(fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
325
- onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
326
- onError(fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap>) => void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
327
- transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
328
- css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
329
- render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap>) => string | RawHtml): Island<TInput, TStateMap>;
353
+ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>, TActionMap extends ActionMap = Record<never, never>> {
354
+ readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap, TActionMap>;
355
+ constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap, TActionMap>);
356
+ input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<never, never>, Record<never, never>, Record<never, never>>;
357
+ input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<never, never>, Record<never, never>, Record<never, never>>;
358
+ input<T extends Record<string, unknown>>(defaults: T): IlhaBuilder<T, Record<never, never>, Record<never, never>, Record<never, never>>;
359
+ as<Tag extends string>(tag: Tag): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
360
+ state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap, TActionMap>;
361
+ derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>, TActionMap>;
362
+ action<K extends string, P = undefined, R = void>(key: K, fn: (props: P, ctx: ActionContext<TInput, TStateMap, TDerivedMap>) => R): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap & Record<K, typeof fn>>;
363
+ on<S extends string>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap, TActionMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap, TActionMap> : HandlerContext<TInput, TStateMap, TDerivedMap, TActionMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
364
+ effect(fn: (ctx: EffectContext<TInput, TStateMap, TDerivedMap, TActionMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
365
+ onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap, TActionMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
366
+ onError(fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap, TActionMap>) => void): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
367
+ transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
368
+ css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<TInput, TStateMap, TDerivedMap, TActionMap>;
369
+ render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap, TActionMap>) => string | RawHtml): Island<TInput, TStateMap>;
330
370
  }
331
371
  declare function ilhaFrom<TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>): (() => void) | null;
332
372
  type IslandRegistry = Record<string, AnyIsland>;
333
373
  declare function mountAll(registry: IslandRegistry, options?: MountOptions): MountResult;
334
- declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
374
+ type RootInput = Record<string, unknown>;
375
+ type RootState = Record<never, never>;
376
+ type RootDerived = Record<never, never>;
377
+ type RootActions = Record<never, never>;
378
+ type RootBuilder = IlhaBuilder<RootInput, RootState, RootDerived, RootActions>;
379
+ type DirectIslandFactory = <TInput extends Record<string, unknown> = RootInput>(fn: (ctx: RenderContext<TInput, RootState, RootDerived, RootActions>) => string | RawHtml) => Island<TInput, RootState>;
380
+ declare const ilha: RootBuilder & DirectIslandFactory & {
335
381
  html: typeof ilhaHtml;
336
382
  raw: typeof ilhaRaw;
337
383
  mount: typeof mountAll;
338
384
  from: typeof ilhaFrom;
339
385
  context: typeof ilhaContextFn & {
340
- /** Remove a context signal from the registry. Returns true if it existed. */delete(key: string): boolean; /** Remove all context signals from the registry (e.g. between tests). */
386
+ /** Remove a context signal from the registry. Returns true if it existed. */
387
+ delete(key: string): boolean;
388
+ /** Remove all context signals from the registry (e.g. between tests). */
341
389
  clear(): void;
342
390
  };
343
391
  signal: typeof ilhaSignal;
@@ -346,16 +394,28 @@ declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, R
346
394
  untrack: typeof untrack;
347
395
  onUncaughtError: typeof onUncaughtError;
348
396
  };
397
+ /** @internal Used by the separate JSX runtime to register a native event handler. */
398
+ declare function __ilhaJsxEvent({ type, handler, modifier }: {
399
+ type: string;
400
+ handler: NativeEventHandler;
401
+ modifier?: NativeEventModifier;
402
+ }): number | undefined;
349
403
  /** @internal Used by the separate JSX runtime entry to preserve island slot composition. */
350
- declare function __ilhaJsxSlot(island: unknown, props: Record<string, unknown> | undefined, key: string | undefined): RawHtml;
404
+ declare function __ilhaJsxSlot({ island, props, key }: {
405
+ island: unknown;
406
+ props: Record<string, unknown> | undefined;
407
+ key: string | undefined;
408
+ }): RawHtml;
351
409
  declare const html: typeof ilhaHtml;
352
410
  declare const raw: typeof ilhaRaw;
353
411
  declare const css: typeof ilhaCss;
354
412
  declare const mount: typeof mountAll;
355
413
  declare const from: typeof ilhaFrom;
356
414
  declare const context: typeof ilhaContextFn & {
357
- /** Remove a context signal from the registry. Returns true if it existed. */delete(key: string): boolean; /** Remove all context signals from the registry (e.g. between tests). */
415
+ /** Remove a context signal from the registry. Returns true if it existed. */
416
+ delete(key: string): boolean;
417
+ /** Remove all context signals from the registry (e.g. between tests). */
358
418
  clear(): void;
359
419
  };
360
420
  //#endregion
361
- export { ilhaSignal as A, context as C, ilha as D, html as E, onUncaughtError as M, raw as N, ilhaComputed as O, untrack as P, batch as S, from as T, MountResult as _, ErrorSource as a, SignalAccessor as b, HandlerContextFor as c, ISLAND_MOUNT_INTERNAL as d, Island as f, MountOptions as g, KeyedIsland as h, ErrorContext as i, mount as j, ilhaEffect as k, HydratableOptions as l, IslandState as m, DerivedValue as n, ExternalSignal as o, IslandDerived as p, EffectContext as r, HandlerContext as s, DerivedAccessor as t, ISLAND_MOUNT_HANDLES as u, OnMountContext as v, css as w, __ilhaJsxSlot as x, RawHtml as y };
421
+ export { __ilhaJsxSlot as A, mount as B, NativeEventModifier as C, SignalSetter as D, SignalAccessor as E, html as F, raw as H, ilha as I, ilhaComputed as L, context as M, css as N, SignalWriter as O, from as P, ilhaEffect as R, NativeEventHandler as S, RawHtml as T, untrack as U, onUncaughtError as V, IslandState as _, EffectContext as a, MountResult as b, ExternalSignal as c, HydratableOptions as d, ISLAND_MOUNT_HANDLES as f, IslandDerived as g, IslandActions as h, DerivedValue as i, batch as j, __ilhaJsxEvent as k, HandlerContext as l, Island as m, ActionContext as n, ErrorContext as o, ISLAND_MOUNT_INTERNAL as p, DerivedAccessor as r, ErrorSource as s, ActionAccessor as t, HandlerContextFor as u, KeyedIsland as v, OnMountContext as w, NativeEventContext as x, MountOptions as y, ilhaSignal as z };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as ilhaSignal, C as context, D as ilha, E as html, M as onUncaughtError, N as raw, O as ilhaComputed, P as untrack, S as batch, T as from, _ as MountResult, a as ErrorSource, b as SignalAccessor, c as HandlerContextFor, d as ISLAND_MOUNT_INTERNAL, f as Island, g as MountOptions, h as KeyedIsland, i as ErrorContext, j as mount, k as ilhaEffect, l as HydratableOptions, m as IslandState, n as DerivedValue, o as ExternalSignal, p as IslandDerived, r as EffectContext, s as HandlerContext, t as DerivedAccessor, u as ISLAND_MOUNT_HANDLES, v as OnMountContext, w as css, x as __ilhaJsxSlot, y as RawHtml } from "./index-Dx_q_SGo.js";
2
- export { DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };
1
+ import { A as __ilhaJsxSlot, B as mount, C as NativeEventModifier, D as SignalSetter, E as SignalAccessor, F as html, H as raw, I as ilha, L as ilhaComputed, M as context, N as css, O as SignalWriter, P as from, R as ilhaEffect, S as NativeEventHandler, T as RawHtml, U as untrack, V as onUncaughtError, _ as IslandState, a as EffectContext, b as MountResult, c as ExternalSignal, d as HydratableOptions, f as ISLAND_MOUNT_HANDLES, g as IslandDerived, h as IslandActions, i as DerivedValue, j as batch, k as __ilhaJsxEvent, l as HandlerContext, m as Island, n as ActionContext, o as ErrorContext, p as ISLAND_MOUNT_INTERNAL, r as DerivedAccessor, s as ErrorSource, t as ActionAccessor, u as HandlerContextFor, v as KeyedIsland, w as OnMountContext, x as NativeEventContext, y as MountOptions, z as ilhaSignal } from "./index-Bi_I3qEL.js";
2
+ export { ActionAccessor, ActionContext, DerivedAccessor, DerivedValue, EffectContext, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_HANDLES, ISLAND_MOUNT_INTERNAL, Island, IslandActions, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, NativeEventContext, NativeEventHandler, NativeEventModifier, OnMountContext, RawHtml, SignalAccessor, SignalSetter, SignalWriter, __ilhaJsxEvent, __ilhaJsxSlot, batch, ilhaComputed as computed, context, css, ilha as default, ilhaEffect as effect, from, html, ilhaSignal, ilhaSignal as signal, mount, onUncaughtError, raw, untrack };