reactolith 1.0.19
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/LICENSE +21 -0
- package/README.md +523 -0
- package/dist/cli/generate-web-types.cjs +607 -0
- package/dist/cli/generate-web-types.cjs.map +1 -0
- package/dist/index.cjs +611 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.mjs +599 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Franz Mayr-Wilding
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# ⚡️ reactolith
|
|
2
|
+
|
|
3
|
+
> **Proof of Concept** – This project is still experimental and **not ready for production**.
|
|
4
|
+
|
|
5
|
+
`reactolith` lets you **write React components directly in HTML** — making it possible to render and hydrate a React app using server-generated HTML from any backend (e.g. Symfony/Twig, Rails, Laravel, Django, etc.).
|
|
6
|
+
|
|
7
|
+
✨ Instead of manually wiring React components everywhere, just return HTML from your backend and `reactolith` will transform it into a live, interactive React application.
|
|
8
|
+
|
|
9
|
+
It even includes a **built-in router** that intercepts link clicks and form submissions, fetches the next page via AJAX, and updates only what changed — **keeping React state intact between navigations**.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🚀 Features
|
|
14
|
+
- 🔌 **Backend-agnostic** – Works with any backend (Symfony, Rails, Laravel, etc.)
|
|
15
|
+
- 🛠 **Use existing backend helpers** (Twig path functions, permission checks, etc.)
|
|
16
|
+
- 🔄 **State preserved across pages** – No resets on navigation
|
|
17
|
+
- 📋 **Form support** – Modify forms dynamically (e.g., add buttons on checkbox click) **without losing state or focus**
|
|
18
|
+
- 🪶 **Lightweight** – Just a few lines of setup, no heavy dependencies
|
|
19
|
+
- 📡 **Real-time updates** – Works with Mercure Server-Sent-Events to push updates from the backend to the frontend
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 📦 Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install reactolith
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Since react and react-dom are peer dependencies, make sure to also install them:
|
|
30
|
+
```bash
|
|
31
|
+
npm install react react-dom
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### How It Works
|
|
37
|
+
|
|
38
|
+
1. **Initial Load**: Symfony renders HTML with Twig, `reactolith` hydrates it into React components
|
|
39
|
+
2. **Navigation**: Clicking links fetches new HTML via AJAX, React reconciles the differences
|
|
40
|
+
3. **Real-time**: Mercure pushes HTML updates from server, UI updates automatically
|
|
41
|
+
4. **State Preserved**: React component state survives both navigation and real-time updates
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 💡 Usage
|
|
46
|
+
|
|
47
|
+
Your backend returns simple HTML:
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<body>
|
|
52
|
+
<div id="reactolith-app">
|
|
53
|
+
<h1>Hello world</h1>
|
|
54
|
+
<ui-button type="primary">This will be a shadcn button</ui-button>
|
|
55
|
+
</div>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Your frontend mounts the reactolith app:
|
|
61
|
+
```ts
|
|
62
|
+
// app.ts
|
|
63
|
+
import loadable from '@loadable/component'
|
|
64
|
+
import { App } from 'reactolith'
|
|
65
|
+
|
|
66
|
+
const component = loadable(
|
|
67
|
+
async ({ is }: { is: string }) => {
|
|
68
|
+
return import(`./components/ui/${is.substring(3)}.tsx`)
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
cacheKey: ({ is }) => is,
|
|
72
|
+
// Since shadcn files don’t export a default,
|
|
73
|
+
// we resolve the correct named export
|
|
74
|
+
resolveComponent: (mod, { is }: { is: string }) => {
|
|
75
|
+
const cmpName = is
|
|
76
|
+
.substring(3)
|
|
77
|
+
.replace(/(^\w|-\w)/g, match => match.replace(/-/, '').toUpperCase())
|
|
78
|
+
return mod[cmpName]
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Uses the HTML element with id="reactolith-app" as root
|
|
84
|
+
new App(component)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 🎨 Example with Custom Root Component & Selector
|
|
88
|
+
```ts
|
|
89
|
+
// app.ts
|
|
90
|
+
import loadable from '@loadable/component'
|
|
91
|
+
import { App } from 'reactolith'
|
|
92
|
+
import { AppProvider } from './providers/app-provider.tsx'
|
|
93
|
+
|
|
94
|
+
const component = loadable(
|
|
95
|
+
async ({ is }: { is: string }) => import(`./components/${is}.tsx`),
|
|
96
|
+
{ cacheKey: ({ is }) => is }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
new App(component, AppProvider, '#app')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// providers/app-provider.tsx
|
|
104
|
+
import React, { ElementType } from "react"
|
|
105
|
+
import { App, RootComponent } from "reactolith"
|
|
106
|
+
import { RouterProvider } from "react-aria-components"
|
|
107
|
+
import { ThemeProvider } from "./theme-provider"
|
|
108
|
+
|
|
109
|
+
export const AppProvider: React.FC<{
|
|
110
|
+
app: App
|
|
111
|
+
element: HTMLElement
|
|
112
|
+
component: ElementType
|
|
113
|
+
}> = ({ app, element, component }) => (
|
|
114
|
+
<React.StrictMode>
|
|
115
|
+
<RouterProvider navigate={app.router.navigate}>
|
|
116
|
+
<ThemeProvider>
|
|
117
|
+
<RootComponent element={element} component={component} />
|
|
118
|
+
</ThemeProvider>
|
|
119
|
+
</RouterProvider>
|
|
120
|
+
</React.StrictMode>
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 🔄 Navigation Without Losing State
|
|
127
|
+
|
|
128
|
+
When navigating, `reactolith` fetches the **next HTML page** and applies **only the differences** using React’s reconciliation algorithm.
|
|
129
|
+
👉 This means component state is preserved (e.g., toggles, inputs, focus).
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<!-- page1.html -->
|
|
133
|
+
<div id="reactolith-app">
|
|
134
|
+
<h1>Page 1</h1>
|
|
135
|
+
<ui-toggle json-pressed="false">Toggle</ui-toggle>
|
|
136
|
+
<a href="page2.html">Go to page 2</a>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<!-- page2.html -->
|
|
142
|
+
<div id="reactolith-app">
|
|
143
|
+
<h1>Page 2</h1>
|
|
144
|
+
<ui-toggle json-pressed="true">Toggle</ui-toggle>
|
|
145
|
+
<a href="page1.html">Go to page 1</a>
|
|
146
|
+
</div>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Only the `<h1>` text and the `pressed` prop are updated — everything else remains untouched ✅.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Props
|
|
154
|
+
|
|
155
|
+
If you pass props to your reactolith components like this:
|
|
156
|
+
```html
|
|
157
|
+
<my-component enabled name="test" data-foo="baa" as="{my-other-component}" json-config='{ "foo": "baa" }'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
your components will get this props:
|
|
161
|
+
```tsx
|
|
162
|
+
const props = {
|
|
163
|
+
enabled: true,
|
|
164
|
+
name: 'test',
|
|
165
|
+
foot: 'baa',
|
|
166
|
+
as: <MyOtherComponent />,
|
|
167
|
+
config: { foo: 'baa' },
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Slots
|
|
174
|
+
|
|
175
|
+
reactolith also provides a simple slot mechanism: Every child if a reactolith-component with a slot attribute will be
|
|
176
|
+
transformed to a slot property, holding the children of the element:
|
|
177
|
+
|
|
178
|
+
```html
|
|
179
|
+
<my-component>
|
|
180
|
+
<template slot="header"><h1>My header content</h1></template>
|
|
181
|
+
<div slot="footer">My footer content</div>
|
|
182
|
+
</my-component>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
your components will get this props:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
function MyComponent({ header, footer } : { header : ReactNode, footer : ReactNode }) {
|
|
189
|
+
<article>
|
|
190
|
+
<header>{header}</header>
|
|
191
|
+
<div>My content</div>
|
|
192
|
+
<footer>{footer}</footer>
|
|
193
|
+
<aside>
|
|
194
|
+
<footer>{footer}</footer>
|
|
195
|
+
</aside>
|
|
196
|
+
</article>
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 📡 Real-time Updates with Mercure
|
|
203
|
+
|
|
204
|
+
`reactolith` supports **Server-Sent Events (SSE)** via [Mercure](https://mercure.rocks/) for real-time updates from your backend. When the server publishes an update, the HTML is automatically rendered — just like with router navigation.
|
|
205
|
+
|
|
206
|
+
Mercure automatically subscribes to the **current URL pathname** as the topic and re-subscribes when the route changes.
|
|
207
|
+
|
|
208
|
+
### Auto-Configuration (Recommended)
|
|
209
|
+
|
|
210
|
+
The easiest way to configure Mercure is to add the `data-mercure-hub-url` attribute to your root element:
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<div id="reactolith-app" data-mercure-hub-url="https://example.com/.well-known/mercure">
|
|
214
|
+
<!-- Your content -->
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- With credentials (cookies): -->
|
|
218
|
+
<div id="reactolith-app"
|
|
219
|
+
data-mercure-hub-url="https://example.com/.well-known/mercure"
|
|
220
|
+
data-mercure-with-credentials>
|
|
221
|
+
<!-- Your content -->
|
|
222
|
+
</div>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { App, Mercure } from "reactolith";
|
|
227
|
+
|
|
228
|
+
const app = new App(component);
|
|
229
|
+
// mercureConfig is automatically set from data-mercure-hub-url attribute
|
|
230
|
+
|
|
231
|
+
const mercure = new Mercure(app);
|
|
232
|
+
mercure.subscribe(app.mercureConfig!);
|
|
233
|
+
|
|
234
|
+
// optional listen to events
|
|
235
|
+
mercure.on("sse:connected", (url) => {
|
|
236
|
+
console.log("Connected to Mercure hub");
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Manual Configuration
|
|
241
|
+
|
|
242
|
+
Alternatively, you can configure Mercure programmatically:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { App, Mercure } from "reactolith";
|
|
246
|
+
|
|
247
|
+
const app = new App(component);
|
|
248
|
+
const mercure = new Mercure(app);
|
|
249
|
+
|
|
250
|
+
// Subscribe to Mercure hub (uses current pathname as topic)
|
|
251
|
+
mercure.subscribe({
|
|
252
|
+
hubUrl: "https://example.com/.well-known/mercure",
|
|
253
|
+
withCredentials: true, // Include cookies for authentication
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
When the user navigates to a different route, Mercure automatically reconnects with the new pathname as the topic.
|
|
258
|
+
|
|
259
|
+
### Auto-Refetch on Empty Messages
|
|
260
|
+
|
|
261
|
+
When Mercure receives an **empty message** (or whitespace-only), it automatically refetches the current route. This makes it easy to invalidate the current page from the backend without having to render and send the full HTML:
|
|
262
|
+
|
|
263
|
+
**Backend (simple invalidation):**
|
|
264
|
+
```php
|
|
265
|
+
// Just notify that the page should refresh - no HTML needed
|
|
266
|
+
$hub->publish(new Update('/dashboard', ''));
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Instead of:
|
|
270
|
+
```php
|
|
271
|
+
// Old way: render and send full HTML
|
|
272
|
+
$html = $twig->render('dashboard.html.twig', $data);
|
|
273
|
+
$hub->publish(new Update('/dashboard', $html));
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
This triggers a GET request to the current URL and renders the response.
|
|
277
|
+
|
|
278
|
+
### Mercure Events
|
|
279
|
+
|
|
280
|
+
| Event | Arguments | Description |
|
|
281
|
+
|-------|-----------|-------------|
|
|
282
|
+
| `sse:connected` | `url` | Connection established |
|
|
283
|
+
| `sse:disconnected` | `url` | Connection closed |
|
|
284
|
+
| `sse:message` | `event, html` | Message received |
|
|
285
|
+
| `render:success` | `event, html` | HTML rendered successfully |
|
|
286
|
+
| `render:failed` | `event, html` | Render failed (no root element) |
|
|
287
|
+
| `refetch:started` | `event` | Auto-refetch triggered (empty message) |
|
|
288
|
+
| `refetch:success` | `event, html` | Auto-refetch completed successfully |
|
|
289
|
+
| `refetch:failed` | `event, error` | Auto-refetch failed |
|
|
290
|
+
| `sse:error` | `error` | Connection error |
|
|
291
|
+
|
|
292
|
+
### Live Data with useMercureTopic
|
|
293
|
+
|
|
294
|
+
For simple live values (like notification counts, user status), use the `useMercureTopic` hook to subscribe to Mercure topics that send JSON data:
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { useMercureTopic } from 'reactolith';
|
|
298
|
+
|
|
299
|
+
// Simple types - inferred from initial value
|
|
300
|
+
function NotificationBadge() {
|
|
301
|
+
const count = useMercureTopic('/notifications/count', 0);
|
|
302
|
+
|
|
303
|
+
if (count === 0) return null;
|
|
304
|
+
return <span className="badge">{count}</span>;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Explicit type parameter
|
|
308
|
+
function UserStatus({ userId }: { userId: number }) {
|
|
309
|
+
const status = useMercureTopic<'online' | 'offline' | 'away'>(
|
|
310
|
+
`/user/${userId}/status`,
|
|
311
|
+
'offline'
|
|
312
|
+
);
|
|
313
|
+
return <span className={status}>{status}</span>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Complex types with interfaces
|
|
317
|
+
interface DashboardStats {
|
|
318
|
+
visitors: number;
|
|
319
|
+
sales: number;
|
|
320
|
+
conversion: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function Dashboard() {
|
|
324
|
+
const stats = useMercureTopic<DashboardStats>('/dashboard/stats', {
|
|
325
|
+
visitors: 0,
|
|
326
|
+
sales: 0,
|
|
327
|
+
conversion: 0,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div>
|
|
332
|
+
<span>Visitors: {stats.visitors}</span>
|
|
333
|
+
<span>Sales: {stats.sales}</span>
|
|
334
|
+
<span>Conversion: {stats.conversion}%</span>
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Backend:**
|
|
341
|
+
```php
|
|
342
|
+
// Push JSON data to topic
|
|
343
|
+
$hub->publish(new Update(
|
|
344
|
+
'/notifications/count',
|
|
345
|
+
json_encode(42)
|
|
346
|
+
));
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Note:** When using `useMercureTopic`, make sure `app.mercureConfig` is set. You can either:
|
|
350
|
+
- Use the auto-configuration by adding `data-mercure-hub-url` to your root element (recommended), or
|
|
351
|
+
- Set it manually:
|
|
352
|
+
```typescript
|
|
353
|
+
const app = new App(component);
|
|
354
|
+
app.mercureConfig = {
|
|
355
|
+
hubUrl: "/.well-known/mercure",
|
|
356
|
+
withCredentials: true,
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Custom Live Regions (Partial Updates)
|
|
361
|
+
|
|
362
|
+
For partial updates (e.g., updating a sidebar across all pages), you can create your own live region component. The `mercureConfig` is accessible via `useApp()`:
|
|
363
|
+
|
|
364
|
+
**Setup:**
|
|
365
|
+
```typescript
|
|
366
|
+
import { App, Mercure, MercureLive } from 'reactolith';
|
|
367
|
+
import loadable from '@loadable/component';
|
|
368
|
+
|
|
369
|
+
const component = loadable(
|
|
370
|
+
async ({ is }: { is: string }) => {
|
|
371
|
+
// The mapping is up to you, reactolith only provides the MercureLive Component (don't lazy load it!)
|
|
372
|
+
if (is === 'mercure-live') {
|
|
373
|
+
return MercureLive;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Your default implementaiton
|
|
377
|
+
return import(`./components/${is}.tsx`);
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
cacheKey: ({ is }) => is,
|
|
381
|
+
resolveComponent: (mod, { is }) => {
|
|
382
|
+
if (is === 'mercure-live') {
|
|
383
|
+
return mod;
|
|
384
|
+
}
|
|
385
|
+
return mod.default || mod[is];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
const app = new App(component);
|
|
392
|
+
const mercure = new Mercure(app);
|
|
393
|
+
|
|
394
|
+
// Store config for components to access
|
|
395
|
+
app.mercureConfig = {
|
|
396
|
+
hubUrl: "/.well-known/mercure",
|
|
397
|
+
withCredentials: true,
|
|
398
|
+
};
|
|
399
|
+
mercure.subscribe(app.mercureConfig);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Beispiel: Live Sidebar
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
// components/sidebar.tsx
|
|
406
|
+
export function Sidebar({ children }: { children: React.ReactNode }) {
|
|
407
|
+
return (
|
|
408
|
+
<aside className="sidebar">
|
|
409
|
+
{children}
|
|
410
|
+
</aside>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**HTML Usage:**
|
|
416
|
+
```html
|
|
417
|
+
<div id="reactolith-app">
|
|
418
|
+
<nav>...</nav>
|
|
419
|
+
|
|
420
|
+
<!-- Diese Region wird live aktualisiert -->
|
|
421
|
+
<mercure-live topic="/sidebar">
|
|
422
|
+
<sidebar>
|
|
423
|
+
<ul>
|
|
424
|
+
<li>Initial menu item 1</li>
|
|
425
|
+
<li>Initial menu item 2</li>
|
|
426
|
+
</ul>
|
|
427
|
+
</sidebar>
|
|
428
|
+
</mercure-live>
|
|
429
|
+
|
|
430
|
+
<main>...</main>
|
|
431
|
+
</div>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Backend:**
|
|
435
|
+
```php
|
|
436
|
+
// Render die Sidebar neu
|
|
437
|
+
$html = $twig->render('_sidebar.html.twig', [
|
|
438
|
+
'menuItems' => $updatedMenuItems
|
|
439
|
+
]);
|
|
440
|
+
|
|
441
|
+
// Push zu allen Clients
|
|
442
|
+
$hub->publish(new Update('/sidebar', $html));
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Template (_sidebar.html.twig):**
|
|
446
|
+
```twig
|
|
447
|
+
<sidebar>
|
|
448
|
+
<ul>
|
|
449
|
+
{% for item in menuItems %}
|
|
450
|
+
<li>{{ item.label }}</li>
|
|
451
|
+
{% endfor %}
|
|
452
|
+
</ul>
|
|
453
|
+
</sidebar>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 🧩 IDE Autocomplete (Web-Types)
|
|
459
|
+
|
|
460
|
+
`reactolith` includes a CLI tool to generate [web-types](https://github.com/JetBrains/web-types) for your custom components. This enables **autocomplete and validation** in IDEs like WebStorm, PhpStorm, and VS Code (with appropriate plugins).
|
|
461
|
+
|
|
462
|
+
### Generate web-types.json
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
npx generate-web-types -c src/components/ui -o web-types.json -n my-app
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Options:**
|
|
469
|
+
| Option | Short | Description | Default |
|
|
470
|
+
|--------|-------|-------------|---------|
|
|
471
|
+
| `--components` | `-c` | Components directory | `components/ui` |
|
|
472
|
+
| `--tsconfig` | `-t` | TypeScript config file | `tsconfig.app.json` (or `tsconfig.json`) |
|
|
473
|
+
| `--out` | `-o` | Output file | `web-types.json` |
|
|
474
|
+
| `--name` | `-n` | Library name | `reactolith-components` |
|
|
475
|
+
| `--version` | `-v` | Library version | `1.0.0` |
|
|
476
|
+
| `--prefix` | `-p` | Element name prefix | `""` |
|
|
477
|
+
| `--help` | `-h` | Show help | |
|
|
478
|
+
|
|
479
|
+
### Configure your project
|
|
480
|
+
|
|
481
|
+
Add the generated file to your `package.json`:
|
|
482
|
+
|
|
483
|
+
```json
|
|
484
|
+
{
|
|
485
|
+
"name": "my-app",
|
|
486
|
+
"web-types": "./web-types.json"
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Result
|
|
491
|
+
|
|
492
|
+
After restarting your IDE, you'll get:
|
|
493
|
+
- ✅ **Autocomplete** for custom element names (e.g., `<ui-button>`)
|
|
494
|
+
- ✅ **Prop suggestions** with types and descriptions
|
|
495
|
+
- ✅ **Slot hints** for components with children/slots
|
|
496
|
+
- ✅ **Validation** for required props and valid values
|
|
497
|
+
|
|
498
|
+
**Tip:** Add `npx generate-web-types ...` to your build script to keep web-types in sync:
|
|
499
|
+
|
|
500
|
+
```json
|
|
501
|
+
{
|
|
502
|
+
"scripts": {
|
|
503
|
+
"build": "vite build && npx generate-web-types -c src/components/ui -o web-types.json"
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 🤝 Contributing
|
|
511
|
+
|
|
512
|
+
Contributions are welcome!
|
|
513
|
+
Feel free to open an issue or submit a PR.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## 🛠 Development Build
|
|
518
|
+
If you’re contributing to this library:
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
npm install
|
|
522
|
+
npm run build
|
|
523
|
+
```
|