@epoch32/visage 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/bundle.dev.js +4875 -0
  4. package/dist/index.js +9633 -0
  5. package/dist/src/api/index.js +4002 -0
  6. package/dist/src/charts/index.js +2717 -0
  7. package/dist/src/devtools/index.js +4677 -0
  8. package/dist/src/dom/index.js +1550 -0
  9. package/dist/src/form/index.js +1415 -0
  10. package/dist/src/index.js +11826 -0
  11. package/dist/src/jsx-runtime/index.js +1630 -0
  12. package/dist/src/query/index.js +2422 -0
  13. package/dist/src/router/index.js +1856 -0
  14. package/dist/src/rpc/index.js +3484 -0
  15. package/dist/src/server/index.js +1173 -0
  16. package/dist/src/signals/index.js +1077 -0
  17. package/dist/src/style/index.js +2340 -0
  18. package/dist/src/template/index.js +1149 -0
  19. package/dist/src/ui/index.js +4155 -0
  20. package/dist/src/virtual/index.js +1955 -0
  21. package/dist/src/vite/index.js +984 -0
  22. package/dist/vite/index.js +707 -0
  23. package/package.json +140 -4
  24. package/src/api/client.ts +371 -0
  25. package/src/api/file-router.ts +183 -0
  26. package/src/api/handler.ts +326 -0
  27. package/src/api/index.ts +71 -0
  28. package/src/api/middleware.ts +54 -0
  29. package/src/api/pragma.ts +155 -0
  30. package/src/api/procedure.ts +172 -0
  31. package/src/api/types.ts +249 -0
  32. package/src/charts/Area.tsx +111 -0
  33. package/src/charts/Axis.tsx +126 -0
  34. package/src/charts/Bar.tsx +86 -0
  35. package/src/charts/CartesianGrid.tsx +72 -0
  36. package/src/charts/Chart.tsx +269 -0
  37. package/src/charts/Cursor.tsx +42 -0
  38. package/src/charts/Legend.tsx +56 -0
  39. package/src/charts/Line.tsx +106 -0
  40. package/src/charts/Pie.tsx +88 -0
  41. package/src/charts/Tooltip.tsx +127 -0
  42. package/src/charts/context.ts +30 -0
  43. package/src/charts/index.ts +14 -0
  44. package/src/charts/path.ts +155 -0
  45. package/src/charts/scale.ts +133 -0
  46. package/src/charts/types.ts +50 -0
  47. package/src/devtools/DomTab.tsx +134 -0
  48. package/src/devtools/GraphInspector.tsx +196 -0
  49. package/src/devtools/GraphTab.tsx +315 -0
  50. package/src/devtools/QueryTab.tsx +200 -0
  51. package/src/devtools/RpcTab.tsx +190 -0
  52. package/src/devtools/SignalsTab.tsx +129 -0
  53. package/src/devtools/component.tsx +243 -0
  54. package/src/devtools/highlighter.ts +134 -0
  55. package/src/devtools/hub.ts +175 -0
  56. package/src/devtools/index.ts +20 -0
  57. package/src/devtools/styles.ts +313 -0
  58. package/src/devtools/types.ts +45 -0
  59. package/src/dom/async.ts +232 -0
  60. package/src/dom/control-flow.ts +353 -0
  61. package/src/dom/index.ts +32 -0
  62. package/src/dom/lazy.ts +82 -0
  63. package/src/dom/render.ts +286 -0
  64. package/src/dom/resource.ts +120 -0
  65. package/src/form/form.ts +218 -0
  66. package/src/form/index.ts +17 -0
  67. package/src/form/schema.ts +299 -0
  68. package/src/index.ts +413 -0
  69. package/src/jsx-dev-runtime/index.ts +1 -0
  70. package/src/jsx-runtime/index.ts +112 -0
  71. package/src/jsx-runtime/jsx.d.ts +278 -0
  72. package/src/query/create-mutation.ts +64 -0
  73. package/src/query/create-query.ts +426 -0
  74. package/src/query/focus-manager.ts +68 -0
  75. package/src/query/index.ts +63 -0
  76. package/src/query/mutation-cache.ts +138 -0
  77. package/src/query/online-manager.ts +67 -0
  78. package/src/query/query-cache.ts +369 -0
  79. package/src/query/query-client.ts +248 -0
  80. package/src/query/types.ts +221 -0
  81. package/src/query/utils.ts +132 -0
  82. package/src/router/index.ts +19 -0
  83. package/src/router/router.tsx +352 -0
  84. package/src/rpc/client.ts +317 -0
  85. package/src/rpc/error.ts +70 -0
  86. package/src/rpc/index.ts +52 -0
  87. package/src/rpc/procedure.ts +88 -0
  88. package/src/rpc/router.ts +105 -0
  89. package/src/rpc/server.ts +511 -0
  90. package/src/rpc/types.ts +328 -0
  91. package/src/server/hydrate.ts +21 -0
  92. package/src/server/index.ts +10 -0
  93. package/src/server/render-to-stream.ts +31 -0
  94. package/src/server/render-to-string.ts +125 -0
  95. package/src/signals/computed.ts +217 -0
  96. package/src/signals/context.ts +89 -0
  97. package/src/signals/effect.ts +161 -0
  98. package/src/signals/index.ts +57 -0
  99. package/src/signals/scope.ts +132 -0
  100. package/src/signals/signal.ts +203 -0
  101. package/src/signals/store.ts +163 -0
  102. package/src/signals/system.ts +124 -0
  103. package/src/signals/types.ts +64 -0
  104. package/src/style/component.tsx +122 -0
  105. package/src/style/create-style.ts +229 -0
  106. package/src/style/cx.ts +75 -0
  107. package/src/style/hash.ts +19 -0
  108. package/src/style/index.ts +8 -0
  109. package/src/style/layout.tsx +406 -0
  110. package/src/style/manager.ts +87 -0
  111. package/src/style/parser.ts +257 -0
  112. package/src/style/scale.ts +237 -0
  113. package/src/template/index.ts +4 -0
  114. package/src/template/template.ts +126 -0
  115. package/src/ui/accordion.tsx +286 -0
  116. package/src/ui/command-dialog.tsx +49 -0
  117. package/src/ui/command.tsx +400 -0
  118. package/src/ui/data-table.tsx +322 -0
  119. package/src/ui/dialog.tsx +362 -0
  120. package/src/ui/dropdown-menu.tsx +381 -0
  121. package/src/ui/icon.tsx +69 -0
  122. package/src/ui/index.ts +138 -0
  123. package/src/ui/layout.tsx +13 -0
  124. package/src/ui/popover.tsx +249 -0
  125. package/src/ui/switch.tsx +114 -0
  126. package/src/ui/tabs.tsx +297 -0
  127. package/src/ui/toast.tsx +280 -0
  128. package/src/ui/tooltip.tsx +262 -0
  129. package/src/ui/utils.ts +110 -0
  130. package/src/virtual/index.ts +12 -0
  131. package/src/virtual/virtual-list.tsx +185 -0
  132. package/src/virtual/virtualizer.ts +274 -0
  133. package/src/vite/hmr.ts +38 -0
  134. package/src/vite/index.ts +3 -0
  135. package/src/vite/lucide.ts +124 -0
  136. package/src/vite/plugin.ts +102 -0
  137. package/src/vite/rpc-middleware.ts +82 -0
  138. package/src/vite/types.ts +65 -0
@@ -0,0 +1,280 @@
1
+ import { For } from "../dom/control-flow";
2
+ import type { Child } from "../dom/render";
3
+ import { type ReadonlySignal, signal, type WritableSignal } from "../signals";
4
+ import { createContext, useContext } from "../signals/context";
5
+ import { createId } from "./utils";
6
+
7
+ export type ToastVariant = "default" | "success" | "error" | "warning" | "info";
8
+
9
+ export interface ToastData {
10
+ id: string;
11
+ title?: Child;
12
+ description?: Child;
13
+ variant?: ToastVariant;
14
+ duration?: number;
15
+ onDismiss?: () => void;
16
+ }
17
+
18
+ export interface ToastOptions {
19
+ title?: Child;
20
+ description?: Child;
21
+ variant?: ToastVariant;
22
+ duration?: number;
23
+ onDismiss?: () => void;
24
+ }
25
+
26
+ export interface ToastManager {
27
+ (options: ToastOptions | string): string;
28
+ toasts: ReadonlySignal<ToastData[]>;
29
+ show: (options: ToastOptions | string) => string;
30
+ success: (title: string, options?: Omit<ToastOptions, "title" | "variant">) => string;
31
+ error: (title: string, options?: Omit<ToastOptions, "title" | "variant">) => string;
32
+ warning: (title: string, options?: Omit<ToastOptions, "title" | "variant">) => string;
33
+ info: (title: string, options?: Omit<ToastOptions, "title" | "variant">) => string;
34
+ dismiss: (id: string) => void;
35
+ clear: () => void;
36
+ }
37
+
38
+ export function createToastManager(): ToastManager {
39
+ const toasts: WritableSignal<ToastData[]> = signal<ToastData[]>([]);
40
+
41
+ const dismiss = (id: string) => {
42
+ const item = toasts().find((t) => t.id === id);
43
+ if (item?.onDismiss) item.onDismiss();
44
+ toasts.update((list) => list.filter((t) => t.id !== id));
45
+ };
46
+
47
+ const show = (options: ToastOptions | string): string => {
48
+ const id = createId("toast");
49
+ const normalized: ToastOptions = typeof options === "string" ? { title: options } : options;
50
+
51
+ const data: ToastData = {
52
+ id,
53
+ title: normalized.title,
54
+ description: normalized.description,
55
+ variant: normalized.variant ?? "default",
56
+ duration: normalized.duration ?? 5000,
57
+ onDismiss: normalized.onDismiss
58
+ };
59
+
60
+ toasts.update((list) => [...list, data]);
61
+
62
+ if (data.duration && data.duration > 0) {
63
+ setTimeout(() => {
64
+ dismiss(id);
65
+ }, data.duration);
66
+ }
67
+
68
+ return id;
69
+ };
70
+
71
+ const success = (title: string, options?: Omit<ToastOptions, "title" | "variant">) =>
72
+ show({ ...options, title, variant: "success" });
73
+
74
+ const error = (title: string, options?: Omit<ToastOptions, "title" | "variant">) =>
75
+ show({ ...options, title, variant: "error" });
76
+
77
+ const warning = (title: string, options?: Omit<ToastOptions, "title" | "variant">) =>
78
+ show({ ...options, title, variant: "warning" });
79
+
80
+ const info = (title: string, options?: Omit<ToastOptions, "title" | "variant">) =>
81
+ show({ ...options, title, variant: "info" });
82
+
83
+ const clear = () => {
84
+ toasts.set([]);
85
+ };
86
+
87
+ const fn = function toast(options: ToastOptions | string): string {
88
+ return show(options);
89
+ } as ToastManager;
90
+
91
+ fn.toasts = toasts;
92
+ fn.show = show;
93
+ fn.success = success;
94
+ fn.error = error;
95
+ fn.warning = warning;
96
+ fn.info = info;
97
+ fn.dismiss = dismiss;
98
+ fn.clear = clear;
99
+
100
+ return fn;
101
+ }
102
+
103
+ export const defaultToastManager = createToastManager();
104
+
105
+ export const toast = defaultToastManager;
106
+
107
+ export const ToastContext = createContext<ToastManager>(defaultToastManager);
108
+
109
+ export function useToast(): ToastManager {
110
+ return useContext(ToastContext) ?? defaultToastManager;
111
+ }
112
+
113
+ export interface ToastProviderProps {
114
+ manager?: ToastManager;
115
+ children: Child | (() => Child);
116
+ }
117
+
118
+ export function ToastProvider(props: ToastProviderProps): Child {
119
+ const manager = props.manager ?? defaultToastManager;
120
+ return ToastContext.Provider({
121
+ value: manager,
122
+ children: props.children
123
+ });
124
+ }
125
+
126
+ export interface ToastViewportProps {
127
+ as?: string;
128
+ class?: any;
129
+ style?: any;
130
+ children?: (item: ToastData) => Child;
131
+ [key: string]: any;
132
+ }
133
+
134
+ export function ToastViewport(props: ToastViewportProps): Child {
135
+ const manager = useToast();
136
+ const { as: Tag = "section", children, class: className, style, ...rest } = props;
137
+
138
+ const defaultRender = (t: ToastData) => (
139
+ <Toast
140
+ id={t.id}
141
+ variant={t.variant}
142
+ class={`visage-toast toast toast-${t.variant ?? "default"}`}
143
+ onDismiss={() => manager.dismiss(t.id)}
144
+ >
145
+ <div class="visage-toast-content">
146
+ {t.title && <ToastTitle class="visage-toast-title">{t.title}</ToastTitle>}
147
+ {t.description && (
148
+ <ToastDescription class="visage-toast-description">{t.description}</ToastDescription>
149
+ )}
150
+ </div>
151
+ <ToastClose class="visage-toast-close" onClick={() => manager.dismiss(t.id)} />
152
+ </Toast>
153
+ );
154
+
155
+ const renderItem = children ?? defaultRender;
156
+
157
+ return (
158
+ <Tag
159
+ role="region"
160
+ aria-label="Notifications"
161
+ class={className}
162
+ style={style}
163
+ tabIndex={-1}
164
+ {...rest}
165
+ >
166
+ <For each={manager.toasts}>{(t) => renderItem(t)}</For>
167
+ </Tag>
168
+ );
169
+ }
170
+
171
+ export interface ToastProps {
172
+ id?: string;
173
+ variant?: ToastVariant;
174
+ onDismiss?: () => void;
175
+ as?: string;
176
+ class?: any;
177
+ style?: any;
178
+ children: Child | (() => Child);
179
+ [key: string]: any;
180
+ }
181
+
182
+ export function ToastRoot(props: ToastProps): Child {
183
+ const {
184
+ id,
185
+ variant = "default",
186
+ onDismiss,
187
+ as: Tag = "div",
188
+ children,
189
+ class: className,
190
+ style,
191
+ ...rest
192
+ } = props;
193
+
194
+ const role = variant === "error" ? "alert" : "status";
195
+ const ariaLive = variant === "error" ? "assertive" : "polite";
196
+
197
+ return (
198
+ <Tag
199
+ id={id}
200
+ role={role}
201
+ aria-live={ariaLive}
202
+ data-variant={variant}
203
+ data-state="open"
204
+ class={className}
205
+ style={style}
206
+ {...rest}
207
+ >
208
+ {children}
209
+ </Tag>
210
+ );
211
+ }
212
+
213
+ export interface ToastTitleProps {
214
+ as?: string;
215
+ class?: any;
216
+ style?: any;
217
+ children: Child | (() => Child);
218
+ [key: string]: any;
219
+ }
220
+
221
+ export function ToastTitle(props: ToastTitleProps): Child {
222
+ const { as: Tag = "div", children, class: className, style, ...rest } = props;
223
+ return (
224
+ <Tag class={className} style={style} {...rest}>
225
+ {children}
226
+ </Tag>
227
+ );
228
+ }
229
+
230
+ export interface ToastDescriptionProps {
231
+ as?: string;
232
+ class?: any;
233
+ style?: any;
234
+ children: Child | (() => Child);
235
+ [key: string]: any;
236
+ }
237
+
238
+ export function ToastDescription(props: ToastDescriptionProps): Child {
239
+ const { as: Tag = "div", children, class: className, style, ...rest } = props;
240
+ return (
241
+ <Tag class={className} style={style} {...rest}>
242
+ {children}
243
+ </Tag>
244
+ );
245
+ }
246
+
247
+ export interface ToastCloseProps {
248
+ as?: string;
249
+ class?: any;
250
+ style?: any;
251
+ onClick?: (e: MouseEvent) => void;
252
+ children?: Child | (() => Child);
253
+ [key: string]: any;
254
+ }
255
+
256
+ export function ToastClose(props: ToastCloseProps): Child {
257
+ const { as: Tag = "button", children = "✕", onClick, class: className, style, ...rest } = props;
258
+
259
+ return (
260
+ <Tag
261
+ type={Tag === "button" ? "button" : undefined}
262
+ aria-label="Close notification"
263
+ class={className}
264
+ style={style}
265
+ onClick={onClick}
266
+ {...rest}
267
+ >
268
+ {children}
269
+ </Tag>
270
+ );
271
+ }
272
+
273
+ export const Toast = Object.assign(ToastRoot, {
274
+ Root: ToastRoot,
275
+ Provider: ToastProvider,
276
+ Viewport: ToastViewport,
277
+ Title: ToastTitle,
278
+ Description: ToastDescription,
279
+ Close: ToastClose
280
+ });
@@ -0,0 +1,262 @@
1
+ import { Portal, Show } from "../dom/control-flow";
2
+ import type { Child } from "../dom/render";
3
+ import { onCleanup, type ReadonlySignal } from "../signals";
4
+ import { createContext, useContext } from "../signals/context";
5
+ import { createControllableSignal, createId } from "./utils";
6
+
7
+ export interface TooltipContextValue {
8
+ open: ReadonlySignal<boolean>;
9
+ setOpen: (open: boolean) => void;
10
+ openDelay: number;
11
+ closeDelay: number;
12
+ tooltipId: string;
13
+ triggerRef: { current: HTMLElement | null };
14
+ }
15
+
16
+ export const TooltipContext = createContext<TooltipContextValue | null>(null);
17
+
18
+ export function useTooltipContext(): TooltipContextValue | null {
19
+ return useContext(TooltipContext);
20
+ }
21
+
22
+ export interface TooltipProps {
23
+ open?: () => boolean | undefined;
24
+ defaultOpen?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
26
+ openDelay?: number;
27
+ closeDelay?: number;
28
+ content?: Child | (() => Child);
29
+ side?: "top" | "bottom" | "left" | "right";
30
+ triggerClass?: any;
31
+ triggerAs?: string;
32
+ contentClass?: any;
33
+ contentStyle?: any;
34
+ portalled?: boolean;
35
+ children?: Child | (() => Child);
36
+ }
37
+
38
+ export function TooltipRoot(props: TooltipProps): Child {
39
+ const [open, setOpen] = createControllableSignal<boolean>({
40
+ value: props.open,
41
+ defaultValue: props.defaultOpen ?? false,
42
+ onChange: props.onOpenChange
43
+ });
44
+
45
+ const tooltipId = createId("tooltip");
46
+ const triggerRef: { current: HTMLElement | null } = { current: null };
47
+
48
+ const ctx: TooltipContextValue = {
49
+ open,
50
+ setOpen,
51
+ openDelay: props.openDelay ?? 150,
52
+ closeDelay: props.closeDelay ?? 100,
53
+ tooltipId,
54
+ triggerRef
55
+ };
56
+
57
+ if (props.content !== undefined) {
58
+ return TooltipContext.Provider({
59
+ value: ctx,
60
+ children: () => (
61
+ <span
62
+ class="visage-tooltip-root"
63
+ style={{ position: "relative", display: "inline-flex", alignItems: "center" }}
64
+ >
65
+ <TooltipTrigger
66
+ as={props.triggerAs ?? "button"}
67
+ class={props.triggerClass}
68
+ style={{
69
+ background: "transparent",
70
+ border: "none",
71
+ padding: 0,
72
+ display: "inline-flex",
73
+ alignItems: "center",
74
+ cursor: "help"
75
+ }}
76
+ >
77
+ {typeof props.children === "function" ? props.children() : props.children}
78
+ </TooltipTrigger>
79
+ <TooltipContent
80
+ side={props.side}
81
+ class={props.contentClass}
82
+ style={props.contentStyle}
83
+ portalled={props.portalled ?? false}
84
+ >
85
+ {typeof props.content === "function" ? props.content() : props.content}
86
+ </TooltipContent>
87
+ </span>
88
+ )
89
+ });
90
+ }
91
+
92
+ return TooltipContext.Provider({
93
+ value: ctx,
94
+ children: props.children
95
+ });
96
+ }
97
+
98
+ export interface TooltipTriggerProps {
99
+ as?: string;
100
+ class?: any;
101
+ style?: any;
102
+ onMouseEnter?: (e: MouseEvent) => void;
103
+ onMouseLeave?: (e: MouseEvent) => void;
104
+ onFocus?: (e: FocusEvent) => void;
105
+ onBlur?: (e: FocusEvent) => void;
106
+ children: Child | (() => Child);
107
+ [key: string]: any;
108
+ }
109
+
110
+ export function TooltipTrigger(props: TooltipTriggerProps): Child {
111
+ const ctx = useContext(TooltipContext);
112
+ let openTimer: any = null;
113
+ let closeTimer: any = null;
114
+
115
+ const clearTimers = () => {
116
+ if (openTimer) clearTimeout(openTimer);
117
+ if (closeTimer) clearTimeout(closeTimer);
118
+ openTimer = null;
119
+ closeTimer = null;
120
+ };
121
+
122
+ const handleMouseEnter = (e: MouseEvent) => {
123
+ if (props.onMouseEnter) props.onMouseEnter(e);
124
+ clearTimers();
125
+ if (!ctx) return;
126
+ if (ctx.openDelay > 0) {
127
+ openTimer = setTimeout(() => ctx.setOpen(true), ctx.openDelay);
128
+ } else {
129
+ ctx.setOpen(true);
130
+ }
131
+ };
132
+
133
+ const handleMouseLeave = (e: MouseEvent) => {
134
+ if (props.onMouseLeave) props.onMouseLeave(e);
135
+ clearTimers();
136
+ if (!ctx) return;
137
+ if (ctx.closeDelay > 0) {
138
+ closeTimer = setTimeout(() => ctx.setOpen(false), ctx.closeDelay);
139
+ } else {
140
+ ctx.setOpen(false);
141
+ }
142
+ };
143
+
144
+ const handleFocus = (e: FocusEvent) => {
145
+ if (props.onFocus) props.onFocus(e);
146
+ clearTimers();
147
+ ctx?.setOpen(true);
148
+ };
149
+
150
+ const handleBlur = (e: FocusEvent) => {
151
+ if (props.onBlur) props.onBlur(e);
152
+ clearTimers();
153
+ ctx?.setOpen(false);
154
+ };
155
+
156
+ onCleanup(clearTimers);
157
+
158
+ const {
159
+ as: Tag = "button",
160
+ children,
161
+ onMouseEnter,
162
+ onMouseLeave,
163
+ onFocus,
164
+ onBlur,
165
+ class: className,
166
+ style,
167
+ ...rest
168
+ } = props;
169
+
170
+ return (
171
+ <Tag
172
+ ref={(el: HTMLElement | null) => {
173
+ if (ctx) ctx.triggerRef.current = el;
174
+ }}
175
+ type={Tag === "button" ? "button" : undefined}
176
+ tabIndex={Tag === "button" ? undefined : 0}
177
+ aria-describedby={ctx?.tooltipId}
178
+ data-state={() => (ctx?.open() ? "open" : "closed")}
179
+ class={className}
180
+ style={{ display: "inline-flex", alignItems: "center", ...style }}
181
+ onMouseEnter={handleMouseEnter}
182
+ onMouseLeave={handleMouseLeave}
183
+ onFocus={handleFocus}
184
+ onBlur={handleBlur}
185
+ {...rest}
186
+ >
187
+ {children}
188
+ </Tag>
189
+ );
190
+ }
191
+
192
+ export interface TooltipPortalProps {
193
+ mount?: HTMLElement | null;
194
+ children: Child | (() => Child);
195
+ }
196
+
197
+ export function TooltipPortal(props: TooltipPortalProps): Child {
198
+ return <Portal mount={props.mount}>{props.children}</Portal>;
199
+ }
200
+
201
+ export interface TooltipContentProps {
202
+ forceMount?: boolean;
203
+ portalled?: boolean;
204
+ mount?: HTMLElement | null;
205
+ as?: string;
206
+ class?: any;
207
+ style?: any;
208
+ children: Child | (() => Child);
209
+ [key: string]: any;
210
+ }
211
+
212
+ export function TooltipContent(props: TooltipContentProps): Child {
213
+ const ctx = useContext(TooltipContext);
214
+ const isOpen = () => (ctx ? ctx.open() : false);
215
+
216
+ const {
217
+ forceMount,
218
+ as: Tag = "div",
219
+ children,
220
+ class: className,
221
+ style,
222
+ portalled = true,
223
+ mount,
224
+ ...rest
225
+ } = props;
226
+
227
+ const content = forceMount ? (
228
+ <Tag
229
+ id={ctx?.tooltipId}
230
+ role="tooltip"
231
+ data-state={() => (isOpen() ? "open" : "closed")}
232
+ hidden={() => !isOpen()}
233
+ class={className}
234
+ style={style}
235
+ {...rest}
236
+ >
237
+ {children}
238
+ </Tag>
239
+ ) : (
240
+ <Show when={isOpen}>
241
+ <Tag
242
+ id={ctx?.tooltipId}
243
+ role="tooltip"
244
+ data-state="open"
245
+ class={className}
246
+ style={style}
247
+ {...rest}
248
+ >
249
+ {children}
250
+ </Tag>
251
+ </Show>
252
+ );
253
+
254
+ return portalled ? <Portal mount={mount}>{content}</Portal> : content;
255
+ }
256
+
257
+ export const Tooltip = Object.assign(TooltipRoot, {
258
+ Root: TooltipRoot,
259
+ Trigger: TooltipTrigger,
260
+ Portal: TooltipPortal,
261
+ Content: TooltipContent
262
+ });
@@ -0,0 +1,110 @@
1
+ import { computed, type ReadonlySignal, signal } from "../signals";
2
+
3
+ let idCounter = 0;
4
+
5
+ /**
6
+ * Generate a unique, deterministic ID for ARIA attributes
7
+ */
8
+ export function createId(prefix = "visage"): string {
9
+ return `${prefix}-${++idCounter}`;
10
+ }
11
+
12
+ /**
13
+ * Helper for components that support both controlled and uncontrolled signal/value patterns
14
+ */
15
+ export function createControllableSignal<T>(options: {
16
+ value?: () => T | undefined;
17
+ defaultValue?: T;
18
+ onChange?: (value: T) => void;
19
+ }): [ReadonlySignal<T>, (next: T | ((prev: T) => T)) => void] {
20
+ const isControlled = typeof options.value === "function";
21
+ const internal = signal<T>(options.defaultValue as T, { internal: true });
22
+
23
+ const getter = computed<T>(
24
+ () => {
25
+ if (isControlled) {
26
+ const externalVal = options.value!();
27
+ if (externalVal !== undefined) return externalVal;
28
+ }
29
+ return internal();
30
+ },
31
+ { internal: true }
32
+ );
33
+
34
+ const setter = (next: T | ((prev: T) => T)) => {
35
+ const currentVal = getter();
36
+ const nextVal = typeof next === "function" ? (next as (prev: T) => T)(currentVal) : next;
37
+
38
+ if (!isControlled) {
39
+ internal.set(nextVal);
40
+ }
41
+ if (options.onChange && nextVal !== currentVal) {
42
+ options.onChange(nextVal);
43
+ }
44
+ };
45
+
46
+ return [getter, setter];
47
+ }
48
+
49
+ /**
50
+ * Setup a focus trap inside a container element
51
+ */
52
+ export function trapFocus(container: HTMLElement, event: KeyboardEvent) {
53
+ if (event.key !== "Tab") return;
54
+
55
+ const focusables = container.querySelectorAll<HTMLElement>(
56
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
57
+ );
58
+
59
+ if (focusables.length === 0) {
60
+ event.preventDefault();
61
+ return;
62
+ }
63
+
64
+ const first = focusables[0]!;
65
+ const last = focusables[focusables.length - 1]!;
66
+
67
+ if (event.shiftKey) {
68
+ if (document.activeElement === first) {
69
+ last.focus();
70
+ event.preventDefault();
71
+ }
72
+ } else {
73
+ if (document.activeElement === last) {
74
+ first.focus();
75
+ event.preventDefault();
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Listen for click outside a target element
82
+ */
83
+ export function onClickOutside(
84
+ elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined,
85
+ handler: (event: MouseEvent | TouchEvent) => void
86
+ ): () => void {
87
+ const listener = (event: MouseEvent | TouchEvent) => {
88
+ const target = event.target as Node | null;
89
+ if (!target) return;
90
+
91
+ const list = Array.isArray(elements) ? elements : [elements];
92
+ const isInside = list.some((el) => el?.contains(target));
93
+
94
+ if (!isInside) {
95
+ handler(event);
96
+ }
97
+ };
98
+
99
+ if (typeof document !== "undefined") {
100
+ document.addEventListener("mousedown", listener, true);
101
+ document.addEventListener("touchstart", listener, true);
102
+ }
103
+
104
+ return () => {
105
+ if (typeof document !== "undefined") {
106
+ document.removeEventListener("mousedown", listener, true);
107
+ document.removeEventListener("touchstart", listener, true);
108
+ }
109
+ };
110
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ VirtualGrid,
3
+ type VirtualGridProps,
4
+ VirtualList,
5
+ type VirtualListProps
6
+ } from "./virtual-list";
7
+ export {
8
+ createVirtualizer,
9
+ type VirtualItem,
10
+ type Virtualizer,
11
+ type VirtualizerOptions
12
+ } from "./virtualizer";