@epoch32/visage 0.0.0-stage → 0.0.0
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 +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRoot,
|
|
3
|
+
effect,
|
|
4
|
+
getActiveScope,
|
|
5
|
+
isSignal,
|
|
6
|
+
onCleanup,
|
|
7
|
+
type ReadonlySignal,
|
|
8
|
+
runInScope,
|
|
9
|
+
Scope
|
|
10
|
+
} from "../signals";
|
|
11
|
+
import { hasReactiveClass, normalizeClass } from "../style/cx";
|
|
12
|
+
import { formatStyleProp } from "../style/scale";
|
|
13
|
+
|
|
14
|
+
export type ReactiveValue<T> = T | ReadonlySignal<T> | (() => T);
|
|
15
|
+
export type Child =
|
|
16
|
+
| Node
|
|
17
|
+
| string
|
|
18
|
+
| number
|
|
19
|
+
| boolean
|
|
20
|
+
| null
|
|
21
|
+
| undefined
|
|
22
|
+
| ReadonlySignal<any>
|
|
23
|
+
| (() => any)
|
|
24
|
+
| Child[];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a property is a reactive getter or signal
|
|
28
|
+
*/
|
|
29
|
+
export function isReactive<T>(value: unknown): value is ReadonlySignal<T> | (() => T) {
|
|
30
|
+
return isSignal(value) || typeof value === "function";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract current value whether static, signal, or getter
|
|
35
|
+
*/
|
|
36
|
+
export function resolveValue<T>(val: ReactiveValue<T>): T {
|
|
37
|
+
if (typeof val === "function") {
|
|
38
|
+
return (val as () => T)();
|
|
39
|
+
}
|
|
40
|
+
return val;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mount a Visage component or element to a DOM container
|
|
45
|
+
*/
|
|
46
|
+
export function render(fn: () => Child, container: HTMLElement | DocumentFragment): () => void {
|
|
47
|
+
return createRoot((dispose) => {
|
|
48
|
+
container.textContent = "";
|
|
49
|
+
const node = insertChild(container, fn());
|
|
50
|
+
onCleanup(() => {
|
|
51
|
+
if (node && node instanceof Node && node.parentNode === container) {
|
|
52
|
+
container.removeChild(node);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return dispose;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Bind dynamic or static attributes, properties, styles, and events to an Element
|
|
61
|
+
*/
|
|
62
|
+
export function setAttribute(el: HTMLElement | SVGElement, key: string, value: any): void {
|
|
63
|
+
// Event listeners (e.g. onClick, onInput, onclick)
|
|
64
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
65
|
+
const eventName = key.startsWith("onCapture")
|
|
66
|
+
? key.slice(9).toLowerCase()
|
|
67
|
+
: key.slice(2).toLowerCase();
|
|
68
|
+
const useCapture = key.startsWith("onCapture");
|
|
69
|
+
|
|
70
|
+
el.addEventListener(eventName, value as EventListener, useCapture);
|
|
71
|
+
onCleanup(() => {
|
|
72
|
+
el.removeEventListener(eventName, value as EventListener, useCapture);
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Ref callback
|
|
78
|
+
if (key === "ref" && typeof value === "function") {
|
|
79
|
+
value(el);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Reactive class / className with array or object support
|
|
84
|
+
if (key === "class" || key === "className") {
|
|
85
|
+
if (hasReactiveClass(value)) {
|
|
86
|
+
effect(() => {
|
|
87
|
+
const cls = normalizeClass(value);
|
|
88
|
+
if (cls) el.setAttribute("class", cls);
|
|
89
|
+
else el.removeAttribute("class");
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
const cls = normalizeClass(value);
|
|
93
|
+
if (cls) el.setAttribute("class", cls);
|
|
94
|
+
else el.removeAttribute("class");
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Reactive attributes
|
|
100
|
+
if (isReactive(value)) {
|
|
101
|
+
effect(() => {
|
|
102
|
+
const resolved = resolveValue(value);
|
|
103
|
+
applyProp(el, key, resolved);
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
applyProp(el, key, value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setStyleProperty(el: HTMLElement | SVGElement, prop: string, val: any): void {
|
|
111
|
+
const formatted = formatStyleProp(prop, val);
|
|
112
|
+
if (prop.startsWith("--") || prop.includes("-")) {
|
|
113
|
+
if (formatted === "") {
|
|
114
|
+
el.style.removeProperty(prop);
|
|
115
|
+
} else {
|
|
116
|
+
el.style.setProperty(prop, formatted);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
(el.style as any)[prop] = formatted;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function applyProp(el: HTMLElement | SVGElement, key: string, value: any) {
|
|
124
|
+
if (
|
|
125
|
+
value === null ||
|
|
126
|
+
value === undefined ||
|
|
127
|
+
(value === false && !key.startsWith("aria-") && !key.startsWith("data-"))
|
|
128
|
+
) {
|
|
129
|
+
if (key === "className" || key === "class") {
|
|
130
|
+
el.removeAttribute("class");
|
|
131
|
+
} else if (key === "style" && typeof value === "string") {
|
|
132
|
+
el.removeAttribute("style");
|
|
133
|
+
} else if (key in el && !(el instanceof SVGElement)) {
|
|
134
|
+
try {
|
|
135
|
+
(el as any)[key] = typeof (el as any)[key] === "boolean" ? false : "";
|
|
136
|
+
} catch {
|
|
137
|
+
el.removeAttribute(key);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
el.removeAttribute(key);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (key === "className" || key === "class") {
|
|
146
|
+
const cls = normalizeClass(value);
|
|
147
|
+
if (cls) {
|
|
148
|
+
el.setAttribute("class", cls);
|
|
149
|
+
} else {
|
|
150
|
+
el.removeAttribute("class");
|
|
151
|
+
}
|
|
152
|
+
} else if (key === "style") {
|
|
153
|
+
if (typeof value === "string") {
|
|
154
|
+
el.setAttribute("style", value);
|
|
155
|
+
} else if (typeof value === "object" && value !== null) {
|
|
156
|
+
for (const styleKey in value) {
|
|
157
|
+
const styleVal = value[styleKey];
|
|
158
|
+
if (isReactive(styleVal)) {
|
|
159
|
+
effect(() => {
|
|
160
|
+
setStyleProperty(el, styleKey, resolveValue(styleVal));
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
setStyleProperty(el, styleKey, styleVal);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else if (key === "dangerouslySetInnerHTML" && value && typeof value.__html === "string") {
|
|
168
|
+
el.innerHTML = value.__html;
|
|
169
|
+
} else if (key in el && !(el instanceof SVGElement) && key !== "list" && key !== "form") {
|
|
170
|
+
try {
|
|
171
|
+
(el as any)[key] = value;
|
|
172
|
+
} catch {
|
|
173
|
+
el.setAttribute(key, String(value));
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
if (key.startsWith("aria-") || key.startsWith("data-")) {
|
|
177
|
+
el.setAttribute(key, String(value));
|
|
178
|
+
} else if (value === true) {
|
|
179
|
+
el.setAttribute(key, "");
|
|
180
|
+
} else {
|
|
181
|
+
el.setAttribute(key, String(value));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Insert a child node into a parent container with reactive fine-grained updates
|
|
188
|
+
*/
|
|
189
|
+
export function insertChild(parent: Node, child: Child, before: Node | null = null): Node | null {
|
|
190
|
+
if (child === null || child === undefined || child === false || child === true) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (child instanceof Node) {
|
|
195
|
+
parent.insertBefore(child, before);
|
|
196
|
+
return child;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (Array.isArray(child)) {
|
|
200
|
+
const fragment = document.createDocumentFragment();
|
|
201
|
+
for (const item of child) {
|
|
202
|
+
insertChild(fragment, item);
|
|
203
|
+
}
|
|
204
|
+
parent.insertBefore(fragment, before);
|
|
205
|
+
return fragment;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isReactive(child)) {
|
|
209
|
+
// Fine-grained reactive binding
|
|
210
|
+
const marker = document.createTextNode("");
|
|
211
|
+
parent.insertBefore(marker, before);
|
|
212
|
+
|
|
213
|
+
let childScope: Scope | null = null;
|
|
214
|
+
let currentNodes: Node[] = [];
|
|
215
|
+
|
|
216
|
+
effect(() => {
|
|
217
|
+
const resolved = resolveValue(child as ReactiveValue<any>);
|
|
218
|
+
|
|
219
|
+
// Ultra-fast path: mutate existing single text node without recreating DOM
|
|
220
|
+
if (
|
|
221
|
+
(typeof resolved === "string" || typeof resolved === "number") &&
|
|
222
|
+
currentNodes.length === 1 &&
|
|
223
|
+
currentNodes[0]!.nodeType === Node.TEXT_NODE
|
|
224
|
+
) {
|
|
225
|
+
(currentNodes[0] as Text).data = String(resolved);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (childScope) {
|
|
230
|
+
childScope.dispose();
|
|
231
|
+
}
|
|
232
|
+
childScope = new Scope(getActiveScope());
|
|
233
|
+
|
|
234
|
+
runInScope(childScope, () => {
|
|
235
|
+
// Clean up previous rendered nodes from DOM
|
|
236
|
+
for (const node of currentNodes) {
|
|
237
|
+
if (node.parentNode) {
|
|
238
|
+
node.parentNode.removeChild(node);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
currentNodes = [];
|
|
242
|
+
|
|
243
|
+
if (resolved === null || resolved === undefined || resolved === false) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const frag = document.createDocumentFragment();
|
|
248
|
+
if (
|
|
249
|
+
typeof resolved === "string" ||
|
|
250
|
+
typeof resolved === "number" ||
|
|
251
|
+
typeof resolved === "boolean"
|
|
252
|
+
) {
|
|
253
|
+
frag.appendChild(document.createTextNode(String(resolved)));
|
|
254
|
+
} else {
|
|
255
|
+
insertChild(frag, resolved);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
currentNodes = Array.from(frag.childNodes);
|
|
259
|
+
if (marker.parentNode) {
|
|
260
|
+
marker.parentNode.insertBefore(frag, marker);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
onCleanup(() => {
|
|
266
|
+
if (childScope) {
|
|
267
|
+
childScope.dispose();
|
|
268
|
+
}
|
|
269
|
+
for (const node of currentNodes) {
|
|
270
|
+
if (node.parentNode) {
|
|
271
|
+
node.parentNode.removeChild(node);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (marker.parentNode) {
|
|
275
|
+
marker.parentNode.removeChild(marker);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return marker;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Primitive static child
|
|
283
|
+
const textNode = document.createTextNode(String(child));
|
|
284
|
+
parent.insertBefore(textNode, before);
|
|
285
|
+
return textNode;
|
|
286
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { effect, type ReadonlySignal, signal, type WritableSignal } from "../signals";
|
|
2
|
+
import { createContext, useContext } from "../signals/context";
|
|
3
|
+
import { type ReactiveValue, resolveValue } from "./render";
|
|
4
|
+
|
|
5
|
+
// Suspense Context for tracking active async requests
|
|
6
|
+
export interface SuspenseContextValue {
|
|
7
|
+
registerPromise(p: Promise<any>): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const SuspenseContext = createContext<SuspenseContextValue | null>(null);
|
|
11
|
+
|
|
12
|
+
export interface ResourceActions<T> {
|
|
13
|
+
mutate(value: T): void;
|
|
14
|
+
refetch(): Promise<T>;
|
|
15
|
+
readonly loading: ReadonlySignal<boolean>;
|
|
16
|
+
readonly error: ReadonlySignal<any>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Resource<T> = ReadonlySignal<T | undefined> & ResourceActions<T>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create an asynchronous resource tied to optional source signals and suspense boundaries.
|
|
23
|
+
*
|
|
24
|
+
* Automatically tracks async state, registers pending promises with `<Suspense>`,
|
|
25
|
+
* and supports manual mutation and refetching.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* const [userId, setUserId] = createSignal("user-1");
|
|
30
|
+
* const user = createResource(userId, (id) => fetchUser(id));
|
|
31
|
+
*
|
|
32
|
+
* // Read data, loading state, error
|
|
33
|
+
* <div>{() => user()?.name}</div>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param sourceOrFetcher - A reactive source signal or direct async fetcher function
|
|
37
|
+
* @param maybeFetcher - Async fetcher taking the resolved source value
|
|
38
|
+
* @returns A reactive Resource accessor with `.loading`, `.error`, `.mutate`, and `.refetch`
|
|
39
|
+
*/
|
|
40
|
+
export function createResource<T>(fetcher: () => Promise<T>): Resource<T>;
|
|
41
|
+
export function createResource<T, S>(
|
|
42
|
+
source: ReactiveValue<S>,
|
|
43
|
+
fetcher: (source: S) => Promise<T>
|
|
44
|
+
): Resource<T>;
|
|
45
|
+
export function createResource<T, S>(
|
|
46
|
+
sourceOrFetcher: ReactiveValue<S> | (() => Promise<T>),
|
|
47
|
+
maybeFetcher?: (source: S) => Promise<T>
|
|
48
|
+
): Resource<T> {
|
|
49
|
+
const isDirect = typeof maybeFetcher !== "function";
|
|
50
|
+
const fetcher = isDirect ? (sourceOrFetcher as () => Promise<T>) : maybeFetcher!;
|
|
51
|
+
const source = isDirect ? null : (sourceOrFetcher as ReactiveValue<S>);
|
|
52
|
+
|
|
53
|
+
const data: WritableSignal<T | undefined> = signal<T | undefined>(undefined);
|
|
54
|
+
const loading = signal<boolean>(true);
|
|
55
|
+
const error = signal<any>(null);
|
|
56
|
+
|
|
57
|
+
let currentPromise: Promise<T> | null = null;
|
|
58
|
+
let requestId = 0;
|
|
59
|
+
|
|
60
|
+
const load = async (arg?: S): Promise<T> => {
|
|
61
|
+
const currentReq = ++requestId;
|
|
62
|
+
loading.set(true);
|
|
63
|
+
error.set(null);
|
|
64
|
+
|
|
65
|
+
const promise = isDirect ? (fetcher as () => Promise<T>)() : fetcher(arg!);
|
|
66
|
+
currentPromise = promise;
|
|
67
|
+
|
|
68
|
+
const suspense = useContext(SuspenseContext);
|
|
69
|
+
if (suspense) {
|
|
70
|
+
suspense.registerPromise(promise);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const res = await promise;
|
|
75
|
+
if (currentReq === requestId) {
|
|
76
|
+
data.set(res);
|
|
77
|
+
loading.set(false);
|
|
78
|
+
currentPromise = null;
|
|
79
|
+
}
|
|
80
|
+
return res;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (currentReq === requestId) {
|
|
83
|
+
error.set(err);
|
|
84
|
+
loading.set(false);
|
|
85
|
+
currentPromise = null;
|
|
86
|
+
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (source) {
|
|
92
|
+
effect(() => {
|
|
93
|
+
const sourceVal = resolveValue(source);
|
|
94
|
+
if (sourceVal !== false && sourceVal !== null && sourceVal !== undefined) {
|
|
95
|
+
load(sourceVal).catch(() => {});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
load().catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const accessor = ((): T | undefined => {
|
|
103
|
+
if (loading() && currentPromise) {
|
|
104
|
+
const suspense = useContext(SuspenseContext);
|
|
105
|
+
if (suspense) {
|
|
106
|
+
suspense.registerPromise(currentPromise);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return data();
|
|
110
|
+
}) as Resource<T>;
|
|
111
|
+
|
|
112
|
+
Object.defineProperties(accessor, {
|
|
113
|
+
loading: { get: () => loading },
|
|
114
|
+
error: { get: () => error },
|
|
115
|
+
mutate: { value: (val: T) => data.set(val) },
|
|
116
|
+
refetch: { value: () => (source ? load(resolveValue(source)) : load()) }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return accessor;
|
|
120
|
+
}
|
package/src/form/form.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { batch, computed, type ReadonlySignal, signal, type WritableSignal } from "../signals";
|
|
2
|
+
import { type SchemaLike, validateWithSchema } from "./schema";
|
|
3
|
+
|
|
4
|
+
export interface FieldState<T> {
|
|
5
|
+
value: WritableSignal<T>;
|
|
6
|
+
error: ReadonlySignal<string | null>;
|
|
7
|
+
touched: WritableSignal<boolean>;
|
|
8
|
+
isDirty: ReadonlySignal<boolean>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FormFields<T> = {
|
|
12
|
+
[K in keyof T]-?: FieldState<T[K]>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface FormOptions<T extends Record<string, any> = Record<string, any>> {
|
|
16
|
+
initialValues: T;
|
|
17
|
+
schema?: SchemaLike<any>;
|
|
18
|
+
validateOn?: "change" | "blur" | "submit";
|
|
19
|
+
onSubmit?: (values: T) => void | Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Form<T> {
|
|
23
|
+
fields: FormFields<T>;
|
|
24
|
+
values: () => T;
|
|
25
|
+
errors: ReadonlySignal<Partial<Record<keyof T, string>>>;
|
|
26
|
+
isValid: ReadonlySignal<boolean>;
|
|
27
|
+
isDirty: ReadonlySignal<boolean>;
|
|
28
|
+
isSubmitting: ReadonlySignal<boolean>;
|
|
29
|
+
submitCount: ReadonlySignal<number>;
|
|
30
|
+
bind: (name: keyof T) => {
|
|
31
|
+
name: string;
|
|
32
|
+
value: WritableSignal<any>;
|
|
33
|
+
onInput: (e: any) => void;
|
|
34
|
+
onBlur: () => void;
|
|
35
|
+
};
|
|
36
|
+
handleSubmit: (e?: Event) => Promise<boolean>;
|
|
37
|
+
reset: (values?: Partial<T>) => void;
|
|
38
|
+
setFieldValue: <K extends keyof T>(name: K, value: T[K]) => void;
|
|
39
|
+
setFieldError: (name: keyof T, error: string | null) => void;
|
|
40
|
+
validate: () => boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a fine-grained reactive form with built-in schema validation (ArkType, Zod, Valibot, or native v)
|
|
45
|
+
*/
|
|
46
|
+
export function createForm<T extends Record<string, any> = Record<string, any>>(
|
|
47
|
+
options: FormOptions<T>
|
|
48
|
+
): Form<T> {
|
|
49
|
+
const initialValues = { ...options.initialValues } as T;
|
|
50
|
+
const validateMode = options.validateOn ?? "change";
|
|
51
|
+
|
|
52
|
+
const fields = {} as FormFields<T>;
|
|
53
|
+
const isSubmitting = signal(false);
|
|
54
|
+
const submitCount = signal(0);
|
|
55
|
+
const customErrors = signal<Partial<Record<keyof T, string>>>({});
|
|
56
|
+
|
|
57
|
+
for (const key in initialValues) {
|
|
58
|
+
const initVal = initialValues[key];
|
|
59
|
+
const valSignal = signal(initVal);
|
|
60
|
+
const touchedSignal = signal(false);
|
|
61
|
+
const isDirty = computed(() => !Object.is(valSignal(), initialValues[key]));
|
|
62
|
+
|
|
63
|
+
(fields as any)[key] = {
|
|
64
|
+
value: valSignal,
|
|
65
|
+
touched: touchedSignal,
|
|
66
|
+
isDirty,
|
|
67
|
+
error: computed(() => {
|
|
68
|
+
const custom = customErrors()[key];
|
|
69
|
+
if (custom) return custom;
|
|
70
|
+
|
|
71
|
+
if (validateMode === "blur" && !touchedSignal() && submitCount() === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!options.schema) return null;
|
|
76
|
+
|
|
77
|
+
const currentValues = getValues();
|
|
78
|
+
const res = validateWithSchema(options.schema, currentValues);
|
|
79
|
+
if (!res.success && res.issues) {
|
|
80
|
+
const match = res.issues.find((i) => i.path === key || i.path === `${key}`);
|
|
81
|
+
return match ? match.message : null;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
})
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getValues = (): T => {
|
|
89
|
+
const res: Record<string, any> = {};
|
|
90
|
+
for (const key in fields) {
|
|
91
|
+
res[key] = (fields as any)[key].value();
|
|
92
|
+
}
|
|
93
|
+
return res as T;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const errors = computed<Partial<Record<keyof T, string>>>(() => {
|
|
97
|
+
const errMap: Partial<Record<keyof T, string>> = {};
|
|
98
|
+
for (const key in fields) {
|
|
99
|
+
const err = (fields as any)[key].error();
|
|
100
|
+
if (err) {
|
|
101
|
+
errMap[key] = err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return errMap;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const isValid = computed(() => {
|
|
108
|
+
const errs = errors();
|
|
109
|
+
return Object.keys(errs).length === 0;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const isDirty = computed(() => {
|
|
113
|
+
for (const key in fields) {
|
|
114
|
+
if ((fields as any)[key].isDirty()) return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const validate = (): boolean => {
|
|
120
|
+
if (!options.schema) return true;
|
|
121
|
+
const res = validateWithSchema(options.schema, getValues());
|
|
122
|
+
return res.success;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleSubmit = async (e?: Event): Promise<boolean> => {
|
|
126
|
+
if (e?.preventDefault) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
submitCount.update((c) => c + 1);
|
|
131
|
+
|
|
132
|
+
batch(() => {
|
|
133
|
+
for (const key in fields) {
|
|
134
|
+
(fields as any)[key].touched.set(true);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const valid = validate();
|
|
139
|
+
if (!valid) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (options.onSubmit) {
|
|
144
|
+
isSubmitting.set(true);
|
|
145
|
+
try {
|
|
146
|
+
await options.onSubmit(getValues());
|
|
147
|
+
} finally {
|
|
148
|
+
isSubmitting.set(false);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const bind = (name: keyof T) => {
|
|
156
|
+
const field = (fields as any)[name];
|
|
157
|
+
if (!field) {
|
|
158
|
+
throw new Error(`Field '${String(name)}' not found in form`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
name: String(name),
|
|
163
|
+
value: field.value,
|
|
164
|
+
onInput: (e: any) => {
|
|
165
|
+
const val =
|
|
166
|
+
e?.target?.type === "checkbox"
|
|
167
|
+
? e.target.checked
|
|
168
|
+
: e?.target?.value !== undefined
|
|
169
|
+
? e.target.value
|
|
170
|
+
: e;
|
|
171
|
+
field.value.set(val);
|
|
172
|
+
},
|
|
173
|
+
onBlur: () => {
|
|
174
|
+
field.touched.set(true);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const reset = (newValues?: Partial<T>) => {
|
|
180
|
+
const resetTarget = { ...initialValues, ...newValues } as Record<string, any>;
|
|
181
|
+
batch(() => {
|
|
182
|
+
for (const key in fields) {
|
|
183
|
+
(fields as any)[key].value.set(resetTarget[key]);
|
|
184
|
+
(fields as any)[key].touched.set(false);
|
|
185
|
+
}
|
|
186
|
+
customErrors.set({});
|
|
187
|
+
submitCount.set(0);
|
|
188
|
+
isSubmitting.set(false);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const setFieldValue = <K extends keyof T>(name: K, value: T[K]) => {
|
|
193
|
+
(fields as any)[name]?.value.set(value);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const setFieldError = (name: keyof T, error: string | null) => {
|
|
197
|
+
customErrors.update((prev) => ({
|
|
198
|
+
...prev,
|
|
199
|
+
[name]: error ?? undefined
|
|
200
|
+
}));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
fields,
|
|
205
|
+
values: getValues,
|
|
206
|
+
errors,
|
|
207
|
+
isValid,
|
|
208
|
+
isDirty,
|
|
209
|
+
isSubmitting,
|
|
210
|
+
submitCount,
|
|
211
|
+
bind,
|
|
212
|
+
handleSubmit,
|
|
213
|
+
reset,
|
|
214
|
+
setFieldValue,
|
|
215
|
+
setFieldError,
|
|
216
|
+
validate
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createForm,
|
|
3
|
+
type FieldState,
|
|
4
|
+
type Form,
|
|
5
|
+
type FormFields,
|
|
6
|
+
type FormOptions
|
|
7
|
+
} from "./form";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
Schema,
|
|
11
|
+
type SchemaLike,
|
|
12
|
+
type StandardSchemaV1,
|
|
13
|
+
type ValidationIssue,
|
|
14
|
+
type ValidationResult,
|
|
15
|
+
v,
|
|
16
|
+
validateWithSchema
|
|
17
|
+
} from "./schema";
|