@vertz/ui 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 +339 -857
- package/dist/css/public.d.ts +24 -27
- package/dist/css/public.js +5 -1
- package/dist/form/public.d.ts +94 -38
- package/dist/form/public.js +5 -3
- package/dist/index.d.ts +754 -167
- package/dist/index.js +606 -84
- package/dist/internals.d.ts +192 -23
- package/dist/internals.js +151 -102
- package/dist/jsx-runtime/index.d.ts +44 -17
- package/dist/jsx-runtime/index.js +26 -7
- package/dist/query/public.d.ts +73 -7
- package/dist/query/public.js +12 -4
- package/dist/router/public.d.ts +199 -26
- package/dist/router/public.js +22 -7
- package/dist/shared/chunk-0xcmwgdb.js +288 -0
- package/dist/shared/{chunk-j8vzvne3.js → chunk-9e92w0wt.js} +4 -1
- package/dist/shared/chunk-g4rch80a.js +33 -0
- package/dist/shared/chunk-hh0dhmb4.js +528 -0
- package/dist/shared/{chunk-pgymxpn1.js → chunk-hrd0mft1.js} +136 -34
- package/dist/shared/chunk-jrtrk5z4.js +125 -0
- package/dist/shared/chunk-ka5ked7n.js +188 -0
- package/dist/shared/chunk-n91rwj2r.js +483 -0
- package/dist/shared/chunk-prj7nm08.js +67 -0
- package/dist/shared/chunk-q6cpe5k7.js +230 -0
- package/dist/shared/{chunk-f1ynwam4.js → chunk-qacth5ah.js} +162 -36
- package/dist/shared/chunk-ryb49346.js +374 -0
- package/dist/shared/chunk-v3yyf79g.js +48 -0
- package/dist/test/index.d.ts +67 -6
- package/dist/test/index.js +4 -3
- package/package.json +14 -9
- package/dist/shared/chunk-bp3v6s9j.js +0 -62
- package/dist/shared/chunk-d8h2eh8d.js +0 -141
- package/dist/shared/chunk-tsdpgmks.js +0 -98
- package/dist/shared/chunk-xd9d7q5p.js +0 -115
- package/dist/shared/chunk-zbbvx05f.js +0 -202
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAdapter,
|
|
3
|
+
isRenderNode
|
|
4
|
+
} from "./chunk-g4rch80a.js";
|
|
5
|
+
import {
|
|
6
|
+
domEffect
|
|
7
|
+
} from "./chunk-hrd0mft1.js";
|
|
8
|
+
import {
|
|
9
|
+
SVG_NS,
|
|
10
|
+
isSVGTag,
|
|
11
|
+
normalizeSVGAttr
|
|
12
|
+
} from "./chunk-prj7nm08.js";
|
|
13
|
+
|
|
14
|
+
// src/hydrate/hydration-context.ts
|
|
15
|
+
var isHydrating = false;
|
|
16
|
+
var currentNode = null;
|
|
17
|
+
var cursorStack = [];
|
|
18
|
+
var hydrationRoot = null;
|
|
19
|
+
var claimedNodes = null;
|
|
20
|
+
function isDebug() {
|
|
21
|
+
if (typeof process !== "undefined" && true) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return typeof globalThis !== "undefined" && globalThis.__VERTZ_HYDRATION_DEBUG__ === true;
|
|
25
|
+
}
|
|
26
|
+
function startHydration(root) {
|
|
27
|
+
if (isHydrating) {
|
|
28
|
+
throw new Error("[hydrate] startHydration() called while hydration is already active. " + "Concurrent hydration is not supported.");
|
|
29
|
+
}
|
|
30
|
+
isHydrating = true;
|
|
31
|
+
currentNode = root.firstChild;
|
|
32
|
+
cursorStack.length = 0;
|
|
33
|
+
if (isDebug()) {
|
|
34
|
+
hydrationRoot = root;
|
|
35
|
+
claimedNodes = new WeakSet;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function endHydration() {
|
|
39
|
+
if (isDebug()) {
|
|
40
|
+
if (currentNode) {
|
|
41
|
+
console.debug("[hydrate] Hydration ended with unclaimed nodes remaining. " + "This may indicate SSR/client tree mismatch or browser extension nodes.");
|
|
42
|
+
}
|
|
43
|
+
if (cursorStack.length > 0) {
|
|
44
|
+
console.debug(`[hydrate] Hydration ended with unbalanced cursor stack (depth: ${cursorStack.length}). ` + "Check that __enterChildren/__exitChildren calls are paired.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (hydrationRoot && claimedNodes) {
|
|
48
|
+
const unclaimed = findUnclaimedNodes(hydrationRoot, claimedNodes);
|
|
49
|
+
if (unclaimed.length > 0) {
|
|
50
|
+
console.warn(`[hydrate] ${unclaimed.length} SSR node(s) not claimed during hydration:
|
|
51
|
+
` + unclaimed.map((n) => ` - ${describeNode(n)}`).join(`
|
|
52
|
+
`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
hydrationRoot = null;
|
|
56
|
+
claimedNodes = null;
|
|
57
|
+
isHydrating = false;
|
|
58
|
+
currentNode = null;
|
|
59
|
+
cursorStack.length = 0;
|
|
60
|
+
}
|
|
61
|
+
function getIsHydrating() {
|
|
62
|
+
return isHydrating;
|
|
63
|
+
}
|
|
64
|
+
function pauseHydration() {
|
|
65
|
+
isHydrating = false;
|
|
66
|
+
}
|
|
67
|
+
function resumeHydration() {
|
|
68
|
+
isHydrating = true;
|
|
69
|
+
}
|
|
70
|
+
function claimElement(tag) {
|
|
71
|
+
const upperTag = tag.toUpperCase();
|
|
72
|
+
while (currentNode) {
|
|
73
|
+
if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
74
|
+
const el = currentNode;
|
|
75
|
+
if (el.tagName === upperTag) {
|
|
76
|
+
if (isDebug()) {
|
|
77
|
+
const id = el.id ? `#${el.id}` : "";
|
|
78
|
+
const cls = el.className ? `.${el.className.split(" ")[0]}` : "";
|
|
79
|
+
console.debug(`[hydrate] claimElement(<${tag}${id}${cls}>) ✓ depth=${cursorStack.length}`);
|
|
80
|
+
}
|
|
81
|
+
if (claimedNodes)
|
|
82
|
+
claimedNodes.add(el);
|
|
83
|
+
currentNode = el.nextSibling;
|
|
84
|
+
return el;
|
|
85
|
+
}
|
|
86
|
+
if (isDebug()) {
|
|
87
|
+
console.debug(`[hydrate] Skipping non-matching node: <${el.tagName.toLowerCase()}> (expected <${tag}>)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
currentNode = currentNode.nextSibling;
|
|
91
|
+
}
|
|
92
|
+
if (isDebug()) {
|
|
93
|
+
console.warn(`[hydrate] Expected <${tag}> but no matching SSR node found. Creating new element.`);
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function claimText() {
|
|
98
|
+
while (currentNode) {
|
|
99
|
+
if (currentNode.nodeType === Node.TEXT_NODE) {
|
|
100
|
+
const text = currentNode;
|
|
101
|
+
if (isDebug()) {
|
|
102
|
+
const preview = text.data.length > 30 ? text.data.slice(0, 30) + "..." : text.data;
|
|
103
|
+
console.debug(`[hydrate] claimText("${preview}") ✓ depth=${cursorStack.length}`);
|
|
104
|
+
}
|
|
105
|
+
if (claimedNodes)
|
|
106
|
+
claimedNodes.add(text);
|
|
107
|
+
currentNode = text.nextSibling;
|
|
108
|
+
return text;
|
|
109
|
+
}
|
|
110
|
+
if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
currentNode = currentNode.nextSibling;
|
|
114
|
+
}
|
|
115
|
+
if (isDebug()) {
|
|
116
|
+
console.warn("[hydrate] Expected text node but no matching SSR node found.");
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function claimComment() {
|
|
121
|
+
while (currentNode) {
|
|
122
|
+
if (currentNode.nodeType === Node.COMMENT_NODE) {
|
|
123
|
+
const comment = currentNode;
|
|
124
|
+
if (claimedNodes)
|
|
125
|
+
claimedNodes.add(comment);
|
|
126
|
+
currentNode = comment.nextSibling;
|
|
127
|
+
return comment;
|
|
128
|
+
}
|
|
129
|
+
currentNode = currentNode.nextSibling;
|
|
130
|
+
}
|
|
131
|
+
if (isDebug()) {
|
|
132
|
+
console.warn("[hydrate] Expected comment node but no matching SSR node found.");
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function enterChildren(el) {
|
|
137
|
+
cursorStack.push(currentNode);
|
|
138
|
+
currentNode = el.firstChild;
|
|
139
|
+
}
|
|
140
|
+
function exitChildren() {
|
|
141
|
+
if (cursorStack.length === 0) {
|
|
142
|
+
if (isDebug()) {
|
|
143
|
+
console.warn("[hydrate] exitChildren() called with empty stack. " + "This likely means __exitChildren was called without a matching __enterChildren.");
|
|
144
|
+
}
|
|
145
|
+
currentNode = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
currentNode = cursorStack.pop() ?? null;
|
|
149
|
+
}
|
|
150
|
+
function findUnclaimedNodes(root, claimed) {
|
|
151
|
+
const unclaimed = [];
|
|
152
|
+
function walk(node) {
|
|
153
|
+
let child = node.firstChild;
|
|
154
|
+
while (child) {
|
|
155
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
156
|
+
const el = child;
|
|
157
|
+
if (el.tagName.includes("-")) {
|
|
158
|
+
child = child.nextSibling;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (child.nodeType === Node.ELEMENT_NODE && claimed.has(child) && child.tagName === "SPAN" && child.style.display === "contents") {
|
|
163
|
+
child = child.nextSibling;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!claimed.has(child)) {
|
|
167
|
+
if (child.nodeType === Node.ELEMENT_NODE || child.nodeType === Node.TEXT_NODE || child.nodeType === Node.COMMENT_NODE) {
|
|
168
|
+
unclaimed.push(child);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
172
|
+
walk(child);
|
|
173
|
+
}
|
|
174
|
+
child = child.nextSibling;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
walk(root);
|
|
178
|
+
return unclaimed;
|
|
179
|
+
}
|
|
180
|
+
function describeNode(node) {
|
|
181
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
182
|
+
const el = node;
|
|
183
|
+
const id = el.id ? `#${el.id}` : "";
|
|
184
|
+
const cls = el.className ? `.${String(el.className).split(" ")[0]}` : "";
|
|
185
|
+
return `<${el.tagName.toLowerCase()}${id}${cls}>`;
|
|
186
|
+
}
|
|
187
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
188
|
+
const data = node.data;
|
|
189
|
+
const preview = data.length > 20 ? data.slice(0, 20) + "..." : data;
|
|
190
|
+
return `text("${preview}")`;
|
|
191
|
+
}
|
|
192
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
193
|
+
return `<!-- ${node.data} -->`;
|
|
194
|
+
}
|
|
195
|
+
return `[node type=${node.nodeType}]`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/dom/element.ts
|
|
199
|
+
var MAX_THUNK_DEPTH = 100;
|
|
200
|
+
function resolveAndAppend(parent, value, depth = 0) {
|
|
201
|
+
if (depth >= MAX_THUNK_DEPTH) {
|
|
202
|
+
throw new Error("resolveAndAppend: max recursion depth exceeded — possible circular thunk");
|
|
203
|
+
}
|
|
204
|
+
if (value == null || typeof value === "boolean") {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === "function") {
|
|
208
|
+
resolveAndAppend(parent, value(), depth + 1);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
for (const item of value) {
|
|
213
|
+
resolveAndAppend(parent, item, depth);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (isRenderNode(value)) {
|
|
218
|
+
parent.appendChild(value);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const text = typeof value === "string" ? value : String(value);
|
|
222
|
+
parent.appendChild(getAdapter().createTextNode(text));
|
|
223
|
+
}
|
|
224
|
+
function __text(fn) {
|
|
225
|
+
if (getIsHydrating()) {
|
|
226
|
+
const claimed = claimText();
|
|
227
|
+
if (claimed) {
|
|
228
|
+
const node2 = claimed;
|
|
229
|
+
node2.dispose = domEffect(() => {
|
|
230
|
+
node2.data = fn();
|
|
231
|
+
});
|
|
232
|
+
return node2;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const node = getAdapter().createTextNode("");
|
|
236
|
+
node.dispose = domEffect(() => {
|
|
237
|
+
node.data = fn();
|
|
238
|
+
});
|
|
239
|
+
return node;
|
|
240
|
+
}
|
|
241
|
+
function __child(fn) {
|
|
242
|
+
let wrapper;
|
|
243
|
+
if (getIsHydrating()) {
|
|
244
|
+
const claimed = claimElement("span");
|
|
245
|
+
if (claimed) {
|
|
246
|
+
wrapper = claimed;
|
|
247
|
+
while (wrapper.firstChild) {
|
|
248
|
+
wrapper.removeChild(wrapper.firstChild);
|
|
249
|
+
}
|
|
250
|
+
pauseHydration();
|
|
251
|
+
try {
|
|
252
|
+
wrapper.dispose = domEffect(() => {
|
|
253
|
+
const value = fn();
|
|
254
|
+
if (isRenderNode(value) && wrapper.childNodes.length === 1 && wrapper.firstChild === value) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
while (wrapper.firstChild) {
|
|
258
|
+
wrapper.removeChild(wrapper.firstChild);
|
|
259
|
+
}
|
|
260
|
+
resolveAndAppend(wrapper, value);
|
|
261
|
+
});
|
|
262
|
+
} finally {
|
|
263
|
+
resumeHydration();
|
|
264
|
+
}
|
|
265
|
+
return wrapper;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
wrapper = getAdapter().createElement("span");
|
|
269
|
+
wrapper.style.display = "contents";
|
|
270
|
+
wrapper.dispose = domEffect(() => {
|
|
271
|
+
const value = fn();
|
|
272
|
+
if (isRenderNode(value) && wrapper.childNodes.length === 1 && wrapper.firstChild === value) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
while (wrapper.firstChild) {
|
|
276
|
+
wrapper.removeChild(wrapper.firstChild);
|
|
277
|
+
}
|
|
278
|
+
resolveAndAppend(wrapper, value);
|
|
279
|
+
});
|
|
280
|
+
return wrapper;
|
|
281
|
+
}
|
|
282
|
+
function resolveAndInsert(parent, value, depth = 0) {
|
|
283
|
+
if (depth >= MAX_THUNK_DEPTH) {
|
|
284
|
+
throw new Error("__insert: max recursion depth exceeded — possible circular thunk");
|
|
285
|
+
}
|
|
286
|
+
if (value == null || typeof value === "boolean") {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === "function") {
|
|
290
|
+
resolveAndInsert(parent, value(), depth + 1);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(value)) {
|
|
294
|
+
for (const item of value) {
|
|
295
|
+
resolveAndInsert(parent, item, depth);
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
insertLeaf(parent, value);
|
|
300
|
+
}
|
|
301
|
+
function insertLeaf(parent, value) {
|
|
302
|
+
if (getIsHydrating()) {
|
|
303
|
+
if (isRenderNode(value)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
claimText();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (isRenderNode(value)) {
|
|
310
|
+
parent.appendChild(value);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const text = typeof value === "string" ? value : String(value);
|
|
314
|
+
parent.appendChild(getAdapter().createTextNode(text));
|
|
315
|
+
}
|
|
316
|
+
function __insert(parent, value) {
|
|
317
|
+
if (value == null || typeof value === "boolean") {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
resolveAndInsert(parent, value);
|
|
321
|
+
}
|
|
322
|
+
function __element(tag, props) {
|
|
323
|
+
if (getIsHydrating()) {
|
|
324
|
+
const claimed = claimElement(tag);
|
|
325
|
+
if (claimed) {
|
|
326
|
+
if (props && typeof process !== "undefined" && true) {
|
|
327
|
+
for (const [key, value] of Object.entries(props)) {
|
|
328
|
+
if (key === "role" || key.startsWith("aria-")) {
|
|
329
|
+
const actual = claimed.getAttribute(key);
|
|
330
|
+
if (actual !== value) {
|
|
331
|
+
console.warn(`[hydrate] ARIA mismatch on <${tag}>: ${key}="${actual}" (expected "${value}")`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return claimed;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const adapter = getAdapter();
|
|
340
|
+
const svg = isSVGTag(tag);
|
|
341
|
+
const el = svg ? adapter.createElementNS(SVG_NS, tag) : adapter.createElement(tag);
|
|
342
|
+
if (props) {
|
|
343
|
+
for (const [key, value] of Object.entries(props)) {
|
|
344
|
+
const attrName = svg ? normalizeSVGAttr(key) : key;
|
|
345
|
+
el.setAttribute(attrName, value);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return el;
|
|
349
|
+
}
|
|
350
|
+
function __append(parent, child) {
|
|
351
|
+
if (getIsHydrating())
|
|
352
|
+
return;
|
|
353
|
+
parent.appendChild(child);
|
|
354
|
+
}
|
|
355
|
+
function __staticText(text) {
|
|
356
|
+
if (getIsHydrating()) {
|
|
357
|
+
const claimed = claimText();
|
|
358
|
+
if (claimed)
|
|
359
|
+
return claimed;
|
|
360
|
+
}
|
|
361
|
+
return getAdapter().createTextNode(text);
|
|
362
|
+
}
|
|
363
|
+
function __enterChildren(el) {
|
|
364
|
+
if (getIsHydrating()) {
|
|
365
|
+
enterChildren(el);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function __exitChildren() {
|
|
369
|
+
if (getIsHydrating()) {
|
|
370
|
+
exitChildren();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export { startHydration, endHydration, getIsHydrating, claimText, claimComment, __text, __child, __insert, __element, __append, __staticText, __enterChildren, __exitChildren };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
domEffect
|
|
3
|
+
} from "./chunk-hrd0mft1.js";
|
|
4
|
+
|
|
5
|
+
// src/dom/attributes.ts
|
|
6
|
+
function __attr(el, name, fn) {
|
|
7
|
+
return domEffect(() => {
|
|
8
|
+
const value = fn();
|
|
9
|
+
if (value == null || value === false) {
|
|
10
|
+
el.removeAttribute(name);
|
|
11
|
+
} else if (value === true) {
|
|
12
|
+
el.setAttribute(name, "");
|
|
13
|
+
} else {
|
|
14
|
+
el.setAttribute(name, value);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function __show(el, fn) {
|
|
19
|
+
const originalDisplay = el.style.display;
|
|
20
|
+
return domEffect(() => {
|
|
21
|
+
el.style.display = fn() ? originalDisplay : "none";
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function __classList(el, classMap) {
|
|
25
|
+
const disposers = [];
|
|
26
|
+
for (const [className, fn] of Object.entries(classMap)) {
|
|
27
|
+
disposers.push(domEffect(() => {
|
|
28
|
+
if (fn()) {
|
|
29
|
+
el.classList.add(className);
|
|
30
|
+
} else {
|
|
31
|
+
el.classList.remove(className);
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
return () => {
|
|
36
|
+
for (const dispose of disposers) {
|
|
37
|
+
dispose();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/dom/events.ts
|
|
43
|
+
function __on(el, event, handler) {
|
|
44
|
+
el.addEventListener(event, handler);
|
|
45
|
+
return () => el.removeEventListener(event, handler);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { __attr, __show, __classList, __on };
|
package/dist/test/index.d.ts
CHANGED
|
@@ -138,9 +138,34 @@ type ExtractParams<T extends string> = [ExtractParamsFromSegments<WithoutWildcar
|
|
|
138
138
|
} : Record<string, never> : HasWildcard<T> extends true ? { [K in ExtractParamsFromSegments<WithoutWildcard<T>>] : string } & {
|
|
139
139
|
"*": string;
|
|
140
140
|
} : { [K in ExtractParamsFromSegments<WithoutWildcard<T>>] : string };
|
|
141
|
+
/**
|
|
142
|
+
* Convert a route pattern to the union of URL shapes it accepts.
|
|
143
|
+
* - Static: `'/'` → `'/'`
|
|
144
|
+
* - Param: `'/tasks/:id'` → `` `/tasks/${string}` ``
|
|
145
|
+
* - Wildcard: `'/files/*'` → `` `/files/${string}` ``
|
|
146
|
+
* - Multi: `'/users/:id/posts/:postId'` → `` `/users/${string}/posts/${string}` ``
|
|
147
|
+
* - Fallback: `string` → `string` (backward compat)
|
|
148
|
+
*/
|
|
149
|
+
type PathWithParams<T extends string> = T extends `${infer Before}*` ? `${PathWithParams<Before>}${string}` : T extends `${infer Before}:${string}/${infer After}` ? `${Before}${string}/${PathWithParams<`${After}`>}` : T extends `${infer Before}:${string}` ? `${Before}${string}` : T;
|
|
150
|
+
/**
|
|
151
|
+
* Union of all valid URL shapes for a route map.
|
|
152
|
+
* Maps each route pattern key through `PathWithParams` to produce the accepted URL shapes.
|
|
153
|
+
*
|
|
154
|
+
* Example:
|
|
155
|
+
* ```
|
|
156
|
+
* RoutePaths<{ '/': ..., '/tasks/:id': ... }> = '/' | `/tasks/${string}`
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
type RoutePaths<TRouteMap extends Record<string, unknown>> = { [K in keyof TRouteMap & string] : PathWithParams<K> }[keyof TRouteMap & string];
|
|
141
160
|
/** Simple schema interface for search param parsing. */
|
|
142
161
|
interface SearchParamSchema<T> {
|
|
143
|
-
parse(data: unknown):
|
|
162
|
+
parse(data: unknown): {
|
|
163
|
+
ok: true;
|
|
164
|
+
data: T;
|
|
165
|
+
} | {
|
|
166
|
+
ok: false;
|
|
167
|
+
error: unknown;
|
|
168
|
+
};
|
|
144
169
|
}
|
|
145
170
|
/** A route configuration for a single path. */
|
|
146
171
|
interface RouteConfig<
|
|
@@ -168,6 +193,30 @@ interface RouteConfig<
|
|
|
168
193
|
interface RouteDefinitionMap {
|
|
169
194
|
[pattern: string]: RouteConfig;
|
|
170
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Loose route config used as the generic constraint for `defineRoutes`.
|
|
198
|
+
* Uses `Record<string, string>` for loader params so any concrete loader
|
|
199
|
+
* that accesses string params (e.g., `params.id`) satisfies the constraint.
|
|
200
|
+
*/
|
|
201
|
+
interface RouteConfigLike {
|
|
202
|
+
component: () => Node | Promise<{
|
|
203
|
+
default: () => Node;
|
|
204
|
+
}>;
|
|
205
|
+
/**
|
|
206
|
+
* Method syntax (`loader?(ctx): R`) is intentional — it enables **bivariant**
|
|
207
|
+
* parameter checking under `strictFunctionTypes`. Property syntax
|
|
208
|
+
* (`loader?: (ctx) => R`) would be contravariant, causing `RouteConfig<string>`
|
|
209
|
+
* (whose loader has `params: Record<string, never>`) to fail assignability
|
|
210
|
+
* against this constraint's `params: Record<string, string>`.
|
|
211
|
+
*/
|
|
212
|
+
loader?(ctx: {
|
|
213
|
+
params: Record<string, string>;
|
|
214
|
+
signal: AbortSignal;
|
|
215
|
+
}): unknown;
|
|
216
|
+
errorComponent?: (error: Error) => Node;
|
|
217
|
+
searchParams?: SearchParamSchema<unknown>;
|
|
218
|
+
children?: Record<string, RouteConfigLike>;
|
|
219
|
+
}
|
|
171
220
|
/** Internal compiled route. */
|
|
172
221
|
interface CompiledRoute {
|
|
173
222
|
/** The original path pattern. */
|
|
@@ -219,8 +268,20 @@ interface NavigateOptions {
|
|
|
219
268
|
/** Use history.replaceState instead of pushState. */
|
|
220
269
|
replace?: boolean;
|
|
221
270
|
}
|
|
222
|
-
/**
|
|
223
|
-
|
|
271
|
+
/**
|
|
272
|
+
* The router instance returned by createRouter.
|
|
273
|
+
*
|
|
274
|
+
* Generic over the route map `T`. Defaults to `RouteDefinitionMap` (string
|
|
275
|
+
* index signature) for backward compatibility — unparameterized `Router`
|
|
276
|
+
* accepts any string in `navigate()`.
|
|
277
|
+
*
|
|
278
|
+
* Method syntax on `navigate`, `revalidate`, and `dispose` enables bivariant
|
|
279
|
+
* parameter checking under `strictFunctionTypes`. This means `Router<T>` is
|
|
280
|
+
* assignable to `Router` (the unparameterized default), which is required for
|
|
281
|
+
* storing typed routers in the `RouterContext` without contravariance errors.
|
|
282
|
+
* At call sites, TypeScript still enforces the `RoutePaths<T>` constraint.
|
|
283
|
+
*/
|
|
284
|
+
interface Router<T extends Record<string, RouteConfigLike> = RouteDefinitionMap> {
|
|
224
285
|
/** Current matched route (reactive signal). */
|
|
225
286
|
current: Signal<RouteMatch | null>;
|
|
226
287
|
/** Loader data from the current route's loaders (reactive signal). */
|
|
@@ -230,11 +291,11 @@ interface Router {
|
|
|
230
291
|
/** Parsed search params from the current route (reactive signal). */
|
|
231
292
|
searchParams: Signal<Record<string, unknown>>;
|
|
232
293
|
/** Navigate to a new URL path. */
|
|
233
|
-
navigate
|
|
294
|
+
navigate(url: RoutePaths<T>, options?: NavigateOptions): Promise<void>;
|
|
234
295
|
/** Re-run all loaders for the current route. */
|
|
235
|
-
revalidate
|
|
296
|
+
revalidate(): Promise<void>;
|
|
236
297
|
/** Remove popstate listener and clean up the router. */
|
|
237
|
-
dispose
|
|
298
|
+
dispose(): void;
|
|
238
299
|
}
|
|
239
300
|
/** Options for `createTestRouter`. */
|
|
240
301
|
interface TestRouterOptions {
|
package/dist/test/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRouter
|
|
3
|
-
} from "../shared/chunk-
|
|
3
|
+
} from "../shared/chunk-ka5ked7n.js";
|
|
4
4
|
import {
|
|
5
5
|
defineRoutes
|
|
6
|
-
} from "../shared/chunk-
|
|
7
|
-
import"../shared/chunk-
|
|
6
|
+
} from "../shared/chunk-9e92w0wt.js";
|
|
7
|
+
import"../shared/chunk-jrtrk5z4.js";
|
|
8
|
+
import"../shared/chunk-hrd0mft1.js";
|
|
8
9
|
|
|
9
10
|
// src/test/interactions.ts
|
|
10
11
|
async function click(el) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz UI framework — signals, components, JSX runtime",
|
|
@@ -58,19 +58,24 @@
|
|
|
58
58
|
],
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "bunup",
|
|
61
|
-
"test": "
|
|
62
|
-
"test:watch": "
|
|
61
|
+
"test": "bun test",
|
|
62
|
+
"test:watch": "bun test --watch",
|
|
63
63
|
"typecheck": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@vertz/fetch": "0.2.1"
|
|
67
|
+
},
|
|
65
68
|
"devDependencies": {
|
|
66
|
-
"@
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
69
|
+
"@happy-dom/global-registrator": "^20.7.0",
|
|
70
|
+
"@vertz/schema": "0.2.2",
|
|
71
|
+
"bunup": "^0.16.31",
|
|
72
|
+
"happy-dom": "^20.7.0",
|
|
73
|
+
"typescript": "^5.7.0"
|
|
71
74
|
},
|
|
72
75
|
"engines": {
|
|
73
76
|
"node": ">=22"
|
|
74
77
|
},
|
|
75
|
-
"sideEffects":
|
|
78
|
+
"sideEffects": [
|
|
79
|
+
"dist/shared/*.js"
|
|
80
|
+
]
|
|
76
81
|
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
effect,
|
|
3
|
-
useContext
|
|
4
|
-
} from "./chunk-pgymxpn1.js";
|
|
5
|
-
|
|
6
|
-
// src/router/link.ts
|
|
7
|
-
function createLink(currentPath, navigate) {
|
|
8
|
-
return function Link(props) {
|
|
9
|
-
const el = document.createElement("a");
|
|
10
|
-
el.setAttribute("href", props.href);
|
|
11
|
-
el.textContent = props.children;
|
|
12
|
-
if (props.className) {
|
|
13
|
-
el.classList.add(props.className);
|
|
14
|
-
}
|
|
15
|
-
if (props.activeClass) {
|
|
16
|
-
const activeClass = props.activeClass;
|
|
17
|
-
effect(() => {
|
|
18
|
-
if (currentPath.value === props.href) {
|
|
19
|
-
el.classList.add(activeClass);
|
|
20
|
-
} else {
|
|
21
|
-
el.classList.remove(activeClass);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
el.addEventListener("click", (event) => {
|
|
26
|
-
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
event.preventDefault();
|
|
30
|
-
navigate(props.href);
|
|
31
|
-
});
|
|
32
|
-
return el;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/router/outlet.ts
|
|
37
|
-
function createOutlet(outletCtx) {
|
|
38
|
-
return function Outlet() {
|
|
39
|
-
const ctx = useContext(outletCtx);
|
|
40
|
-
if (!ctx || !ctx.childComponent) {
|
|
41
|
-
return document.createComment("outlet:empty");
|
|
42
|
-
}
|
|
43
|
-
return ctx.childComponent();
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// src/router/search-params.ts
|
|
48
|
-
function parseSearchParams(urlParams, schema) {
|
|
49
|
-
const raw = {};
|
|
50
|
-
for (const [key, value] of urlParams.entries()) {
|
|
51
|
-
raw[key] = value;
|
|
52
|
-
}
|
|
53
|
-
if (schema) {
|
|
54
|
-
return schema.parse(raw);
|
|
55
|
-
}
|
|
56
|
-
return raw;
|
|
57
|
-
}
|
|
58
|
-
function useSearchParams(searchSignal) {
|
|
59
|
-
return searchSignal.value;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export { createLink, createOutlet, parseSearchParams, useSearchParams };
|