@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.
Files changed (52) hide show
  1. package/dist/api/index.js +55 -4085
  2. package/dist/charts/index.js +48 -2806
  3. package/dist/devtools/index.js +18 -4766
  4. package/dist/dom/index.js +32 -1639
  5. package/dist/form/index.js +13 -1498
  6. package/dist/index-061jf06a.js +848 -0
  7. package/dist/index-0madxn60.js +442 -0
  8. package/dist/index-1xtrtz1t.js +256 -0
  9. package/dist/index-2zz0g2n2.js +212 -0
  10. package/dist/index-5s255b3w.js +303 -0
  11. package/dist/index-6kfe1nr4.js +1179 -0
  12. package/dist/index-8czhpea8.js +338 -0
  13. package/dist/index-8x8xz80v.js +727 -0
  14. package/dist/index-921pn75b.js +0 -0
  15. package/dist/index-965ryh36.js +537 -0
  16. package/dist/index-9fvarkrx.js +98 -0
  17. package/dist/index-a4xns6cc.js +219 -0
  18. package/dist/index-b5wc752a.js +1696 -0
  19. package/dist/index-df5e614s.js +334 -0
  20. package/dist/index-dyvgtp43.js +1094 -0
  21. package/dist/index-grhdtcb5.js +234 -0
  22. package/dist/index-h6tjqk6e.js +158 -0
  23. package/dist/index-js199er4.js +101 -0
  24. package/dist/index-x2rs3vab.js +124 -0
  25. package/dist/index-y9bq7h9g.js +1162 -0
  26. package/dist/index-zr6cvset.js +2092 -0
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +458 -11915
  29. package/dist/jsx-runtime/index.js +13 -1719
  30. package/dist/query/index.js +47 -2505
  31. package/dist/router/index.js +25 -1945
  32. package/dist/rpc/index.js +39 -3567
  33. package/dist/server/index.js +11 -1256
  34. package/dist/signals/index.js +52 -1160
  35. package/dist/style/index.js +77 -2429
  36. package/dist/template/index.js +7 -1232
  37. package/dist/ui/index.d.ts +1 -1
  38. package/dist/ui/index.js +150 -4244
  39. package/dist/ui/tabs.d.ts +23 -1
  40. package/dist/virtual/index.js +12 -2044
  41. package/dist/vite/index.js +5 -744
  42. package/package.json +2 -2
  43. package/src/dom/render.ts +20 -7
  44. package/src/index.ts +2 -0
  45. package/src/signals/computed.ts +19 -12
  46. package/src/signals/context.ts +1 -2
  47. package/src/signals/effect.ts +7 -7
  48. package/src/signals/signal.ts +16 -12
  49. package/src/signals/system.ts +50 -21
  50. package/src/ui/index.ts +2 -0
  51. package/src/ui/tabs.tsx +211 -5
  52. 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.2",
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) el.setAttribute("class", cls);
89
- else el.removeAttribute("class");
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) el.setAttribute("class", cls);
94
- else el.removeAttribute("class");
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,
@@ -2,7 +2,12 @@
2
2
  * Visage Lazy, Glitch-Free Computed State Engine
3
3
  */
4
4
  import type { Scope } from "./scope";
5
- import { currentScope, currentSubscriber, setCurrentScope, setCurrentSubscriber } from "./system";
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 = currentScope;
34
- if (currentScope) {
35
- currentScope.addSubscriber(this);
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
- if (currentSubscriber !== null && currentSubscriber.state !== SubscriberState.DISPOSED) {
45
- this._subscribers.add(currentSubscriber);
46
- currentSubscriber.sources.add(this);
47
- currentSubscriber.sourceVersions.set(this, this.version);
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 = currentSubscriber;
117
+ const prevSubscriber = getCurrentSubscriber();
112
118
  setCurrentSubscriber(this);
113
- const prevScope = currentScope;
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
- if (currentScope) {
199
- currentScope.addCleanup(() => {
204
+ const curScope = getCurrentScope();
205
+ if (curScope) {
206
+ curScope.addCleanup(() => {
200
207
  devtools.unregisterSignal(sigId);
201
208
  });
202
209
  }
@@ -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
- ? untrack(() => (props.children as (...args: any[]) => any)())
56
+ ? (props.children as (...args: any[]) => any)()
58
57
  : props.children;
59
58
  if (typeof document !== "undefined") {
60
59
  const frag = document.createDocumentFragment();
@@ -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 = currentScope;
32
- if (currentScope) {
33
- currentScope.addSubscriber(this);
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 = currentSubscriber;
88
+ const prevSubscriber = getCurrentSubscriber();
89
89
  setCurrentSubscriber(this);
90
- const prevScope = currentScope;
90
+ const prevScope = getCurrentScope();
91
91
  setCurrentScope(this.scope);
92
92
 
93
93
  try {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Visage Core Writable Signal & State Primitives
3
3
  */
4
- import { batch, currentScope, currentSubscriber, warnedCyclicSubscribers } from "./system";
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
- if (currentSubscriber !== null && currentSubscriber.state !== SubscriberState.DISPOSED) {
36
- this._subscribers.add(currentSubscriber);
37
- currentSubscriber.sources.add(this);
38
- currentSubscriber.sourceVersions.set(this, this.version);
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
- if (currentSubscriber !== null && this._subscribers.has(currentSubscriber)) {
49
- if (!warnedCyclicSubscribers.has(currentSubscriber)) {
50
- warnedCyclicSubscribers.add(currentSubscriber);
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
- if (currentScope) {
232
- currentScope.addCleanup(() => {
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
- if (currentScope) {
272
- currentScope.addCleanup(() => {
274
+ const curScope = getCurrentScope();
275
+ if (curScope) {
276
+ curScope.addCleanup(() => {
273
277
  devtools.unregisterSignal(sigId);
274
278
  });
275
279
  }
@@ -3,55 +3,81 @@
3
3
  */
4
4
  import { type ISubscriber, SubscriberState } from "./types";
5
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;
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
- if (batchDepth === 0) {
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
- currentSubscriber = null;
146
+ const prev = ctx.currentSubscriber;
147
+ setCurrentSubscriber(null);
119
148
  try {
120
149
  return fn();
121
150
  } finally {
122
- currentSubscriber = prev;
151
+ setCurrentSubscriber(prev);
123
152
  }
124
153
  }
package/src/ui/index.ts CHANGED
@@ -92,6 +92,8 @@ export {
92
92
  type SwitchThumbProps
93
93
  } from "./switch";
94
94
  export {
95
+ type CreateTabsOptions,
96
+ createTabs,
95
97
  Tabs,
96
98
  TabsContent,
97
99
  type TabsContentProps,
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 { ReadonlySignal } from "../signals";
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
- {typeof children === "function" ? children() : children}
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
- if (!isControlled) {
39
- internal.set(nextVal);
40
- }
38
+ internal.set(nextVal);
41
39
  if (options.onChange && nextVal !== currentVal) {
42
40
  options.onChange(nextVal);
43
41
  }