dalila 1.8.4 → 1.9.1
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 +7 -0
- package/dist/components/ui/runtime.js +2 -1
- package/dist/core/dev.d.ts +11 -2
- package/dist/core/dev.js +26 -2
- package/dist/core/devtools.d.ts +65 -0
- package/dist/core/devtools.js +452 -0
- package/dist/core/for.d.ts +20 -0
- package/dist/core/for.js +40 -0
- package/dist/core/scope.js +3 -0
- package/dist/core/signal.js +40 -3
- package/dist/runtime/bind.js +433 -26
- package/package.json +4 -2
- package/src/components/ui/accordion/accordion.css +14 -8
- package/src/components/ui/badge/badge.css +13 -7
- package/src/components/ui/breadcrumb/breadcrumb.css +3 -3
- package/src/components/ui/button/button.css +21 -12
- package/src/components/ui/calendar/calendar.css +20 -6
- package/src/components/ui/card/card.css +18 -7
- package/src/components/ui/checkbox/checkbox.css +7 -5
- package/src/components/ui/chip/chip.css +7 -2
- package/src/components/ui/collapsible/collapsible.css +10 -4
- package/src/components/ui/combobox/combobox.css +18 -10
- package/src/components/ui/dialog/dialog.css +13 -2
- package/src/components/ui/drawer/drawer.css +11 -2
- package/src/components/ui/dropdown/dropdown.css +17 -8
- package/src/components/ui/dropzone/dropzone.css +17 -5
- package/src/components/ui/empty-state/empty-state.css +2 -2
- package/src/components/ui/form/form.css +1 -1
- package/src/components/ui/input/input.css +25 -6
- package/src/components/ui/pagination/pagination.css +10 -3
- package/src/components/ui/popover/popover.css +9 -9
- package/src/components/ui/radio/radio.css +5 -3
- package/src/components/ui/separator/separator.css +3 -3
- package/src/components/ui/skeleton/skeleton.css +3 -13
- package/src/components/ui/slider/slider.css +5 -5
- package/src/components/ui/table/table.css +4 -4
- package/src/components/ui/tabs/tabs.css +5 -1
- package/src/components/ui/toast/toast.css +15 -3
- package/src/components/ui/toggle/toggle.css +11 -9
- package/src/components/ui/tokens/tokens.css +12 -4
- package/src/components/ui/tooltip/tooltip.css +21 -5
- package/src/components/ui/typography/typography.css +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ bind(document.getElementById('app')!, ctx);
|
|
|
74
74
|
- [when](./docs/core/when.md) — Conditional visibility
|
|
75
75
|
- [match](./docs/core/match.md) — Switch-style rendering
|
|
76
76
|
- [for](./docs/core/for.md) — List rendering with keyed diffing
|
|
77
|
+
- [Virtual Lists](./docs/core/virtual.md) — Fixed-height windowed rendering for large datasets
|
|
77
78
|
|
|
78
79
|
### Data
|
|
79
80
|
|
|
@@ -94,6 +95,12 @@ bind(document.getElementById('app')!, ctx);
|
|
|
94
95
|
- [Scheduler](./docs/core/scheduler.md) — Batching and coordination
|
|
95
96
|
- [Keys](./docs/core/key.md) — Cache key encoding
|
|
96
97
|
- [Dev Mode](./docs/core/dev.md) — Warnings and helpers
|
|
98
|
+
- [Devtools Extension](./devtools-extension/README.md) — Browser panel for reactive graph and scopes
|
|
99
|
+
|
|
100
|
+
Firefox extension workflows:
|
|
101
|
+
|
|
102
|
+
- `npm run devtools:firefox:run` — launch Firefox with extension loaded for dev
|
|
103
|
+
- `npm run devtools:firefox:build` — package extension artifact for submission/signing
|
|
97
104
|
|
|
98
105
|
## Features
|
|
99
106
|
|
|
@@ -363,7 +363,8 @@ export function mountUI(root, options) {
|
|
|
363
363
|
}
|
|
364
364
|
// Attach drawers
|
|
365
365
|
for (const [key, drawer] of Object.entries(options.drawers ?? {})) {
|
|
366
|
-
const
|
|
366
|
+
const fallbackTag = drawer.side() === "bottom" ? "d-sheet" : "d-drawer";
|
|
367
|
+
const el = findByUI(mountedRoot, key, fallbackTag);
|
|
367
368
|
if (el)
|
|
368
369
|
drawer._attachTo(el);
|
|
369
370
|
}
|
package/dist/core/dev.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
import { DevtoolsEvent, DevtoolsRuntimeOptions, DevtoolsSnapshot } from "./devtools.js";
|
|
1
2
|
export declare function setDevMode(enabled: boolean): void;
|
|
2
3
|
export declare function isInDevMode(): boolean;
|
|
4
|
+
export interface InitDevToolsOptions extends DevtoolsRuntimeOptions {
|
|
5
|
+
}
|
|
6
|
+
export declare function setDevtoolsEnabled(enabled: boolean, options?: DevtoolsRuntimeOptions): void;
|
|
7
|
+
export declare function isDevtoolsEnabled(): boolean;
|
|
8
|
+
export declare function configureDevtools(options: DevtoolsRuntimeOptions): void;
|
|
9
|
+
export declare function getDevtoolsSnapshot(): DevtoolsSnapshot;
|
|
10
|
+
export declare function onDevtoolsEvent(listener: (event: DevtoolsEvent) => void): () => void;
|
|
11
|
+
export declare function resetDevtools(): void;
|
|
3
12
|
/**
|
|
4
|
-
* Initialize dev tools
|
|
13
|
+
* Initialize dev tools runtime bridge for graph inspection.
|
|
5
14
|
* Returns a promise for future async initialization support.
|
|
6
15
|
*/
|
|
7
|
-
export declare function initDevTools(): Promise<void>;
|
|
16
|
+
export declare function initDevTools(options?: InitDevToolsOptions): Promise<void>;
|
package/dist/core/dev.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { configure, getSnapshot, isEnabled, reset, setEnabled, subscribe, } from "./devtools.js";
|
|
1
2
|
let isDevMode = true;
|
|
2
3
|
export function setDevMode(enabled) {
|
|
3
4
|
isDevMode = enabled;
|
|
@@ -5,10 +6,33 @@ export function setDevMode(enabled) {
|
|
|
5
6
|
export function isInDevMode() {
|
|
6
7
|
return isDevMode;
|
|
7
8
|
}
|
|
9
|
+
export function setDevtoolsEnabled(enabled, options) {
|
|
10
|
+
setEnabled(enabled, options);
|
|
11
|
+
}
|
|
12
|
+
export function isDevtoolsEnabled() {
|
|
13
|
+
return isEnabled();
|
|
14
|
+
}
|
|
15
|
+
export function configureDevtools(options) {
|
|
16
|
+
configure(options);
|
|
17
|
+
}
|
|
18
|
+
export function getDevtoolsSnapshot() {
|
|
19
|
+
return getSnapshot();
|
|
20
|
+
}
|
|
21
|
+
export function onDevtoolsEvent(listener) {
|
|
22
|
+
return subscribe(listener);
|
|
23
|
+
}
|
|
24
|
+
export function resetDevtools() {
|
|
25
|
+
reset();
|
|
26
|
+
}
|
|
8
27
|
/**
|
|
9
|
-
* Initialize dev tools
|
|
28
|
+
* Initialize dev tools runtime bridge for graph inspection.
|
|
10
29
|
* Returns a promise for future async initialization support.
|
|
11
30
|
*/
|
|
12
|
-
export async function initDevTools() {
|
|
31
|
+
export async function initDevTools(options = {}) {
|
|
13
32
|
setDevMode(true);
|
|
33
|
+
setEnabled(true, {
|
|
34
|
+
exposeGlobalHook: options.exposeGlobalHook ?? true,
|
|
35
|
+
dispatchEvents: options.dispatchEvents ?? true,
|
|
36
|
+
maxEvents: options.maxEvents,
|
|
37
|
+
});
|
|
14
38
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type DevtoolsNodeType = "scope" | "signal" | "computed" | "effect" | "effectAsync";
|
|
2
|
+
export type DevtoolsEdgeKind = "dependency" | "ownership";
|
|
3
|
+
export interface DevtoolsNode {
|
|
4
|
+
id: number;
|
|
5
|
+
type: DevtoolsNodeType;
|
|
6
|
+
label: string;
|
|
7
|
+
disposed: boolean;
|
|
8
|
+
scopeId: number | null;
|
|
9
|
+
parentScopeId: number | null;
|
|
10
|
+
reads: number;
|
|
11
|
+
writes: number;
|
|
12
|
+
runs: number;
|
|
13
|
+
lastValue: string;
|
|
14
|
+
lastRunAt: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DevtoolsEdge {
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
kind: DevtoolsEdgeKind;
|
|
21
|
+
}
|
|
22
|
+
export interface DevtoolsEvent {
|
|
23
|
+
type: string;
|
|
24
|
+
at: number;
|
|
25
|
+
payload: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface DevtoolsSnapshot {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
nodes: DevtoolsNode[];
|
|
30
|
+
edges: DevtoolsEdge[];
|
|
31
|
+
events: DevtoolsEvent[];
|
|
32
|
+
}
|
|
33
|
+
export interface DevtoolsRuntimeOptions {
|
|
34
|
+
maxEvents?: number;
|
|
35
|
+
exposeGlobalHook?: boolean;
|
|
36
|
+
dispatchEvents?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface DevtoolsHighlightOptions {
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function configure(options?: DevtoolsRuntimeOptions): void;
|
|
42
|
+
export declare function setEnabled(next: boolean, options?: DevtoolsRuntimeOptions): void;
|
|
43
|
+
export declare function isEnabled(): boolean;
|
|
44
|
+
export declare function reset(): void;
|
|
45
|
+
export declare function subscribe(listener: (event: DevtoolsEvent) => void): () => void;
|
|
46
|
+
export declare function getSnapshot(): DevtoolsSnapshot;
|
|
47
|
+
export declare function registerScope(scopeRef: object, parentScopeRef: object | null): void;
|
|
48
|
+
export declare function withDevtoolsDomTarget<T>(element: Element | null, fn: () => T): T;
|
|
49
|
+
export declare function linkScopeToDom(scopeRef: object, element: Element, label?: string): void;
|
|
50
|
+
export declare function disposeScope(scopeRef: object): void;
|
|
51
|
+
export declare function registerSignal(signalRef: object, type: "signal" | "computed", options?: {
|
|
52
|
+
scopeRef?: object | null;
|
|
53
|
+
initialValue?: unknown;
|
|
54
|
+
}): void;
|
|
55
|
+
export declare function registerEffect(effectRef: object, type: "effect" | "effectAsync", scopeRef: object | null): void;
|
|
56
|
+
export declare function aliasEffectToNode(effectRef: object, targetRef: object): void;
|
|
57
|
+
export declare function linkSubscriberSetToSignal(subscriberSetRef: object, signalRef: object): void;
|
|
58
|
+
export declare function trackSignalRead(signalRef: object): void;
|
|
59
|
+
export declare function trackSignalWrite(signalRef: object, nextValue: unknown): void;
|
|
60
|
+
export declare function trackEffectRun(effectRef: object): void;
|
|
61
|
+
export declare function trackEffectDispose(effectRef: object): void;
|
|
62
|
+
export declare function trackDependency(signalRef: object, effectRef: object): void;
|
|
63
|
+
export declare function untrackDependencyBySet(subscriberSetRef: object, effectRef: object): void;
|
|
64
|
+
export declare function clearHighlights(): void;
|
|
65
|
+
export declare function highlightNode(nodeId: number, options?: DevtoolsHighlightOptions): boolean;
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
const DEFAULT_MAX_EVENTS = 500;
|
|
2
|
+
const GLOBAL_HOOK_KEY = "__DALILA_DEVTOOLS__";
|
|
3
|
+
const GLOBAL_EVENT_NAME = "dalila:devtools:event";
|
|
4
|
+
let enabled = false;
|
|
5
|
+
let maxEvents = DEFAULT_MAX_EVENTS;
|
|
6
|
+
let exposeGlobalHook = false;
|
|
7
|
+
let dispatchEvents = false;
|
|
8
|
+
let nextId = 1;
|
|
9
|
+
let refToId = new WeakMap();
|
|
10
|
+
const nodes = new Map();
|
|
11
|
+
const dependencyEdges = new Map();
|
|
12
|
+
const ownershipEdges = new Map();
|
|
13
|
+
let subscriberSetToSignalId = new WeakMap();
|
|
14
|
+
let effectAliasToNodeId = new WeakMap();
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
let events = [];
|
|
17
|
+
const nodeDomTargets = new Map();
|
|
18
|
+
const highlightTimers = new WeakMap();
|
|
19
|
+
let currentDomTarget = null;
|
|
20
|
+
const HIGHLIGHT_ATTR = "data-dalila-devtools-highlight";
|
|
21
|
+
const HIGHLIGHT_LABEL_ATTR = "data-dalila-devtools-label";
|
|
22
|
+
const HIGHLIGHT_STYLE_ID = "dalila-devtools-highlight-style";
|
|
23
|
+
function canUseGlobalDispatch() {
|
|
24
|
+
return typeof globalThis.dispatchEvent === "function";
|
|
25
|
+
}
|
|
26
|
+
function canUseDOM() {
|
|
27
|
+
return typeof document !== "undefined" && typeof Element !== "undefined";
|
|
28
|
+
}
|
|
29
|
+
function clearHighlightOnElement(element) {
|
|
30
|
+
const timer = highlightTimers.get(element);
|
|
31
|
+
if (timer !== undefined && typeof globalThis.clearTimeout === "function") {
|
|
32
|
+
globalThis.clearTimeout(timer);
|
|
33
|
+
highlightTimers.delete(element);
|
|
34
|
+
}
|
|
35
|
+
element.removeAttribute(HIGHLIGHT_ATTR);
|
|
36
|
+
element.removeAttribute(HIGHLIGHT_LABEL_ATTR);
|
|
37
|
+
}
|
|
38
|
+
function ensureHighlightStyle() {
|
|
39
|
+
if (!canUseDOM())
|
|
40
|
+
return;
|
|
41
|
+
if (document.getElementById(HIGHLIGHT_STYLE_ID))
|
|
42
|
+
return;
|
|
43
|
+
const style = document.createElement("style");
|
|
44
|
+
style.id = HIGHLIGHT_STYLE_ID;
|
|
45
|
+
style.textContent = `
|
|
46
|
+
[${HIGHLIGHT_ATTR}="1"] {
|
|
47
|
+
outline: 2px solid #0f6d67 !important;
|
|
48
|
+
outline-offset: 2px !important;
|
|
49
|
+
box-shadow: 0 0 0 3px rgba(15, 109, 103, 0.22) !important;
|
|
50
|
+
transition: outline-color 120ms ease, box-shadow 120ms ease;
|
|
51
|
+
}`;
|
|
52
|
+
(document.head || document.documentElement).append(style);
|
|
53
|
+
}
|
|
54
|
+
function resolveNodeDomTargets(nodeId) {
|
|
55
|
+
const direct = nodeDomTargets.get(nodeId);
|
|
56
|
+
if (direct && direct.size > 0)
|
|
57
|
+
return Array.from(direct);
|
|
58
|
+
const node = nodes.get(nodeId);
|
|
59
|
+
if (!node)
|
|
60
|
+
return [];
|
|
61
|
+
if (node.type !== "scope" && node.scopeId !== null) {
|
|
62
|
+
const scopeTargets = nodeDomTargets.get(node.scopeId);
|
|
63
|
+
if (scopeTargets && scopeTargets.size > 0)
|
|
64
|
+
return Array.from(scopeTargets);
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
function addDomTarget(nodeId, element) {
|
|
69
|
+
let targets = nodeDomTargets.get(nodeId);
|
|
70
|
+
if (!targets) {
|
|
71
|
+
targets = new Set();
|
|
72
|
+
nodeDomTargets.set(nodeId, targets);
|
|
73
|
+
}
|
|
74
|
+
targets.add(element);
|
|
75
|
+
}
|
|
76
|
+
function previewValue(value) {
|
|
77
|
+
const type = typeof value;
|
|
78
|
+
if (type === "string") {
|
|
79
|
+
const quoted = JSON.stringify(value);
|
|
80
|
+
return quoted.length > 120 ? `${quoted.slice(0, 117)}...` : quoted;
|
|
81
|
+
}
|
|
82
|
+
if (value === null ||
|
|
83
|
+
type === "number" ||
|
|
84
|
+
type === "boolean" ||
|
|
85
|
+
type === "undefined" ||
|
|
86
|
+
type === "bigint" ||
|
|
87
|
+
type === "symbol") {
|
|
88
|
+
return String(value);
|
|
89
|
+
}
|
|
90
|
+
if (type === "function") {
|
|
91
|
+
const fn = value;
|
|
92
|
+
return `[Function ${fn.name || "anonymous"}]`;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const json = JSON.stringify(value);
|
|
96
|
+
if (json === undefined)
|
|
97
|
+
return Object.prototype.toString.call(value);
|
|
98
|
+
return json.length > 120 ? `${json.slice(0, 117)}...` : json;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return Object.prototype.toString.call(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function getNodeId(ref) {
|
|
105
|
+
const alias = effectAliasToNodeId.get(ref);
|
|
106
|
+
if (alias)
|
|
107
|
+
return alias;
|
|
108
|
+
const existing = refToId.get(ref);
|
|
109
|
+
if (existing)
|
|
110
|
+
return existing;
|
|
111
|
+
const id = nextId++;
|
|
112
|
+
refToId.set(ref, id);
|
|
113
|
+
return id;
|
|
114
|
+
}
|
|
115
|
+
function edgeKey(from, to, kind) {
|
|
116
|
+
return `${kind}:${from}->${to}`;
|
|
117
|
+
}
|
|
118
|
+
function emit(type, payload) {
|
|
119
|
+
if (!enabled)
|
|
120
|
+
return;
|
|
121
|
+
const event = {
|
|
122
|
+
type,
|
|
123
|
+
at: Date.now(),
|
|
124
|
+
payload,
|
|
125
|
+
};
|
|
126
|
+
events.push(event);
|
|
127
|
+
if (events.length > maxEvents) {
|
|
128
|
+
events = events.slice(events.length - maxEvents);
|
|
129
|
+
}
|
|
130
|
+
for (const listener of listeners) {
|
|
131
|
+
try {
|
|
132
|
+
listener(event);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error("[Dalila] Devtools listener threw:", error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (dispatchEvents && canUseGlobalDispatch() && typeof CustomEvent !== "undefined") {
|
|
139
|
+
try {
|
|
140
|
+
globalThis.dispatchEvent(new CustomEvent(GLOBAL_EVENT_NAME, {
|
|
141
|
+
detail: event,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ignore environments without full event support.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function createNode(ref, type, label, options) {
|
|
150
|
+
const id = getNodeId(ref);
|
|
151
|
+
if (nodes.has(id))
|
|
152
|
+
return id;
|
|
153
|
+
const scopeId = options?.scopeRef ? getNodeId(options.scopeRef) : null;
|
|
154
|
+
const parentScopeId = options?.parentScopeRef ? getNodeId(options.parentScopeRef) : null;
|
|
155
|
+
const registeredScopeId = scopeId !== null && nodes.has(scopeId) ? scopeId : null;
|
|
156
|
+
const registeredParentScopeId = parentScopeId !== null && nodes.has(parentScopeId) ? parentScopeId : null;
|
|
157
|
+
nodes.set(id, {
|
|
158
|
+
id,
|
|
159
|
+
type,
|
|
160
|
+
label,
|
|
161
|
+
disposed: false,
|
|
162
|
+
scopeId: registeredScopeId,
|
|
163
|
+
parentScopeId: registeredParentScopeId,
|
|
164
|
+
reads: 0,
|
|
165
|
+
writes: 0,
|
|
166
|
+
runs: 0,
|
|
167
|
+
lastValue: options && "initialValue" in options ? previewValue(options.initialValue) : "",
|
|
168
|
+
lastRunAt: 0,
|
|
169
|
+
createdAt: Date.now(),
|
|
170
|
+
});
|
|
171
|
+
if (registeredScopeId !== null) {
|
|
172
|
+
addOwnershipEdge(registeredScopeId, id);
|
|
173
|
+
}
|
|
174
|
+
if (type === "scope" && registeredParentScopeId !== null) {
|
|
175
|
+
addOwnershipEdge(registeredParentScopeId, id);
|
|
176
|
+
}
|
|
177
|
+
emit("node.create", { id, type, label, scopeId: registeredScopeId, parentScopeId: registeredParentScopeId });
|
|
178
|
+
return id;
|
|
179
|
+
}
|
|
180
|
+
function addOwnershipEdge(from, to) {
|
|
181
|
+
if (!nodes.has(from) || !nodes.has(to))
|
|
182
|
+
return;
|
|
183
|
+
const key = edgeKey(from, to, "ownership");
|
|
184
|
+
if (ownershipEdges.has(key))
|
|
185
|
+
return;
|
|
186
|
+
const edge = { from, to, kind: "ownership" };
|
|
187
|
+
ownershipEdges.set(key, edge);
|
|
188
|
+
emit("edge.add", { from: edge.from, to: edge.to, kind: edge.kind });
|
|
189
|
+
}
|
|
190
|
+
function upsertDependencyEdge(from, to) {
|
|
191
|
+
const key = edgeKey(from, to, "dependency");
|
|
192
|
+
if (dependencyEdges.has(key))
|
|
193
|
+
return;
|
|
194
|
+
const edge = { from, to, kind: "dependency" };
|
|
195
|
+
dependencyEdges.set(key, edge);
|
|
196
|
+
emit("edge.add", { from: edge.from, to: edge.to, kind: edge.kind });
|
|
197
|
+
}
|
|
198
|
+
function removeDependencyEdge(from, to) {
|
|
199
|
+
const key = edgeKey(from, to, "dependency");
|
|
200
|
+
const existing = dependencyEdges.get(key);
|
|
201
|
+
if (!existing)
|
|
202
|
+
return;
|
|
203
|
+
dependencyEdges.delete(key);
|
|
204
|
+
emit("edge.remove", { from: existing.from, to: existing.to, kind: existing.kind });
|
|
205
|
+
}
|
|
206
|
+
function markDisposed(ref) {
|
|
207
|
+
if (!enabled)
|
|
208
|
+
return;
|
|
209
|
+
const node = nodes.get(getNodeId(ref));
|
|
210
|
+
if (!node || node.disposed)
|
|
211
|
+
return;
|
|
212
|
+
node.disposed = true;
|
|
213
|
+
emit("node.dispose", { id: node.id, type: node.type });
|
|
214
|
+
}
|
|
215
|
+
function markRead(ref) {
|
|
216
|
+
if (!enabled)
|
|
217
|
+
return;
|
|
218
|
+
const node = nodes.get(getNodeId(ref));
|
|
219
|
+
if (!node)
|
|
220
|
+
return;
|
|
221
|
+
node.reads += 1;
|
|
222
|
+
}
|
|
223
|
+
function markWrite(ref, nextValue) {
|
|
224
|
+
if (!enabled)
|
|
225
|
+
return;
|
|
226
|
+
const node = nodes.get(getNodeId(ref));
|
|
227
|
+
if (!node)
|
|
228
|
+
return;
|
|
229
|
+
node.writes += 1;
|
|
230
|
+
node.lastValue = previewValue(nextValue);
|
|
231
|
+
emit("signal.write", { id: node.id, type: node.type, nextValue: node.lastValue });
|
|
232
|
+
}
|
|
233
|
+
function markRun(ref) {
|
|
234
|
+
if (!enabled)
|
|
235
|
+
return;
|
|
236
|
+
const node = nodes.get(getNodeId(ref));
|
|
237
|
+
if (!node)
|
|
238
|
+
return;
|
|
239
|
+
node.runs += 1;
|
|
240
|
+
node.lastRunAt = Date.now();
|
|
241
|
+
}
|
|
242
|
+
function installGlobalHook() {
|
|
243
|
+
if (!exposeGlobalHook)
|
|
244
|
+
return;
|
|
245
|
+
const host = globalThis;
|
|
246
|
+
if (host[GLOBAL_HOOK_KEY])
|
|
247
|
+
return;
|
|
248
|
+
host[GLOBAL_HOOK_KEY] = {
|
|
249
|
+
version: 1,
|
|
250
|
+
getSnapshot,
|
|
251
|
+
subscribe,
|
|
252
|
+
reset,
|
|
253
|
+
setEnabled,
|
|
254
|
+
configure,
|
|
255
|
+
highlightNode,
|
|
256
|
+
clearHighlights,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
export function configure(options = {}) {
|
|
260
|
+
if (typeof options.maxEvents === "number" && Number.isFinite(options.maxEvents) && options.maxEvents > 0) {
|
|
261
|
+
maxEvents = Math.floor(options.maxEvents);
|
|
262
|
+
if (events.length > maxEvents) {
|
|
263
|
+
events = events.slice(events.length - maxEvents);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (typeof options.exposeGlobalHook === "boolean") {
|
|
267
|
+
exposeGlobalHook = options.exposeGlobalHook;
|
|
268
|
+
}
|
|
269
|
+
if (typeof options.dispatchEvents === "boolean") {
|
|
270
|
+
dispatchEvents = options.dispatchEvents;
|
|
271
|
+
}
|
|
272
|
+
if (enabled)
|
|
273
|
+
installGlobalHook();
|
|
274
|
+
}
|
|
275
|
+
export function setEnabled(next, options) {
|
|
276
|
+
if (options)
|
|
277
|
+
configure(options);
|
|
278
|
+
if (next) {
|
|
279
|
+
enabled = true;
|
|
280
|
+
installGlobalHook();
|
|
281
|
+
emit("devtools.enabled", { enabled: true });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (enabled) {
|
|
285
|
+
emit("devtools.enabled", { enabled: false });
|
|
286
|
+
}
|
|
287
|
+
enabled = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export function isEnabled() {
|
|
291
|
+
return enabled;
|
|
292
|
+
}
|
|
293
|
+
export function reset() {
|
|
294
|
+
// Identity maps must be reset together with graph state. Otherwise existing
|
|
295
|
+
// refs keep stale ids and can generate edges pointing to missing nodes.
|
|
296
|
+
nextId = 1;
|
|
297
|
+
refToId = new WeakMap();
|
|
298
|
+
subscriberSetToSignalId = new WeakMap();
|
|
299
|
+
effectAliasToNodeId = new WeakMap();
|
|
300
|
+
dependencyEdges.clear();
|
|
301
|
+
ownershipEdges.clear();
|
|
302
|
+
nodes.clear();
|
|
303
|
+
nodeDomTargets.clear();
|
|
304
|
+
currentDomTarget = null;
|
|
305
|
+
events = [];
|
|
306
|
+
if (enabled) {
|
|
307
|
+
emit("devtools.reset", {});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
export function subscribe(listener) {
|
|
311
|
+
listeners.add(listener);
|
|
312
|
+
return () => listeners.delete(listener);
|
|
313
|
+
}
|
|
314
|
+
export function getSnapshot() {
|
|
315
|
+
return {
|
|
316
|
+
enabled,
|
|
317
|
+
nodes: Array.from(nodes.values()).map((node) => ({ ...node })),
|
|
318
|
+
edges: [...Array.from(ownershipEdges.values()), ...Array.from(dependencyEdges.values())],
|
|
319
|
+
events: events.map((event) => ({ ...event, payload: { ...event.payload } })),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export function registerScope(scopeRef, parentScopeRef) {
|
|
323
|
+
if (!enabled)
|
|
324
|
+
return;
|
|
325
|
+
createNode(scopeRef, "scope", "scope", {
|
|
326
|
+
parentScopeRef,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
export function withDevtoolsDomTarget(element, fn) {
|
|
330
|
+
if (!element || !canUseDOM())
|
|
331
|
+
return fn();
|
|
332
|
+
if (!(element instanceof Element))
|
|
333
|
+
return fn();
|
|
334
|
+
const prev = currentDomTarget;
|
|
335
|
+
currentDomTarget = element;
|
|
336
|
+
try {
|
|
337
|
+
return fn();
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
currentDomTarget = prev;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export function linkScopeToDom(scopeRef, element, label) {
|
|
344
|
+
if (!enabled || !canUseDOM())
|
|
345
|
+
return;
|
|
346
|
+
if (!(element instanceof Element))
|
|
347
|
+
return;
|
|
348
|
+
const scopeId = getNodeId(scopeRef);
|
|
349
|
+
const node = nodes.get(scopeId);
|
|
350
|
+
if (!node)
|
|
351
|
+
return;
|
|
352
|
+
addDomTarget(scopeId, element);
|
|
353
|
+
if (label && node.label === "scope") {
|
|
354
|
+
node.label = label;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
export function disposeScope(scopeRef) {
|
|
358
|
+
markDisposed(scopeRef);
|
|
359
|
+
}
|
|
360
|
+
export function registerSignal(signalRef, type, options) {
|
|
361
|
+
if (!enabled)
|
|
362
|
+
return;
|
|
363
|
+
createNode(signalRef, type, type, {
|
|
364
|
+
scopeRef: options?.scopeRef ?? null,
|
|
365
|
+
initialValue: options?.initialValue,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
export function registerEffect(effectRef, type, scopeRef) {
|
|
369
|
+
if (!enabled)
|
|
370
|
+
return;
|
|
371
|
+
const id = createNode(effectRef, type, type, {
|
|
372
|
+
scopeRef,
|
|
373
|
+
});
|
|
374
|
+
if (currentDomTarget) {
|
|
375
|
+
addDomTarget(id, currentDomTarget);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
export function aliasEffectToNode(effectRef, targetRef) {
|
|
379
|
+
if (!enabled)
|
|
380
|
+
return;
|
|
381
|
+
const targetId = getNodeId(targetRef);
|
|
382
|
+
effectAliasToNodeId.set(effectRef, targetId);
|
|
383
|
+
}
|
|
384
|
+
export function linkSubscriberSetToSignal(subscriberSetRef, signalRef) {
|
|
385
|
+
if (!enabled)
|
|
386
|
+
return;
|
|
387
|
+
subscriberSetToSignalId.set(subscriberSetRef, getNodeId(signalRef));
|
|
388
|
+
}
|
|
389
|
+
export function trackSignalRead(signalRef) {
|
|
390
|
+
markRead(signalRef);
|
|
391
|
+
}
|
|
392
|
+
export function trackSignalWrite(signalRef, nextValue) {
|
|
393
|
+
markWrite(signalRef, nextValue);
|
|
394
|
+
}
|
|
395
|
+
export function trackEffectRun(effectRef) {
|
|
396
|
+
markRun(effectRef);
|
|
397
|
+
}
|
|
398
|
+
export function trackEffectDispose(effectRef) {
|
|
399
|
+
markDisposed(effectRef);
|
|
400
|
+
}
|
|
401
|
+
export function trackDependency(signalRef, effectRef) {
|
|
402
|
+
if (!enabled)
|
|
403
|
+
return;
|
|
404
|
+
const from = getNodeId(signalRef);
|
|
405
|
+
const to = getNodeId(effectRef);
|
|
406
|
+
if (!nodes.has(from) || !nodes.has(to))
|
|
407
|
+
return;
|
|
408
|
+
upsertDependencyEdge(from, to);
|
|
409
|
+
}
|
|
410
|
+
export function untrackDependencyBySet(subscriberSetRef, effectRef) {
|
|
411
|
+
if (!enabled)
|
|
412
|
+
return;
|
|
413
|
+
const from = subscriberSetToSignalId.get(subscriberSetRef);
|
|
414
|
+
if (!from)
|
|
415
|
+
return;
|
|
416
|
+
const to = getNodeId(effectRef);
|
|
417
|
+
removeDependencyEdge(from, to);
|
|
418
|
+
}
|
|
419
|
+
export function clearHighlights() {
|
|
420
|
+
if (!canUseDOM())
|
|
421
|
+
return;
|
|
422
|
+
for (const targets of nodeDomTargets.values()) {
|
|
423
|
+
for (const element of targets) {
|
|
424
|
+
clearHighlightOnElement(element);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
export function highlightNode(nodeId, options = {}) {
|
|
429
|
+
if (!enabled || !canUseDOM())
|
|
430
|
+
return false;
|
|
431
|
+
const targets = resolveNodeDomTargets(nodeId);
|
|
432
|
+
if (targets.length === 0)
|
|
433
|
+
return false;
|
|
434
|
+
ensureHighlightStyle();
|
|
435
|
+
const node = nodes.get(nodeId);
|
|
436
|
+
const label = node ? `#${node.id} ${node.label || node.type}` : `#${nodeId}`;
|
|
437
|
+
const durationMs = typeof options.durationMs === "number" && Number.isFinite(options.durationMs) && options.durationMs > 0
|
|
438
|
+
? Math.floor(options.durationMs)
|
|
439
|
+
: 650;
|
|
440
|
+
for (const element of targets) {
|
|
441
|
+
clearHighlightOnElement(element);
|
|
442
|
+
element.setAttribute(HIGHLIGHT_ATTR, "1");
|
|
443
|
+
element.setAttribute(HIGHLIGHT_LABEL_ATTR, label);
|
|
444
|
+
if (typeof globalThis.setTimeout === "function") {
|
|
445
|
+
const timer = globalThis.setTimeout(() => {
|
|
446
|
+
clearHighlightOnElement(element);
|
|
447
|
+
}, durationMs);
|
|
448
|
+
highlightTimers.set(element, timer);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
package/dist/core/for.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
interface DisposableFragment extends DocumentFragment {
|
|
2
2
|
dispose(): void;
|
|
3
3
|
}
|
|
4
|
+
export interface VirtualRangeInput {
|
|
5
|
+
itemCount: number;
|
|
6
|
+
itemHeight: number;
|
|
7
|
+
scrollTop: number;
|
|
8
|
+
viewportHeight: number;
|
|
9
|
+
overscan?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface VirtualRange {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
topOffset: number;
|
|
15
|
+
bottomOffset: number;
|
|
16
|
+
totalHeight: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compute the visible range for a fixed-height virtualized list.
|
|
20
|
+
*
|
|
21
|
+
* `start`/`end` use the [start, end) convention.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeVirtualRange(input: VirtualRangeInput): VirtualRange;
|
|
4
24
|
/**
|
|
5
25
|
* Low-level keyed list rendering with fine-grained reactivity.
|
|
6
26
|
*
|
package/dist/core/for.js
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { effect, signal } from './signal.js';
|
|
2
2
|
import { isInDevMode } from './dev.js';
|
|
3
3
|
import { createScope, withScope, getCurrentScope } from './scope.js';
|
|
4
|
+
function clamp(value, min, max) {
|
|
5
|
+
return Math.max(min, Math.min(max, value));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compute the visible range for a fixed-height virtualized list.
|
|
9
|
+
*
|
|
10
|
+
* `start`/`end` use the [start, end) convention.
|
|
11
|
+
*/
|
|
12
|
+
export function computeVirtualRange(input) {
|
|
13
|
+
const itemCount = Number.isFinite(input.itemCount) ? Math.max(0, Math.floor(input.itemCount)) : 0;
|
|
14
|
+
const itemHeight = Number.isFinite(input.itemHeight) ? Math.max(1, input.itemHeight) : 1;
|
|
15
|
+
const scrollTop = Number.isFinite(input.scrollTop) ? Math.max(0, input.scrollTop) : 0;
|
|
16
|
+
const viewportHeight = Number.isFinite(input.viewportHeight)
|
|
17
|
+
? Math.max(0, input.viewportHeight)
|
|
18
|
+
: 0;
|
|
19
|
+
const overscan = Number.isFinite(input.overscan) ? Math.max(0, Math.floor(input.overscan ?? 0)) : 0;
|
|
20
|
+
const totalHeight = itemCount * itemHeight;
|
|
21
|
+
if (itemCount === 0) {
|
|
22
|
+
return {
|
|
23
|
+
start: 0,
|
|
24
|
+
end: 0,
|
|
25
|
+
topOffset: 0,
|
|
26
|
+
bottomOffset: 0,
|
|
27
|
+
totalHeight,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const visibleStart = Math.floor(scrollTop / itemHeight);
|
|
31
|
+
const visibleEnd = Math.ceil((scrollTop + viewportHeight) / itemHeight);
|
|
32
|
+
const start = clamp(visibleStart - overscan, 0, itemCount);
|
|
33
|
+
const end = clamp(visibleEnd + overscan, start, itemCount);
|
|
34
|
+
const topOffset = start * itemHeight;
|
|
35
|
+
const bottomOffset = Math.max(0, totalHeight - (end * itemHeight));
|
|
36
|
+
return {
|
|
37
|
+
start,
|
|
38
|
+
end,
|
|
39
|
+
topOffset,
|
|
40
|
+
bottomOffset,
|
|
41
|
+
totalHeight,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
4
44
|
const autoDisposeByDocument = new WeakMap();
|
|
5
45
|
const getMutationObserverCtor = (doc) => {
|
|
6
46
|
if (doc.defaultView?.MutationObserver)
|