ilha 0.4.4 → 0.6.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 +28 -36
- package/dist/index.d.ts +35 -28
- package/dist/index.js +393 -78
- package/dist/jsx-dev-runtime.d.ts +3 -0
- package/dist/jsx-dev-runtime.js +2 -0
- package/dist/jsx-runtime.d.ts +3 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/jsx-types-DSrM2NdI.d.ts +26 -0
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -326,40 +326,6 @@ An error thrown inside an `.onError()` handler does not break other registered h
|
|
|
326
326
|
|
|
327
327
|
---
|
|
328
328
|
|
|
329
|
-
### `.bind(selector, stateKey | externalSignal)`
|
|
330
|
-
|
|
331
|
-
Two-way binds a form element to a state key or an external signal. Handles `input`, `select`, `textarea`, `checkbox`, `radio`, and `number` inputs automatically.
|
|
332
|
-
|
|
333
|
-
```ts
|
|
334
|
-
ilha
|
|
335
|
-
.state("name", "")
|
|
336
|
-
.state("agreed", false)
|
|
337
|
-
.bind("input.name", "name")
|
|
338
|
-
.bind("input[type=checkbox]", "agreed")
|
|
339
|
-
.render(
|
|
340
|
-
({ state }) => html`
|
|
341
|
-
<form>
|
|
342
|
-
<input class="name" value="${state.name}" />
|
|
343
|
-
<input type="checkbox" />
|
|
344
|
-
<p>Hello, ${state.name}! Agreed: ${state.agreed}</p>
|
|
345
|
-
</form>
|
|
346
|
-
`,
|
|
347
|
-
);
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
You can also bind to an external signal — either a free-standing one created with `signal()` or a named global one created with `context()`:
|
|
351
|
-
|
|
352
|
-
```ts
|
|
353
|
-
import { signal, context } from "ilha";
|
|
354
|
-
|
|
355
|
-
const username = signal("");
|
|
356
|
-
const theme = context("app.theme", "light");
|
|
357
|
-
|
|
358
|
-
ilha.bind("input.name", username).bind("select.theme", theme).render(/* … */);
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
329
|
### `.css(strings, ...values)`
|
|
364
330
|
|
|
365
331
|
Attaches scoped styles to the island. Accepts a tagged template literal or a plain string. The CSS is automatically wrapped in a `@scope` rule bounded to the island host, so styles are contained within the island and do not leak into child islands.
|
|
@@ -616,7 +582,7 @@ const Footer = ilha.render(() => html`<footer>Logged in as ${username()}</footer
|
|
|
616
582
|
username("alice");
|
|
617
583
|
```
|
|
618
584
|
|
|
619
|
-
|
|
585
|
+
Works naturally with `bind:` template syntax for two-way form bindings against module-level state.
|
|
620
586
|
|
|
621
587
|
---
|
|
622
588
|
|
|
@@ -635,7 +601,7 @@ theme("dark"); // → sets to "dark"
|
|
|
635
601
|
|
|
636
602
|
Safe to call in both SSR and browser environments.
|
|
637
603
|
|
|
638
|
-
> **`signal()` vs `context()`** — both return the same accessor shape and can be
|
|
604
|
+
> **`signal()` vs `context()`** — both return the same accessor shape and can be used with `bind:` template syntax. Use `signal()` for one-off shared state where you'd hold the reference yourself; use `context()` when you want a name-keyed registry so the same signal can be looked up from anywhere by string key.
|
|
639
605
|
|
|
640
606
|
---
|
|
641
607
|
|
|
@@ -713,6 +679,32 @@ Interpolation rules:
|
|
|
713
679
|
| Island / Island call | Emitted as `data-ilha-slot` host element |
|
|
714
680
|
| Array | Each item processed recursively (no commas) |
|
|
715
681
|
|
|
682
|
+
**Template bindings** — use `bind:property=${signal}` inside `html\`\`` to create two-way bindings between form elements and signals:
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
ilha.state("name", "").render(
|
|
686
|
+
({ state }) => html`
|
|
687
|
+
<input bind:value=${state.name} />
|
|
688
|
+
<p>Hello, ${state.name()}!</p>
|
|
689
|
+
`,
|
|
690
|
+
);
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Supported bindings:
|
|
694
|
+
|
|
695
|
+
| Binding | Element | Bound property | Trigger event |
|
|
696
|
+
| -------------------- | ------------------------------------------------- | ------------------- | ------------- |
|
|
697
|
+
| `bind:value` | `<input>`, `<textarea>`, `<select>` | `value` | `input` |
|
|
698
|
+
| `bind:valueAsNumber` | `<input type="number">` | `valueAsNumber` | `input` |
|
|
699
|
+
| `bind:valueAsDate` | `<input type="date">` | `valueAsDate` | `input` |
|
|
700
|
+
| `bind:checked` | `<input type="checkbox">` | `checked` | `change` |
|
|
701
|
+
| `bind:group` | `<input type="radio">`, `<input type="checkbox">` | `checked` / `value` | `change` |
|
|
702
|
+
| `bind:open` | `<details>` | `open` | `toggle` |
|
|
703
|
+
| `bind:files` | `<input type="file">` | `files` | `change` |
|
|
704
|
+
| `bind:this` | Any element | element reference | — |
|
|
705
|
+
|
|
706
|
+
`bind:group` connects multiple inputs to a single signal — radio buttons hold the selected `value`, checkboxes hold an array of checked values. `bind:this` writes the DOM element into a signal on mount and `null` on unmount. External signals from `signal()` or `context()` work as binding targets too, enabling shared state across islands.
|
|
707
|
+
|
|
716
708
|
**List rendering pattern:**
|
|
717
709
|
|
|
718
710
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,14 @@ interface MarkedSignalAccessor<T> {
|
|
|
55
55
|
}
|
|
56
56
|
declare function ilhaRaw(value: string): RawHtml;
|
|
57
57
|
declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (string | number)[]): string;
|
|
58
|
+
type JsxChild = unknown;
|
|
59
|
+
type JsxProps = Record<string, unknown> | null | undefined;
|
|
60
|
+
type JsxType = string | ((props: Record<string, unknown>) => unknown);
|
|
61
|
+
declare function ilhaJsx(type: JsxType, props: JsxProps, ...children: JsxChild[]): RawHtml;
|
|
62
|
+
declare function ilhaFragment(props: {
|
|
63
|
+
children?: JsxChild;
|
|
64
|
+
} | null, ...children: JsxChild[]): RawHtml;
|
|
65
|
+
declare function ilhaJsxDEV(type: JsxType, props: JsxProps): RawHtml;
|
|
58
66
|
declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
|
|
59
67
|
type ContextSignal<T> = {
|
|
60
68
|
(): T;
|
|
@@ -64,7 +72,8 @@ declare function ilhaContext<T>(key: string, initial: T): ContextSignal<T>;
|
|
|
64
72
|
/**
|
|
65
73
|
* Create a free-standing reactive signal that lives outside any island.
|
|
66
74
|
* Useful for sharing state across islands without prop drilling, or for
|
|
67
|
-
* binding form inputs to module-level state via
|
|
75
|
+
* binding form inputs to module-level state via the `bind:value=${signal}`
|
|
76
|
+
* template syntax.
|
|
68
77
|
*
|
|
69
78
|
* The returned accessor is a getter when called with no arguments and a
|
|
70
79
|
* setter when called with one. Reading it inside a `.derived()`, `.effect()`,
|
|
@@ -106,16 +115,12 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
|
|
|
106
115
|
fn: DerivedFn<TInput, TStateMap, unknown>;
|
|
107
116
|
}
|
|
108
117
|
type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedValue<TDerivedMap[K]> };
|
|
109
|
-
|
|
118
|
+
interface ExternalSignal<T = unknown> {
|
|
110
119
|
(): T;
|
|
111
120
|
(value: T): void;
|
|
112
|
-
};
|
|
113
|
-
interface BindEntry<TStateMap extends Record<string, unknown>> {
|
|
114
|
-
selector: string;
|
|
115
|
-
target: (keyof TStateMap & string) | ExternalSignal;
|
|
116
121
|
}
|
|
117
122
|
type SignalAccessor<T> = MarkedSignalAccessor<T>;
|
|
118
|
-
type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> =
|
|
123
|
+
type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
|
|
119
124
|
type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]> };
|
|
120
125
|
interface HydratableOptions {
|
|
121
126
|
name: string;
|
|
@@ -156,14 +161,14 @@ type EffectContext<TInput, TStateMap extends Record<string, unknown>> = {
|
|
|
156
161
|
*/
|
|
157
162
|
signal: AbortSignal;
|
|
158
163
|
};
|
|
159
|
-
type OnMountContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<
|
|
164
|
+
type OnMountContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
|
|
160
165
|
state: IslandState<TStateMap>;
|
|
161
166
|
derived: IslandDerived<TDerivedMap>;
|
|
162
167
|
input: TInput;
|
|
163
168
|
host: Element;
|
|
164
169
|
hydrated: boolean;
|
|
165
170
|
};
|
|
166
|
-
type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<
|
|
171
|
+
type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
|
|
167
172
|
state: IslandState<TStateMap>;
|
|
168
173
|
derived: IslandDerived<TDerivedMap>;
|
|
169
174
|
input: TInput;
|
|
@@ -180,17 +185,15 @@ type HandlerContext<TInput, TStateMap extends Record<string, unknown>, TDerivedM
|
|
|
180
185
|
*/
|
|
181
186
|
signal: AbortSignal;
|
|
182
187
|
};
|
|
183
|
-
type
|
|
184
|
-
type
|
|
185
|
-
type
|
|
186
|
-
type OnSelectorString = `@${WithModifiers<HTMLEventName>}` | `${string}@${WithModifiers<HTMLEventName>}`;
|
|
187
|
-
type HandlerContextFor<TInput, TStateMap extends Record<string, unknown>, TEventName extends string, TDerivedMap extends Record<string, unknown> = Record<string, never>> = {
|
|
188
|
+
type HTMLEventFor<E extends string> = E extends keyof HTMLElementEventMap ? HTMLElementEventMap[E] : Event;
|
|
189
|
+
type HTMLTargetFor<E extends string> = E extends keyof HTMLElementEventMap ? NonNullable<HTMLElementEventMap[E]["target"]> extends Element ? NonNullable<HTMLElementEventMap[E]["target"]> : Element : Element;
|
|
190
|
+
type HandlerContextFor<TInput, TStateMap extends Record<string, unknown>, TEventName extends string, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
|
|
188
191
|
state: IslandState<TStateMap>;
|
|
189
192
|
derived: IslandDerived<TDerivedMap>;
|
|
190
193
|
input: TInput;
|
|
191
194
|
host: Element;
|
|
192
|
-
target:
|
|
193
|
-
event: TEventName
|
|
195
|
+
target: HTMLTargetFor<TEventName>;
|
|
196
|
+
event: HTMLEventFor<TEventName>;
|
|
194
197
|
/**
|
|
195
198
|
* AbortSignal that fires when the island unmounts. If the handler's selector
|
|
196
199
|
* was registered with the `:abortable` modifier, the signal is also aborted
|
|
@@ -203,7 +206,7 @@ interface StateEntry<TInput> {
|
|
|
203
206
|
key: string;
|
|
204
207
|
init: StateInit<TInput, unknown>;
|
|
205
208
|
}
|
|
206
|
-
interface OnEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<
|
|
209
|
+
interface OnEntry<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> {
|
|
207
210
|
selector: string;
|
|
208
211
|
event: string;
|
|
209
212
|
options: AddEventListenerOptions;
|
|
@@ -217,7 +220,7 @@ interface EffectEntry<TInput, TStateMap extends Record<string, unknown>> {
|
|
|
217
220
|
* from `.on()` handlers; `"effect"` covers sync throws from `.effect()` runs
|
|
218
221
|
* (async work spawned inside an effect is not awaited by the runtime). */
|
|
219
222
|
type ErrorSource = "on" | "effect";
|
|
220
|
-
type ErrorContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<
|
|
223
|
+
type ErrorContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> = {
|
|
221
224
|
error: Error;
|
|
222
225
|
source: ErrorSource;
|
|
223
226
|
state: IslandState<TStateMap>;
|
|
@@ -251,20 +254,16 @@ interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDeri
|
|
|
251
254
|
onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap>[];
|
|
252
255
|
onErrors: OnErrorEntry<TInput, TStateMap, TDerivedMap>[];
|
|
253
256
|
transition: TransitionOptions | null;
|
|
254
|
-
binds: BindEntry<TStateMap>[];
|
|
255
257
|
css: string | null;
|
|
256
258
|
}
|
|
257
|
-
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<
|
|
259
|
+
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<never, never>> {
|
|
258
260
|
readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>;
|
|
259
261
|
constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>);
|
|
260
|
-
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<
|
|
261
|
-
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<
|
|
262
|
+
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<never, never>, Record<never, never>>;
|
|
263
|
+
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<never, never>, Record<never, never>>;
|
|
262
264
|
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap>;
|
|
263
265
|
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>>;
|
|
264
|
-
|
|
265
|
-
bind<T>(selector: string, externalSignal: ExternalSignal<T>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
266
|
-
on<S extends OnSelectorString>(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>;
|
|
267
|
-
on(selectorOrCombined: string, handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
266
|
+
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>;
|
|
268
267
|
effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
269
268
|
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
270
269
|
onError(fn: (ctx: ErrorContext<TInput, TStateMap, TDerivedMap>) => void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
@@ -275,9 +274,13 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
|
|
|
275
274
|
declare function ilhaFrom<TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>): (() => void) | null;
|
|
276
275
|
type IslandRegistry = Record<string, AnyIsland>;
|
|
277
276
|
declare function mountAll(registry: IslandRegistry, options?: MountOptions): MountResult;
|
|
278
|
-
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<
|
|
277
|
+
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
|
|
279
278
|
html: typeof ilhaHtml;
|
|
280
279
|
raw: typeof ilhaRaw;
|
|
280
|
+
jsx: typeof ilhaJsx;
|
|
281
|
+
jsxs: typeof ilhaJsx;
|
|
282
|
+
jsxDEV: typeof ilhaJsxDEV;
|
|
283
|
+
Fragment: typeof ilhaFragment;
|
|
281
284
|
mount: typeof mountAll;
|
|
282
285
|
from: typeof ilhaFrom;
|
|
283
286
|
context: typeof ilhaContext;
|
|
@@ -287,9 +290,13 @@ declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>,
|
|
|
287
290
|
};
|
|
288
291
|
declare const html: typeof ilhaHtml;
|
|
289
292
|
declare const raw: typeof ilhaRaw;
|
|
293
|
+
declare const jsx: typeof ilhaJsx;
|
|
294
|
+
declare const jsxs: typeof ilhaJsx;
|
|
295
|
+
declare const jsxDEV: typeof ilhaJsxDEV;
|
|
296
|
+
declare const Fragment: typeof ilhaFragment;
|
|
290
297
|
declare const css: typeof ilhaCss;
|
|
291
298
|
declare const mount: typeof mountAll;
|
|
292
299
|
declare const from: typeof ilhaFrom;
|
|
293
300
|
declare const context: typeof ilhaContext;
|
|
294
301
|
//#endregion
|
|
295
|
-
export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, mount, raw, untrack };
|
|
302
|
+
export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
|
package/dist/index.js
CHANGED
|
@@ -109,7 +109,8 @@ function pushRenderCtx(liveHost, asyncChildren) {
|
|
|
109
109
|
slots: /* @__PURE__ */ new Map(),
|
|
110
110
|
positional: 0,
|
|
111
111
|
liveHost,
|
|
112
|
-
pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0
|
|
112
|
+
pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0,
|
|
113
|
+
binds: []
|
|
113
114
|
};
|
|
114
115
|
renderCtxStack.push(ctx);
|
|
115
116
|
return ctx;
|
|
@@ -190,8 +191,120 @@ function ilhaCss(strings, ...values) {
|
|
|
190
191
|
}
|
|
191
192
|
return result;
|
|
192
193
|
}
|
|
194
|
+
const SAFE_NAME_RE = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
|
|
195
|
+
const SAFE_BIND_LOCAL_RE = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
196
|
+
const VOID_ELEMENTS = new Set([
|
|
197
|
+
"area",
|
|
198
|
+
"base",
|
|
199
|
+
"br",
|
|
200
|
+
"col",
|
|
201
|
+
"embed",
|
|
202
|
+
"hr",
|
|
203
|
+
"img",
|
|
204
|
+
"input",
|
|
205
|
+
"link",
|
|
206
|
+
"meta",
|
|
207
|
+
"param",
|
|
208
|
+
"source",
|
|
209
|
+
"track",
|
|
210
|
+
"wbr"
|
|
211
|
+
]);
|
|
212
|
+
const SAFE_CSS_PROP_RE = /^(-{2}[a-zA-Z][a-zA-Z0-9-]*|-?[a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
213
|
+
const URL_ATTRS = new Set([
|
|
214
|
+
"href",
|
|
215
|
+
"src",
|
|
216
|
+
"action",
|
|
217
|
+
"formaction",
|
|
218
|
+
"cite",
|
|
219
|
+
"data",
|
|
220
|
+
"poster"
|
|
221
|
+
]);
|
|
222
|
+
const SAFE_URL_RE = /^(?!javascript:|data:text\/html|data:text\/xml|data:application\/xhtml\+xml|data:image\/svg|vbscript:)/i;
|
|
223
|
+
function normalizeClass(value) {
|
|
224
|
+
if (Array.isArray(value)) return value.filter(Boolean).join(" ");
|
|
225
|
+
if (value && typeof value === "object") return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([name]) => name).join(" ");
|
|
226
|
+
return String(value);
|
|
227
|
+
}
|
|
228
|
+
function normalizeJsxChildren(props, children) {
|
|
229
|
+
const propChildren = props && "children" in props ? props.children : void 0;
|
|
230
|
+
return (children.length > 0 ? children : propChildren === void 0 ? [] : [propChildren]).flat(1);
|
|
231
|
+
}
|
|
232
|
+
function serializeStyle(value) {
|
|
233
|
+
return Object.entries(value).map(([k, v]) => {
|
|
234
|
+
if (!SAFE_CSS_PROP_RE.test(k)) return "";
|
|
235
|
+
const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
236
|
+
let safeV = String(v).replace(/["<>{};]/g, "");
|
|
237
|
+
if (/expression\(/i.test(safeV) || /^javascript:/i.test(safeV)) return "";
|
|
238
|
+
return `${prop}:${safeV}`;
|
|
239
|
+
}).filter(Boolean).join(";");
|
|
240
|
+
}
|
|
241
|
+
function pushJsxAttr(chunks, values, name, value) {
|
|
242
|
+
if (value == null || value === false || name === "children" || name === "key") return;
|
|
243
|
+
if (name === "__proto__" || name === "constructor" || name === "prototype") return;
|
|
244
|
+
if (name.startsWith("bind:")) {
|
|
245
|
+
const [prefix, localName, ...rest] = name.split(":");
|
|
246
|
+
if (prefix !== "bind" || rest.length > 0 || !localName || !SAFE_BIND_LOCAL_RE.test(localName)) return;
|
|
247
|
+
if (!isSignalAccessor(value)) return;
|
|
248
|
+
const safeName = `${prefix}:${localName}`;
|
|
249
|
+
chunks[chunks.length - 1] += ` ${safeName}=`;
|
|
250
|
+
values.push(value);
|
|
251
|
+
chunks.push("");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!SAFE_NAME_RE.test(name)) return;
|
|
255
|
+
let safeName = name;
|
|
256
|
+
if (safeName === "className") safeName = "class";
|
|
257
|
+
if (safeName === "htmlFor") safeName = "for";
|
|
258
|
+
if (safeName.startsWith("on")) return;
|
|
259
|
+
if (safeName === "class") value = normalizeClass(value);
|
|
260
|
+
if (safeName === "style" && value && typeof value === "object") value = serializeStyle(value);
|
|
261
|
+
if ((URL_ATTRS.has(safeName) || /:(href|src|action|formaction|cite|data|poster)$/.test(safeName)) && typeof value === "string" && !SAFE_URL_RE.test(value.trim())) return;
|
|
262
|
+
if (value === true) {
|
|
263
|
+
chunks[chunks.length - 1] += ` ${safeName}`;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
chunks[chunks.length - 1] += ` ${safeName}="`;
|
|
267
|
+
values.push(value);
|
|
268
|
+
chunks.push("\"");
|
|
269
|
+
}
|
|
270
|
+
function renderJsxElement(type, props, children) {
|
|
271
|
+
const chunks = [`<${type}`];
|
|
272
|
+
const values = [];
|
|
273
|
+
if (props) for (const [name, value] of Object.entries(props)) pushJsxAttr(chunks, values, name, value);
|
|
274
|
+
chunks[chunks.length - 1] += ">";
|
|
275
|
+
if (!VOID_ELEMENTS.has(type)) {
|
|
276
|
+
for (const child of children) {
|
|
277
|
+
values.push(child);
|
|
278
|
+
chunks.push("");
|
|
279
|
+
}
|
|
280
|
+
chunks[chunks.length - 1] += `</${type}>`;
|
|
281
|
+
} else if (children.length > 0 && __DEV__) warn(`Void element <${type}> should not have children. They will be ignored.`);
|
|
282
|
+
return ilhaHtml(chunks, ...values);
|
|
283
|
+
}
|
|
284
|
+
function ilhaJsx(type, props, ...children) {
|
|
285
|
+
const normalizedChildren = normalizeJsxChildren(props, children);
|
|
286
|
+
if (typeof type === "function") {
|
|
287
|
+
const componentProps = {
|
|
288
|
+
...props ?? {},
|
|
289
|
+
...normalizedChildren.length > 0 ? { children: normalizedChildren } : {}
|
|
290
|
+
};
|
|
291
|
+
delete componentProps["key"];
|
|
292
|
+
const out = type(Object.keys(componentProps).length ? componentProps : {});
|
|
293
|
+
if (typeof out === "string") return ilhaHtml`${out}`;
|
|
294
|
+
if (out && typeof out === "object" && RAW in out) return out;
|
|
295
|
+
return ilhaHtml`${out}`;
|
|
296
|
+
}
|
|
297
|
+
return renderJsxElement(type, props, normalizedChildren);
|
|
298
|
+
}
|
|
299
|
+
function ilhaFragment(props, ...children) {
|
|
300
|
+
const normalizedChildren = normalizeJsxChildren(props, children);
|
|
301
|
+
return ilhaHtml(["", ...normalizedChildren.map(() => "")], ...normalizedChildren);
|
|
302
|
+
}
|
|
303
|
+
function ilhaJsxDEV(type, props) {
|
|
304
|
+
return ilhaJsx(type, props);
|
|
305
|
+
}
|
|
193
306
|
function interpolateValue(v) {
|
|
194
|
-
if (v == null) return "";
|
|
307
|
+
if (v == null || v === true || v === false) return "";
|
|
195
308
|
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
196
309
|
if (typeof v === "object" && RAW in v) return v.value;
|
|
197
310
|
if (isIslandCall(v)) {
|
|
@@ -203,12 +316,117 @@ function interpolateValue(v) {
|
|
|
203
316
|
if (typeof v === "function") return escapeHtml(v());
|
|
204
317
|
return escapeHtml(v);
|
|
205
318
|
}
|
|
319
|
+
const BIND_VALID_KINDS = new Set([
|
|
320
|
+
"value",
|
|
321
|
+
"checked",
|
|
322
|
+
"valueAsNumber",
|
|
323
|
+
"valueAsDate",
|
|
324
|
+
"files",
|
|
325
|
+
"open",
|
|
326
|
+
"group",
|
|
327
|
+
"this"
|
|
328
|
+
]);
|
|
329
|
+
const BIND_PREFIX_RE = /\bbind:([a-zA-Z]+)\s*=\s*("|')?$/;
|
|
330
|
+
function formatDateForInput(d) {
|
|
331
|
+
if (d instanceof Date) {
|
|
332
|
+
if (isNaN(d.getTime())) {
|
|
333
|
+
if (__DEV__) warn("bind:valueAsDate received an invalid Date object — value attribute will be empty.");
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
337
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
338
|
+
}
|
|
339
|
+
return "";
|
|
340
|
+
}
|
|
341
|
+
function extractElementStaticValue(prefix) {
|
|
342
|
+
const lastOpen = prefix.lastIndexOf("<");
|
|
343
|
+
if (lastOpen === -1) return null;
|
|
344
|
+
const tagPrefix = prefix.slice(lastOpen);
|
|
345
|
+
if (tagPrefix.includes(">")) return null;
|
|
346
|
+
const m = tagPrefix.match(/\bvalue\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/);
|
|
347
|
+
if (!m) return null;
|
|
348
|
+
return m[2] ?? m[3] ?? m[4] ?? null;
|
|
349
|
+
}
|
|
350
|
+
function emitBindSSR(kind, index, accessor, prefixForStaticPeek) {
|
|
351
|
+
const spec = `${kind}:${index}`;
|
|
352
|
+
let v;
|
|
353
|
+
try {
|
|
354
|
+
v = accessor();
|
|
355
|
+
} catch {
|
|
356
|
+
v = void 0;
|
|
357
|
+
}
|
|
358
|
+
switch (kind) {
|
|
359
|
+
case "value": return [` value="${escapeHtml(v ?? "")}"`, spec];
|
|
360
|
+
case "valueAsNumber": return [` value="${escapeHtml(v == null ? "" : String(v))}"`, spec];
|
|
361
|
+
case "valueAsDate": return [` value="${escapeHtml(formatDateForInput(v))}"`, spec];
|
|
362
|
+
case "checked": return [v ? ` checked` : ``, spec];
|
|
363
|
+
case "open": return [v ? ` open` : ``, spec];
|
|
364
|
+
case "files": return [``, spec];
|
|
365
|
+
case "this": return [``, spec];
|
|
366
|
+
case "group": {
|
|
367
|
+
const optionValue = extractElementStaticValue(prefixForStaticPeek);
|
|
368
|
+
if (optionValue == null) return [``, spec];
|
|
369
|
+
return [(Array.isArray(v) ? v.map(String).includes(optionValue) : v != null && String(v) === optionValue) ? ` checked` : ``, spec];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
206
373
|
function ilhaHtml(strings, ...values) {
|
|
207
374
|
let result = "";
|
|
375
|
+
let stripLeadingQuote = null;
|
|
376
|
+
let pendingBindSpecs = "";
|
|
377
|
+
const ctx = currentRenderCtx();
|
|
208
378
|
for (let i = 0; i < strings.length; i++) {
|
|
209
|
-
|
|
210
|
-
if (
|
|
379
|
+
let chunk = strings[i];
|
|
380
|
+
if (stripLeadingQuote !== null) {
|
|
381
|
+
if (chunk.startsWith(stripLeadingQuote)) chunk = chunk.slice(1);
|
|
382
|
+
stripLeadingQuote = null;
|
|
383
|
+
}
|
|
384
|
+
if (pendingBindSpecs !== "") {
|
|
385
|
+
const gtIdx = chunk.indexOf(">");
|
|
386
|
+
if (gtIdx !== -1) {
|
|
387
|
+
chunk = chunk.slice(0, gtIdx) + ` data-ilha-bind="${pendingBindSpecs}">` + chunk.slice(gtIdx + 1);
|
|
388
|
+
pendingBindSpecs = "";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (i >= values.length) {
|
|
392
|
+
result += chunk;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const value = values[i];
|
|
396
|
+
const m = chunk.match(BIND_PREFIX_RE);
|
|
397
|
+
if (m && isSignalAccessor(value)) {
|
|
398
|
+
const name = m[1];
|
|
399
|
+
const openQuote = m[2] ?? null;
|
|
400
|
+
if (!BIND_VALID_KINDS.has(name)) {
|
|
401
|
+
if (__DEV__) warn(`Unknown bind:${name} — supported bindings are ${[...BIND_VALID_KINDS].map((k) => `bind:${k}`).join(", ")}.`);
|
|
402
|
+
result += chunk + interpolateValue(value);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (!ctx) {
|
|
406
|
+
if (__DEV__) warn(`bind:${name} used outside an island render — bindings only work in .render(). The value is reflected once but not wired.`);
|
|
407
|
+
const stripped = chunk.slice(0, chunk.length - m[0].length);
|
|
408
|
+
const quote = openQuote ?? "\"";
|
|
409
|
+
result += stripped + name + "=" + quote + interpolateValue(value) + quote;
|
|
410
|
+
if (openQuote) stripLeadingQuote = openQuote;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const matchStart = chunk.length - m[0].length;
|
|
414
|
+
const cleanPrefix = chunk.slice(0, matchStart).replace(/\s+$/, "");
|
|
415
|
+
const index = ctx.binds.length;
|
|
416
|
+
ctx.binds.push({
|
|
417
|
+
kind: name,
|
|
418
|
+
accessor: value
|
|
419
|
+
});
|
|
420
|
+
const [valueAttrs, spec] = emitBindSSR(name, index, value, cleanPrefix);
|
|
421
|
+
result += cleanPrefix + valueAttrs;
|
|
422
|
+
pendingBindSpecs += (pendingBindSpecs ? "," : "") + spec;
|
|
423
|
+
if (openQuote) stripLeadingQuote = openQuote;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (m && __DEV__) warn(`bind:${m[1]} requires a signal accessor — got ${typeof value}. Use ilha.signal() or a .state() accessor.`);
|
|
427
|
+
result += chunk + interpolateValue(value);
|
|
211
428
|
}
|
|
429
|
+
if (pendingBindSpecs !== "") result += ` data-ilha-bind="${pendingBindSpecs}"`;
|
|
212
430
|
return {
|
|
213
431
|
[RAW]: true,
|
|
214
432
|
value: dedentString(result)
|
|
@@ -231,7 +449,8 @@ function ilhaContext(key, initial) {
|
|
|
231
449
|
/**
|
|
232
450
|
* Create a free-standing reactive signal that lives outside any island.
|
|
233
451
|
* Useful for sharing state across islands without prop drilling, or for
|
|
234
|
-
* binding form inputs to module-level state via
|
|
452
|
+
* binding form inputs to module-level state via the `bind:value=${signal}`
|
|
453
|
+
* template syntax.
|
|
235
454
|
*
|
|
236
455
|
* The returned accessor is a getter when called with no arguments and a
|
|
237
456
|
* setter when called with one. Reading it inside a `.derived()`, `.effect()`,
|
|
@@ -240,11 +459,10 @@ function ilhaContext(key, initial) {
|
|
|
240
459
|
*/
|
|
241
460
|
function ilhaSignal(initial) {
|
|
242
461
|
const s = signal(initial);
|
|
243
|
-
|
|
462
|
+
return markSignalAccessor((...args) => {
|
|
244
463
|
if (args.length === 0) return s();
|
|
245
464
|
s(args[0]);
|
|
246
|
-
};
|
|
247
|
-
return accessor;
|
|
465
|
+
});
|
|
248
466
|
}
|
|
249
467
|
/**
|
|
250
468
|
* Run `fn` with reactive tracking suspended. Reading signals inside `fn`
|
|
@@ -411,63 +629,159 @@ function createDerivedProxy(entries, state, input, derivedSnapshot) {
|
|
|
411
629
|
setup
|
|
412
630
|
};
|
|
413
631
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
632
|
+
const BIND_SENTINEL_ATTR = "data-ilha-bind";
|
|
633
|
+
function parseBindSentinel(value) {
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const part of value.split(",")) {
|
|
636
|
+
const [kind, idx] = part.split(":");
|
|
637
|
+
if (!kind || !idx) continue;
|
|
638
|
+
const i = Number(idx);
|
|
639
|
+
if (!Number.isInteger(i) || i < 0) continue;
|
|
640
|
+
out.push({
|
|
641
|
+
kind,
|
|
642
|
+
index: i
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
function resolveBindOps(el, kind) {
|
|
648
|
+
const input = el;
|
|
649
|
+
switch (kind) {
|
|
650
|
+
case "value": return {
|
|
651
|
+
event: el.tagName === "SELECT" ? "change" : "input",
|
|
652
|
+
read: (el) => el.value,
|
|
653
|
+
write: (el, v) => el.value = v == null ? "" : String(v)
|
|
654
|
+
};
|
|
655
|
+
case "valueAsNumber": return {
|
|
656
|
+
event: "input",
|
|
657
|
+
read: (el) => {
|
|
658
|
+
const n = el.valueAsNumber;
|
|
659
|
+
return Number.isNaN(n) ? null : n;
|
|
660
|
+
},
|
|
661
|
+
write: (el, v) => el.value = v == null || Number.isNaN(v) ? "" : String(v)
|
|
662
|
+
};
|
|
663
|
+
case "valueAsDate": return {
|
|
664
|
+
event: "input",
|
|
665
|
+
read: (el) => el.valueAsDate,
|
|
666
|
+
write: (el, v) => {
|
|
667
|
+
el.value = formatDateForInput(v);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
case "checked": return {
|
|
671
|
+
event: "change",
|
|
672
|
+
read: (el) => el.checked,
|
|
673
|
+
write: (el, v) => el.checked = Boolean(v)
|
|
674
|
+
};
|
|
675
|
+
case "files": return {
|
|
676
|
+
event: "change",
|
|
677
|
+
read: (el) => el.files,
|
|
678
|
+
write: () => {}
|
|
679
|
+
};
|
|
680
|
+
case "open": return {
|
|
681
|
+
event: "toggle",
|
|
682
|
+
read: (el) => el.open,
|
|
683
|
+
write: (el, v) => el.open = Boolean(v)
|
|
684
|
+
};
|
|
685
|
+
case "this": return {
|
|
686
|
+
event: null,
|
|
687
|
+
read: () => void 0,
|
|
688
|
+
write: () => {}
|
|
689
|
+
};
|
|
690
|
+
case "group": {
|
|
691
|
+
const isCheckbox = input.type === "checkbox";
|
|
692
|
+
return {
|
|
693
|
+
event: "change",
|
|
694
|
+
read: (el) => {
|
|
695
|
+
const i = el;
|
|
696
|
+
if (isCheckbox) return {
|
|
697
|
+
__ilhaGroup: true,
|
|
698
|
+
value: i.value,
|
|
699
|
+
checked: i.checked
|
|
700
|
+
};
|
|
701
|
+
return i.checked ? i.value : void 0;
|
|
702
|
+
},
|
|
703
|
+
write: (el, v) => {
|
|
704
|
+
const i = el;
|
|
705
|
+
if (isCheckbox) i.checked = (Array.isArray(v) ? v.map(String) : []).includes(i.value);
|
|
706
|
+
else i.checked = v != null && String(v) === i.value;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
432
709
|
}
|
|
433
|
-
}
|
|
434
|
-
if (tag === "input" && type === "number") return {
|
|
435
|
-
prop: "valueAsNumber",
|
|
436
|
-
event: "input",
|
|
437
|
-
read: (el) => el.valueAsNumber,
|
|
438
|
-
write: (el, v) => el.value = String(v ?? "")
|
|
439
|
-
};
|
|
440
|
-
return {
|
|
441
|
-
prop: "value",
|
|
442
|
-
event: tag === "select" ? "change" : "input",
|
|
443
|
-
read: (el) => el.value,
|
|
444
|
-
write: (el, v) => el.value = String(v ?? "")
|
|
445
|
-
};
|
|
710
|
+
}
|
|
446
711
|
}
|
|
447
|
-
function
|
|
712
|
+
function applyTemplateBindings(host, binds) {
|
|
713
|
+
if (binds.length === 0) return () => {};
|
|
448
714
|
const cleanups = [];
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
715
|
+
const elements = [];
|
|
716
|
+
if (host.hasAttribute(BIND_SENTINEL_ATTR)) elements.push(host);
|
|
717
|
+
for (const el of host.querySelectorAll(`[${BIND_SENTINEL_ATTR}]`)) elements.push(el);
|
|
718
|
+
for (const el of elements) {
|
|
719
|
+
const specs = parseBindSentinel(el.getAttribute(BIND_SENTINEL_ATTR));
|
|
720
|
+
for (const spec of specs) {
|
|
721
|
+
const record = binds[spec.index];
|
|
722
|
+
if (!record) {
|
|
723
|
+
if (__DEV__) warn(`bind:${spec.kind} index ${spec.index} not found in render — the data-ilha-bind sentinel may have been hand-edited or survived a stale render.`);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (record.kind !== spec.kind) {
|
|
727
|
+
if (__DEV__) warn(`bind:${spec.kind} sentinel points at a binding registered as bind:${record.kind}. Sentinel may be stale.`);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const { event, read, write } = resolveBindOps(el, spec.kind);
|
|
731
|
+
const accessor = record.accessor;
|
|
732
|
+
if (spec.kind === "this") {
|
|
733
|
+
accessor(el);
|
|
734
|
+
cleanups.push(() => accessor(null));
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
write(el, accessor());
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (__DEV__) console.error(`[ilha] bind:${spec.kind} write failed:`, err);
|
|
741
|
+
}
|
|
742
|
+
if (event === null) continue;
|
|
457
743
|
const listener = () => {
|
|
458
|
-
const raw = read(
|
|
459
|
-
if (
|
|
744
|
+
const raw = read(el);
|
|
745
|
+
if (spec.kind === "group") {
|
|
746
|
+
const groupRead = raw;
|
|
747
|
+
if (groupRead === void 0) return;
|
|
748
|
+
if (typeof groupRead === "object" && groupRead.__ilhaGroup) {
|
|
749
|
+
const currentArr = accessor();
|
|
750
|
+
const arr = Array.isArray(currentArr) ? [...currentArr] : [];
|
|
751
|
+
const idx = arr.findIndex((x) => String(x) === groupRead.value);
|
|
752
|
+
if (groupRead.checked && idx === -1) {
|
|
753
|
+
let coercedVal = groupRead.value;
|
|
754
|
+
const templateVal = Array.isArray(currentArr) && currentArr.length > 0 ? currentArr[0] : void 0;
|
|
755
|
+
if (templateVal !== void 0) {
|
|
756
|
+
if (typeof templateVal === "number") {
|
|
757
|
+
const n = Number(coercedVal);
|
|
758
|
+
coercedVal = Number.isNaN(n) ? coercedVal : n;
|
|
759
|
+
} else if (typeof templateVal === "boolean") coercedVal = Boolean(coercedVal);
|
|
760
|
+
}
|
|
761
|
+
arr.push(coercedVal);
|
|
762
|
+
} else if (!groupRead.checked && idx !== -1) arr.splice(idx, 1);
|
|
763
|
+
accessor(arr);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const currentVal = accessor();
|
|
767
|
+
let coerced = groupRead;
|
|
768
|
+
if (typeof currentVal === "number" && typeof groupRead === "string") {
|
|
769
|
+
const n = Number(groupRead);
|
|
770
|
+
coerced = Number.isNaN(n) ? 0 : n;
|
|
771
|
+
} else if (typeof currentVal === "boolean") coerced = Boolean(groupRead);
|
|
772
|
+
accessor(coerced);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
460
775
|
const currentVal = accessor();
|
|
461
|
-
let value;
|
|
462
|
-
if (typeof currentVal === "number") {
|
|
776
|
+
let value = raw;
|
|
777
|
+
if (typeof currentVal === "number" && typeof raw === "string") {
|
|
463
778
|
const n = Number(raw);
|
|
464
|
-
value = isNaN(n) ? 0 : n;
|
|
779
|
+
value = Number.isNaN(n) ? 0 : n;
|
|
465
780
|
} else if (typeof currentVal === "boolean") value = Boolean(raw);
|
|
466
|
-
else value = raw;
|
|
467
781
|
accessor(value);
|
|
468
782
|
};
|
|
469
|
-
|
|
470
|
-
cleanups.push(() =>
|
|
783
|
+
el.addEventListener(event, listener);
|
|
784
|
+
cleanups.push(() => el.removeEventListener(event, listener));
|
|
471
785
|
}
|
|
472
786
|
}
|
|
473
787
|
return () => cleanups.forEach((c) => c());
|
|
@@ -529,7 +843,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
529
843
|
onMounts: [],
|
|
530
844
|
onErrors: [],
|
|
531
845
|
transition: null,
|
|
532
|
-
binds: [],
|
|
533
846
|
css: null
|
|
534
847
|
});
|
|
535
848
|
}
|
|
@@ -553,15 +866,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
553
866
|
}]
|
|
554
867
|
});
|
|
555
868
|
}
|
|
556
|
-
bind(selector, target) {
|
|
557
|
-
return new IlhaBuilder({
|
|
558
|
-
...this._cfg,
|
|
559
|
-
binds: [...this._cfg.binds, {
|
|
560
|
-
selector,
|
|
561
|
-
target
|
|
562
|
-
}]
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
869
|
on(selectorOrCombined, handler) {
|
|
566
870
|
const parsed = parseOnArgs(selectorOrCombined);
|
|
567
871
|
return new IlhaBuilder({
|
|
@@ -617,7 +921,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
617
921
|
});
|
|
618
922
|
}
|
|
619
923
|
render(fn) {
|
|
620
|
-
const { schema, states, deriveds, ons, effects, onMounts, onErrors, transition,
|
|
924
|
+
const { schema, states, deriveds, ons, effects, onMounts, onErrors, transition, css: cssSource } = this._cfg;
|
|
621
925
|
const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
|
|
622
926
|
function resolveInput(props) {
|
|
623
927
|
const value = props ?? {};
|
|
@@ -635,7 +939,8 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
635
939
|
try {
|
|
636
940
|
return {
|
|
637
941
|
html: await resolveAsyncChildren(html, ctx),
|
|
638
|
-
slots: ctx.slots
|
|
942
|
+
slots: ctx.slots,
|
|
943
|
+
binds: ctx.binds
|
|
639
944
|
};
|
|
640
945
|
} finally {
|
|
641
946
|
popRenderCtx();
|
|
@@ -644,7 +949,8 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
644
949
|
}
|
|
645
950
|
return {
|
|
646
951
|
html,
|
|
647
|
-
slots: ctx.slots
|
|
952
|
+
slots: ctx.slots,
|
|
953
|
+
binds: ctx.binds
|
|
648
954
|
};
|
|
649
955
|
} finally {
|
|
650
956
|
if (!isAsync) popRenderCtx();
|
|
@@ -986,7 +1292,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
986
1292
|
}), hasExistingContent ? host : void 0);
|
|
987
1293
|
if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
|
|
988
1294
|
attachListeners();
|
|
989
|
-
let stopBindings =
|
|
1295
|
+
let stopBindings = applyTemplateBindings(host, initial.binds);
|
|
990
1296
|
cleanups.push(() => stopBindings());
|
|
991
1297
|
mountSlots(initial.slots);
|
|
992
1298
|
cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
|
|
@@ -1050,7 +1356,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1050
1356
|
let initialized = false;
|
|
1051
1357
|
const initialRenderedHtml = initial.html;
|
|
1052
1358
|
const stopRender = effect(() => {
|
|
1053
|
-
const { html: rendered, slots: newSlotMap } = renderWithCtx(() => fn({
|
|
1359
|
+
const { html: rendered, slots: newSlotMap, binds: newBinds } = renderWithCtx(() => fn({
|
|
1054
1360
|
state,
|
|
1055
1361
|
derived,
|
|
1056
1362
|
input
|
|
@@ -1084,24 +1390,26 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1084
1390
|
stub.replaceWith(liveEl);
|
|
1085
1391
|
}
|
|
1086
1392
|
attachListeners();
|
|
1087
|
-
stopBindings =
|
|
1393
|
+
stopBindings = applyTemplateBindings(host, newBinds);
|
|
1088
1394
|
mountSlots(newSlotMap);
|
|
1089
1395
|
});
|
|
1090
|
-
cleanups.push(stopRender);
|
|
1091
|
-
cleanups.push(detachListeners);
|
|
1092
1396
|
let tornDown = false;
|
|
1093
1397
|
const unmount = () => {
|
|
1094
1398
|
if (tornDown) return;
|
|
1095
1399
|
tornDown = true;
|
|
1096
1400
|
if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
|
|
1401
|
+
stopRender();
|
|
1402
|
+
detachListeners();
|
|
1097
1403
|
if (transition?.leave) {
|
|
1098
1404
|
const result = transition.leave(host);
|
|
1099
1405
|
if (result instanceof Promise) {
|
|
1100
|
-
result.then(() =>
|
|
1406
|
+
result.then(() => {
|
|
1407
|
+
for (const c of cleanups) c();
|
|
1408
|
+
}).catch(console.error);
|
|
1101
1409
|
return;
|
|
1102
1410
|
}
|
|
1103
1411
|
}
|
|
1104
|
-
|
|
1412
|
+
for (const c of cleanups) c();
|
|
1105
1413
|
};
|
|
1106
1414
|
const updateProps = (nextProps) => {
|
|
1107
1415
|
if (tornDown) return;
|
|
@@ -1272,12 +1580,15 @@ const rootBuilder = new IlhaBuilder({
|
|
|
1272
1580
|
onMounts: [],
|
|
1273
1581
|
onErrors: [],
|
|
1274
1582
|
transition: null,
|
|
1275
|
-
binds: [],
|
|
1276
1583
|
css: null
|
|
1277
1584
|
});
|
|
1278
1585
|
const ilha = Object.assign(rootBuilder, {
|
|
1279
1586
|
html: ilhaHtml,
|
|
1280
1587
|
raw: ilhaRaw,
|
|
1588
|
+
jsx: ilhaJsx,
|
|
1589
|
+
jsxs: ilhaJsx,
|
|
1590
|
+
jsxDEV: ilhaJsxDEV,
|
|
1591
|
+
Fragment: ilhaFragment,
|
|
1281
1592
|
mount: mountAll,
|
|
1282
1593
|
from: ilhaFrom,
|
|
1283
1594
|
context: ilhaContext,
|
|
@@ -1287,9 +1598,13 @@ const ilha = Object.assign(rootBuilder, {
|
|
|
1287
1598
|
});
|
|
1288
1599
|
const html = ilhaHtml;
|
|
1289
1600
|
const raw = ilhaRaw;
|
|
1601
|
+
const jsx = ilhaJsx;
|
|
1602
|
+
const jsxs = ilhaJsx;
|
|
1603
|
+
const jsxDEV = ilhaJsxDEV;
|
|
1604
|
+
const Fragment = ilhaFragment;
|
|
1290
1605
|
const css = ilhaCss;
|
|
1291
1606
|
const mount = mountAll;
|
|
1292
1607
|
const from = ilhaFrom;
|
|
1293
1608
|
const context = ilhaContext;
|
|
1294
1609
|
//#endregion
|
|
1295
|
-
export { ISLAND_MOUNT_INTERNAL, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, mount, raw, untrack };
|
|
1610
|
+
export { Fragment, ISLAND_MOUNT_INTERNAL, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RawHtml } from "./index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/jsx-types.d.ts
|
|
4
|
+
declare namespace JSX {
|
|
5
|
+
type Child = unknown;
|
|
6
|
+
type ElementType = string | ((props: any) => any);
|
|
7
|
+
interface Element extends RawHtml {}
|
|
8
|
+
interface ElementChildrenAttribute {
|
|
9
|
+
children: {};
|
|
10
|
+
}
|
|
11
|
+
interface IntrinsicAttributes {
|
|
12
|
+
key?: string | number;
|
|
13
|
+
}
|
|
14
|
+
type IntrinsicElementProps = {
|
|
15
|
+
children?: Child;
|
|
16
|
+
class?: string | unknown[] | Record<string, boolean>;
|
|
17
|
+
className?: string | unknown[] | Record<string, boolean>;
|
|
18
|
+
htmlFor?: string;
|
|
19
|
+
[name: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
interface IntrinsicElements {
|
|
22
|
+
[name: string]: IntrinsicElementProps;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { JSX as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilha",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A tiny, framework-free island architecture library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryuz <ryuzer@proton.me>",
|
|
@@ -12,10 +12,21 @@
|
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
14
|
"type": "module",
|
|
15
|
+
"browser": {
|
|
16
|
+
"process": false
|
|
17
|
+
},
|
|
15
18
|
"exports": {
|
|
16
19
|
".": {
|
|
17
20
|
"types": "./dist/index.d.ts",
|
|
18
21
|
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./jsx-runtime": {
|
|
24
|
+
"types": "./dist/jsx-runtime.d.ts",
|
|
25
|
+
"import": "./dist/jsx-runtime.js"
|
|
26
|
+
},
|
|
27
|
+
"./jsx-dev-runtime": {
|
|
28
|
+
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
29
|
+
"import": "./dist/jsx-dev-runtime.js"
|
|
19
30
|
}
|
|
20
31
|
},
|
|
21
32
|
"scripts": {
|
|
@@ -23,7 +34,7 @@
|
|
|
23
34
|
"test": "bun test"
|
|
24
35
|
},
|
|
25
36
|
"dependencies": {
|
|
26
|
-
"alien-signals": "3.2.
|
|
37
|
+
"alien-signals": "3.2.1"
|
|
27
38
|
},
|
|
28
39
|
"devDependencies": {
|
|
29
40
|
"zod": "^4.4.3"
|