@epoch32/visage 0.0.0-stage → 1.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.
Files changed (135) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/src/api/index.js +4002 -0
  4. package/dist/src/charts/index.js +2717 -0
  5. package/dist/src/devtools/index.js +4677 -0
  6. package/dist/src/dom/index.js +1550 -0
  7. package/dist/src/form/index.js +1415 -0
  8. package/dist/src/index.js +11826 -0
  9. package/dist/src/jsx-runtime/index.js +1630 -0
  10. package/dist/src/query/index.js +2422 -0
  11. package/dist/src/router/index.js +1856 -0
  12. package/dist/src/rpc/index.js +3484 -0
  13. package/dist/src/server/index.js +1173 -0
  14. package/dist/src/signals/index.js +1077 -0
  15. package/dist/src/style/index.js +2340 -0
  16. package/dist/src/template/index.js +1149 -0
  17. package/dist/src/ui/index.js +4155 -0
  18. package/dist/src/virtual/index.js +1955 -0
  19. package/dist/src/vite/index.js +984 -0
  20. package/package.json +140 -4
  21. package/src/api/client.ts +371 -0
  22. package/src/api/file-router.ts +183 -0
  23. package/src/api/handler.ts +326 -0
  24. package/src/api/index.ts +71 -0
  25. package/src/api/middleware.ts +54 -0
  26. package/src/api/pragma.ts +155 -0
  27. package/src/api/procedure.ts +172 -0
  28. package/src/api/types.ts +249 -0
  29. package/src/charts/Area.tsx +111 -0
  30. package/src/charts/Axis.tsx +126 -0
  31. package/src/charts/Bar.tsx +86 -0
  32. package/src/charts/CartesianGrid.tsx +72 -0
  33. package/src/charts/Chart.tsx +269 -0
  34. package/src/charts/Cursor.tsx +42 -0
  35. package/src/charts/Legend.tsx +56 -0
  36. package/src/charts/Line.tsx +106 -0
  37. package/src/charts/Pie.tsx +88 -0
  38. package/src/charts/Tooltip.tsx +127 -0
  39. package/src/charts/context.ts +30 -0
  40. package/src/charts/index.ts +14 -0
  41. package/src/charts/path.ts +155 -0
  42. package/src/charts/scale.ts +133 -0
  43. package/src/charts/types.ts +50 -0
  44. package/src/devtools/DomTab.tsx +134 -0
  45. package/src/devtools/GraphInspector.tsx +196 -0
  46. package/src/devtools/GraphTab.tsx +315 -0
  47. package/src/devtools/QueryTab.tsx +200 -0
  48. package/src/devtools/RpcTab.tsx +190 -0
  49. package/src/devtools/SignalsTab.tsx +129 -0
  50. package/src/devtools/component.tsx +243 -0
  51. package/src/devtools/highlighter.ts +134 -0
  52. package/src/devtools/hub.ts +175 -0
  53. package/src/devtools/index.ts +20 -0
  54. package/src/devtools/styles.ts +313 -0
  55. package/src/devtools/types.ts +45 -0
  56. package/src/dom/async.ts +232 -0
  57. package/src/dom/control-flow.ts +353 -0
  58. package/src/dom/index.ts +32 -0
  59. package/src/dom/lazy.ts +82 -0
  60. package/src/dom/render.ts +286 -0
  61. package/src/dom/resource.ts +120 -0
  62. package/src/form/form.ts +218 -0
  63. package/src/form/index.ts +17 -0
  64. package/src/form/schema.ts +299 -0
  65. package/src/index.ts +413 -0
  66. package/src/jsx-dev-runtime/index.ts +1 -0
  67. package/src/jsx-runtime/index.ts +112 -0
  68. package/src/jsx-runtime/jsx.d.ts +278 -0
  69. package/src/query/create-mutation.ts +64 -0
  70. package/src/query/create-query.ts +426 -0
  71. package/src/query/focus-manager.ts +68 -0
  72. package/src/query/index.ts +63 -0
  73. package/src/query/mutation-cache.ts +138 -0
  74. package/src/query/online-manager.ts +67 -0
  75. package/src/query/query-cache.ts +369 -0
  76. package/src/query/query-client.ts +248 -0
  77. package/src/query/types.ts +221 -0
  78. package/src/query/utils.ts +132 -0
  79. package/src/router/index.ts +19 -0
  80. package/src/router/router.tsx +352 -0
  81. package/src/rpc/client.ts +317 -0
  82. package/src/rpc/error.ts +70 -0
  83. package/src/rpc/index.ts +52 -0
  84. package/src/rpc/procedure.ts +88 -0
  85. package/src/rpc/router.ts +105 -0
  86. package/src/rpc/server.ts +511 -0
  87. package/src/rpc/types.ts +328 -0
  88. package/src/server/hydrate.ts +21 -0
  89. package/src/server/index.ts +10 -0
  90. package/src/server/render-to-stream.ts +31 -0
  91. package/src/server/render-to-string.ts +125 -0
  92. package/src/signals/computed.ts +217 -0
  93. package/src/signals/context.ts +89 -0
  94. package/src/signals/effect.ts +161 -0
  95. package/src/signals/index.ts +57 -0
  96. package/src/signals/scope.ts +132 -0
  97. package/src/signals/signal.ts +203 -0
  98. package/src/signals/store.ts +163 -0
  99. package/src/signals/system.ts +124 -0
  100. package/src/signals/types.ts +64 -0
  101. package/src/style/component.tsx +122 -0
  102. package/src/style/create-style.ts +229 -0
  103. package/src/style/cx.ts +75 -0
  104. package/src/style/hash.ts +19 -0
  105. package/src/style/index.ts +8 -0
  106. package/src/style/layout.tsx +406 -0
  107. package/src/style/manager.ts +87 -0
  108. package/src/style/parser.ts +257 -0
  109. package/src/style/scale.ts +237 -0
  110. package/src/template/index.ts +4 -0
  111. package/src/template/template.ts +126 -0
  112. package/src/ui/accordion.tsx +286 -0
  113. package/src/ui/command-dialog.tsx +49 -0
  114. package/src/ui/command.tsx +400 -0
  115. package/src/ui/data-table.tsx +322 -0
  116. package/src/ui/dialog.tsx +362 -0
  117. package/src/ui/dropdown-menu.tsx +381 -0
  118. package/src/ui/icon.tsx +69 -0
  119. package/src/ui/index.ts +138 -0
  120. package/src/ui/layout.tsx +13 -0
  121. package/src/ui/popover.tsx +249 -0
  122. package/src/ui/switch.tsx +114 -0
  123. package/src/ui/tabs.tsx +297 -0
  124. package/src/ui/toast.tsx +280 -0
  125. package/src/ui/tooltip.tsx +262 -0
  126. package/src/ui/utils.ts +110 -0
  127. package/src/virtual/index.ts +12 -0
  128. package/src/virtual/virtual-list.tsx +185 -0
  129. package/src/virtual/virtualizer.ts +274 -0
  130. package/src/vite/hmr.ts +38 -0
  131. package/src/vite/index.ts +3 -0
  132. package/src/vite/lucide.ts +124 -0
  133. package/src/vite/plugin.ts +102 -0
  134. package/src/vite/rpc-middleware.ts +82 -0
  135. package/src/vite/types.ts +65 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Visage Core Writable Signal & State Primitives
3
+ */
4
+ import { batch, currentScope, currentSubscriber, warnedCyclicSubscribers } from "./system";
5
+ import {
6
+ type IDependency,
7
+ type ISubscriber,
8
+ type ReadonlySignal,
9
+ SIGNAL_BRAND,
10
+ type SignalOptions,
11
+ SubscriberState,
12
+ type WritableSignal
13
+ } from "./types";
14
+
15
+ export type { SignalOptions };
16
+
17
+ /**
18
+ * Core Writable Signal implementation
19
+ */
20
+ export class Signal<T> implements IDependency {
21
+ [SIGNAL_BRAND] = true as const;
22
+ private _value: T;
23
+ private _subscribers: Set<ISubscriber> = new Set();
24
+ private _equals: (a: T, b: T) => boolean;
25
+ version = 0;
26
+
27
+ constructor(initialValue: T, equals?: (a: T, b: T) => boolean) {
28
+ this._value = initialValue;
29
+ this._equals = equals ?? Object.is;
30
+ }
31
+
32
+ get(): T {
33
+ if (currentSubscriber !== null && currentSubscriber.state !== SubscriberState.DISPOSED) {
34
+ this._subscribers.add(currentSubscriber);
35
+ currentSubscriber.sources.add(this);
36
+ currentSubscriber.sourceVersions.set(this, this.version);
37
+ }
38
+ return this._value;
39
+ }
40
+
41
+ set(newValue: T): void {
42
+ if (this._equals(this._value, newValue)) {
43
+ return;
44
+ }
45
+
46
+ if (currentSubscriber !== null && this._subscribers.has(currentSubscriber)) {
47
+ if (!warnedCyclicSubscribers.has(currentSubscriber)) {
48
+ warnedCyclicSubscribers.add(currentSubscriber);
49
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
50
+ console.warn(
51
+ `[Visage Reactivity] Cyclic update detected: An active subscriber modified a signal that it depends on. ` +
52
+ `To read a signal inside an effect/computed without creating a circular dependency, wrap the read in untrack():\n` +
53
+ ` untrack(() => signal())`
54
+ );
55
+ }
56
+ }
57
+ }
58
+
59
+ this._value = newValue;
60
+ this.version++;
61
+ batch(() => {
62
+ this.notify();
63
+ });
64
+ }
65
+
66
+ update(fn: (prev: T) => T): void {
67
+ this.set(fn(this._value));
68
+ }
69
+
70
+ get value(): T {
71
+ return this.get();
72
+ }
73
+
74
+ set value(val: T) {
75
+ this.set(val);
76
+ }
77
+
78
+ removeSubscriber(sub: ISubscriber): void {
79
+ this._subscribers.delete(sub);
80
+ }
81
+
82
+ private notify(): void {
83
+ if (this._subscribers.size === 0) return;
84
+
85
+ const subs = Array.from(this._subscribers);
86
+ for (const sub of subs) {
87
+ sub.markDirty(SubscriberState.DIRTY);
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Factory for creating writable, fine-grained reactive signals.
94
+ *
95
+ * Signals are the core primitive of Visage's reactivity. Reading a signal inside
96
+ * a component JSX body, `computed()`, or `effect()` automatically registers that
97
+ * computation as a dependent subscriber.
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * const count = signal(0);
102
+ *
103
+ * // Read value
104
+ * console.log(count()); // 0
105
+ * console.log(count.value); // 0
106
+ *
107
+ * // Write value
108
+ * count.set(1);
109
+ * count(2);
110
+ * count.update(prev => prev + 1);
111
+ *
112
+ * // In JSX
113
+ * <div>Current count: {count}</div>
114
+ * ```
115
+ *
116
+ * @param initialValue - The initial value stored in the signal
117
+ * @param optionsOrEquals - Custom equality comparison function or SignalOptions
118
+ * @returns A writable reactive signal function accessor
119
+ */
120
+ export function signal<T>(
121
+ initialValue: T,
122
+ optionsOrEquals?: ((a: T, b: T) => boolean) | SignalOptions<T>
123
+ ): WritableSignal<T> {
124
+ const equals = typeof optionsOrEquals === "function" ? optionsOrEquals : optionsOrEquals?.equals;
125
+ const isInternal = typeof optionsOrEquals === "object" ? optionsOrEquals?.internal : false;
126
+ const customName = typeof optionsOrEquals === "object" ? optionsOrEquals?.name : undefined;
127
+
128
+ const s = new Signal(initialValue, equals);
129
+
130
+ const accessor = ((...args: [value?: T]): T => {
131
+ if (args.length === 0) {
132
+ return s.get();
133
+ }
134
+ s.set(args[0] as T);
135
+ return args[0] as T;
136
+ }) as WritableSignal<T>;
137
+
138
+ Object.defineProperties(accessor, {
139
+ [SIGNAL_BRAND]: { value: true },
140
+ get: { value: () => s.get() },
141
+ set: { value: (v: T) => s.set(v) },
142
+ update: { value: (fn: (prev: T) => T) => s.update(fn) },
143
+ value: {
144
+ get: () => s.get(),
145
+ set: (v: T) => s.set(v)
146
+ }
147
+ });
148
+
149
+ if (!isInternal && typeof window !== "undefined" && (window as any).__VISAGE_DEVTOOLS__) {
150
+ const devtools = (window as any).__VISAGE_DEVTOOLS__;
151
+ const sigId = devtools.registerSignal({
152
+ name: customName,
153
+ type: "signal",
154
+ getValue: () => s.get(),
155
+ version: () => s.version,
156
+ subscribersCount: () => (s as any)._subscribers?.size ?? 0,
157
+ dependents: () =>
158
+ Array.from((s as any)._subscribers ?? [])
159
+ .map((sub: any) => sub._devId)
160
+ .filter(Boolean)
161
+ });
162
+ (s as any)._devId = sigId;
163
+ if (currentScope) {
164
+ currentScope.addCleanup(() => {
165
+ devtools.unregisterSignal(sigId);
166
+ });
167
+ }
168
+ }
169
+
170
+ return accessor;
171
+ }
172
+
173
+ /**
174
+ * Tuple-based signal constructor returning `[getter, setter]` (Solid / Preact Signals convention).
175
+ *
176
+ * @example
177
+ * ```tsx
178
+ * const [count, setCount] = createSignal(0);
179
+ *
180
+ * setCount(5);
181
+ * setCount(prev => prev + 1);
182
+ *
183
+ * <div>Count: {count}</div>
184
+ * ```
185
+ *
186
+ * @param initialValue - The initial value stored in the signal
187
+ * @param optionsOrEquals - Custom equality comparison function or SignalOptions
188
+ * @returns A tuple containing `[ReadonlySignal<T>, (value: T | ((prev: T) => T)) => void]`
189
+ */
190
+ export function createSignal<T>(
191
+ initialValue: T,
192
+ optionsOrEquals?: ((a: T, b: T) => boolean) | SignalOptions<T>
193
+ ): [ReadonlySignal<T>, (value: T | ((prev: T) => T)) => void] {
194
+ const s = signal(initialValue, optionsOrEquals);
195
+ const setter = (value: T | ((prev: T) => T)) => {
196
+ if (typeof value === "function") {
197
+ s.update(value as (prev: T) => T);
198
+ } else {
199
+ s.set(value);
200
+ }
201
+ };
202
+ return [s, setter];
203
+ }
@@ -0,0 +1,163 @@
1
+ import { Signal } from "./signal";
2
+ import { batch } from "./system";
3
+ import { isSignal } from "./types";
4
+
5
+ const STORE_RAW = Symbol("visage.store.raw");
6
+ const STORE_SIGNALS = Symbol("visage.store.signals");
7
+ const STORE_PROXY = Symbol("visage.store.proxy");
8
+
9
+ export type Store<T> = T;
10
+
11
+ function isObject(val: unknown): val is Record<string | symbol, any> {
12
+ return (
13
+ typeof val === "object" &&
14
+ val !== null &&
15
+ (typeof Node === "undefined" || !(val instanceof Node)) &&
16
+ !isSignal(val)
17
+ );
18
+ }
19
+
20
+ function getOrCreatePropertySignal(target: any, prop: string | symbol): Signal<any> {
21
+ let signalsMap: Map<string | symbol, Signal<any>> = target[STORE_SIGNALS];
22
+ if (!signalsMap) {
23
+ signalsMap = new Map();
24
+ Object.defineProperty(target, STORE_SIGNALS, {
25
+ value: signalsMap,
26
+ enumerable: false,
27
+ writable: true
28
+ });
29
+ }
30
+
31
+ let sig = signalsMap.get(prop);
32
+ if (!sig) {
33
+ sig = new Signal(target[prop]);
34
+ signalsMap.set(prop, sig);
35
+ }
36
+ return sig;
37
+ }
38
+
39
+ function createStoreProxy<T extends object>(target: T): T {
40
+ if (!isObject(target)) return target;
41
+ if ((target as any)[STORE_PROXY]) return (target as any)[STORE_PROXY];
42
+
43
+ const proxy = new Proxy(target, {
44
+ get(target, prop, receiver) {
45
+ if (prop === STORE_RAW) return target;
46
+ if (prop === STORE_PROXY) return proxy;
47
+
48
+ // Track property access
49
+ if (typeof prop === "string" || typeof prop === "symbol") {
50
+ const sig = getOrCreatePropertySignal(target, prop);
51
+ sig.get(); // Register dependency
52
+ }
53
+
54
+ const value = Reflect.get(target, prop, receiver);
55
+
56
+ // Lazily wrap nested objects in proxy
57
+ if (isObject(value)) {
58
+ return createStoreProxy(value);
59
+ }
60
+
61
+ return value;
62
+ },
63
+
64
+ set(target, prop, value, receiver) {
65
+ const prevValue = Reflect.get(target, prop, receiver);
66
+ if (Object.is(prevValue, value)) {
67
+ return true;
68
+ }
69
+
70
+ const success = Reflect.set(target, prop, value, receiver);
71
+ if (success) {
72
+ const sig = getOrCreatePropertySignal(target, prop);
73
+ sig.set(value);
74
+
75
+ // If array length or property keys changed, notify length/keys
76
+ if (Array.isArray(target) && prop !== "length") {
77
+ const lenSig = getOrCreatePropertySignal(target, "length");
78
+ lenSig.set(target.length);
79
+ }
80
+ }
81
+ return success;
82
+ },
83
+
84
+ deleteProperty(target, prop) {
85
+ const hadProp = Reflect.has(target, prop);
86
+ const success = Reflect.deleteProperty(target, prop);
87
+ if (hadProp && success) {
88
+ const sig = getOrCreatePropertySignal(target, prop);
89
+ sig.set(undefined);
90
+ }
91
+ return success;
92
+ }
93
+ });
94
+
95
+ Object.defineProperty(target, STORE_PROXY, {
96
+ value: proxy,
97
+ enumerable: false,
98
+ writable: true
99
+ });
100
+
101
+ return proxy;
102
+ }
103
+
104
+ /**
105
+ * Create a deep reactive proxy store with per-property fine-grained signal tracking.
106
+ *
107
+ * Nested objects and arrays are lazily wrapped in reactive proxies. Reading a property
108
+ * tracks that exact key, and updating a property only notifies computations observing that key.
109
+ *
110
+ * @example
111
+ * ```tsx
112
+ * const state = createStore({
113
+ * user: { name: "Alice", role: "admin" },
114
+ * items: ["Apple", "Banana"]
115
+ * });
116
+ *
117
+ * // Fine-grained read
118
+ * <div>User: {() => state.user.name}</div>
119
+ *
120
+ * // Fine-grained write (mutates only user.name subscribers)
121
+ * state.user.name = "Bob";
122
+ * state.items.push("Cherry");
123
+ * ```
124
+ *
125
+ * @param initialState - An object or array representing the initial state tree
126
+ * @returns A deeply reactive proxy store
127
+ */
128
+ export function createStore<T extends object>(initialState: T): Store<T> {
129
+ if (!isObject(initialState)) {
130
+ throw new Error("createStore requires an object or array as initial state");
131
+ }
132
+ return createStoreProxy(initialState);
133
+ }
134
+
135
+ /**
136
+ * Mutate a store inside a synchronous batch operation.
137
+ *
138
+ * @example
139
+ * ```tsx
140
+ * mutateStore(state, (draft) => {
141
+ * draft.user.name = "Charlie";
142
+ * draft.user.role = "user";
143
+ * });
144
+ * ```
145
+ */
146
+ export function mutateStore<T extends object>(store: Store<T>, mutator: (draft: T) => void): void {
147
+ batch(() => {
148
+ mutator(store);
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Unwrap a reactive proxy store to get the underlying raw, un-proxied JavaScript object.
154
+ *
155
+ * @param store - The reactive store proxy
156
+ * @returns The raw underlying object
157
+ */
158
+ export function unwrapStore<T>(store: T): T {
159
+ if (isObject(store) && (store as any)[STORE_RAW]) {
160
+ return (store as any)[STORE_RAW];
161
+ }
162
+ return store;
163
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Visage Reactivity System Execution Context & Scheduler
3
+ */
4
+ import { type ISubscriber, SubscriberState } from "./types";
5
+
6
+ export let currentSubscriber: ISubscriber | null = null;
7
+ export let currentScope: any = null;
8
+ export let batchDepth = 0;
9
+ export const pendingEffects: Set<any> = new Set();
10
+ export const warnedCyclicSubscribers: WeakSet<ISubscriber> = new WeakSet();
11
+ export let isFlushing = false;
12
+
13
+ export function setCurrentSubscriber(sub: ISubscriber | null): void {
14
+ currentSubscriber = sub;
15
+ }
16
+
17
+ export function getCurrentSubscriber(): ISubscriber | null {
18
+ return currentSubscriber;
19
+ }
20
+
21
+ export function setCurrentScope(scope: any): void {
22
+ currentScope = scope;
23
+ }
24
+
25
+ export function getCurrentScope(): any {
26
+ return currentScope;
27
+ }
28
+
29
+ export function addPendingEffect(eff: any): void {
30
+ pendingEffects.add(eff);
31
+ }
32
+
33
+ export function removePendingEffect(eff: any): void {
34
+ pendingEffects.delete(eff);
35
+ }
36
+
37
+ const MAX_FLUSH_COUNT = 100;
38
+
39
+ export function flushEffects(): void {
40
+ if (isFlushing || batchDepth > 0) return;
41
+ isFlushing = true;
42
+ let iterations = 0;
43
+ try {
44
+ while (pendingEffects.size > 0) {
45
+ if (++iterations > MAX_FLUSH_COUNT) {
46
+ pendingEffects.clear();
47
+ throw new Error(
48
+ `[Visage Reactivity] Maximum update depth exceeded (${MAX_FLUSH_COUNT} cycles). ` +
49
+ `An effect continuously triggered state mutations (infinite loop detected). ` +
50
+ `Ensure effects that modify signals do not unconditionally depend on them, or use untrack() to read state.`
51
+ );
52
+ }
53
+ const effectsToRun = Array.from(pendingEffects);
54
+ pendingEffects.clear();
55
+
56
+ for (const eff of effectsToRun) {
57
+ if (eff.state === SubscriberState.DIRTY || eff.state === SubscriberState.CHECK_DIRTY) {
58
+ eff.run();
59
+ }
60
+ }
61
+ }
62
+ } finally {
63
+ isFlushing = false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Batch multiple signal mutations so downstreams and effects update synchronously once at completion.
69
+ *
70
+ * Prevents intermediate flicker, wasted recomputations, and glitch states.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * const firstName = signal("Jane");
75
+ * const lastName = signal("Doe");
76
+ *
77
+ * batch(() => {
78
+ * firstName.set("John");
79
+ * lastName.set("Smith");
80
+ * // Downstream computed and effects run only ONCE here
81
+ * });
82
+ * ```
83
+ *
84
+ * @param fn - A function performing multiple signal updates
85
+ * @returns The return value of `fn`
86
+ */
87
+ export function batch<T>(fn: () => T): T {
88
+ batchDepth++;
89
+ try {
90
+ return fn();
91
+ } finally {
92
+ batchDepth--;
93
+ if (batchDepth === 0) {
94
+ flushEffects();
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Read reactive state without subscribing the current active effect or computed computation.
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * effect(() => {
105
+ * const id = currentId(); // Subscribed: effect re-runs when currentId changes
106
+ *
107
+ * // Not subscribed: effect does NOT re-run when analyticsCounter changes
108
+ * const count = untrack(() => analyticsCounter());
109
+ * sendMetric(id, count);
110
+ * });
111
+ * ```
112
+ *
113
+ * @param fn - A function reading reactive signals without tracking
114
+ * @returns The return value of `fn`
115
+ */
116
+ export function untrack<T>(fn: () => T): T {
117
+ const prev = currentSubscriber;
118
+ currentSubscriber = null;
119
+ try {
120
+ return fn();
121
+ } finally {
122
+ currentSubscriber = prev;
123
+ }
124
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Visage Fine-Grained Reactive Engine Type Definitions
3
+ */
4
+
5
+ export type CleanupFn = () => void;
6
+
7
+ // State flags for subscriber status
8
+ export enum SubscriberState {
9
+ CLEAN = 0,
10
+ CHECK_DIRTY = 1,
11
+ DIRTY = 2,
12
+ DISPOSED = 3
13
+ }
14
+
15
+ export const SIGNAL_BRAND = Symbol("visage.signal");
16
+ export const COMPUTED_BRAND = Symbol("visage.computed");
17
+
18
+ export interface ReadonlySignal<T> {
19
+ get(): T;
20
+ readonly value: T;
21
+ (): T;
22
+ readonly [SIGNAL_BRAND]: true;
23
+ }
24
+
25
+ export interface WritableSignal<T> extends ReadonlySignal<T> {
26
+ set(value: T): void;
27
+ update(fn: (prev: T) => T): void;
28
+ (value?: T): T;
29
+ value: T;
30
+ }
31
+
32
+ export interface SignalOptions<T> {
33
+ name?: string;
34
+ equals?: (a: T, b: T) => boolean;
35
+ internal?: boolean;
36
+ }
37
+
38
+ export interface ComputedOptions {
39
+ name?: string;
40
+ internal?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Check if a value is a Visage signal or computed
45
+ */
46
+ export function isSignal<T = unknown>(value: unknown): value is ReadonlySignal<T> {
47
+ return typeof value === "function" && (SIGNAL_BRAND in value || COMPUTED_BRAND in value);
48
+ }
49
+
50
+ export interface ISubscriber {
51
+ state: SubscriberState;
52
+ sources: Set<IDependency>;
53
+ sourceVersions: Map<IDependency, number>;
54
+ markDirty(targetState?: SubscriberState): void;
55
+ dispose(): void;
56
+ }
57
+
58
+ export interface IDependency {
59
+ version: number;
60
+ removeSubscriber(sub: ISubscriber): void;
61
+ }
62
+
63
+ export type Subscriber = ISubscriber;
64
+ export type Dependency = IDependency;
@@ -0,0 +1,122 @@
1
+ import type { Child } from "../dom/render";
2
+ import { effect, onCleanup } from "../signals";
3
+ import { objectToCSS, type StyleDefinition } from "./create-style";
4
+ import { getScopeId } from "./hash";
5
+ import { styleManager } from "./manager";
6
+ import { scopeCSS } from "./parser";
7
+
8
+ export interface StyleProps {
9
+ children?: string | StyleDefinition | (() => string | StyleDefinition);
10
+ classes?: StyleDefinition | (() => StyleDefinition);
11
+ passToChildren?: boolean;
12
+ id?: string;
13
+ }
14
+
15
+ /**
16
+ * Scoped Component Style Block
17
+ *
18
+ * Automatically hashes and scopes CSS rules to the parent component/container,
19
+ * preventing style leaks and collisions. Supports CSS strings or JavaScript style objects.
20
+ *
21
+ * Examples:
22
+ *
23
+ * 1. Object syntax:
24
+ * ```tsx
25
+ * export function Card(props) {
26
+ * return (
27
+ * <div class="card">
28
+ * <h2>{props.title}</h2>
29
+ * <Style classes={{
30
+ * card: { background: "var(--card-bg)", padding: "20px", borderRadius: "8px" },
31
+ * "& h2": { fontSize: "1.2rem", color: "var(--text)" }
32
+ * }} />
33
+ * </div>
34
+ * );
35
+ * }
36
+ * ```
37
+ *
38
+ * 2. Component Scoped CSS string:
39
+ * ```tsx
40
+ * export function StepItem(props) {
41
+ * return (
42
+ * <div class="step-node">
43
+ * <span class="step-num">{props.step}</span>
44
+ * <span>{props.title}</span>
45
+ *
46
+ * <Style>{`
47
+ * .step-node { display: flex; align-items: center; gap: 8px; }
48
+ * .step-num { font-weight: 600; color: var(--primary); }
49
+ * `}</Style>
50
+ * </div>
51
+ * );
52
+ * }
53
+ * ```
54
+ */
55
+ export function Style(props: StyleProps): Child {
56
+ const getRawCSS = (): string => {
57
+ if (props.classes) {
58
+ const cls = typeof props.classes === "function" ? props.classes() : props.classes;
59
+ return objectToCSS(cls);
60
+ }
61
+ if (props.children) {
62
+ const ch = typeof props.children === "function" ? props.children() : props.children;
63
+ return typeof ch === "object" ? objectToCSS(ch) : ch;
64
+ }
65
+ return "";
66
+ };
67
+
68
+ const rawCSS = getRawCSS();
69
+ const scopeId = props.id ?? getScopeId(rawCSS);
70
+ const scopeAttr = `data-${scopeId}`;
71
+
72
+ // In browser, create a marker node to locate parent element and tag it
73
+ if (typeof document !== "undefined") {
74
+ const marker = document.createTextNode("");
75
+ let unsubscribe: (() => void) | null = null;
76
+
77
+ effect(() => {
78
+ const currentCSS = getRawCSS();
79
+ const currentScopeId = props.id ?? getScopeId(currentCSS);
80
+ const scoped = scopeCSS(currentCSS, {
81
+ scopeId: currentScopeId,
82
+ passToChildren: props.passToChildren ?? true
83
+ });
84
+
85
+ if (unsubscribe) {
86
+ unsubscribe();
87
+ }
88
+ unsubscribe = styleManager.inject(currentScopeId, scoped);
89
+
90
+ // Attach data-v-xyz attribute to parent container
91
+ const attachScope = () => {
92
+ const parent = (marker.parentElement || marker.parentNode) as HTMLElement | null;
93
+ if (parent && typeof parent.setAttribute === "function") {
94
+ parent.setAttribute(`data-${currentScopeId}`, "");
95
+ }
96
+ };
97
+ attachScope();
98
+ queueMicrotask(attachScope);
99
+ });
100
+
101
+ onCleanup(() => {
102
+ if (unsubscribe) {
103
+ unsubscribe();
104
+ }
105
+ const parent = (marker.parentElement || marker.parentNode) as HTMLElement | null;
106
+ if (parent && typeof parent.removeAttribute === "function") {
107
+ parent.removeAttribute(scopeAttr);
108
+ }
109
+ });
110
+
111
+ return marker;
112
+ }
113
+
114
+ // SSR environment: inject CSS into SSR registry
115
+ const scoped = scopeCSS(rawCSS, {
116
+ scopeId,
117
+ passToChildren: props.passToChildren ?? true
118
+ });
119
+ styleManager.inject(scopeId, scoped);
120
+
121
+ return null;
122
+ }