round-core 0.0.4 → 0.0.6

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.
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v3
15
+ - uses: actions/checkout@v4
16
16
 
17
17
  - name: Setup Bun
18
18
  uses: oven-sh/setup-bun@v1
@@ -38,7 +38,7 @@ jobs:
38
38
  run: bun run bench:runtime
39
39
 
40
40
  - name: Upload Report
41
- uses: actions/upload-artifact@v3
41
+ uses: actions/upload-artifact@v4
42
42
  with:
43
43
  name: benchmark-report
44
44
  path: benchmarks/reports/build-bench.json
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  npm install round-core
17
17
  ```
18
18
 
19
- Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**). This keeps rendering predictable, small, and fast for interactive apps.
19
+ Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**) and **bindables**. This keeps rendering predictable, small, and fast for interactive apps.
20
20
 
21
21
  ## What Round is focused on
22
22
 
@@ -93,7 +93,7 @@ This scaffolds a minimal Round app with `src/app.round` and an example `src/coun
93
93
 
94
94
  ## `.round` files
95
95
 
96
- A `.round` file is a JSX-based component module (ESM) compiled by the Round toolchain.
96
+ A `.round` file is a JSX-based component module (ESM) compiled by the Round toolchain. you can also use .jsx files but you wont get the round JSX superset features like conditional rendering and other features.
97
97
 
98
98
  Example `src/app.round`:
99
99
 
@@ -322,27 +322,55 @@ This compiles roughly to a `.map(...)` under the hood.
322
322
 
323
323
  ## Routing
324
324
 
325
- Round includes router primitives intended for SPA navigation.
325
+ Round includes router primitives intended for SPA navigation. All route paths must start with a forward slash `/`.
326
326
 
327
- Typical usage:
327
+ ### Basic Usage
328
328
 
329
329
  ```jsx
330
- import { Route } from 'round-core';
330
+ import { Route, Link } from 'round-core';
331
331
 
332
332
  export default function App() {
333
333
  return (
334
334
  <div>
335
- <Route route="/" title="Home">
336
- <div>Home</div>
335
+ <nav>
336
+ <Link href="/">Home</Link>
337
+ <Link href="/about">About</Link>
338
+ </nav>
339
+
340
+ <Route route="/" title="Home" exact>
341
+ <div>Welcome Home</div>
337
342
  </Route>
338
343
  <Route route="/about" title="About">
339
- <div>About</div>
344
+ <div>About Us Content</div>
340
345
  </Route>
341
346
  </div>
342
347
  );
343
348
  }
344
349
  ```
345
350
 
351
+ ### Nested Routing and Layouts
352
+
353
+ Routes can be nested to create hierarchical layouts. Child routes automatically inherit and combine paths with their parents.
354
+
355
+ - **Prefix Matching**: By default, routes use prefix matching (except for the root `/`). This allows a parent route to stay rendered as a "shell" or layout while its children are visited.
356
+ - **Exact Matching**: Use the `exact` prop to ensure a route only renders when the path matches precisely (default for root `/`).
357
+
358
+ ```jsx
359
+ <Route route="/dashboard" title="Dashboard">
360
+ <h1>Dashboard Shell</h1>
361
+
362
+ {/* This route matches /dashboard/profile */}
363
+ <Route route="/dashboard/profile">
364
+ <h2>User Profile</h2>
365
+ </Route>
366
+
367
+ {/* This route matches /dashboard/settings */}
368
+ <Route route="/dashboard/settings">
369
+ <h2>Settings</h2>
370
+ </Route>
371
+ </Route>
372
+ ```
373
+
346
374
  ## Suspense and lazy loading
347
375
 
348
376
  Round supports `Suspense` for promise-based rendering and `lazy()` for code splitting.
package/bun.lock CHANGED
@@ -4,6 +4,7 @@
4
4
  "": {
5
5
  "name": "round-core",
6
6
  "dependencies": {
7
+ "happy-dom": "^20.0.11",
7
8
  "marked": "^12.0.2",
8
9
  "vite": "^5.0.0",
9
10
  "vitest": "^1.6.0",
@@ -153,6 +154,8 @@
153
154
 
154
155
  "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="],
155
156
 
157
+ "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
158
+
156
159
  "@vitest/expect": ["@vitest/expect@1.6.1", "", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="],
157
160
 
158
161
  "@vitest/runner": ["@vitest/runner@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="],
@@ -241,6 +244,8 @@
241
244
 
242
245
  "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
243
246
 
247
+ "happy-dom": ["happy-dom@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="],
248
+
244
249
  "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
245
250
 
246
251
  "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
@@ -405,10 +410,16 @@
405
410
 
406
411
  "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
407
412
 
413
+ "happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
414
+
415
+ "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
416
+
408
417
  "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
409
418
 
410
419
  "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
411
420
 
412
421
  "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
422
+
423
+ "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
413
424
  }
414
425
  }
package/dist/cli.js CHANGED
@@ -10,6 +10,19 @@ function onSignal() {
10
10
  }
11
11
  process.on("SIGINT", onSignal);
12
12
  process.on("SIGTERM", onSignal);
13
+ process.on("exit", () => {
14
+ cleanupTemporaryFiles();
15
+ });
16
+ const temporaryFiles = /* @__PURE__ */ new Set();
17
+ function cleanupTemporaryFiles() {
18
+ for (const f of temporaryFiles) {
19
+ try {
20
+ if (fs.existsSync(f)) fs.unlinkSync(f);
21
+ } catch {
22
+ }
23
+ }
24
+ temporaryFiles.clear();
25
+ }
13
26
  function normalizePath(p) {
14
27
  return p.replaceAll("\\", "/");
15
28
  }
@@ -122,31 +135,6 @@ function resolveFrom(baseDir, p) {
122
135
  if (path.isAbsolute(p)) return p;
123
136
  return path.resolve(baseDir, p);
124
137
  }
125
- function ensureIndexHtml(rootDir, entryRel, title = "Round") {
126
- const indexPath = path.join(rootDir, "index.html");
127
- if (fs.existsSync(indexPath)) return;
128
- const entryPath = entryRel.startsWith("/") ? entryRel : `/${entryRel}`;
129
- fs.writeFileSync(indexPath, [
130
- "<!DOCTYPE html>",
131
- '<html lang="en">',
132
- "<head>",
133
- ' <meta charset="UTF-8" />',
134
- ' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
135
- ` <title>${title}</title>`,
136
- "</head>",
137
- "<body>",
138
- ' <div id="app"></div>',
139
- ' <script type="module">',
140
- " import { render } from 'round-core';",
141
- ` import App from '${entryPath}';`,
142
- "",
143
- ' render(App, document.getElementById("app"));',
144
- " <\/script>",
145
- "</body>",
146
- "</html>",
147
- ""
148
- ].join("\n"), "utf8");
149
- }
150
138
  function parseArgs(argv) {
151
139
  const args = { _: [] };
152
140
  for (let i = 0; i < argv.length; i++) {
@@ -224,7 +212,6 @@ Usage:
224
212
  const pkgPath = path.join(projectDir, "package.json");
225
213
  const configPath = path.join(projectDir, "round.config.json");
226
214
  const viteConfigPath = path.join(projectDir, "vite.config.js");
227
- const indexHtmlPath = path.join(projectDir, "index.html");
228
215
  const appRoundPath = path.join(srcDir, "app.round");
229
216
  const counterRoundPath = path.join(srcDir, "counter.round");
230
217
  writeFileIfMissing(pkgPath, JSON.stringify({
@@ -279,26 +266,6 @@ Usage:
279
266
  "});",
280
267
  ""
281
268
  ].join("\n"));
282
- writeFileIfMissing(indexHtmlPath, [
283
- "<!DOCTYPE html>",
284
- '<html lang="en">',
285
- "<head>",
286
- ' <meta charset="UTF-8" />',
287
- ' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
288
- ` <title>${name}</title>`,
289
- "</head>",
290
- "<body>",
291
- ' <div id="app"></div>',
292
- ' <script type="module">',
293
- " import { render } from 'round-core';",
294
- " import App from '/src/app.round';",
295
- "",
296
- " render(App, document.getElementById('app'));",
297
- " <\/script>",
298
- "</body>",
299
- "</html>",
300
- ""
301
- ].join("\n"));
302
269
  writeFileIfMissing(appRoundPath, [
303
270
  "import { Route } from 'round-core';",
304
271
  'import { Counter } from "./counter"',
@@ -369,6 +336,40 @@ function coerceNumber(v, fallback) {
369
336
  const n = Number(v);
370
337
  return Number.isFinite(n) ? n : fallback;
371
338
  }
339
+ function generateIndexHtml(config) {
340
+ const name = config?.name ?? "Round";
341
+ const rawEntry = config?.entry ?? "./src/app.round";
342
+ let entry = rawEntry;
343
+ if (entry.startsWith("./")) entry = entry.slice(1);
344
+ if (!entry.startsWith("/")) entry = "/" + entry;
345
+ return [
346
+ "<!DOCTYPE html>",
347
+ '<html lang="en">',
348
+ "<head>",
349
+ ' <meta charset="UTF-8" />',
350
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
351
+ ` <title>${name}</title>`,
352
+ "</head>",
353
+ "<body>",
354
+ ' <div id="app"></div>',
355
+ ' <script type="module">',
356
+ " import { render } from 'round-core';",
357
+ ` import App from '${entry}';`,
358
+ "",
359
+ " render(App, document.getElementById('app'));",
360
+ " <\/script>",
361
+ "</body>",
362
+ "</html>"
363
+ ].join("\n");
364
+ }
365
+ function writeTemporaryIndex(rootDir, config) {
366
+ const indexPath = path.resolve(rootDir, "index.html");
367
+ if (fs.existsSync(indexPath)) return false;
368
+ const content = generateIndexHtml(config);
369
+ fs.writeFileSync(indexPath, content, "utf8");
370
+ temporaryFiles.add(indexPath);
371
+ return true;
372
+ }
372
373
  async function runDev({ rootDir, configPathAbs, config }) {
373
374
  const startedAt = Date.now();
374
375
  const configDir = path.dirname(configPathAbs);
@@ -376,8 +377,7 @@ async function runDev({ rootDir, configPathAbs, config }) {
376
377
  if (!entryAbs || !fs.existsSync(entryAbs)) {
377
378
  throw new Error(`Entry not found: ${entryAbs ?? "(missing entry)"} (config: ${configPathAbs})`);
378
379
  }
379
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
380
- ensureIndexHtml(rootDir, entryRel, config?.name ?? "Round");
380
+ normalizePath(path.relative(rootDir, entryAbs));
381
381
  let viteServer = null;
382
382
  let restarting = false;
383
383
  let restartTimer = null;
@@ -387,11 +387,11 @@ async function runDev({ rootDir, configPathAbs, config }) {
387
387
  if (!entryAbs2 || !fs.existsSync(entryAbs2)) {
388
388
  throw new Error(`Entry not found: ${entryAbs2 ?? "(missing entry)"} (config: ${configPathAbs})`);
389
389
  }
390
- const entryRel2 = normalizePath(path.relative(rootDir, entryAbs2));
391
- ensureIndexHtml(rootDir, entryRel2, nextConfig?.name ?? "Round");
390
+ normalizePath(path.relative(rootDir, entryAbs2));
392
391
  const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
393
392
  const open2 = Boolean(nextConfig?.dev?.open);
394
393
  const base2 = nextConfig?.routing?.base ?? "/";
394
+ writeTemporaryIndex(rootDir, nextConfig);
395
395
  if (showBanner) {
396
396
  banner();
397
397
  process.stdout.write(`${c(" Config", "gray")} ${configPathAbs}
@@ -458,10 +458,10 @@ async function runBuild({ rootDir, configPathAbs, config }) {
458
458
  if (!entryAbs || !fs.existsSync(entryAbs)) {
459
459
  throw new Error(`Entry not found: ${entryAbs ?? "(missing entry)"} (config: ${configPathAbs})`);
460
460
  }
461
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
462
- ensureIndexHtml(rootDir, entryRel, config?.name ?? "Round");
461
+ normalizePath(path.relative(rootDir, entryAbs));
463
462
  const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, "./dist");
464
463
  const base = config?.routing?.base ?? "/";
464
+ writeTemporaryIndex(rootDir, config);
465
465
  banner();
466
466
  process.stdout.write(`${c(" Config", "gray")} ${configPathAbs}
467
467
  `);
@@ -497,10 +497,7 @@ async function runPreview({ rootDir, configPathAbs, config }) {
497
497
  const base = config?.routing?.base ?? "/";
498
498
  const previewPort = coerceNumber(config?.dev?.port, 5173);
499
499
  const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
500
- if (entryAbs && fs.existsSync(entryAbs)) {
501
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
502
- ensureIndexHtml(rootDir, entryRel, config?.name ?? "Round");
503
- }
500
+ if (entryAbs && fs.existsSync(entryAbs)) ;
504
501
  banner();
505
502
  process.stdout.write(`${c("Config:", "cyan")} ${configPathAbs}
506
503
  `);
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Round Framework Type Definitions
3
+ */
4
+
5
+ export interface RoundSignal<T> {
6
+ /**
7
+ * Get or set the current value.
8
+ */
9
+ (newValue?: T): T;
10
+
11
+ /**
12
+ * Get the current value (reactive).
13
+ */
14
+ value: T;
15
+
16
+ /**
17
+ * Get the current value without tracking dependencies.
18
+ */
19
+ peek(): T;
20
+
21
+ /**
22
+ * Creates a transformed view of this signal.
23
+ */
24
+ transform<U>(fromInput: (v: U) => T, toOutput: (v: T) => U): RoundSignal<U>;
25
+
26
+ /**
27
+ * Attaches validation logic to the signal.
28
+ */
29
+ validate(validator: (next: T, prev: T) => string | boolean | undefined | null, options?: {
30
+ /** Timing of validation: 'input' (default) or 'blur'. */
31
+ validateOn?: 'input' | 'blur';
32
+ /** Whether to run validation immediately on startup. */
33
+ validateInitial?: boolean;
34
+ }): RoundSignal<T> & {
35
+ /** Signal containing the current validation error message. */
36
+ error: RoundSignal<string | null>;
37
+ /** Manually trigger validation check. Returns true if valid. */
38
+ check(): boolean
39
+ };
40
+
41
+ /**
42
+ * Creates a read/write view of a specific property path.
43
+ */
44
+ $pick<K extends keyof T>(path: K): RoundSignal<T[K]>;
45
+ $pick(path: string | string[]): RoundSignal<any>;
46
+
47
+ /**
48
+ * Internal: marks the signal as bindable for two-way bindings.
49
+ */
50
+ bind?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Creates a reactive signal.
55
+ */
56
+ export function signal<T>(initialValue?: T): RoundSignal<T>;
57
+
58
+ /**
59
+ * Creates a bindable signal intended for two-way DOM bindings.
60
+ */
61
+ export function bindable<T>(initialValue?: T): RoundSignal<T>;
62
+
63
+ /**
64
+ * Run a function without tracking any signals it reads.
65
+ * Any signals accessed inside the function will not become dependencies of the current effect.
66
+ */
67
+ export function untrack<T>(fn: () => T): T;
68
+
69
+ /**
70
+ * Create a reactive side-effect that runs whenever its signal dependencies change.
71
+ */
72
+ export function effect(fn: () => void | (() => void), options?: {
73
+ /** If false, the effect won't run immediately on creation. Defaults to true. */
74
+ onLoad?: boolean
75
+ }): () => void;
76
+
77
+ /**
78
+ * Create a reactive side-effect with explicit dependencies.
79
+ */
80
+ export function effect(deps: any[], fn: () => void | (() => void), options?: {
81
+ /** If false, the effect won't run immediately on creation. Defaults to true. */
82
+ onLoad?: boolean
83
+ }): () => void;
84
+
85
+ /**
86
+ * Create a read-only computed signal derived from other signals.
87
+ */
88
+ export function derive<T>(fn: () => T): () => T;
89
+
90
+ /**
91
+ * Create a read/write view of a specific path within a signal object.
92
+ */
93
+ export function pick<T = any>(root: RoundSignal<any>, path: string | string[]): RoundSignal<T>;
94
+
95
+ /**
96
+ * Store API
97
+ */
98
+ export interface RoundStore<T> {
99
+ /**
100
+ * Access a specific key from the store as a bindable signal.
101
+ */
102
+ use<K extends keyof T>(key: K): RoundSignal<T[K]>;
103
+
104
+ /**
105
+ * Update a specific key in the store.
106
+ */
107
+ set<K extends keyof T>(key: K, value: T[K]): T[K];
108
+
109
+ /**
110
+ * Batch update multiple keys in the store.
111
+ */
112
+ patch(obj: Partial<T>): void;
113
+
114
+ /**
115
+ * Get a snapshot of the current state.
116
+ */
117
+ snapshot(options?: {
118
+ /** If true, the returned values will be reactive signals. */
119
+ reactive?: boolean
120
+ }): T;
121
+
122
+ /**
123
+ * Enable persistence for the store.
124
+ */
125
+ persist(storageKey: string, options?: {
126
+ /** The storage implementation (defaults to localStorage). */
127
+ storage?: Storage;
128
+ /** Debounce time in milliseconds for writes. */
129
+ debounce?: number;
130
+ /** Array of keys to exclude from persistence. */
131
+ exclude?: string[];
132
+ }): RoundStore<T>;
133
+
134
+ /**
135
+ * Action methods defined during store creation.
136
+ */
137
+ actions: Record<string, Function>;
138
+ }
139
+
140
+ /**
141
+ * Create a shared global state store with actions and optional persistence.
142
+ */
143
+ export function createStore<T, A extends Record<string, (state: T, ...args: any[]) => Partial<T> | void>>(
144
+ initialState: T,
145
+ actions?: A
146
+ ): RoundStore<T> & { [K in keyof A]: (...args: Parameters<A[K]> extends [any, ...infer P] ? P : never) => any };
147
+
148
+ /**
149
+ * Router API
150
+ */
151
+ export interface RouteProps {
152
+ /** The path to match. Must start with a forward slash. */
153
+ route?: string;
154
+ /** If true, only matches if the path is exactly the same. */
155
+ exact?: boolean;
156
+ /** Page title to set in the document header when active. */
157
+ title?: string;
158
+ /** Meta description to set in the document header when active. */
159
+ description?: string;
160
+ /** Advanced head configuration including links and meta tags. */
161
+ head?: any;
162
+ /** Fragment or elements to render when matched. */
163
+ children?: any;
164
+ }
165
+
166
+ /**
167
+ * Define a route that renders its children when the path matches.
168
+ */
169
+ export function Route(props: RouteProps): any;
170
+
171
+ /**
172
+ * An alias for Route, typically used for top-level pages.
173
+ */
174
+ export function Page(props: RouteProps): any;
175
+
176
+ export interface LinkProps {
177
+ /** The destination path. */
178
+ href: string;
179
+ /** Alias for href. */
180
+ to?: string;
181
+ /** Use SPA navigation (prevents full page reloads). Defaults to true. */
182
+ spa?: boolean;
183
+ /** Force a full page reload on navigation. */
184
+ reload?: boolean;
185
+ /** Custom click event handler. */
186
+ onClick?: (e: MouseEvent) => void;
187
+ /** Link content (text or elements). */
188
+ children?: any;
189
+ [key: string]: any;
190
+ }
191
+
192
+ /**
193
+ * A standard link component that performs SPA navigation.
194
+ */
195
+ export function Link(props: LinkProps): any;
196
+
197
+ /**
198
+ * Define a fallback component or content for when no routes match.
199
+ */
200
+ export function NotFound(props: {
201
+ /** Optional component to render for the 404 state. */
202
+ component?: any;
203
+ /** Fallback content. ignored if 'component' is provided. */
204
+ children?: any
205
+ }): any;
206
+
207
+ /**
208
+ * Navigate to a different path programmatically.
209
+ */
210
+ export function navigate(to: string, options?: {
211
+ /** If true, replaces the current history entry instead of pushing. */
212
+ replace?: boolean
213
+ }): void;
214
+
215
+ /**
216
+ * Hook to get a reactive function returning the current normalized pathname.
217
+ */
218
+ export function usePathname(): () => string;
219
+
220
+ /**
221
+ * Get the current normalized pathname.
222
+ */
223
+ export function getPathname(): string;
224
+
225
+ /**
226
+ * Hook to get a reactive function returning the current location object.
227
+ */
228
+ export function useLocation(): () => { pathname: string; search: string; hash: string };
229
+
230
+ /**
231
+ * Get the current location object (pathname, search, hash).
232
+ */
233
+ export function getLocation(): { pathname: string; search: string; hash: string };
234
+
235
+ /**
236
+ * Hook to get a reactive function returning whether the current path has no matches.
237
+ */
238
+ export function useIsNotFound(): () => boolean;
239
+
240
+ /**
241
+ * Get whether the current path is NOT matched by any defined route.
242
+ */
243
+ export function getIsNotFound(): boolean;
244
+
245
+ /**
246
+ * DOM & Context API
247
+ */
248
+
249
+ /**
250
+ * Create a DOM element or instance a component.
251
+ */
252
+ export function createElement(tag: any, props?: any, ...children: any[]): any;
253
+
254
+ /**
255
+ * A grouping component that returns its children without a wrapper element.
256
+ */
257
+ export function Fragment(props: { children?: any }): any;
258
+
259
+ export interface Context<T> {
260
+ /** Internal identifier for the context. */
261
+ id: number;
262
+ /** Default value used when no Provider is found in the tree. */
263
+ defaultValue: T;
264
+ /** Component that provides a value to all its descendants. */
265
+ Provider: (props: { value: T; children?: any }) => any;
266
+ }
267
+
268
+ /**
269
+ * Create a new Context object for sharing state between components.
270
+ */
271
+ export function createContext<T>(defaultValue?: T): Context<T>;
272
+
273
+ /**
274
+ * Read the current value of a context from the component tree.
275
+ */
276
+ export function readContext<T>(ctx: Context<T>): T;
277
+
278
+ /**
279
+ * Returns a reactive function that reads the current context value.
280
+ */
281
+ export function bindContext<T>(ctx: Context<T>): () => T;
282
+
283
+ /**
284
+ * Async & Code Splitting
285
+ */
286
+
287
+ /**
288
+ * Mark a component for lazy loading (code-splitting).
289
+ * Expects a function returning a dynamic import promise.
290
+ */
291
+ export function lazy<T>(fn: () => Promise<{ default: T }>): T;
292
+
293
+ export interface SuspenseProps {
294
+ /** Content to show while children (e.g. lazy components) are loading. */
295
+ fallback: any;
296
+ /** Content that might trigger a loading state. */
297
+ children?: any;
298
+ }
299
+
300
+ /**
301
+ * Component that boundaries async operations and renders a fallback while loading.
302
+ */
303
+ export function Suspense(props: SuspenseProps): any;
304
+
305
+ /**
306
+ * Head Management
307
+ */
308
+
309
+ /**
310
+ * Define static head metadata (titles, meta tags, favicons, etc.).
311
+ */
312
+ export function startHead(head: any): any;
313
+
314
+ /**
315
+ * Markdown
316
+ */
317
+
318
+ /**
319
+ * Component that renders Markdown content into HTML.
320
+ */
321
+ export function Markdown(props: {
322
+ /** The markdown string or a function returning it. */
323
+ content: string | (() => string);
324
+ /** Remark/Rehype configuration options. */
325
+ options?: any
326
+ }): any;