@hypen-space/core 0.2.0 → 0.2.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.
- package/README.md +373 -0
- package/dist/src/components/builtin.js +89 -0
- package/dist/src/components/builtin.js.map +10 -0
- package/dist/src/discovery.js +234 -0
- package/dist/src/discovery.js.map +10 -0
- package/dist/src/engine.browser.js.map +2 -2
- package/dist/src/engine.js +2 -2
- package/dist/src/engine.js.map +3 -3
- package/dist/src/index.js +25 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader.js +77 -0
- package/dist/src/loader.js.map +10 -0
- package/package.json +19 -1
- package/src/components/builtin.ts +169 -0
- package/src/discovery.ts +416 -0
- package/src/engine.browser.ts +1 -2
- package/src/engine.ts +2 -2
- package/src/index.ts +29 -0
- package/src/loader.ts +136 -0
- package/dist/engine.d.ts +0 -101
- package/dist/events.d.ts +0 -78
- package/dist/index.browser.d.ts +0 -13
- package/dist/index.d.ts +0 -33
- package/dist/remote/index.d.ts +0 -6
- package/dist/router.d.ts +0 -93
- package/dist/state.d.ts +0 -30
package/README.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# @hypen-space/core
|
|
2
|
+
|
|
3
|
+
The reactive runtime for Hypen - a declarative UI language that separates what your UI looks like from how it behaves.
|
|
4
|
+
|
|
5
|
+
## What is Hypen?
|
|
6
|
+
|
|
7
|
+
Hypen lets you write UI templates in a clean, declarative syntax:
|
|
8
|
+
|
|
9
|
+
```hypen
|
|
10
|
+
Column {
|
|
11
|
+
Text("Hello, ${state.user.name}!")
|
|
12
|
+
Button(onClick: @actions.logout) {
|
|
13
|
+
Text("Sign Out")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The `@hypen-space/core` package provides the engine that:
|
|
19
|
+
|
|
20
|
+
1. **Parses** your Hypen templates
|
|
21
|
+
2. **Tracks** reactive state bindings (like `${state.user.name}`)
|
|
22
|
+
3. **Generates patches** when state changes (instead of re-rendering everything)
|
|
23
|
+
4. **Dispatches actions** triggered from the UI (like `@actions.logout`)
|
|
24
|
+
|
|
25
|
+
You provide a renderer that applies those patches to your platform (DOM, Canvas, Native, etc).
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @hypen-space/core
|
|
31
|
+
# or
|
|
32
|
+
bun add @hypen-space/core
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Core Concepts
|
|
36
|
+
|
|
37
|
+
### Templates
|
|
38
|
+
|
|
39
|
+
Hypen templates describe your UI structure. Components can have:
|
|
40
|
+
|
|
41
|
+
- **Arguments**: `Text("Hello")` or `Button(disabled: true)`
|
|
42
|
+
- **Children**: Nested inside `{ }` braces
|
|
43
|
+
- **Applicators**: Chained styling like `.padding(16).color(blue)`
|
|
44
|
+
|
|
45
|
+
### State Bindings
|
|
46
|
+
|
|
47
|
+
Use `${state.path}` to bind template values to your module's state:
|
|
48
|
+
|
|
49
|
+
```hypen
|
|
50
|
+
Text("Count: ${state.count}")
|
|
51
|
+
Text("User: ${state.user.name}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When state changes, only the affected parts of the UI update.
|
|
55
|
+
|
|
56
|
+
### Actions
|
|
57
|
+
|
|
58
|
+
Use `@actions.name` to dispatch events from the UI to your module:
|
|
59
|
+
|
|
60
|
+
```hypen
|
|
61
|
+
Button(onClick: @actions.increment) { Text("+") }
|
|
62
|
+
Button(onClick: @actions.submitForm) { Text("Submit") }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Patches
|
|
66
|
+
|
|
67
|
+
The engine doesn't manipulate the UI directly. Instead, it emits **patches** - minimal instructions describing what changed:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
{ type: "Create", id: "1", elementType: "Text", props: { text: "Hello" } }
|
|
71
|
+
{ type: "SetProp", id: "1", name: "text", value: "Hello, World" }
|
|
72
|
+
{ type: "Remove", id: "1" }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Your renderer applies these patches to the actual platform.
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Engine, app } from "@hypen-space/core";
|
|
81
|
+
|
|
82
|
+
// 1. Define your module's state and actions
|
|
83
|
+
const counter = app
|
|
84
|
+
.defineState({ count: 0 })
|
|
85
|
+
.onAction("increment", ({ state }) => state.count++)
|
|
86
|
+
.onAction("decrement", ({ state }) => state.count--)
|
|
87
|
+
.build();
|
|
88
|
+
|
|
89
|
+
// 2. Initialize the engine
|
|
90
|
+
const engine = new Engine();
|
|
91
|
+
await engine.init();
|
|
92
|
+
|
|
93
|
+
// 3. Connect your renderer
|
|
94
|
+
engine.setRenderCallback((patches) => {
|
|
95
|
+
myRenderer.applyPatches(patches);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 4. Register the module and render
|
|
99
|
+
engine.setModule("counter", counter.actions, counter.stateKeys, counter.initialState);
|
|
100
|
+
|
|
101
|
+
engine.renderSource(`
|
|
102
|
+
Column {
|
|
103
|
+
Text("Count: \${state.count}")
|
|
104
|
+
Row {
|
|
105
|
+
Button(onClick: @actions.decrement) { Text("-") }
|
|
106
|
+
Button(onClick: @actions.increment) { Text("+") }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Modules
|
|
113
|
+
|
|
114
|
+
Modules manage state and handle actions. Use the `app` builder to define them:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { app } from "@hypen-space/core";
|
|
118
|
+
|
|
119
|
+
interface UserState {
|
|
120
|
+
user: { id: string; name: string } | null;
|
|
121
|
+
loading: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const userModule = app
|
|
125
|
+
.defineState<UserState>({ user: null, loading: false })
|
|
126
|
+
|
|
127
|
+
// Called when module is created
|
|
128
|
+
.onCreated(async ({ state, next }) => {
|
|
129
|
+
state.loading = true;
|
|
130
|
+
next(); // Sync state changes to engine
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Handle actions dispatched from UI
|
|
134
|
+
.onAction("loadUser", async ({ state, action, next }) => {
|
|
135
|
+
const userId = action.payload?.id;
|
|
136
|
+
state.user = await fetchUser(userId);
|
|
137
|
+
state.loading = false;
|
|
138
|
+
next();
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
.onAction("logout", ({ state, next }) => {
|
|
142
|
+
state.user = null;
|
|
143
|
+
next();
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Called when module is destroyed
|
|
147
|
+
.onDestroyed(({ state }) => {
|
|
148
|
+
console.log("Cleanup");
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
.build();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The `next()` callback synchronizes state changes with the engine after async operations.
|
|
155
|
+
|
|
156
|
+
## State
|
|
157
|
+
|
|
158
|
+
State is automatically tracked via Proxy. Mutations trigger UI updates:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { createObservableState, batchStateUpdates, getStateSnapshot } from "@hypen-space/core";
|
|
162
|
+
|
|
163
|
+
const state = createObservableState({ count: 0, items: [] }, {
|
|
164
|
+
onChange: (path, oldVal, newVal) => {
|
|
165
|
+
console.log(`${path.join(".")} changed: ${oldVal} -> ${newVal}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Direct mutations are tracked
|
|
170
|
+
state.count = 5;
|
|
171
|
+
state.items.push("item");
|
|
172
|
+
|
|
173
|
+
// Batch multiple updates into one render cycle
|
|
174
|
+
batchStateUpdates(state, () => {
|
|
175
|
+
state.count = 10;
|
|
176
|
+
state.items = ["a", "b", "c"];
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Get an immutable snapshot
|
|
180
|
+
const snapshot = getStateSnapshot(state);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Custom Renderers
|
|
184
|
+
|
|
185
|
+
Extend `BaseRenderer` to render to any platform:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { BaseRenderer } from "@hypen-space/core";
|
|
189
|
+
|
|
190
|
+
class MyRenderer extends BaseRenderer {
|
|
191
|
+
protected onCreate(id: string, type: string, props: Record<string, any>) {
|
|
192
|
+
// Create an element
|
|
193
|
+
}
|
|
194
|
+
protected onSetProp(id: string, name: string, value: any) {
|
|
195
|
+
// Update a property
|
|
196
|
+
}
|
|
197
|
+
protected onSetText(id: string, text: string) {
|
|
198
|
+
// Set text content
|
|
199
|
+
}
|
|
200
|
+
protected onInsert(parentId: string, id: string, beforeId?: string) {
|
|
201
|
+
// Insert into parent
|
|
202
|
+
}
|
|
203
|
+
protected onMove(parentId: string, id: string, beforeId?: string) {
|
|
204
|
+
// Reorder element
|
|
205
|
+
}
|
|
206
|
+
protected onRemove(id: string) {
|
|
207
|
+
// Remove element
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
See `@hypen-space/web` for a DOM renderer implementation.
|
|
213
|
+
|
|
214
|
+
## Routing
|
|
215
|
+
|
|
216
|
+
Built-in hash or pathname routing:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { HypenRouter } from "@hypen-space/core";
|
|
220
|
+
|
|
221
|
+
const router = new HypenRouter();
|
|
222
|
+
|
|
223
|
+
router.navigate("/products/123");
|
|
224
|
+
|
|
225
|
+
router.subscribe((state) => {
|
|
226
|
+
console.log("Path:", state.currentPath);
|
|
227
|
+
console.log("Params:", state.params); // { id: "123" }
|
|
228
|
+
console.log("Query:", state.query);
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Use built-in components in templates:
|
|
233
|
+
|
|
234
|
+
```hypen
|
|
235
|
+
Router {
|
|
236
|
+
Route(path: "/") { HomePage }
|
|
237
|
+
Route(path: "/products/:id") { ProductPage }
|
|
238
|
+
}
|
|
239
|
+
Link(to: "/products/42") { Text("View Product") }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Component Discovery
|
|
243
|
+
|
|
244
|
+
For larger apps, organize components as files and auto-discover them:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { discoverComponents, loadDiscoveredComponents } from "@hypen-space/core";
|
|
248
|
+
|
|
249
|
+
const components = await discoverComponents("./src/components");
|
|
250
|
+
const loaded = await loadDiscoveredComponents(components);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Supported file patterns:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Counter/
|
|
257
|
+
├── component.hypen # Template
|
|
258
|
+
└── component.ts # Module
|
|
259
|
+
|
|
260
|
+
Counter.hypen # Or sibling files
|
|
261
|
+
Counter.ts
|
|
262
|
+
|
|
263
|
+
Counter/
|
|
264
|
+
├── index.hypen # Or index-based
|
|
265
|
+
└── index.ts
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Watch for changes (hot reload):
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { watchComponents } from "@hypen-space/core";
|
|
272
|
+
|
|
273
|
+
const watcher = watchComponents("./src/components", {
|
|
274
|
+
onUpdate: (c) => console.log("Updated:", c.name),
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Component Loader
|
|
279
|
+
|
|
280
|
+
Register components programmatically:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { componentLoader, ComponentLoader } from "@hypen-space/core";
|
|
284
|
+
|
|
285
|
+
// Global loader
|
|
286
|
+
componentLoader.register("Counter", counterModule, counterTemplate);
|
|
287
|
+
componentLoader.get("Counter");
|
|
288
|
+
componentLoader.has("Counter");
|
|
289
|
+
|
|
290
|
+
// Or create your own
|
|
291
|
+
const loader = new ComponentLoader();
|
|
292
|
+
await loader.loadFromComponentsDir("./src/components");
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Remote UI
|
|
296
|
+
|
|
297
|
+
Connect to a Hypen server for server-driven UI:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { RemoteEngine } from "@hypen-space/core";
|
|
301
|
+
|
|
302
|
+
const remote = new RemoteEngine("ws://localhost:3000", {
|
|
303
|
+
autoReconnect: true,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
remote
|
|
307
|
+
.onPatches((patches) => renderer.applyPatches(patches))
|
|
308
|
+
.onStateUpdate((state) => console.log("Server state:", state))
|
|
309
|
+
.onConnect(() => console.log("Connected"));
|
|
310
|
+
|
|
311
|
+
await remote.connect();
|
|
312
|
+
remote.dispatchAction("loadData", { page: 1 });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Global Context
|
|
316
|
+
|
|
317
|
+
Share state and events across modules:
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { HypenGlobalContext } from "@hypen-space/core";
|
|
321
|
+
|
|
322
|
+
const context = new HypenGlobalContext();
|
|
323
|
+
|
|
324
|
+
context.registerModule("auth", authModule);
|
|
325
|
+
context.registerModule("cart", cartModule);
|
|
326
|
+
|
|
327
|
+
// Cross-module access
|
|
328
|
+
const user = context.getModule("auth").getState().user;
|
|
329
|
+
|
|
330
|
+
// Event bus
|
|
331
|
+
context.on("userLoggedIn", (user) => { /* ... */ });
|
|
332
|
+
context.emit("userLoggedIn", { id: "1", name: "Ian" });
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Browser vs Node.js
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// Node.js / Bundler - WASM loads automatically
|
|
339
|
+
import { Engine } from "@hypen-space/core";
|
|
340
|
+
const engine = new Engine();
|
|
341
|
+
await engine.init();
|
|
342
|
+
|
|
343
|
+
// Browser - specify WASM path
|
|
344
|
+
import { BrowserEngine } from "@hypen-space/core";
|
|
345
|
+
const engine = new BrowserEngine();
|
|
346
|
+
await engine.init({ wasmPath: "/hypen_engine_bg.wasm" });
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Package Exports
|
|
350
|
+
|
|
351
|
+
| Export | Description |
|
|
352
|
+
|--------|-------------|
|
|
353
|
+
| `@hypen-space/core` | Main entry point |
|
|
354
|
+
| `@hypen-space/core/engine` | Low-level WASM engine |
|
|
355
|
+
| `@hypen-space/core/engine/browser` | Browser-optimized engine |
|
|
356
|
+
| `@hypen-space/core/app` | Module builder |
|
|
357
|
+
| `@hypen-space/core/state` | Observable state utilities |
|
|
358
|
+
| `@hypen-space/core/renderer` | Abstract renderer |
|
|
359
|
+
| `@hypen-space/core/router` | Routing system |
|
|
360
|
+
| `@hypen-space/core/context` | Global context |
|
|
361
|
+
| `@hypen-space/core/remote` | Remote UI protocol |
|
|
362
|
+
| `@hypen-space/core/loader` | Component loader |
|
|
363
|
+
| `@hypen-space/core/discovery` | Component discovery |
|
|
364
|
+
| `@hypen-space/core/components` | Built-in Router, Route, Link |
|
|
365
|
+
|
|
366
|
+
## Requirements
|
|
367
|
+
|
|
368
|
+
- Node.js >= 18.0.0
|
|
369
|
+
- TypeScript 5+ (optional)
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
app
|
|
3
|
+
} from "../app.js";
|
|
4
|
+
import"../state.js";
|
|
5
|
+
import"../../chunk-5va59f7m.js";
|
|
6
|
+
|
|
7
|
+
// src/components/builtin.ts
|
|
8
|
+
var Router = app.defineState({
|
|
9
|
+
currentPath: "/",
|
|
10
|
+
matchedRoute: null,
|
|
11
|
+
routeParams: {}
|
|
12
|
+
}, { name: "__Router" }).onCreated((state, context) => {
|
|
13
|
+
if (!context) {
|
|
14
|
+
console.error("[Router] Requires global context");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const router = context.__router;
|
|
18
|
+
if (!router) {
|
|
19
|
+
console.error("[Router] Router not found in context");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const hypenEngine = context.__hypenEngine;
|
|
23
|
+
const updateRouteVisibility = async (currentPath) => {
|
|
24
|
+
const routeElements = document.querySelectorAll('[data-hypen-type="route"]');
|
|
25
|
+
let matchFound = false;
|
|
26
|
+
for (let index = 0;index < routeElements.length; index++) {
|
|
27
|
+
const routeEl = routeElements[index];
|
|
28
|
+
const htmlEl = routeEl;
|
|
29
|
+
const routePath = htmlEl.dataset.routePath || "/";
|
|
30
|
+
const isMatch = routePath === currentPath;
|
|
31
|
+
htmlEl.style.display = isMatch ? "flex" : "none";
|
|
32
|
+
if (isMatch) {
|
|
33
|
+
matchFound = true;
|
|
34
|
+
const componentName = htmlEl.dataset.routeComponent;
|
|
35
|
+
const isLazy = htmlEl.dataset.routeLazy === "true";
|
|
36
|
+
const hasContent = htmlEl.children.length > 0;
|
|
37
|
+
if (componentName && !hasContent && hypenEngine) {
|
|
38
|
+
try {
|
|
39
|
+
await hypenEngine.renderLazyRoute(routePath, componentName, htmlEl);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`[Router] Failed to render route ${routePath}:`, err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!matchFound) {
|
|
47
|
+
console.warn(`[Router] No route matched path: ${currentPath}. Available routes:`, Array.from(routeElements).map((el) => el.dataset.routePath));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
updateRouteVisibility(state.currentPath);
|
|
52
|
+
}, 100);
|
|
53
|
+
router.onNavigate((routeState) => {
|
|
54
|
+
state.currentPath = routeState.currentPath;
|
|
55
|
+
state.routeParams = routeState.params;
|
|
56
|
+
updateRouteVisibility(routeState.currentPath);
|
|
57
|
+
});
|
|
58
|
+
}).build();
|
|
59
|
+
var Route = app.defineState({}, { name: "__Route" }).build();
|
|
60
|
+
var Link = app.defineState({
|
|
61
|
+
to: "/",
|
|
62
|
+
isActive: false
|
|
63
|
+
}, { name: "__Link" }).onAction("navigate", ({ state, context }) => {
|
|
64
|
+
const router = context?.__router;
|
|
65
|
+
if (!router) {
|
|
66
|
+
console.error("[Link] Requires router context");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const targetPath = state.to;
|
|
70
|
+
router.push(targetPath);
|
|
71
|
+
}).onCreated((state, context) => {
|
|
72
|
+
if (!context)
|
|
73
|
+
return;
|
|
74
|
+
const router = context.__router;
|
|
75
|
+
if (router) {
|
|
76
|
+
router.onNavigate((routeState) => {
|
|
77
|
+
state.isActive = routeState.currentPath === state.to;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}).build();
|
|
81
|
+
export {
|
|
82
|
+
Router,
|
|
83
|
+
Route,
|
|
84
|
+
Link
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { Router, Route, Link };
|
|
88
|
+
|
|
89
|
+
//# debugId=DCABC4D99E5064EE64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/components/builtin.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Built-in Hypen Components\n * Framework-provided components like Router and Route\n */\n\nimport { app } from \"../app.js\";\nimport type { HypenRouter } from \"../router.js\";\n\n/**\n * Router Component\n * Automatically matches routes and renders the appropriate child Route\n *\n * Usage in Hypen DSL:\n * Router {\n * Route(path: \"/\") { HomePage }\n * Route(path: \"/products\") { ProductList }\n * Route(path: \"/users/:id\") { UserProfile }\n * }\n */\nexport const Router = app\n .defineState(\n {\n currentPath: \"/\",\n matchedRoute: null as any,\n routeParams: {} as Record<string, string>,\n },\n { name: \"__Router\" }\n )\n .onCreated((state, context) => {\n if (!context) {\n console.error(\"[Router] Requires global context\");\n return;\n }\n\n // Subscribe to router changes\n const router = (context as any).__router as HypenRouter;\n if (!router) {\n console.error(\"[Router] Router not found in context\");\n return;\n }\n\n // Get access to the Hypen engine for lazy rendering\n const hypenEngine = (context as any).__hypenEngine;\n\n // Function to render only the matching route\n const updateRouteVisibility = async (currentPath: string) => {\n // Find all Route elements in the DOM\n const routeElements = document.querySelectorAll(\n '[data-hypen-type=\"route\"]'\n );\n\n let matchFound = false;\n\n for (let index = 0; index < routeElements.length; index++) {\n const routeEl = routeElements[index];\n const htmlEl = routeEl as HTMLElement;\n const routePath = htmlEl.dataset.routePath || \"/\";\n\n // Simple path matching (exact match for now)\n const isMatch = routePath === currentPath;\n\n // Only show the matching route, hide all others\n htmlEl.style.display = isMatch ? \"flex\" : \"none\";\n\n if (isMatch) {\n matchFound = true;\n\n // Check if this route needs to be rendered\n const componentName = htmlEl.dataset.routeComponent;\n const isLazy = htmlEl.dataset.routeLazy === \"true\";\n const hasContent = htmlEl.children.length > 0;\n\n // Render if route has no content and has a component name\n if (componentName && !hasContent && hypenEngine) {\n try {\n await hypenEngine.renderLazyRoute(\n routePath,\n componentName,\n htmlEl\n );\n } catch (err) {\n console.error(`[Router] Failed to render route ${routePath}:`, err);\n }\n }\n }\n }\n\n if (!matchFound) {\n console.warn(\n `[Router] No route matched path: ${currentPath}. Available routes:`,\n Array.from(routeElements).map(\n (el: Element) => (el as HTMLElement).dataset.routePath\n )\n );\n }\n };\n\n // Initial route visibility (after DOM is ready)\n setTimeout(() => {\n updateRouteVisibility(state.currentPath);\n }, 100);\n\n router.onNavigate((routeState) => {\n state.currentPath = routeState.currentPath;\n state.routeParams = routeState.params;\n\n // Update route visibility when path changes\n updateRouteVisibility(routeState.currentPath);\n });\n })\n .build();\n\n/**\n * Route Component\n * Defines a route pattern and its content\n * This is just a marker component - Router processes it\n *\n * Props:\n * - path: string - Route pattern (e.g., \"/\", \"/users/:id\", \"/dashboard/*\")\n *\n * Usage:\n * Route(path: \"/products\") {\n * ProductList\n * }\n */\nexport const Route = app.defineState({}, { name: \"__Route\" }).build();\n\n/**\n * Navigation Link Component\n * Navigates to a route when clicked\n *\n * Props:\n * - to: string - Target path\n *\n * Usage:\n * Link(to: \"/products\") {\n * Text(\"View Products\")\n * }\n */\nexport const Link = app\n .defineState(\n {\n to: \"/\",\n isActive: false,\n },\n { name: \"__Link\" }\n )\n .onAction(\"navigate\", ({ state, context }) => {\n const router = (context as any)?.__router as HypenRouter;\n if (!router) {\n console.error(\"[Link] Requires router context\");\n return;\n }\n\n const targetPath = state.to;\n router.push(targetPath);\n })\n .onCreated((state, context) => {\n if (!context) return;\n\n // Check if current path matches this link's target\n const router = (context as any).__router as HypenRouter;\n if (router) {\n router.onNavigate((routeState) => {\n state.isActive = routeState.currentPath === state.to;\n });\n }\n })\n .build();\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;AAmBO,IAAM,SAAS,IACnB,YACC;AAAA,EACE,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa,CAAC;AAChB,GACA,EAAE,MAAM,WAAW,CACrB,EACC,UAAU,CAAC,OAAO,YAAY;AAAA,EAC7B,IAAI,CAAC,SAAS;AAAA,IACZ,QAAQ,MAAM,kCAAkC;AAAA,IAChD;AAAA,EACF;AAAA,EAGA,MAAM,SAAU,QAAgB;AAAA,EAChC,IAAI,CAAC,QAAQ;AAAA,IACX,QAAQ,MAAM,sCAAsC;AAAA,IACpD;AAAA,EACF;AAAA,EAGA,MAAM,cAAe,QAAgB;AAAA,EAGrC,MAAM,wBAAwB,OAAO,gBAAwB;AAAA,IAE3D,MAAM,gBAAgB,SAAS,iBAC7B,2BACF;AAAA,IAEA,IAAI,aAAa;AAAA,IAEjB,SAAS,QAAQ,EAAG,QAAQ,cAAc,QAAQ,SAAS;AAAA,MACzD,MAAM,UAAU,cAAc;AAAA,MAC9B,MAAM,SAAS;AAAA,MACf,MAAM,YAAY,OAAO,QAAQ,aAAa;AAAA,MAG9C,MAAM,UAAU,cAAc;AAAA,MAG9B,OAAO,MAAM,UAAU,UAAU,SAAS;AAAA,MAE1C,IAAI,SAAS;AAAA,QACX,aAAa;AAAA,QAGb,MAAM,gBAAgB,OAAO,QAAQ;AAAA,QACrC,MAAM,SAAS,OAAO,QAAQ,cAAc;AAAA,QAC5C,MAAM,aAAa,OAAO,SAAS,SAAS;AAAA,QAG5C,IAAI,iBAAiB,CAAC,cAAc,aAAa;AAAA,UAC/C,IAAI;AAAA,YACF,MAAM,YAAY,gBAChB,WACA,eACA,MACF;AAAA,YACA,OAAO,KAAK;AAAA,YACZ,QAAQ,MAAM,mCAAmC,cAAc,GAAG;AAAA;AAAA,QAEtE;AAAA,MACF;AAAA,IACF;AAAA,IAEA,IAAI,CAAC,YAAY;AAAA,MACf,QAAQ,KACN,mCAAmC,kCACnC,MAAM,KAAK,aAAa,EAAE,IACxB,CAAC,OAAiB,GAAmB,QAAQ,SAC/C,CACF;AAAA,IACF;AAAA;AAAA,EAIF,WAAW,MAAM;AAAA,IACf,sBAAsB,MAAM,WAAW;AAAA,KACtC,GAAG;AAAA,EAEN,OAAO,WAAW,CAAC,eAAe;AAAA,IAChC,MAAM,cAAc,WAAW;AAAA,IAC/B,MAAM,cAAc,WAAW;AAAA,IAG/B,sBAAsB,WAAW,WAAW;AAAA,GAC7C;AAAA,CACF,EACA,MAAM;AAeF,IAAM,QAAQ,IAAI,YAAY,CAAC,GAAG,EAAE,MAAM,UAAU,CAAC,EAAE,MAAM;AAc7D,IAAM,OAAO,IACjB,YACC;AAAA,EACE,IAAI;AAAA,EACJ,UAAU;AACZ,GACA,EAAE,MAAM,SAAS,CACnB,EACC,SAAS,YAAY,GAAG,OAAO,cAAc;AAAA,EAC5C,MAAM,SAAU,SAAiB;AAAA,EACjC,IAAI,CAAC,QAAQ;AAAA,IACX,QAAQ,MAAM,gCAAgC;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,MAAM;AAAA,EACzB,OAAO,KAAK,UAAU;AAAA,CACvB,EACA,UAAU,CAAC,OAAO,YAAY;AAAA,EAC7B,IAAI,CAAC;AAAA,IAAS;AAAA,EAGd,MAAM,SAAU,QAAgB;AAAA,EAChC,IAAI,QAAQ;AAAA,IACV,OAAO,WAAW,CAAC,eAAe;AAAA,MAChC,MAAM,WAAW,WAAW,gBAAgB,MAAM;AAAA,KACnD;AAAA,EACH;AAAA,CACD,EACA,MAAM;",
|
|
8
|
+
"debugId": "DCABC4D99E5064EE64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|