@nativele/ts-dom 0.5.0 → 0.5.2

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.
@@ -0,0 +1,319 @@
1
+ ---
2
+ name: nativele-state-management
3
+ description: "Decide where state lives in an app built with Nativele (component state, a shared createStore, or an existing Zustand, Redux Toolkit or nanostores store) and connect external stores to this.bind with a small adapter: selectors that limit re-rendering, actions and dispatch from event handlers, unsubscribing when a component is removed. Use when a project depends on @nativele/ts-dom and on zustand, @reduxjs/toolkit, redux or nanostores, or when components need to share data."
4
+ metadata:
5
+ tier: official
6
+ nativele: ">=0.5.2 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "zustand@5.0.16 @reduxjs/toolkit@2.14.0 redux@5.0.1 nanostores@1.5.5 vite@8.3.6 typescript@7.0.2 create-vite@9.3.4"
9
+ author: "Nativele maintainers"
10
+ detects: "zustand @reduxjs/toolkit redux nanostores"
11
+ ---
12
+
13
+ # Nativele state management
14
+
15
+ ## When to use
16
+
17
+ - An app that already uses Nativele (`@nativele/ts-dom`) has to decide where a piece of data lives: in one component, in a store shared by several components, or in a state library.
18
+ - The app already keeps state in Zustand, Redux Toolkit, Redux or nanostores, and Nativele components have to show it and change it.
19
+ - Signals: `zustand`, `@reduxjs/toolkit`, `redux` or `nanostores` next to `@nativele/ts-dom` in `package.json`; a value passed down through several layers of components only so a deep one can show it.
20
+ - The starting point is a Vite app set up with Nativele by `nativele-vite` (`npx ts-dom skills add nativele-vite`), for example one started with `npm create vite@latest -- --template vanilla-ts`. This Skill does not teach the state libraries themselves, and it does not write the components (`nativele-core`).
21
+ - Not for React, Vue or another framework's own bindings to these libraries (`react-redux`, Zustand's hooks): here the Nativele components subscribe themselves.
22
+
23
+ ## Versions
24
+
25
+ | Tool | Verified version |
26
+ |---|---|
27
+ | @nativele/ts-dom | 0.5.2 |
28
+ | zustand (`zustand/vanilla`) | 5.0.16 |
29
+ | @reduxjs/toolkit | 2.14.0 |
30
+ | redux | 5.0.1 (installed by `@reduxjs/toolkit`; a `legacy_createStore` store with the adapter was checked by hand with `tsc` and Node, the example project does not run it) |
31
+ | nanostores | 1.5.5 (`atom` only) |
32
+ | vite | 8.3.6 |
33
+ | typescript | 7.0.2 |
34
+ | create-vite (`vanilla-ts` template, the starting point) | 9.3.4 |
35
+
36
+ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nativele repository (`skills/nativele-state-management/example`), which CI runs again when the example or the library changes and once a month. With a different major version of a state library, treat the adapter and every limitation about that library as unverified.
37
+
38
+ ## Set-up
39
+
40
+ 1. Decide where each piece of data lives:
41
+
42
+ | The data is | Put it in | Why |
43
+ |---|---|---|
44
+ | used by one component, or by it and the children it passes props to (a field's text, open or closed, the selected row) | the component's `state` | it is changed with `this.set` and rendered by the component that owns it |
45
+ | read or changed by components that are not parent and child, or would be passed through layers that do not use it (the signed-in user, a theme) | a `createStore` from `@nativele/ts-dom` in its own module | every component that needs it imports it and binds it; the layers between them do not know about it |
46
+ | already kept in Zustand, Redux or nanostores, or shared with code that is not Nativele, or in need of what the library offers (its middleware, devtools, persistence) | that library's store, connected with the adapter below | the store stays the one source; Nativele components only read it and send it actions |
47
+
48
+ Do not move an existing Zustand or Redux store into `createStore` to use Nativele, and do not add a state library for data that `state` or `createStore` already holds.
49
+
50
+ 2. Keep each store in its own module outside the components folder, for example `src/stores/`, and import it from the components that use it. `ts-dom check src/nv` runs `tsc` over the whole project, so it type-checks these modules too.
51
+ 3. For Zustand and Redux, add the adapter below as `src/stores/from-store.ts`. `this.bind` takes an object with `get()` and `subscribe(fn)`, calls `fn` at once with `get()`, calls it again on every change, and ends the subscription when the element is removed (see the components guide). A Zustand or Redux store has `getState()` instead of `get()`, and a Redux listener gets no value, so neither can be passed as it is (limitations 1 and 2). The adapter reads the state itself, applies a selector, and calls back only when the selected value changes under `Object.is`:
52
+
53
+ ```ts
54
+ // src/stores/from-store.ts
55
+ import type { ReadableStore } from "@nativele/ts-dom";
56
+
57
+ export interface ExternalStore<S> {
58
+ getState(): S;
59
+ subscribe(listener: () => void): () => void;
60
+ }
61
+
62
+ export function fromStore<S, T>(store: ExternalStore<S>, select: (state: S) => T): ReadableStore<T> {
63
+ return {
64
+ get: () => select(store.getState()),
65
+ subscribe(fn) {
66
+ let last = select(store.getState());
67
+ return store.subscribe(() => {
68
+ const next = select(store.getState());
69
+ if (Object.is(next, last)) return;
70
+ last = next;
71
+ fn(next);
72
+ });
73
+ },
74
+ };
75
+ }
76
+ ```
77
+
78
+ 4. In each component, make one source per value it shows (`fromStore(store, (s) => s.items.length)`, at module level), bind it in `connected()`, and copy the value into `state` with `this.set`. Bind in `connected()`, never in the constructor (limitation 6).
79
+ 5. Change the store from the component's `data-on-*` handlers: call a Zustand action (`cart.getState().add("apple")`), dispatch a Redux action (`store.dispatch(increment())`), or set a nanostores atom (`$theme.set("dark")`). The store notifies synchronously, so the view has changed when the handler returns.
80
+ 6. For a nanostores atom, bind `{ get: $theme.get, subscribe: $theme.listen }`, not the atom itself (limitation 3).
81
+
82
+ ## Example
83
+
84
+ The stores:
85
+
86
+ ```ts
87
+ // src/stores/cart.ts (Zustand)
88
+ import { createStore } from "zustand/vanilla";
89
+
90
+ export interface CartState {
91
+ items: string[];
92
+ note: string;
93
+ add(item: string): void;
94
+ setNote(note: string): void;
95
+ }
96
+
97
+ export const cart = createStore<CartState>()((set) => ({
98
+ items: [],
99
+ note: "",
100
+ add: (item) => set((s) => ({ items: [...s.items, item] })),
101
+ setNote: (note) => set({ note }),
102
+ }));
103
+ ```
104
+
105
+ ```ts
106
+ // src/stores/counter.ts (Redux Toolkit)
107
+ import { configureStore, createSlice, type PayloadAction } from "@reduxjs/toolkit";
108
+
109
+ const counter = createSlice({
110
+ name: "counter",
111
+ initialState: { value: 0 },
112
+ reducers: {
113
+ increment: (state) => {
114
+ state.value += 1;
115
+ },
116
+ },
117
+ });
118
+
119
+ const filter = createSlice({
120
+ name: "filter",
121
+ initialState: "all" as "all" | "done",
122
+ reducers: { setFilter: (_state, action: PayloadAction<"all" | "done">) => action.payload },
123
+ });
124
+
125
+ export const store = configureStore({ reducer: { counter: counter.reducer, filter: filter.reducer } });
126
+ export const { increment } = counter.actions;
127
+ export const { setFilter } = filter.actions;
128
+ ```
129
+
130
+ ```ts
131
+ // src/stores/theme.ts (nanostores)
132
+ import { atom } from "nanostores";
133
+
134
+ export const $theme = atom<"light" | "dark">("light");
135
+ ```
136
+
137
+ ```ts
138
+ // src/stores/session.ts (Nativele)
139
+ import { createStore } from "@nativele/ts-dom";
140
+
141
+ export const session = createStore({ user: "guest" });
142
+ ```
143
+
144
+ A component that shows one slice of the Zustand store, and one that changes it:
145
+
146
+ ```html
147
+ <!-- src/nv/cart-badge.html -->
148
+ <span>{{ count }} in cart</span>
149
+ ```
150
+
151
+ ```ts
152
+ // src/nv/cart-badge.ts
153
+ import { Component, define } from "@nativele/ts-dom";
154
+ import { cart } from "../stores/cart.ts";
155
+ import { fromStore } from "../stores/from-store.ts";
156
+ import tpl from "./cart-badge.html.js";
157
+
158
+ const itemCount = fromStore(cart, (s) => s.items.length);
159
+
160
+ export class CartBadge extends Component(tpl, { state: { count: 0 } }) {
161
+ override connected(): void {
162
+ this.bind(itemCount, (count) => this.set({ count }));
163
+ }
164
+ }
165
+ define("cart-badge", CartBadge);
166
+ ```
167
+
168
+ ```html
169
+ <!-- src/nv/cart-form.html -->
170
+ <button type="button" data-on-click="addApple">add apple</button>
171
+ <input data-ref="note" data-on-input="onNote" />
172
+ ```
173
+
174
+ ```ts
175
+ // src/nv/cart-form.ts
176
+ import { Component, define } from "@nativele/ts-dom";
177
+ import { cart } from "../stores/cart.ts";
178
+ import tpl from "./cart-form.html.js";
179
+
180
+ export class CartForm extends Component(tpl) {
181
+ addApple(): void {
182
+ cart.getState().add("apple");
183
+ }
184
+ onNote(): void {
185
+ cart.getState().setNote(this.refs.note.value);
186
+ }
187
+ }
188
+ define("cart-form", CartForm);
189
+ ```
190
+
191
+ Typing a note changes `note` only, so `cart-badge` gets no callback and does not render.
192
+
193
+ A component that shows a Redux value and dispatches from a click:
194
+
195
+ ```html
196
+ <!-- src/nv/counter-view.html -->
197
+ <b>{{ value }}</b>
198
+ <button type="button" data-on-click="increment">+1</button>
199
+ ```
200
+
201
+ ```ts
202
+ // src/nv/counter-view.ts
203
+ import { Component, define } from "@nativele/ts-dom";
204
+ import { fromStore } from "../stores/from-store.ts";
205
+ import { increment, store } from "../stores/counter.ts";
206
+ import tpl from "./counter-view.html.js";
207
+
208
+ const counterValue = fromStore(store, (s) => s.counter.value);
209
+
210
+ export class CounterView extends Component(tpl, { state: { value: 0 } }) {
211
+ override connected(): void {
212
+ this.bind(counterValue, (value) => this.set({ value }));
213
+ }
214
+ increment(): void {
215
+ store.dispatch(increment());
216
+ }
217
+ }
218
+ define("counter-view", CounterView);
219
+ ```
220
+
221
+ A nanostores atom and a Nativele store, each bound directly:
222
+
223
+ ```ts
224
+ // src/nv/theme-switch.ts (template: <button type="button" data-on-click="toggle">theme: {{ theme }}</button>)
225
+ export class ThemeSwitch extends Component(tpl, { state: { theme: "light" as "light" | "dark" } }) {
226
+ override connected(): void {
227
+ this.bind({ get: $theme.get, subscribe: $theme.listen }, (theme) => this.set({ theme }));
228
+ }
229
+ toggle(): void {
230
+ $theme.set($theme.get() === "light" ? "dark" : "light");
231
+ }
232
+ }
233
+ define("theme-switch", ThemeSwitch);
234
+
235
+ // src/nv/user-badge.ts (template: <span>{{ user }}</span>); another component calls session.set((s) => ({ ...s, user: "Ada" }))
236
+ export class UserBadge extends Component(tpl, { state: { user: "" } }) {
237
+ override connected(): void {
238
+ this.bind(session, (s) => this.set({ user: s.user }));
239
+ }
240
+ }
241
+ define("user-badge", UserBadge);
242
+ ```
243
+
244
+ `src/main.ts` imports every component module; `index.html` places the tags anywhere on the page (`<cart-badge></cart-badge>` in a section, `<user-badge></user-badge>` in the header), with nothing passed between them.
245
+
246
+ ## Known limitations
247
+
248
+ 1. **A Zustand or Redux store cannot be passed to `bind`.** `this.bind(cart, ...)` fails with `TS2741: Property 'get' is missing in type 'StoreApi<CartState>'`, and `this.bind(store, ...)` with a Redux store with `TS2345: ... Property 'get' is missing`. Use `fromStore`. A Zustand store could be bound whole as `{ get: cart.getState, subscribe: cart.subscribe }` (Zustand 5 passes the state to its listeners), but then see limitation 4.
249
+ 2. **A Redux listener gets no value.** `{ get: store.getState, subscribe: store.subscribe }` fails with `TS2322: Type '(listener: ListenerCallback) => Unsubscribe' is not assignable ... Target signature provides too few arguments`: Redux 5 calls its listeners with no arguments. Use `fromStore`, which calls `getState()` itself.
250
+ 3. **A nanostores atom passes the type check but calls back twice.** `this.bind($theme, fn)` compiles, and `fn` runs twice when the component connects, because nanostores' `subscribe` also calls the listener at once. Bind `{ get: $theme.get, subscribe: $theme.listen }`: `listen` does not call at once, so `fn` runs once on connect and once per change. Not verified: `computed`, `map` and other nanostores stores.
251
+ 4. **Bind a selected value, not the whole store.** A whole store calls back on every change of any field. When the callback copies into `state`, `this.set` skips the render if nothing it copied changed (it compares each field with `Object.is`), but the callback still runs; a callback that writes `this.text`, `refs` or a list itself does that work every time. With `fromStore` and a selector, a change elsewhere in the store runs no callback and no render.
252
+ 5. **A selector must return one value that stays the same when unchanged.** A selector that builds a new object or array (`(s) => ({ count: s.items.length })`) differs under `Object.is` every time, so it calls back on every change of the store. Select each value with its own `fromStore`, or select an object or array the store itself keeps (Zustand and Redux keep an unchanged slice as the same object).
253
+ 6. **Bind in `connected()`.** When the element is removed, `bind` ends every subscription, and no store sends it updates. When the same element is inserted again, `connected()` runs, binds again, and the callback runs at once with the current value, so changes made while it was out show; it then gets each change once, not twice. A `bind` made anywhere else (the constructor, a field, code outside the component) ends on the first removal and is not made again.
254
+ 7. **`createStore` is exported by both `zustand/vanilla` and `@nativele/ts-dom`.** Importing both into one module fails with `TS2300: Duplicate identifier 'createStore'`. Keep each store in its own module (step 2), or rename one import (`import { createStore as createZustandStore } from "zustand/vanilla"`).
255
+ 8. **Store updates are synchronous.** A Zustand action, a Redux `dispatch` of a plain action and a nanostores `set` call `bind` callbacks before they return, so the view has changed when the handler ends.
256
+ 9. Not verified: Zustand middleware (`subscribeWithSelector`, `persist`, `devtools`), RTK Query and auto-batched actions, Redux thunks, an equality function other than `Object.is`, and the state libraries' framework bindings.
257
+
258
+ ## Verify
259
+
260
+ Run in the project:
261
+
262
+ ```sh
263
+ npm run build # expect: ts-dom check: no issues, then ✓ built
264
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
265
+ ```
266
+
267
+ `ts-dom check` type-checks the store modules and the adapter as well. To keep the checks of limitations 1 and 2 in the project, add a file the app never imports, with one wrong use per store under `// @ts-expect-error`:
268
+
269
+ ```ts
270
+ // src/types-check.ts
271
+ import type { CartBadge } from "./nv/cart-badge.ts";
272
+ import { cart } from "./stores/cart.ts";
273
+ import { store } from "./stores/counter.ts";
274
+
275
+ export function typesCheck(el: CartBadge): void {
276
+ // @ts-expect-error a Zustand store has getState, not get
277
+ el.bind(cart, () => {});
278
+ // @ts-expect-error a Redux store has getState, not get
279
+ el.bind(store, () => {});
280
+ // @ts-expect-error a Redux listener gets no value
281
+ el.bind({ get: store.getState, subscribe: store.subscribe }, () => {});
282
+ }
283
+ ```
284
+
285
+ If one of them stops being an error, `ts-dom check` reports `TS2578: Unused '@ts-expect-error' directive`.
286
+
287
+ In the browser, against `npm run dev`: click and type in each component and check that the bound components show the new value; remove a bound element (`el.remove()`), change its store, insert it again, and check that it shows the current value. To prove that a selector limits re-rendering, count calls by wrapping the component's prototype methods before acting, without changing the component:
288
+
289
+ ```js
290
+ const proto = customElements.get("cart-badge").prototype;
291
+ for (const name of ["set", "render"]) {
292
+ const original = proto[name];
293
+ proto[name] = function (...args) { console.log(name); return original.apply(this, args); };
294
+ }
295
+ ```
296
+
297
+ Then typing a note must log nothing for `cart-badge`, and adding an item must log `set` and `render` once each. The example project's [`verify.mjs`](https://github.com/Pixmi/nativele/blob/main/skills/nativele-state-management/example/verify.mjs) does these checks with Playwright for Zustand, Redux, nanostores and `createStore`, plus the `bind` contract itself (an immediate call, no call for an `Object.is`-equal set, nothing after removal), the nanostores double call, selectors that return a new object or a kept slice, and no console errors.
298
+
299
+ ## Troubleshooting
300
+
301
+ | Symptom | Cause | Fix |
302
+ |---|---|---|
303
+ | `TS2741: Property 'get' is missing in type 'StoreApi<...>'` | a Zustand store passed to `bind` | `this.bind(fromStore(cart, (s) => s.x), ...)` |
304
+ | `TS2345: ... Property 'get' is missing in type '... Store<...>'` | a Redux store passed to `bind` | `fromStore(store, (s) => s.x)` |
305
+ | `TS2322: ... '(listener: ListenerCallback) => Unsubscribe' ... Target signature provides too few arguments` | Redux's `subscribe` passed to `bind` as it is | `fromStore`, which calls `getState()` |
306
+ | The callback runs twice when the component connects | a nanostores atom bound directly | bind `{ get: $a.get, subscribe: $a.listen }` |
307
+ | A component renders, or its callback runs, when an unrelated field changes | the whole store is bound, or the selector returns a new object | one `fromStore` per value, each returning a value the store keeps |
308
+ | A component stops updating after it was moved or removed and inserted again | `bind` was called outside `connected()` | call `this.bind` in `connected()` |
309
+ | `TS2300: Duplicate identifier 'createStore'` | `zustand/vanilla` and `@nativele/ts-dom` imported into one module | separate modules, or rename one import |
310
+ | `[missing-binding] "x" is used by {{ x }} ... but the component declares no state, prop or derived value of that name` | a template shows a store field the component has no `state` for | add the field to `state` and copy it in the `bind` callback |
311
+
312
+ ## See also
313
+
314
+ - `bind`, `createStore` and the store contract: https://github.com/Pixmi/nativele/blob/main/docs/components.md#state-store-when-not-using-state
315
+ - Component `state`, `set` and `batch`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-and-view-state-optional
316
+ - Shared stores instead of passing through layers: https://github.com/Pixmi/nativele/blob/main/docs/components.md#avoiding-layer-by-layer-passing-lint-ts-domdata-flow
317
+ - Copying a store value into state: https://github.com/Pixmi/nativele/blob/main/docs/recipes.md#showing-data-from-a-store
318
+ - Moved elements, connecting and subscriptions: https://github.com/Pixmi/nativele/blob/main/docs/lists.md#moving-items-connecting-and-subscriptions
319
+ - Setting Nativele up in Vite: `npx ts-dom skills nativele-vite`
@@ -0,0 +1,259 @@
1
+ ---
2
+ name: nativele-svelte
3
+ description: "Use Nativele web components inside an existing Svelte 5 app: passing props, listening to events, children and slots, lifecycle, template types with svelte-check, and ts-dom check. Use when a project depends on svelte and @nativele/ts-dom."
4
+ metadata:
5
+ tier: experimental
6
+ nativele: ">=0.5.1 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "svelte@5.57.3 vite@8.3.6 @sveltejs/vite-plugin-svelte@7.3.1 svelte-check@4.7.8 @tsconfig/svelte@5.0.8 typescript@6.0.3"
9
+ author: "Nativele maintainers"
10
+ detects: "svelte"
11
+ ---
12
+
13
+ # Nativele + Svelte
14
+
15
+ ## When to use
16
+
17
+ - The project renders with Svelte 5 (with Vite) and wants to use components written with Nativele (`@nativele/ts-dom`), for example a design-system layer or widgets shared with non-Svelte pages.
18
+ - Signals: `svelte` and `@nativele/ts-dom` in `package.json`, `.svelte` files that render tags with a hyphen (`<info-card>`).
19
+ - Not for Svelte 4 or earlier, and not for SvelteKit (server rendering and hydration were not verified). Not for compiling Svelte components into custom elements (`<svelte:options customElement>`): that is the opposite direction.
20
+ - Not for writing the Nativele components themselves: use `nativele-core` (`npx ts-dom skills add nativele-core`).
21
+
22
+ Nativele components are native custom elements. Svelte renders them like any other tag; nothing wraps them.
23
+
24
+ ## Versions
25
+
26
+ | Tool | Verified version |
27
+ |---|---|
28
+ | @nativele/ts-dom | 0.5.1 |
29
+ | svelte | 5.57.3 |
30
+ | @sveltejs/vite-plugin-svelte | 7.3.1 |
31
+ | svelte-check | 4.7.8 |
32
+ | @tsconfig/svelte | 5.0.8 |
33
+ | vite | 8.3.6 |
34
+ | typescript | 6.0.3 |
35
+
36
+ Verified on 2026-11-05 in Google Chrome 148, against the Vite dev server (so Svelte's development warnings would show), in a fresh app made with `npm create vite@latest -- --template svelte-ts`. Runes mode (`$state`) is what was verified; legacy mode was checked only for the `on:` directive (limitation 2). With a different major version of Svelte, treat every step as unverified.
37
+
38
+ ## Set-up
39
+
40
+ 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts`. Write them with the core Skill: `npx ts-dom skills add nativele-core`.
41
+ 2. Install: `npm i @nativele/ts-dom`.
42
+ 3. Leave the tsconfig as it is: Vite's Svelte template has `tsconfig.app.json` extend `@tsconfig/svelte`, which sets `"strict": true`, and its `include` already covers `src/nv`. If the project turned `strict` off, set it back in that file.
43
+ 4. Add the Nativele Vite plugin before the Svelte plugin, pointing at that folder:
44
+
45
+ ```ts
46
+ // vite.config.ts
47
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
48
+ import { defineConfig } from "vite";
49
+ import { tsDom } from "@nativele/ts-dom/vite";
50
+
51
+ export default defineConfig({ plugins: [tsDom({ dirs: ["src/nv"] }), svelte()] });
52
+ ```
53
+
54
+ Pass the same folder to every `ts-dom` command (`npx ts-dom check src/nv`), so there is one `ts-dom-tags.ts`, in `src/nv`. Svelte needs no option to accept hyphenated tags.
55
+
56
+ 5. Add the generated files to `.gitignore`:
57
+
58
+ ```
59
+ *.html.ts
60
+ ts-dom-tags.ts
61
+ ```
62
+
63
+ 6. Import every Nativele component module in `src/main.ts`, before `mount`:
64
+
65
+ ```ts
66
+ // src/main.ts
67
+ import "./nv/info-card.js";
68
+ import "./nv/shadow-card.js";
69
+ import { mount } from "svelte";
70
+ import App from "./App.svelte";
71
+
72
+ mount(App, { target: document.getElementById("app")! });
73
+ ```
74
+
75
+ Svelte 5 sets a value as a property when the element is defined and has that property (limitation 1).
76
+
77
+ 7. Add the template declaration below as `src/nativele-svelte.d.ts`. It reads the tag map that `ts-dom gen` writes (`ts-dom-tags.ts`), so every exported component gets typed props and events in `.svelte` files, and new components need no edit:
78
+
79
+ ```ts
80
+ // src/nativele-svelte.d.ts
81
+ import type { HTMLAttributes } from "svelte/elements";
82
+
83
+ type NativeleTag = Extract<keyof HTMLElementTagNameMap, `${string}-${string}`>;
84
+ type NativeleProps<E> = E extends { readonly props: infer P } ? { [K in keyof P & keyof E]?: E[K] } : unknown;
85
+ type NativeleEvents<E> = E extends { readonly "~emits"?: infer M }
86
+ ? { [K in keyof NonNullable<M> & string as `on${K}`]?: (event: CustomEvent<NonNullable<M>[K]>) => void }
87
+ : unknown;
88
+ type NativeleElementProps<E extends HTMLElement> = Omit<HTMLAttributes<E>, keyof NativeleProps<E>> & NativeleProps<E> & NativeleEvents<E>;
89
+ type NativeleElements = { [K in NativeleTag]: NativeleElementProps<HTMLElementTagNameMap[K]> };
90
+
91
+ declare module "svelte/elements" {
92
+ interface SvelteHTMLElements extends NativeleElements {}
93
+ }
94
+ ```
95
+
96
+ The component class must be exported and passed to `define()` in a folder that `ts-dom gen` scans, so it appears in `ts-dom-tags.ts`.
97
+
98
+ 8. Prove the declaration types props with a `.ts` file that the app never imports, one wrong case per component kind under `// @ts-expect-error` (an expect-error comment in Svelte markup does not work, limitation 6):
99
+
100
+ ```ts
101
+ // src/types-check.ts
102
+ import type { SvelteHTMLElements } from "svelte/elements";
103
+
104
+ // @ts-expect-error an object prop given a string
105
+ export const badProp: SvelteHTMLElements["info-card"] = { item: "apple" };
106
+ // @ts-expect-error a prop name the component does not declare
107
+ export const badName: SvelteHTMLElements["info-card"] = { lable: "x" };
108
+ // @ts-expect-error a detail field the event does not have
109
+ export const badEvent: SvelteHTMLElements["info-card"] = { "onitem-select": (e) => e.detail.name };
110
+ ```
111
+
112
+ The generated files are not committed, so on a clean checkout the type check in `npm run check` runs before anything has written them and fails with `TS2307: Cannot find module './x.html.js'`. Generate them first: `"check": "ts-dom gen src/nv && svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"` (the template's script with `ts-dom gen src/nv &&` in front). `npm run build` needs no change: the Vite plugin generates the files.
113
+
114
+ ## Example
115
+
116
+ A Nativele component with props and an event:
117
+
118
+ ```html
119
+ <!-- src/nv/info-card.html -->
120
+ <span>{{ label }}</span>
121
+ <b>{{ count }}</b>
122
+ <i data-ref="itemName"></i>
123
+ <button type="button" data-on-click="pick">Pick</button>
124
+ ```
125
+
126
+ ```ts
127
+ // src/nv/info-card.ts
128
+ import { Component, define, event } from "@nativele/ts-dom";
129
+ import tpl from "./info-card.html.js";
130
+
131
+ export interface Item {
132
+ id: number;
133
+ name: string;
134
+ }
135
+
136
+ export class InfoCard extends Component(tpl, {
137
+ props: { label: "", count: 0, item: null as Item | null },
138
+ emits: { "item-select": event<{ id: number }>() },
139
+ }) {
140
+ pick(): void {
141
+ const item = this.props.item;
142
+ if (item) this.emit("item-select", { id: item.id });
143
+ }
144
+ override render(view: { item: Item | null }): void {
145
+ this.refs.itemName.textContent = view.item ? view.item.name : "-";
146
+ }
147
+ }
148
+ define("info-card", InfoCard);
149
+ ```
150
+
151
+ A component with slots, in Shadow DOM:
152
+
153
+ ```html
154
+ <!-- src/nv/shadow-card.html -->
155
+ <h2><slot name="title">Untitled</slot></h2>
156
+ <div><slot></slot></div>
157
+ ```
158
+
159
+ ```ts
160
+ // src/nv/shadow-card.ts
161
+ import { Component, define } from "@nativele/ts-dom";
162
+ import tpl from "./shadow-card.html.js";
163
+
164
+ export class ShadowCard extends Component(tpl, { shadow: true }) {}
165
+ define("shadow-card", ShadowCard);
166
+ ```
167
+
168
+ Used from Svelte:
169
+
170
+ ```svelte
171
+ <!-- src/App.svelte -->
172
+ <script lang="ts">
173
+ import type { Item } from "./nv/info-card.js";
174
+
175
+ let count = $state(0);
176
+ let item = $state<Item>({ id: 1, name: "apple" });
177
+ let picked = $state<number[]>([]);
178
+ let rows = $state(["a", "b", "c"]);
179
+ let showTitle = $state(true);
180
+ </script>
181
+
182
+ <info-card label="Fruit" {count} {item} onitem-select={(e) => picked.push(e.detail.id)}></info-card>
183
+ <button type="button" onclick={() => { count++; item = { ...item, name: "pear" }; }}>Next</button>
184
+
185
+ <shadow-card>
186
+ {#if showTitle}<span slot="title">Title</span>{/if}
187
+ {#each rows as r (r)}<p>{r}</p>{/each}
188
+ </shadow-card>
189
+ ```
190
+
191
+ - `label`, `count` and `item` arrive as properties; the element has no attribute for them. Changing them from Svelte state re-renders the Nativele component.
192
+ - The event attribute is `on` followed by the exact event name: `onitem-select`. `e.detail` is typed from the component's `emits`.
193
+ - In a `shadow: true` component, `{#if}` and keyed `{#each}` children can change freely.
194
+
195
+ ## Known limitations
196
+
197
+ 1. **Define before mount.** Import the component modules before `mount` (step 6). Svelte 5 decides per value: when the element is defined, it sets a property the element has; when it is not defined yet (for example a module loaded later with `import()`), it sets objects and arrays as properties and writes every other value as an attribute string. Nativele reads both when the element is upgraded, which was verified for string, number and boolean props with one-word names (`count={0}` becomes `count="0"`, `false` becomes `open="false"`, both arrive with the right type). Other cases were not verified, so define first.
198
+ 2. **Event attribute names.** Only `on` plus the exact event name works (`onitem-select`). `onItemSelect` listens to an event named `ItemSelect` and never fires; with the declaration it is also a `svelte-check` error. The legacy directive `on:item-select` fires too, but only use the event attributes: in a runes component `on:` is deprecated (compiler warning `event_directive_deprecated`), a component cannot mix it with event attributes (compile error `mixed_event_handler_syntaxes`), and the declaration in step 7 does not type it. Nativele events do not bubble, so put the handler on the element that emits.
199
+ 3. **Light DOM slots and Svelte-managed children do not mix.** In the default Light DOM mode (`Component(tpl)` with no `shadow`), Nativele moves the host's children into its slot positions, together with the comment nodes Svelte uses as anchors. Svelte does not throw, but it goes wrong silently: an `{#if}` that adds `<span slot="title">` back inserts it next to its anchor, which now sits in the default slot, so the title slot shows its fallback; and `assignedNodes()` stops matching what is on the page. Keyed `{#each}` reordering happened to look right, but do not rely on it. Use one of:
200
+ - `shadow: true` on the component (children can then change freely), or
201
+ - one stable element per slot, and let Svelte change only what is inside it:
202
+
203
+ ```svelte
204
+ <light-card>
205
+ <div slot="title">{#if showTitle}<span>Title</span>{/if}</div>
206
+ <div>{#each rows as r (r)}<p>{r}</p>{/each}</div>
207
+ </light-card>
208
+ ```
209
+
210
+ A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
211
+ 4. **New objects for changed props.** A Nativele component compares props by reference. Changing a `$state` object in place (`item.name = "kiwi"`) does not re-render it; assign a new object (`item = { ...item, name: "kiwi" }`).
212
+ 5. **Write closing tags.** `<info-card ... />` works, but the Svelte compiler warns `element_invalid_self_closing_tag` (shown in the dev server's terminal); write `<info-card ...></info-card>`.
213
+ 6. **Template types need the declaration** in step 7. Without it, Svelte accepts any attribute on a hyphenated tag, and an inline handler's parameter is `any` (`Parameter 'e' implicitly has an 'any' type` under `strict`). With it, `svelte-check` reports a wrong prop type, an unknown prop name, a wrong `e.detail` field and a camelCase handler name. `<!-- @ts-expect-error -->` in markup does not silence an error, so keep the expected-error cases in a `.ts` file (step 8). To type an element reference, use the tag map: `let card: HTMLElementTagNameMap["info-card"] | undefined = $state()` with `bind:this={card}`.
214
+ 7. **`ts-dom check` does not read `.svelte` files.** It runs the project's `tsc`, which checks the Nativele folder and the `.ts` files (including `types-check.ts`); `.svelte` imports resolve through the `*.svelte` module declaration in Svelte's own types, so they cause no error, but the markup and scripts inside `.svelte` files are checked only by `svelte-check`. Run both (Verify).
215
+ 8. **Lifecycle.** Removing an element with `{#if}` runs `disconnected()` and ends its `bind` subscriptions; adding it back runs `connected()` and subscribes again. Reordering a keyed `{#each}` of Nativele elements moves them, which runs `disconnected()` and then `connected()`; each element keeps its slotted children, and every `bind` callback still runs once per store change.
216
+ 9. Not verified: SvelteKit, server rendering and hydration, form controls inside Nativele components submitting with a Svelte form, `$state.raw`, hot updates of `.svelte` files that contain Nativele tags, and Svelte versions other than 5.57.
217
+
218
+ ## Verify
219
+
220
+ Run in the project:
221
+
222
+ ```sh
223
+ npx ts-dom check src/nv # expect: ts-dom check: no issues
224
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
225
+ npm run check # svelte-check plus tsc for vite.config.ts; expect: svelte-check found 0 errors and 0 warnings
226
+ npm run build # expect: built
227
+ ```
228
+
229
+ When `npm run check` is not run in a terminal (an agent, CI), `svelte-check` prints `COMPLETED ... 0 ERRORS 0 WARNINGS ...` instead. Delete `src/nativele-svelte.d.ts` once to see the check work: `svelte-check` must then report `Unused '@ts-expect-error' directive` in `src/types-check.ts`.
230
+
231
+ In the browser, against the dev server (production builds drop Svelte's warnings), render a component with an object prop and an event attribute, and check:
232
+
233
+ - the element has no attribute for the object prop (`el.getAttributeNames()`) and `el.item` is the object;
234
+ - the handler runs once per event;
235
+ - children that Svelte toggles with `{#if}` and reorders, adds and removes with keyed `{#each}` stay in the right slot, both in a `shadow: true` component and in stable wrappers in a Light DOM component, with no errors or warnings in the console. Read a slot with `el.assignedNodes(name)` (`""` is the default slot), which works in both modes; a Light DOM component has no `<slot>` elements once rendered. Svelte leaves comment nodes as anchors among the children, so compare elements, not all nodes.
236
+
237
+ ## Troubleshooting
238
+
239
+ | Symptom | Cause | Fix |
240
+ |---|---|---|
241
+ | `svelte-check` reports `Cannot find module './x.html.js'` on a clean checkout | it runs before the generated files exist | start the `check` script with `ts-dom gen src/nv &&` |
242
+ | The event handler never runs | the attribute is camelCased (`onItemSelect`), or it is on an ancestor | use `on` plus the exact event name (`onitem-select`) on the element that emits |
243
+ | `svelte-check`: `'"on:item-select"' does not exist in type 'NativeleElementProps<...>'` | the legacy `on:` directive, which the declaration does not type | use the event attribute `onitem-select` |
244
+ | `Mixing old (on:item-select) and new syntaxes for event handling is not allowed` | `on:` and event attributes in the same component | use only event attributes |
245
+ | `svelte-check`: `Parameter 'e' implicitly has an 'any' type` on a handler of a Nativele tag | the declaration file is missing, or the component class is not exported | add `src/nativele-svelte.d.ts`, export the class, run `npx ts-dom check src/nv` |
246
+ | `svelte-check`: `Unused '@ts-expect-error' directive` in `types-check.ts` | the declaration is not applied | same as above |
247
+ | A slotted child shows up in the wrong slot, or the fallback shows although Svelte rendered the child | Svelte-managed children directly inside a Light DOM component with slots | `shadow: true`, or a stable wrapper element per slot |
248
+ | The component does not update after `item.name = ...` | the object was changed in place; props compare by reference | assign a new object |
249
+ | `Self-closing HTML tags for non-void elements are ambiguous` | `<info-card />` | write `<info-card></info-card>` |
250
+ | `ts-dom check` reports `unused-ref` | a `data-ref` the class never reads | use it in the class or remove the attribute |
251
+
252
+ ## See also
253
+
254
+ - Components, props and events: https://github.com/Pixmi/nativele/blob/main/docs/components.md
255
+ - How attributes convert to props: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-flow-props-down-events-up
256
+ - Slots, Light DOM and Shadow DOM: https://github.com/Pixmi/nativele/blob/main/docs/components.md#light-default-and-shadow
257
+ - Stores and `bind`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#state-store-when-not-using-state
258
+ - Setup and checks: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md
259
+ - The framework-neutral rules: `npx ts-dom skills nativele-integration`