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 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 using any [Standard Schema](https://standardschema.dev/) compatible validator (e.g. Zod, Valibot, ArkType).
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: "my-island", // registry key for client-side activation
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="my-island" data-ilha-props="…" data-ilha-state="…">…</div>'
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
- ### `type(coerce?)`
499
+ ### `css\`\`` tagged template
434
500
 
435
- Creates a lightweight Standard Schema validator for use with `.input()` — useful when you don't want a full validation library.
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 { type } from "ilha";
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
- const MyIsland = ilha
441
- .input(type((v: unknown) => v as { count: number }))
442
- .render(({ input }) => `<p>${input.count}</p>`);
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({ "my-island": MyIsland });
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({ "my-island": MyIsland })
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, type };
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, type };
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.0",
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
  }