ilha 0.1.0 → 0.2.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
@@ -129,7 +129,6 @@ ilha
129
129
  .on("@click", ({ state }) => state.count(state.count() + 1)) // host click
130
130
  .on("button.inc@click", ({ state }) => state.count(state.count() + 1)) // child click
131
131
  .on("input@input:debounce", ({ state, event }) => {
132
- // with modifier
133
132
  state.query((event.target as HTMLInputElement).value);
134
133
  })
135
134
  .render(({ state }) => html`<div><button class="inc">+</button></div>`);
@@ -223,6 +222,63 @@ You can also bind to an external signal created with `context()`:
223
222
 
224
223
  ---
225
224
 
225
+ ### `.css(strings, ...values)`
226
+
227
+ 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.
228
+
229
+ ```ts
230
+ import { css } from "ilha";
231
+
232
+ const Card = ilha.state("active", false).css`
233
+ .title { font-weight: 700; }
234
+ button { background: teal; color: white; }
235
+ `.render(
236
+ ({ state }) => html`
237
+ <div>
238
+ <p class="title">Hello</p>
239
+ <button>Toggle</button>
240
+ </div>
241
+ `,
242
+ );
243
+ ```
244
+
245
+ Interpolations are supported:
246
+
247
+ ```ts
248
+ const accent = "teal";
249
+
250
+ ilha.css`button { background: ${accent}; }`.render(() => `<button>Go</button>`);
251
+ ```
252
+
253
+ You can also pass a plain string (e.g. from an external `.css` file):
254
+
255
+ ```ts
256
+ import styles from "./card.css?raw";
257
+
258
+ ilha.css(styles).render(() => `<div class="card">…</div>`);
259
+ ```
260
+
261
+ **SSR output** — a `<style data-ilha-css>` tag is prepended as the first child of the island's rendered HTML:
262
+
263
+ ```html
264
+ <style data-ilha-css>
265
+ @scope (:scope) to ([data-ilha]) {
266
+ .title {
267
+ font-weight: 700;
268
+ }
269
+ }
270
+ </style>
271
+ <div>…</div>
272
+ ```
273
+
274
+ **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.
275
+
276
+ **`.hydratable()` integration** — the style tag is included inside the `data-ilha` wrapper regardless of the `snapshot` option.
277
+
278
+ > **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.
279
+
280
+ ---
281
+
226
282
  ### `.slot(name, island)`
227
283
 
228
284
  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.
@@ -430,6 +486,41 @@ raw("<strong>bold</strong>"); // → passes through unescaped
430
486
 
431
487
  ---
432
488
 
489
+ ### `css\`\`` tagged template
490
+
491
+ 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.
492
+
493
+ ```ts
494
+ import { css } from "ilha";
495
+
496
+ const styles = css`
497
+ button {
498
+ background: teal;
499
+ color: white;
500
+ }
501
+ .label {
502
+ font-weight: 700;
503
+ }
504
+ `;
505
+
506
+ ilha.css(styles).render(() => `<button class="label">Go</button>`);
507
+ ```
508
+
509
+ Interpolations work as normal string concatenation:
510
+
511
+ ```ts
512
+ const accent = "coral";
513
+ const styles = css`
514
+ button {
515
+ background: ${accent};
516
+ }
517
+ `;
518
+ ```
519
+
520
+ > **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.
521
+
522
+ ---
523
+
433
524
  ### `type(coerce?)`
434
525
 
435
526
  Creates a lightweight Standard Schema validator for use with `.input()` — useful when you don't want a full validation library.
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,6 +177,7 @@ 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>;
@@ -191,6 +193,7 @@ declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap exte
191
193
  onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots>;
192
194
  slot<K extends string, I extends AnyIsland>(name: K, island: I): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots & Record<K, I>>;
193
195
  transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots>;
196
+ css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<TInput, TStateMap, TDerivedMap, TSlots>;
194
197
  render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap, TSlots>) => string | RawHtml): Island<TInput, TStateMap>;
195
198
  }
196
199
  declare function ilhaFrom<TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>): (() => void) | null;
@@ -206,8 +209,9 @@ declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>,
206
209
  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, type };
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("");
@@ -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,
@@ -821,8 +854,9 @@ function type(coerce) {
821
854
  }
822
855
  const html = ilhaHtml;
823
856
  const raw = ilhaRaw;
857
+ const css = ilhaCss;
824
858
  const mount = mountAll;
825
859
  const from = ilhaFrom;
826
860
  const context = ilhaContext;
827
861
  //#endregion
828
- export { context, ilha as default, from, html, mount, raw, type };
862
+ export { context, css, ilha as default, from, html, mount, raw, type };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilha",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A tiny, framework-free island architecture library",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",