@epoch32/visage 1.1.2 → 1.1.4
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/dist/api/index.js +55 -4085
- package/dist/charts/index.js +48 -2806
- package/dist/devtools/index.js +18 -4766
- package/dist/dom/index.js +32 -1639
- package/dist/form/index.js +13 -1498
- package/dist/index-061jf06a.js +848 -0
- package/dist/index-0madxn60.js +442 -0
- package/dist/index-1xtrtz1t.js +256 -0
- package/dist/index-2zz0g2n2.js +212 -0
- package/dist/index-5s255b3w.js +303 -0
- package/dist/index-6kfe1nr4.js +1179 -0
- package/dist/index-8czhpea8.js +338 -0
- package/dist/index-8x8xz80v.js +727 -0
- package/dist/index-921pn75b.js +0 -0
- package/dist/index-965ryh36.js +537 -0
- package/dist/index-9fvarkrx.js +98 -0
- package/dist/index-a4xns6cc.js +219 -0
- package/dist/index-b5wc752a.js +1696 -0
- package/dist/index-df5e614s.js +334 -0
- package/dist/index-dyvgtp43.js +1094 -0
- package/dist/index-grhdtcb5.js +234 -0
- package/dist/index-h6tjqk6e.js +158 -0
- package/dist/index-js199er4.js +101 -0
- package/dist/index-x2rs3vab.js +124 -0
- package/dist/index-y9bq7h9g.js +1162 -0
- package/dist/index-zr6cvset.js +2092 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +458 -11915
- package/dist/jsx-runtime/index.js +13 -1719
- package/dist/query/index.js +47 -2505
- package/dist/router/index.js +25 -1945
- package/dist/rpc/index.js +39 -3567
- package/dist/server/index.js +11 -1256
- package/dist/signals/index.js +52 -1160
- package/dist/style/index.js +77 -2429
- package/dist/template/index.js +7 -1232
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +150 -4244
- package/dist/ui/tabs.d.ts +23 -1
- package/dist/virtual/index.js +12 -2044
- package/dist/vite/index.js +5 -744
- package/package.json +2 -2
- package/src/dom/render.ts +20 -7
- package/src/index.ts +2 -0
- package/src/signals/computed.ts +19 -12
- package/src/signals/context.ts +1 -2
- package/src/signals/effect.ts +7 -7
- package/src/signals/signal.ts +16 -12
- package/src/signals/system.ts +50 -21
- package/src/ui/index.ts +2 -0
- package/src/ui/tabs.tsx +211 -5
- package/src/ui/utils.ts +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epoch32/visage",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Lightning-fast, zero-VDOM, fine-grained reactive UI library with full TypeScript support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"package.json"
|
|
101
101
|
],
|
|
102
102
|
"scripts": {
|
|
103
|
-
"build": "bun build src/index.ts src/signals/index.ts src/dom/index.ts src/template/index.ts src/api/index.ts src/rpc/index.ts src/query/index.ts src/style/index.ts src/devtools/index.ts src/ui/index.ts src/virtual/index.ts src/form/index.ts src/server/index.ts src/router/index.ts src/vite/index.ts src/jsx-runtime/index.ts src/charts/index.ts --root src --outdir dist/ && bun x tsc -p tsconfig.build.json && cp src/jsx-runtime/jsx.d.ts dist/jsx-runtime/jsx.d.ts",
|
|
103
|
+
"build": "bun build src/index.ts src/signals/index.ts src/dom/index.ts src/template/index.ts src/api/index.ts src/rpc/index.ts src/query/index.ts src/style/index.ts src/devtools/index.ts src/ui/index.ts src/virtual/index.ts src/form/index.ts src/server/index.ts src/router/index.ts src/vite/index.ts src/jsx-runtime/index.ts src/charts/index.ts --root src --outdir dist/ --splitting && bun x tsc -p tsconfig.build.json && cp src/jsx-runtime/jsx.d.ts dist/jsx-runtime/jsx.d.ts",
|
|
104
104
|
"test": "bun test",
|
|
105
105
|
"test:watch": "bun test --watch",
|
|
106
106
|
"test:coverage": "bun test --coverage",
|
package/src/dom/render.ts
CHANGED
|
@@ -68,9 +68,6 @@ export function setAttribute(el: HTMLElement | SVGElement, key: string, value: a
|
|
|
68
68
|
const useCapture = key.startsWith("onCapture");
|
|
69
69
|
|
|
70
70
|
el.addEventListener(eventName, value as EventListener, useCapture);
|
|
71
|
-
onCleanup(() => {
|
|
72
|
-
el.removeEventListener(eventName, value as EventListener, useCapture);
|
|
73
|
-
});
|
|
74
71
|
return;
|
|
75
72
|
}
|
|
76
73
|
|
|
@@ -82,16 +79,27 @@ export function setAttribute(el: HTMLElement | SVGElement, key: string, value: a
|
|
|
82
79
|
|
|
83
80
|
// Reactive class / className with array or object support
|
|
84
81
|
if (key === "class" || key === "className") {
|
|
82
|
+
const isSvg = typeof SVGElement !== "undefined" && el instanceof SVGElement;
|
|
85
83
|
if (hasReactiveClass(value)) {
|
|
86
84
|
effect(() => {
|
|
87
85
|
const cls = normalizeClass(value);
|
|
88
|
-
if (cls)
|
|
89
|
-
|
|
86
|
+
if (cls) {
|
|
87
|
+
el.setAttribute("class", cls);
|
|
88
|
+
if (!isSvg) (el as HTMLElement).className = cls;
|
|
89
|
+
} else {
|
|
90
|
+
el.removeAttribute("class");
|
|
91
|
+
if (!isSvg) (el as HTMLElement).className = "";
|
|
92
|
+
}
|
|
90
93
|
});
|
|
91
94
|
} else {
|
|
92
95
|
const cls = normalizeClass(value);
|
|
93
|
-
if (cls)
|
|
94
|
-
|
|
96
|
+
if (cls) {
|
|
97
|
+
el.setAttribute("class", cls);
|
|
98
|
+
if (!isSvg) (el as HTMLElement).className = cls;
|
|
99
|
+
} else {
|
|
100
|
+
el.removeAttribute("class");
|
|
101
|
+
if (!isSvg) (el as HTMLElement).className = "";
|
|
102
|
+
}
|
|
95
103
|
}
|
|
96
104
|
return;
|
|
97
105
|
}
|
|
@@ -144,10 +152,13 @@ function applyProp(el: HTMLElement | SVGElement, key: string, value: any) {
|
|
|
144
152
|
|
|
145
153
|
if (key === "className" || key === "class") {
|
|
146
154
|
const cls = normalizeClass(value);
|
|
155
|
+
const isSvg = typeof SVGElement !== "undefined" && el instanceof SVGElement;
|
|
147
156
|
if (cls) {
|
|
148
157
|
el.setAttribute("class", cls);
|
|
158
|
+
if (!isSvg) (el as HTMLElement).className = cls;
|
|
149
159
|
} else {
|
|
150
160
|
el.removeAttribute("class");
|
|
161
|
+
if (!isSvg) (el as HTMLElement).className = "";
|
|
151
162
|
}
|
|
152
163
|
} else if (key === "style") {
|
|
153
164
|
if (typeof value === "string") {
|
|
@@ -177,6 +188,8 @@ function applyProp(el: HTMLElement | SVGElement, key: string, value: any) {
|
|
|
177
188
|
el.setAttribute(key, String(value));
|
|
178
189
|
} else if (value === true) {
|
|
179
190
|
el.setAttribute(key, "");
|
|
191
|
+
} else if (value === false) {
|
|
192
|
+
el.removeAttribute(key);
|
|
180
193
|
} else {
|
|
181
194
|
el.setAttribute(key, String(value));
|
|
182
195
|
}
|
package/src/index.ts
CHANGED
|
@@ -322,9 +322,11 @@ export {
|
|
|
322
322
|
type CommandItemProps,
|
|
323
323
|
CommandList,
|
|
324
324
|
type CommandProps,
|
|
325
|
+
type CreateTabsOptions,
|
|
325
326
|
createControllableSignal,
|
|
326
327
|
createIcon,
|
|
327
328
|
createId,
|
|
329
|
+
createTabs,
|
|
328
330
|
createToastManager,
|
|
329
331
|
DataTable,
|
|
330
332
|
type DataTableProps,
|
package/src/signals/computed.ts
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* Visage Lazy, Glitch-Free Computed State Engine
|
|
3
3
|
*/
|
|
4
4
|
import type { Scope } from "./scope";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getCurrentScope,
|
|
7
|
+
getCurrentSubscriber,
|
|
8
|
+
setCurrentScope,
|
|
9
|
+
setCurrentSubscriber
|
|
10
|
+
} from "./system";
|
|
6
11
|
import {
|
|
7
12
|
COMPUTED_BRAND,
|
|
8
13
|
type ComputedOptions,
|
|
@@ -30,9 +35,9 @@ export class Computed<T> implements ISubscriber, IDependency {
|
|
|
30
35
|
|
|
31
36
|
constructor(fn: () => T) {
|
|
32
37
|
this._fn = fn;
|
|
33
|
-
this.scope =
|
|
34
|
-
if (
|
|
35
|
-
|
|
38
|
+
this.scope = getCurrentScope();
|
|
39
|
+
if (this.scope) {
|
|
40
|
+
this.scope.addSubscriber(this);
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -41,10 +46,11 @@ export class Computed<T> implements ISubscriber, IDependency {
|
|
|
41
46
|
return this._fn();
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
const curSub = getCurrentSubscriber();
|
|
50
|
+
if (curSub !== null && curSub.state !== SubscriberState.DISPOSED) {
|
|
51
|
+
this._subscribers.add(curSub);
|
|
52
|
+
curSub.sources.add(this);
|
|
53
|
+
curSub.sourceVersions.set(this, this.version);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
if (this.state === SubscriberState.DIRTY || this.state === SubscriberState.CHECK_DIRTY) {
|
|
@@ -108,9 +114,9 @@ export class Computed<T> implements ISubscriber, IDependency {
|
|
|
108
114
|
private recompute(): void {
|
|
109
115
|
this.cleanupSources();
|
|
110
116
|
|
|
111
|
-
const prevSubscriber =
|
|
117
|
+
const prevSubscriber = getCurrentSubscriber();
|
|
112
118
|
setCurrentSubscriber(this);
|
|
113
|
-
const prevScope =
|
|
119
|
+
const prevScope = getCurrentScope();
|
|
114
120
|
setCurrentScope(this.scope);
|
|
115
121
|
|
|
116
122
|
try {
|
|
@@ -195,8 +201,9 @@ export function computed<T>(fn: () => T, options?: ComputedOptions): ReadonlySig
|
|
|
195
201
|
.filter(Boolean)
|
|
196
202
|
});
|
|
197
203
|
(c as any)._devId = sigId;
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
const curScope = getCurrentScope();
|
|
205
|
+
if (curScope) {
|
|
206
|
+
curScope.addCleanup(() => {
|
|
200
207
|
devtools.unregisterSignal(sigId);
|
|
201
208
|
});
|
|
202
209
|
}
|
package/src/signals/context.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type Child, insertChild } from "../dom/render";
|
|
2
2
|
import { getActiveScope, runInScope, Scope } from "./scope";
|
|
3
|
-
import { untrack } from "./system";
|
|
4
3
|
|
|
5
4
|
export interface Context<T> {
|
|
6
5
|
id: symbol;
|
|
@@ -54,7 +53,7 @@ export function createContext<T>(defaultValue?: T): Context<T | undefined> {
|
|
|
54
53
|
return runInScope(scope, () => {
|
|
55
54
|
const content =
|
|
56
55
|
typeof props.children === "function"
|
|
57
|
-
?
|
|
56
|
+
? (props.children as (...args: any[]) => any)()
|
|
58
57
|
: props.children;
|
|
59
58
|
if (typeof document !== "undefined") {
|
|
60
59
|
const frag = document.createDocumentFragment();
|
package/src/signals/effect.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type { Scope } from "./scope";
|
|
|
6
6
|
import {
|
|
7
7
|
addPendingEffect,
|
|
8
8
|
batchDepth,
|
|
9
|
-
currentScope,
|
|
10
|
-
currentSubscriber,
|
|
11
9
|
flushEffects,
|
|
10
|
+
getCurrentScope,
|
|
11
|
+
getCurrentSubscriber,
|
|
12
12
|
removePendingEffect,
|
|
13
13
|
setCurrentScope,
|
|
14
14
|
setCurrentSubscriber
|
|
@@ -28,9 +28,9 @@ export class Effect implements ISubscriber {
|
|
|
28
28
|
|
|
29
29
|
constructor(fn: () => void | CleanupFn) {
|
|
30
30
|
this._fn = fn;
|
|
31
|
-
this.scope =
|
|
32
|
-
if (
|
|
33
|
-
|
|
31
|
+
this.scope = getCurrentScope();
|
|
32
|
+
if (this.scope) {
|
|
33
|
+
this.scope.addSubscriber(this);
|
|
34
34
|
}
|
|
35
35
|
this.run();
|
|
36
36
|
}
|
|
@@ -85,9 +85,9 @@ export class Effect implements ISubscriber {
|
|
|
85
85
|
this.runCleanups();
|
|
86
86
|
this.cleanupSources();
|
|
87
87
|
|
|
88
|
-
const prevSubscriber =
|
|
88
|
+
const prevSubscriber = getCurrentSubscriber();
|
|
89
89
|
setCurrentSubscriber(this);
|
|
90
|
-
const prevScope =
|
|
90
|
+
const prevScope = getCurrentScope();
|
|
91
91
|
setCurrentScope(this.scope);
|
|
92
92
|
|
|
93
93
|
try {
|
package/src/signals/signal.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Visage Core Writable Signal & State Primitives
|
|
3
3
|
*/
|
|
4
|
-
import { batch,
|
|
4
|
+
import { batch, getCurrentScope, getCurrentSubscriber, warnedCyclicSubscribers } from "./system";
|
|
5
5
|
import {
|
|
6
6
|
type IDependency,
|
|
7
7
|
type ISubscriber,
|
|
@@ -32,10 +32,11 @@ export class Signal<T> implements IDependency {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
get(): T {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const curSub = getCurrentSubscriber();
|
|
36
|
+
if (curSub !== null && curSub.state !== SubscriberState.DISPOSED) {
|
|
37
|
+
this._subscribers.add(curSub);
|
|
38
|
+
curSub.sources.add(this);
|
|
39
|
+
curSub.sourceVersions.set(this, this.version);
|
|
39
40
|
}
|
|
40
41
|
return this._value;
|
|
41
42
|
}
|
|
@@ -45,9 +46,10 @@ export class Signal<T> implements IDependency {
|
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const curSub = getCurrentSubscriber();
|
|
50
|
+
if (curSub !== null && this._subscribers.has(curSub)) {
|
|
51
|
+
if (!warnedCyclicSubscribers.has(curSub)) {
|
|
52
|
+
warnedCyclicSubscribers.add(curSub);
|
|
51
53
|
if (typeof console !== "undefined" && typeof console.warn === "function") {
|
|
52
54
|
console.warn(
|
|
53
55
|
`[Visage Reactivity] Cyclic update detected: An active subscriber modified a signal that it depends on. ` +
|
|
@@ -228,8 +230,9 @@ export function signal<T>(
|
|
|
228
230
|
}
|
|
229
231
|
};
|
|
230
232
|
window.addEventListener("storage", onStorage);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
const curScope = getCurrentScope();
|
|
234
|
+
if (curScope) {
|
|
235
|
+
curScope.addCleanup(() => {
|
|
233
236
|
window.removeEventListener("storage", onStorage);
|
|
234
237
|
});
|
|
235
238
|
}
|
|
@@ -268,8 +271,9 @@ export function signal<T>(
|
|
|
268
271
|
.filter(Boolean)
|
|
269
272
|
});
|
|
270
273
|
(s as any)._devId = sigId;
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
const curScope = getCurrentScope();
|
|
275
|
+
if (curScope) {
|
|
276
|
+
curScope.addCleanup(() => {
|
|
273
277
|
devtools.unregisterSignal(sigId);
|
|
274
278
|
});
|
|
275
279
|
}
|
package/src/signals/system.ts
CHANGED
|
@@ -3,55 +3,81 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type ISubscriber, SubscriberState } from "./types";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
interface ReactivityContext {
|
|
7
|
+
currentSubscriber: ISubscriber | null;
|
|
8
|
+
currentScope: any;
|
|
9
|
+
batchDepth: number;
|
|
10
|
+
pendingEffects: Set<any>;
|
|
11
|
+
warnedCyclicSubscribers: WeakSet<ISubscriber>;
|
|
12
|
+
isFlushing: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const REACTIVITY_KEY = Symbol.for("visage.reactivity.context");
|
|
16
|
+
const g = globalThis as unknown as { [REACTIVITY_KEY]?: ReactivityContext };
|
|
17
|
+
if (!g[REACTIVITY_KEY]) {
|
|
18
|
+
g[REACTIVITY_KEY] = {
|
|
19
|
+
currentSubscriber: null,
|
|
20
|
+
currentScope: null,
|
|
21
|
+
batchDepth: 0,
|
|
22
|
+
pendingEffects: new Set(),
|
|
23
|
+
warnedCyclicSubscribers: new WeakSet(),
|
|
24
|
+
isFlushing: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const ctx = g[REACTIVITY_KEY]!;
|
|
28
|
+
|
|
29
|
+
export let currentSubscriber: ISubscriber | null = ctx.currentSubscriber;
|
|
30
|
+
export let currentScope: any = ctx.currentScope;
|
|
31
|
+
export let batchDepth = ctx.batchDepth;
|
|
32
|
+
export const pendingEffects: Set<any> = ctx.pendingEffects;
|
|
33
|
+
export const warnedCyclicSubscribers: WeakSet<ISubscriber> = ctx.warnedCyclicSubscribers;
|
|
34
|
+
export let isFlushing = ctx.isFlushing;
|
|
12
35
|
|
|
13
36
|
export function setCurrentSubscriber(sub: ISubscriber | null): void {
|
|
37
|
+
ctx.currentSubscriber = sub;
|
|
14
38
|
currentSubscriber = sub;
|
|
15
39
|
}
|
|
16
40
|
|
|
17
41
|
export function getCurrentSubscriber(): ISubscriber | null {
|
|
18
|
-
return currentSubscriber;
|
|
42
|
+
return ctx.currentSubscriber;
|
|
19
43
|
}
|
|
20
44
|
|
|
21
45
|
export function setCurrentScope(scope: any): void {
|
|
46
|
+
ctx.currentScope = scope;
|
|
22
47
|
currentScope = scope;
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
export function getCurrentScope(): any {
|
|
26
|
-
return currentScope;
|
|
51
|
+
return ctx.currentScope;
|
|
27
52
|
}
|
|
28
53
|
|
|
29
54
|
export function addPendingEffect(eff: any): void {
|
|
30
|
-
pendingEffects.add(eff);
|
|
55
|
+
ctx.pendingEffects.add(eff);
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
export function removePendingEffect(eff: any): void {
|
|
34
|
-
pendingEffects.delete(eff);
|
|
59
|
+
ctx.pendingEffects.delete(eff);
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
const MAX_FLUSH_COUNT = 100;
|
|
38
63
|
|
|
39
64
|
export function flushEffects(): void {
|
|
40
|
-
if (isFlushing || batchDepth > 0) return;
|
|
65
|
+
if (ctx.isFlushing || ctx.batchDepth > 0) return;
|
|
66
|
+
ctx.isFlushing = true;
|
|
41
67
|
isFlushing = true;
|
|
42
68
|
let iterations = 0;
|
|
43
69
|
try {
|
|
44
|
-
while (pendingEffects.size > 0) {
|
|
70
|
+
while (ctx.pendingEffects.size > 0) {
|
|
45
71
|
if (++iterations > MAX_FLUSH_COUNT) {
|
|
46
|
-
pendingEffects.clear();
|
|
72
|
+
ctx.pendingEffects.clear();
|
|
47
73
|
throw new Error(
|
|
48
74
|
`[Visage Reactivity] Maximum update depth exceeded (${MAX_FLUSH_COUNT} cycles). ` +
|
|
49
75
|
`An effect continuously triggered state mutations (infinite loop detected). ` +
|
|
50
76
|
`Ensure effects that modify signals do not unconditionally depend on them, or use untrack() to read state.`
|
|
51
77
|
);
|
|
52
78
|
}
|
|
53
|
-
const effectsToRun = Array.from(pendingEffects);
|
|
54
|
-
pendingEffects.clear();
|
|
79
|
+
const effectsToRun = Array.from(ctx.pendingEffects);
|
|
80
|
+
ctx.pendingEffects.clear();
|
|
55
81
|
|
|
56
82
|
for (const eff of effectsToRun) {
|
|
57
83
|
if (eff.state === SubscriberState.DIRTY || eff.state === SubscriberState.CHECK_DIRTY) {
|
|
@@ -60,6 +86,7 @@ export function flushEffects(): void {
|
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
} finally {
|
|
89
|
+
ctx.isFlushing = false;
|
|
63
90
|
isFlushing = false;
|
|
64
91
|
}
|
|
65
92
|
}
|
|
@@ -85,12 +112,14 @@ export function flushEffects(): void {
|
|
|
85
112
|
* @returns The return value of `fn`
|
|
86
113
|
*/
|
|
87
114
|
export function batch<T>(fn: () => T): T {
|
|
88
|
-
batchDepth++;
|
|
115
|
+
ctx.batchDepth++;
|
|
116
|
+
batchDepth = ctx.batchDepth;
|
|
89
117
|
try {
|
|
90
118
|
return fn();
|
|
91
119
|
} finally {
|
|
92
|
-
batchDepth--;
|
|
93
|
-
|
|
120
|
+
ctx.batchDepth--;
|
|
121
|
+
batchDepth = ctx.batchDepth;
|
|
122
|
+
if (ctx.batchDepth === 0) {
|
|
94
123
|
flushEffects();
|
|
95
124
|
}
|
|
96
125
|
}
|
|
@@ -114,11 +143,11 @@ export function batch<T>(fn: () => T): T {
|
|
|
114
143
|
* @returns The return value of `fn`
|
|
115
144
|
*/
|
|
116
145
|
export function untrack<T>(fn: () => T): T {
|
|
117
|
-
const prev = currentSubscriber;
|
|
118
|
-
|
|
146
|
+
const prev = ctx.currentSubscriber;
|
|
147
|
+
setCurrentSubscriber(null);
|
|
119
148
|
try {
|
|
120
149
|
return fn();
|
|
121
150
|
} finally {
|
|
122
|
-
|
|
151
|
+
setCurrentSubscriber(prev);
|
|
123
152
|
}
|
|
124
153
|
}
|
package/src/ui/index.ts
CHANGED
package/src/ui/tabs.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Show } from "../dom/control-flow";
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
|
-
import type
|
|
3
|
+
import { isSignal, type ReadonlySignal, signal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
5
|
import { createControllableSignal, createId } from "./utils";
|
|
6
6
|
|
|
@@ -105,7 +105,7 @@ export function TabsRoot(props: TabsProps): Child {
|
|
|
105
105
|
))}
|
|
106
106
|
</>
|
|
107
107
|
) : null}
|
|
108
|
-
{
|
|
108
|
+
{children}
|
|
109
109
|
</Tag>
|
|
110
110
|
)
|
|
111
111
|
});
|
|
@@ -221,13 +221,14 @@ export function TabsTrigger(props: TabsTriggerProps): Child {
|
|
|
221
221
|
id={ctx ? `${ctx.baseId}-trigger-${value}` : undefined}
|
|
222
222
|
type={Tag === "button" ? "button" : undefined}
|
|
223
223
|
role="tab"
|
|
224
|
-
aria-selected={isSelected}
|
|
224
|
+
aria-selected={() => String(isSelected())}
|
|
225
225
|
aria-controls={ctx ? `${ctx.baseId}-panel-${value}` : undefined}
|
|
226
226
|
data-state={() => (isSelected() ? "active" : "inactive")}
|
|
227
|
+
data-active={() => (isSelected() ? "true" : "false")}
|
|
227
228
|
data-value={value}
|
|
228
229
|
tabIndex={() => (isSelected() ? 0 : -1)}
|
|
229
230
|
disabled={disabled}
|
|
230
|
-
class={className}
|
|
231
|
+
class={() => [className, isSelected() ? "active" : ""].filter(Boolean).join(" ")}
|
|
231
232
|
style={style}
|
|
232
233
|
onClick={handleClick}
|
|
233
234
|
{...rest}
|
|
@@ -289,9 +290,214 @@ export function TabsContent(props: TabsContentProps): Child {
|
|
|
289
290
|
);
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
export interface CreateTabsOptions {
|
|
294
|
+
value?: () => string | undefined;
|
|
295
|
+
defaultValue?: string;
|
|
296
|
+
onValueChange?: (value: string) => void;
|
|
297
|
+
orientation?: "horizontal" | "vertical";
|
|
298
|
+
activationMode?: "automatic" | "manual";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function createTabs(options: CreateTabsOptions = {}) {
|
|
302
|
+
const activeSignal =
|
|
303
|
+
typeof options.value === "function"
|
|
304
|
+
? (options.value as ReadonlySignal<string>)
|
|
305
|
+
: signal<string>(options.defaultValue ?? "");
|
|
306
|
+
|
|
307
|
+
const value = activeSignal;
|
|
308
|
+
const setValue = (next: string) => {
|
|
309
|
+
if (isSignal(activeSignal)) {
|
|
310
|
+
(activeSignal as any).set(next);
|
|
311
|
+
}
|
|
312
|
+
if (options.onValueChange) {
|
|
313
|
+
options.onValueChange(next);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const baseId = createId("tabs");
|
|
318
|
+
const orientation = options.orientation ?? "horizontal";
|
|
319
|
+
const activationMode = options.activationMode ?? "automatic";
|
|
320
|
+
|
|
321
|
+
const Root = (props: {
|
|
322
|
+
as?: string;
|
|
323
|
+
class?: any;
|
|
324
|
+
style?: any;
|
|
325
|
+
children: Child;
|
|
326
|
+
[key: string]: any;
|
|
327
|
+
}) => {
|
|
328
|
+
const { as: Tag = "div", class: className, style, children, ...rest } = props;
|
|
329
|
+
return (
|
|
330
|
+
<Tag data-orientation={orientation} class={className} style={style} {...rest}>
|
|
331
|
+
{children}
|
|
332
|
+
</Tag>
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const List = (props: TabsListProps) => {
|
|
337
|
+
const { as: Tag = "div", children, onKeyDown, class: className, style, ...rest } = props;
|
|
338
|
+
|
|
339
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
340
|
+
if (onKeyDown) onKeyDown(e);
|
|
341
|
+
if (e.defaultPrevented) return;
|
|
342
|
+
|
|
343
|
+
const target = e.target as HTMLElement | null;
|
|
344
|
+
if (target?.getAttribute("role") !== "tab") return;
|
|
345
|
+
|
|
346
|
+
const list = target.closest('[role="tablist"]');
|
|
347
|
+
if (!list) return;
|
|
348
|
+
|
|
349
|
+
const tabs = Array.from(list.querySelectorAll<HTMLElement>('[role="tab"]:not([disabled])'));
|
|
350
|
+
const index = tabs.indexOf(target);
|
|
351
|
+
if (index === -1) return;
|
|
352
|
+
|
|
353
|
+
let nextIndex = index;
|
|
354
|
+
const isHorizontal = orientation === "horizontal";
|
|
355
|
+
const prevKey = isHorizontal ? "ArrowLeft" : "ArrowUp";
|
|
356
|
+
const nextKey = isHorizontal ? "ArrowRight" : "ArrowDown";
|
|
357
|
+
|
|
358
|
+
if (e.key === prevKey) {
|
|
359
|
+
nextIndex = index === 0 ? tabs.length - 1 : index - 1;
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
} else if (e.key === nextKey) {
|
|
362
|
+
nextIndex = index === tabs.length - 1 ? 0 : index + 1;
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
} else if (e.key === "Home") {
|
|
365
|
+
nextIndex = 0;
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
} else if (e.key === "End") {
|
|
368
|
+
nextIndex = tabs.length - 1;
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (nextIndex !== index) {
|
|
373
|
+
const nextTab = tabs[nextIndex]!;
|
|
374
|
+
nextTab.focus();
|
|
375
|
+
if (activationMode === "automatic") {
|
|
376
|
+
const val = nextTab.getAttribute("data-value");
|
|
377
|
+
if (val) setValue(val);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<Tag
|
|
384
|
+
role="tablist"
|
|
385
|
+
aria-orientation={orientation}
|
|
386
|
+
class={className}
|
|
387
|
+
style={style}
|
|
388
|
+
onKeyDown={handleKeyDown}
|
|
389
|
+
{...rest}
|
|
390
|
+
>
|
|
391
|
+
{children}
|
|
392
|
+
</Tag>
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const Trigger = (props: TabsTriggerProps) => {
|
|
397
|
+
const isSelected = () => value() === props.value;
|
|
398
|
+
|
|
399
|
+
const handleClick = (e: MouseEvent) => {
|
|
400
|
+
if (props.onClick) props.onClick(e);
|
|
401
|
+
if (!e.defaultPrevented) {
|
|
402
|
+
setValue(props.value);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const {
|
|
407
|
+
value: tabVal,
|
|
408
|
+
disabled,
|
|
409
|
+
as: Tag = "button",
|
|
410
|
+
children,
|
|
411
|
+
onClick,
|
|
412
|
+
class: className,
|
|
413
|
+
style,
|
|
414
|
+
...rest
|
|
415
|
+
} = props;
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<Tag
|
|
419
|
+
id={`${baseId}-trigger-${tabVal}`}
|
|
420
|
+
type={Tag === "button" ? "button" : undefined}
|
|
421
|
+
role="tab"
|
|
422
|
+
aria-selected={() => String(isSelected())}
|
|
423
|
+
aria-controls={`${baseId}-panel-${tabVal}`}
|
|
424
|
+
data-state={() => (isSelected() ? "active" : "inactive")}
|
|
425
|
+
data-active={() => (isSelected() ? "true" : "false")}
|
|
426
|
+
data-value={tabVal}
|
|
427
|
+
tabIndex={() => (isSelected() ? 0 : -1)}
|
|
428
|
+
disabled={disabled}
|
|
429
|
+
class={() => [className, isSelected() ? "active" : ""].filter(Boolean).join(" ")}
|
|
430
|
+
style={style}
|
|
431
|
+
onClick={handleClick}
|
|
432
|
+
{...rest}
|
|
433
|
+
>
|
|
434
|
+
{children}
|
|
435
|
+
</Tag>
|
|
436
|
+
);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const Content = (props: TabsContentProps) => {
|
|
440
|
+
const isSelected = () => value() === props.value;
|
|
441
|
+
const {
|
|
442
|
+
value: tabVal,
|
|
443
|
+
forceMount,
|
|
444
|
+
as: Tag = "div",
|
|
445
|
+
children,
|
|
446
|
+
class: className,
|
|
447
|
+
style,
|
|
448
|
+
...rest
|
|
449
|
+
} = props;
|
|
450
|
+
|
|
451
|
+
if (forceMount) {
|
|
452
|
+
return (
|
|
453
|
+
<Tag
|
|
454
|
+
id={`${baseId}-panel-${tabVal}`}
|
|
455
|
+
role="tabpanel"
|
|
456
|
+
aria-labelledby={`${baseId}-trigger-${tabVal}`}
|
|
457
|
+
data-state={() => (isSelected() ? "active" : "inactive")}
|
|
458
|
+
hidden={() => !isSelected()}
|
|
459
|
+
tabIndex={0}
|
|
460
|
+
class={className}
|
|
461
|
+
style={style}
|
|
462
|
+
{...rest}
|
|
463
|
+
>
|
|
464
|
+
{children}
|
|
465
|
+
</Tag>
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<Show when={isSelected}>
|
|
471
|
+
<Tag
|
|
472
|
+
id={`${baseId}-panel-${tabVal}`}
|
|
473
|
+
role="tabpanel"
|
|
474
|
+
aria-labelledby={`${baseId}-trigger-${tabVal}`}
|
|
475
|
+
data-state="active"
|
|
476
|
+
tabIndex={0}
|
|
477
|
+
class={className}
|
|
478
|
+
style={style}
|
|
479
|
+
{...rest}
|
|
480
|
+
>
|
|
481
|
+
{children}
|
|
482
|
+
</Tag>
|
|
483
|
+
</Show>
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
value,
|
|
489
|
+
setValue,
|
|
490
|
+
Root,
|
|
491
|
+
List,
|
|
492
|
+
Trigger,
|
|
493
|
+
Content
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
292
497
|
export const Tabs = Object.assign(TabsRoot, {
|
|
293
498
|
Root: TabsRoot,
|
|
294
499
|
List: TabsList,
|
|
295
500
|
Trigger: TabsTrigger,
|
|
296
|
-
Content: TabsContent
|
|
501
|
+
Content: TabsContent,
|
|
502
|
+
create: createTabs
|
|
297
503
|
});
|
package/src/ui/utils.ts
CHANGED
|
@@ -35,9 +35,7 @@ export function createControllableSignal<T>(options: {
|
|
|
35
35
|
const currentVal = getter();
|
|
36
36
|
const nextVal = typeof next === "function" ? (next as (prev: T) => T)(currentVal) : next;
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
internal.set(nextVal);
|
|
40
|
-
}
|
|
38
|
+
internal.set(nextVal);
|
|
41
39
|
if (options.onChange && nextVal !== currentVal) {
|
|
42
40
|
options.onChange(nextVal);
|
|
43
41
|
}
|