ilha 0.1.0 → 0.2.1
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 +102 -15
- package/dist/index.d.ts +6 -2
- package/dist/index.js +43 -18
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ const Counter = ilha
|
|
|
25
25
|
.render(
|
|
26
26
|
({ state }) => html`
|
|
27
27
|
<div>
|
|
28
|
-
<p>Count: ${state.count}</p>
|
|
28
|
+
<p>Count: ${state.count()}</p>
|
|
29
29
|
<button>Increment</button>
|
|
30
30
|
</div>
|
|
31
31
|
`,
|
|
@@ -50,11 +50,21 @@ State is managed with signals — when a signal changes, only the affected islan
|
|
|
50
50
|
|
|
51
51
|
## Builder API
|
|
52
52
|
|
|
53
|
-
Every island starts from the `ilha` builder object (or `ilha.input()` if you need typed props).
|
|
53
|
+
Every island starts from the `ilha` builder object (or `ilha.input<T>()` / `ilha.input(schema)` if you need typed props).
|
|
54
54
|
|
|
55
|
-
### `ilha.input(schema)`
|
|
55
|
+
### `ilha.input<T>()` / `ilha.input(schema)`
|
|
56
56
|
|
|
57
|
-
Declares the island's external input type
|
|
57
|
+
Declares the island's external input type. Two forms:
|
|
58
|
+
|
|
59
|
+
**1. Type-only (no runtime validation):**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const MyIsland = ilha
|
|
63
|
+
.input<{ name: string }>()
|
|
64
|
+
.render(({ input }) => `<p>Hello, ${input.name}!</p>`);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**2. With a [Standard Schema](https://standardschema.dev/) validator** (Zod, Valibot, ArkType, etc.) — runs validation at render time and uses the schema's inferred output type:
|
|
58
68
|
|
|
59
69
|
```ts
|
|
60
70
|
import { z } from "zod";
|
|
@@ -129,7 +139,6 @@ ilha
|
|
|
129
139
|
.on("@click", ({ state }) => state.count(state.count() + 1)) // host click
|
|
130
140
|
.on("button.inc@click", ({ state }) => state.count(state.count() + 1)) // child click
|
|
131
141
|
.on("input@input:debounce", ({ state, event }) => {
|
|
132
|
-
// with modifier
|
|
133
142
|
state.query((event.target as HTMLInputElement).value);
|
|
134
143
|
})
|
|
135
144
|
.render(({ state }) => html`<div><button class="inc">+</button></div>`);
|
|
@@ -223,6 +232,63 @@ You can also bind to an external signal created with `context()`:
|
|
|
223
232
|
|
|
224
233
|
---
|
|
225
234
|
|
|
235
|
+
### `.css(strings, ...values)`
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { css } from "ilha";
|
|
241
|
+
|
|
242
|
+
const Card = ilha.state("active", false).css`
|
|
243
|
+
.title { font-weight: 700; }
|
|
244
|
+
button { background: teal; color: white; }
|
|
245
|
+
`.render(
|
|
246
|
+
({ state }) => html`
|
|
247
|
+
<div>
|
|
248
|
+
<p class="title">Hello</p>
|
|
249
|
+
<button>Toggle</button>
|
|
250
|
+
</div>
|
|
251
|
+
`,
|
|
252
|
+
);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Interpolations are supported:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
const accent = "teal";
|
|
259
|
+
|
|
260
|
+
ilha.css`button { background: ${accent}; }`.render(() => `<button>Go</button>`);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
You can also pass a plain string (e.g. from an external `.css` file):
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import styles from "./card.css?raw";
|
|
267
|
+
|
|
268
|
+
ilha.css(styles).render(() => `<div class="card">…</div>`);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**SSR output** — a `<style data-ilha-css>` tag is prepended as the first child of the island's rendered HTML:
|
|
272
|
+
|
|
273
|
+
```html
|
|
274
|
+
<style data-ilha-css>
|
|
275
|
+
@scope (:scope) to ([data-ilha]) {
|
|
276
|
+
.title {
|
|
277
|
+
font-weight: 700;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
281
|
+
<div>…</div>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Client mount** — the style element is injected once as the first child of the host and preserved across re-renders (morph never replaces it). During hydration, the SSR-emitted `<style>` node is reused and not duplicated.
|
|
285
|
+
|
|
286
|
+
**`.hydratable()` integration** — the style tag is included inside the `data-ilha` wrapper regardless of the `snapshot` option.
|
|
287
|
+
|
|
288
|
+
> **Note:** Calling `.css()` more than once on the same builder chain is not supported. In dev mode a warning is logged and only the last stylesheet is used. Compose all your styles into a single `.css()` call.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
226
292
|
### `.slot(name, island)`
|
|
227
293
|
|
|
228
294
|
Embeds a child island as a named slot. The child island is mounted and managed independently. During SSR the slot renders the child's HTML inline; during client mount the child island is activated for interactivity.
|
|
@@ -314,13 +380,13 @@ Async method that renders the island wrapped in a `data-ilha` hydration containe
|
|
|
314
380
|
const html = await MyIsland.hydratable(
|
|
315
381
|
{ name: "Ilha" },
|
|
316
382
|
{
|
|
317
|
-
name: "
|
|
383
|
+
name: "MyIsland", // registry key for client-side activation
|
|
318
384
|
as: "div", // wrapper tag (default: "div")
|
|
319
385
|
snapshot: true, // embed state + derived as data-ilha-state
|
|
320
386
|
skipOnMount: false, // skip onMount on hydration (default: true when snapshot)
|
|
321
387
|
},
|
|
322
388
|
);
|
|
323
|
-
// → '<div data-ilha="
|
|
389
|
+
// → '<div data-ilha="MyIsland" data-ilha-props="…" data-ilha-state="…">…</div>'
|
|
324
390
|
```
|
|
325
391
|
|
|
326
392
|
**`snapshot` option:**
|
|
@@ -430,18 +496,39 @@ raw("<strong>bold</strong>"); // → passes through unescaped
|
|
|
430
496
|
|
|
431
497
|
---
|
|
432
498
|
|
|
433
|
-
### `
|
|
499
|
+
### `css\`\`` tagged template
|
|
434
500
|
|
|
435
|
-
|
|
501
|
+
A passthrough tagged template for CSS strings. Functionally identical to a plain template literal — no runtime transformation occurs. Its purpose is purely to enable editor tooling (LSP syntax highlighting, Prettier formatting) to recognise the contents as CSS.
|
|
436
502
|
|
|
437
503
|
```ts
|
|
438
|
-
import {
|
|
504
|
+
import { css } from "ilha";
|
|
505
|
+
|
|
506
|
+
const styles = css`
|
|
507
|
+
button {
|
|
508
|
+
background: teal;
|
|
509
|
+
color: white;
|
|
510
|
+
}
|
|
511
|
+
.label {
|
|
512
|
+
font-weight: 700;
|
|
513
|
+
}
|
|
514
|
+
`;
|
|
515
|
+
|
|
516
|
+
ilha.css(styles).render(() => `<button class="label">Go</button>`);
|
|
517
|
+
```
|
|
439
518
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
519
|
+
Interpolations work as normal string concatenation:
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
const accent = "coral";
|
|
523
|
+
const styles = css`
|
|
524
|
+
button {
|
|
525
|
+
background: ${accent};
|
|
526
|
+
}
|
|
527
|
+
`;
|
|
443
528
|
```
|
|
444
529
|
|
|
530
|
+
> **Note:** `css` (the named export) is the plain passthrough tag for tooling. `ilha.css` is the builder chain method that attaches styles to an island. They are intentionally separate.
|
|
531
|
+
|
|
445
532
|
---
|
|
446
533
|
|
|
447
534
|
## SSR + Hydration
|
|
@@ -464,7 +551,7 @@ return `<!doctype html><html><body>${html}</body></html>`;
|
|
|
464
551
|
import { mount } from "ilha";
|
|
465
552
|
import { MyIsland } from "./islands";
|
|
466
553
|
|
|
467
|
-
mount({
|
|
554
|
+
mount({ MyIsland });
|
|
468
555
|
```
|
|
469
556
|
|
|
470
557
|
The client reads `data-ilha-state` to restore signal values from the snapshot, skipping a needless re-render and calling `.onMount()` only if `skipOnMount` is not set.
|
|
@@ -474,7 +561,7 @@ The client reads `data-ilha-state` to restore signal values from the snapshot, s
|
|
|
474
561
|
```
|
|
475
562
|
server client
|
|
476
563
|
────────────────────────────────────── ──────────────────────────────────────────
|
|
477
|
-
.hydratable({ count: 42 }, { mount({
|
|
564
|
+
.hydratable({ count: 42 }, { mount({ MyIsland })
|
|
478
565
|
name: "my-island", → reads data-ilha-state
|
|
479
566
|
snapshot: true → restores signals from snapshot
|
|
480
567
|
}) → skips onMount (skipOnMount: true)
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ interface MarkedSignalAccessor<T> {
|
|
|
48
48
|
[SIGNAL_ACCESSOR]: true;
|
|
49
49
|
}
|
|
50
50
|
declare function ilhaRaw(value: string): RawHtml;
|
|
51
|
+
declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (string | number)[]): string;
|
|
51
52
|
declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
|
|
52
53
|
type ContextSignal<T> = {
|
|
53
54
|
(): T;
|
|
@@ -176,10 +177,12 @@ interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDeri
|
|
|
176
177
|
slots: Record<string, AnyIsland>;
|
|
177
178
|
transition: TransitionOptions | null;
|
|
178
179
|
binds: BindEntry<TStateMap>[];
|
|
180
|
+
css: string | null;
|
|
179
181
|
}
|
|
180
182
|
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<string, never>, TSlots extends SlotMap = Record<string, never>> {
|
|
181
183
|
readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap, TSlots>;
|
|
182
184
|
constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap, TSlots>);
|
|
185
|
+
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<string, never>, Record<string, never>, Record<string, never>>;
|
|
183
186
|
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<string, never>, Record<string, never>, Record<string, never>>;
|
|
184
187
|
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap, TSlots>;
|
|
185
188
|
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>, TSlots>;
|
|
@@ -191,6 +194,7 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
|
|
|
191
194
|
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots>;
|
|
192
195
|
slot<K extends string, I extends AnyIsland>(name: K, island: I): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots & Record<K, I>>;
|
|
193
196
|
transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots>;
|
|
197
|
+
css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots>;
|
|
194
198
|
render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap, TSlots>) => string | RawHtml): Island<TInput, TStateMap>;
|
|
195
199
|
}
|
|
196
200
|
declare function ilhaFrom<TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>): (() => void) | null;
|
|
@@ -203,11 +207,11 @@ declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>,
|
|
|
203
207
|
from: typeof ilhaFrom;
|
|
204
208
|
context: typeof ilhaContext;
|
|
205
209
|
};
|
|
206
|
-
declare function type<TInput, TOutput = TInput>(coerce?: (input: TInput) => TOutput): StandardSchemaV1<TInput, TOutput>;
|
|
207
210
|
declare const html: typeof ilhaHtml;
|
|
208
211
|
declare const raw: typeof ilhaRaw;
|
|
212
|
+
declare const css: typeof ilhaCss;
|
|
209
213
|
declare const mount: typeof mountAll;
|
|
210
214
|
declare const from: typeof ilhaFrom;
|
|
211
215
|
declare const context: typeof ilhaContext;
|
|
212
216
|
//#endregion
|
|
213
|
-
export { DerivedValue, HandlerContext, HandlerContextFor, HydratableOptions, Island, IslandDerived, IslandState, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, SlotAccessor, context, ilha as default, from, html, mount, raw
|
|
217
|
+
export { DerivedValue, HandlerContext, HandlerContextFor, HydratableOptions, Island, IslandDerived, IslandState, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, SlotAccessor, context, css, ilha as default, from, html, mount, raw };
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,10 @@ const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
|
|
|
82
82
|
const SLOT_ATTR = "data-ilha-slot";
|
|
83
83
|
const PROPS_ATTR = "data-ilha-props";
|
|
84
84
|
const STATE_ATTR = "data-ilha-state";
|
|
85
|
+
const CSS_ATTR = "data-ilha-css";
|
|
86
|
+
function buildScopedStyle(css) {
|
|
87
|
+
return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css}}</style>`;
|
|
88
|
+
}
|
|
85
89
|
function makeSlotAccessor(render) {
|
|
86
90
|
const fn = (props) => ({
|
|
87
91
|
[RAW]: true,
|
|
@@ -107,6 +111,15 @@ function ilhaRaw(value) {
|
|
|
107
111
|
value
|
|
108
112
|
};
|
|
109
113
|
}
|
|
114
|
+
function ilhaCss(strings, ...values) {
|
|
115
|
+
if (typeof strings === "string") return strings;
|
|
116
|
+
let result = "";
|
|
117
|
+
for (let i = 0; i < strings.length; i++) {
|
|
118
|
+
result += strings[i];
|
|
119
|
+
if (i < values.length) result += String(values[i]);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
110
123
|
function interpolateValue(v) {
|
|
111
124
|
if (v == null) return "";
|
|
112
125
|
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
@@ -302,7 +315,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
302
315
|
}
|
|
303
316
|
input(schema) {
|
|
304
317
|
return new IlhaBuilder({
|
|
305
|
-
schema,
|
|
318
|
+
schema: schema ?? null,
|
|
306
319
|
states: [],
|
|
307
320
|
deriveds: [],
|
|
308
321
|
ons: [],
|
|
@@ -310,7 +323,8 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
310
323
|
onMounts: [],
|
|
311
324
|
slots: {},
|
|
312
325
|
transition: null,
|
|
313
|
-
binds: []
|
|
326
|
+
binds: [],
|
|
327
|
+
css: null
|
|
314
328
|
});
|
|
315
329
|
}
|
|
316
330
|
state(key, init) {
|
|
@@ -381,8 +395,26 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
381
395
|
transition: opts
|
|
382
396
|
});
|
|
383
397
|
}
|
|
398
|
+
css(strings, ...values) {
|
|
399
|
+
let source;
|
|
400
|
+
if (typeof strings === "string") source = strings;
|
|
401
|
+
else {
|
|
402
|
+
let result = "";
|
|
403
|
+
for (let i = 0; i < strings.length; i++) {
|
|
404
|
+
result += strings[i];
|
|
405
|
+
if (i < values.length) result += String(values[i]);
|
|
406
|
+
}
|
|
407
|
+
source = result;
|
|
408
|
+
}
|
|
409
|
+
if (__DEV__ && this._cfg.css !== null) warn("css(): called more than once on the same builder chain. The previous stylesheet has been discarded. Compose styles into a single .css() call instead.");
|
|
410
|
+
return new IlhaBuilder({
|
|
411
|
+
...this._cfg,
|
|
412
|
+
css: source
|
|
413
|
+
});
|
|
414
|
+
}
|
|
384
415
|
render(fn) {
|
|
385
|
-
const { schema, states, deriveds, ons, effects, onMounts, slots: slotDefs, transition, binds } = this._cfg;
|
|
416
|
+
const { schema, states, deriveds, ons, effects, onMounts, slots: slotDefs, transition, binds, css: cssSource } = this._cfg;
|
|
417
|
+
const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
|
|
386
418
|
function resolveInput(props) {
|
|
387
419
|
const value = props ?? {};
|
|
388
420
|
if (!schema) return value;
|
|
@@ -460,7 +492,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
460
492
|
value: r.result,
|
|
461
493
|
error: void 0
|
|
462
494
|
};
|
|
463
|
-
return unwrapHtml(fn({
|
|
495
|
+
return stylePrefix + unwrapHtml(fn({
|
|
464
496
|
state,
|
|
465
497
|
derived,
|
|
466
498
|
input,
|
|
@@ -490,7 +522,7 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
490
522
|
})).then((resolved) => {
|
|
491
523
|
const derived = {};
|
|
492
524
|
for (const r of resolved) derived[r.key] = r.envelope;
|
|
493
|
-
return unwrapHtml(fn({
|
|
525
|
+
return stylePrefix + unwrapHtml(fn({
|
|
494
526
|
state,
|
|
495
527
|
derived,
|
|
496
528
|
input,
|
|
@@ -613,7 +645,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
613
645
|
listeners.length = 0;
|
|
614
646
|
}
|
|
615
647
|
const slots = makeSlotsProxy(false, host);
|
|
616
|
-
if (!(hydrated && host.childNodes.length > 0)) host.innerHTML = unwrapHtml(fn({
|
|
648
|
+
if (!(hydrated && host.childNodes.length > 0)) host.innerHTML = stylePrefix + unwrapHtml(fn({
|
|
617
649
|
state,
|
|
618
650
|
derived,
|
|
619
651
|
input,
|
|
@@ -642,7 +674,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
642
674
|
}
|
|
643
675
|
let initialized = false;
|
|
644
676
|
const stopRender = effect(() => {
|
|
645
|
-
const html = unwrapHtml(fn({
|
|
677
|
+
const html = stylePrefix + unwrapHtml(fn({
|
|
646
678
|
state,
|
|
647
679
|
derived,
|
|
648
680
|
input,
|
|
@@ -801,7 +833,8 @@ const rootBuilder = new IlhaBuilder({
|
|
|
801
833
|
onMounts: [],
|
|
802
834
|
slots: {},
|
|
803
835
|
transition: null,
|
|
804
|
-
binds: []
|
|
836
|
+
binds: [],
|
|
837
|
+
css: null
|
|
805
838
|
});
|
|
806
839
|
const ilha = Object.assign(rootBuilder, {
|
|
807
840
|
html: ilhaHtml,
|
|
@@ -810,19 +843,11 @@ const ilha = Object.assign(rootBuilder, {
|
|
|
810
843
|
from: ilhaFrom,
|
|
811
844
|
context: ilhaContext
|
|
812
845
|
});
|
|
813
|
-
function type(coerce) {
|
|
814
|
-
return { "~standard": {
|
|
815
|
-
version: 1,
|
|
816
|
-
vendor: "ilha",
|
|
817
|
-
validate(value) {
|
|
818
|
-
return { value: coerce ? coerce(value) : value };
|
|
819
|
-
}
|
|
820
|
-
} };
|
|
821
|
-
}
|
|
822
846
|
const html = ilhaHtml;
|
|
823
847
|
const raw = ilhaRaw;
|
|
848
|
+
const css = ilhaCss;
|
|
824
849
|
const mount = mountAll;
|
|
825
850
|
const from = ilhaFrom;
|
|
826
851
|
const context = ilhaContext;
|
|
827
852
|
//#endregion
|
|
828
|
-
export { context, ilha as default, from, html, mount, raw
|
|
853
|
+
export { context, css, ilha as default, from, html, mount, raw };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilha",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A tiny, framework-free island architecture library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryuz <ryuzer@proton.me>",
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"alien-signals": "3.1.2"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"docsome": "^0.0.6",
|
|
30
29
|
"zod": "^4.3.6"
|
|
31
30
|
}
|
|
32
31
|
}
|