@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,322 @@
1
+ import { For } from "../dom/control-flow";
2
+ import type { Child } from "../dom/render";
3
+ import { createSignal } from "../signals";
4
+ import { createControllableSignal } from "./utils";
5
+
6
+ export type SortDirection = "asc" | "desc" | null;
7
+
8
+ export interface SortState {
9
+ column: string;
10
+ direction: SortDirection;
11
+ }
12
+
13
+ export interface ColumnDef<T = any> {
14
+ id: string;
15
+ header: Child | (() => Child);
16
+ accessorKey?: keyof T;
17
+ cell?: (row: T, index: number) => Child;
18
+ sortable?: boolean;
19
+ width?: string;
20
+ align?: "left" | "center" | "right";
21
+ }
22
+
23
+ export interface DataTableProps<T = any> {
24
+ data: T[] | (() => T[]);
25
+ columns: ColumnDef<T>[];
26
+ sort?: () => SortState | undefined;
27
+ onSortChange?: (sort: SortState) => void;
28
+ filter?: () => string | undefined;
29
+ selection?: "none" | "single" | "multi";
30
+ selectedKeys?: () => string[] | undefined;
31
+ onSelectionChange?: (keys: string[]) => void;
32
+ getRowId?: (row: T, index: number) => string;
33
+ pageSize?: number;
34
+ onRowClick?: (row: T, index: number) => void;
35
+ emptyText?: string;
36
+ class?: any;
37
+ style?: any;
38
+ tableClass?: any;
39
+ headerClass?: any;
40
+ rowClass?: any;
41
+ cellClass?: any;
42
+ paginationClass?: any;
43
+ [key: string]: any;
44
+ }
45
+
46
+ export function DataTable<T extends Record<string, any>>(props: DataTableProps<T>): Child {
47
+ const [sortState, setSortState] = createControllableSignal<SortState>({
48
+ value: props.sort,
49
+ defaultValue: { column: "", direction: null },
50
+ onChange: props.onSortChange
51
+ });
52
+
53
+ const [selectedKeys, setSelectedKeys] = createControllableSignal<string[]>({
54
+ value: props.selectedKeys,
55
+ defaultValue: [],
56
+ onChange: props.onSelectionChange
57
+ });
58
+
59
+ const [currentPage, setCurrentPage] = createSignal(0, { internal: true });
60
+ const pageSize = props.pageSize ?? 10;
61
+ const selectionMode = props.selection ?? "none";
62
+
63
+ const getRowId = (row: T, index: number): string => {
64
+ if (props.getRowId) return props.getRowId(row, index);
65
+ if (row && typeof row === "object" && "id" in row) return String(row.id);
66
+ return String(index);
67
+ };
68
+
69
+ // 1. Filtered Data
70
+ const filteredData = () => {
71
+ const raw = typeof props.data === "function" ? props.data() : props.data;
72
+ if (!raw) return [];
73
+ const filterQuery = props.filter
74
+ ? (typeof props.filter === "function" ? props.filter() : props.filter)?.toLowerCase().trim()
75
+ : "";
76
+ if (!filterQuery) return raw;
77
+
78
+ return raw.filter((row) =>
79
+ Object.values(row).some((val) => String(val).toLowerCase().includes(filterQuery))
80
+ );
81
+ };
82
+
83
+ // 2. Sorted Data
84
+ const sortedData = () => {
85
+ const list = [...filteredData()];
86
+ const currentSort = sortState();
87
+ if (!currentSort.column || !currentSort.direction) return list;
88
+
89
+ return list.sort((a, b) => {
90
+ const aVal = a[currentSort.column];
91
+ const bVal = b[currentSort.column];
92
+ if (aVal === bVal) return 0;
93
+ if (aVal === null || aVal === undefined) return 1;
94
+ if (bVal === null || bVal === undefined) return -1;
95
+
96
+ const comp = aVal < bVal ? -1 : 1;
97
+ return currentSort.direction === "asc" ? comp : -comp;
98
+ });
99
+ };
100
+
101
+ // 3. Paginated Data
102
+ const totalPages = () => Math.ceil(sortedData().length / pageSize) || 1;
103
+ const paginatedData = () => {
104
+ if (!props.pageSize) return sortedData();
105
+ const start = currentPage() * pageSize;
106
+ return sortedData().slice(start, start + pageSize);
107
+ };
108
+
109
+ const handleSort = (colId: string) => {
110
+ const current = sortState();
111
+ if (current.column !== colId) {
112
+ setSortState({ column: colId, direction: "asc" });
113
+ } else if (current.direction === "asc") {
114
+ setSortState({ column: colId, direction: "desc" });
115
+ } else {
116
+ setSortState({ column: "", direction: null });
117
+ }
118
+ };
119
+
120
+ const handleSelectRow = (key: string, e: MouseEvent) => {
121
+ e.stopPropagation();
122
+ if (selectionMode === "single") {
123
+ setSelectedKeys(selectedKeys().includes(key) ? [] : [key]);
124
+ } else if (selectionMode === "multi") {
125
+ const current = selectedKeys();
126
+ if (current.includes(key)) {
127
+ setSelectedKeys(current.filter((k) => k !== key));
128
+ } else {
129
+ setSelectedKeys([...current, key]);
130
+ }
131
+ }
132
+ };
133
+
134
+ const handleSelectAll = () => {
135
+ if (selectionMode !== "multi") return;
136
+ const allCurrentKeys = paginatedData().map((row, i) => getRowId(row, i));
137
+ const allSelected = allCurrentKeys.every((k) => selectedKeys().includes(k));
138
+
139
+ if (allSelected) {
140
+ setSelectedKeys(selectedKeys().filter((k) => !allCurrentKeys.includes(k)));
141
+ } else {
142
+ setSelectedKeys(Array.from(new Set([...selectedKeys(), ...allCurrentKeys])));
143
+ }
144
+ };
145
+
146
+ const {
147
+ class: className,
148
+ style,
149
+ tableClass,
150
+ headerClass,
151
+ rowClass,
152
+ cellClass,
153
+ paginationClass,
154
+ emptyText = "No data available",
155
+ ...rest
156
+ } = props;
157
+
158
+ return (
159
+ <div
160
+ class={className}
161
+ style={{ display: "flex", flexDirection: "column", width: "100%", ...style }}
162
+ {...rest}
163
+ >
164
+ <div style={{ overflowX: "auto", width: "100%" }}>
165
+ <table class={tableClass} style={{ width: "100%", borderCollapse: "collapse" }}>
166
+ <thead class={headerClass}>
167
+ <tr>
168
+ {selectionMode === "multi" ? (
169
+ <th style={{ width: "40px", padding: "8px" }}>
170
+ <input
171
+ type="checkbox"
172
+ checked={() => {
173
+ const keys = paginatedData().map((row, i) => getRowId(row, i));
174
+ return keys.length > 0 && keys.every((k) => selectedKeys().includes(k));
175
+ }}
176
+ onChange={handleSelectAll}
177
+ />
178
+ </th>
179
+ ) : null}
180
+
181
+ {props.columns.map((col) => {
182
+ const isSorted = () => sortState().column === col.id;
183
+ const sortDir = () => (isSorted() ? sortState().direction : null);
184
+
185
+ return (
186
+ <th
187
+ style={{
188
+ width: col.width,
189
+ textAlign: col.align ?? "left",
190
+ cursor: col.sortable ? "pointer" : "default",
191
+ userSelect: col.sortable ? "none" : "auto",
192
+ padding: "10px 12px"
193
+ }}
194
+ onClick={() => {
195
+ if (col.sortable) handleSort(col.id);
196
+ }}
197
+ >
198
+ <div style={{ display: "inline-flex", alignItems: "center", gap: "6px" }}>
199
+ <span>{typeof col.header === "function" ? col.header() : col.header}</span>
200
+ {col.sortable ? (
201
+ <span
202
+ style={{ fontSize: "10px", color: isSorted() ? "#38bdf8" : "#64748b" }}
203
+ >
204
+ {() => (sortDir() === "asc" ? "▲" : sortDir() === "desc" ? "▼" : "⇅")}
205
+ </span>
206
+ ) : null}
207
+ </div>
208
+ </th>
209
+ );
210
+ })}
211
+ </tr>
212
+ </thead>
213
+ <tbody>
214
+ <For
215
+ each={paginatedData}
216
+ fallback={
217
+ <tr>
218
+ <td
219
+ colSpan={props.columns.length + (selectionMode === "multi" ? 1 : 0)}
220
+ style={{ textAlign: "center", padding: "24px", color: "#64748b" }}
221
+ >
222
+ {emptyText}
223
+ </td>
224
+ </tr>
225
+ }
226
+ >
227
+ {(row: T, idx: () => number) => {
228
+ const key = () => getRowId(row, idx());
229
+ const isSelected = () => selectedKeys().includes(key());
230
+
231
+ return (
232
+ <tr
233
+ class={rowClass}
234
+ style={{ cursor: props.onRowClick ? "pointer" : "default" }}
235
+ data-selected={isSelected}
236
+ onClick={() => {
237
+ if (props.onRowClick) props.onRowClick(row, idx());
238
+ }}
239
+ >
240
+ {selectionMode === "multi" ? (
241
+ <td style={{ width: "40px", padding: "8px" }}>
242
+ <input
243
+ type="checkbox"
244
+ checked={isSelected}
245
+ onClick={(e: MouseEvent) => handleSelectRow(key(), e)}
246
+ />
247
+ </td>
248
+ ) : null}
249
+
250
+ {props.columns.map((col) => {
251
+ const content = col.cell
252
+ ? col.cell(row, idx())
253
+ : col.accessorKey
254
+ ? row[col.accessorKey]
255
+ : row[col.id];
256
+
257
+ return (
258
+ <td
259
+ class={cellClass}
260
+ style={{
261
+ textAlign: col.align ?? "left",
262
+ padding: "10px 12px"
263
+ }}
264
+ >
265
+ {typeof content === "function" ? content() : content}
266
+ </td>
267
+ );
268
+ })}
269
+ </tr>
270
+ );
271
+ }}
272
+ </For>
273
+ </tbody>
274
+ </table>
275
+ </div>
276
+
277
+ {/* Pagination Controls */}
278
+ {props.pageSize ? (
279
+ <div
280
+ class={paginationClass}
281
+ style={{
282
+ display: "flex",
283
+ alignItems: "center",
284
+ justifyContent: "space-between",
285
+ padding: "10px 12px",
286
+ borderTop: "1px solid #1e293b",
287
+ fontSize: "12px",
288
+ color: "#94a3b8"
289
+ }}
290
+ >
291
+ <div>
292
+ Showing <strong>{() => currentPage() * pageSize + 1}</strong> to{" "}
293
+ <strong>{() => Math.min((currentPage() + 1) * pageSize, sortedData().length)}</strong>{" "}
294
+ of <strong>{() => sortedData().length}</strong> entries
295
+ </div>
296
+
297
+ <div style={{ display: "flex", gap: "6px", alignItems: "center" }}>
298
+ <button
299
+ type="button"
300
+ disabled={() => currentPage() === 0}
301
+ onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
302
+ style={{ padding: "4px 8px", cursor: "pointer" }}
303
+ >
304
+ Previous
305
+ </button>
306
+ <span>
307
+ Page <strong>{() => currentPage() + 1}</strong> of <strong>{totalPages}</strong>
308
+ </span>
309
+ <button
310
+ type="button"
311
+ disabled={() => currentPage() >= totalPages() - 1}
312
+ onClick={() => setCurrentPage((p) => Math.min(totalPages() - 1, p + 1))}
313
+ style={{ padding: "4px 8px", cursor: "pointer" }}
314
+ >
315
+ Next
316
+ </button>
317
+ </div>
318
+ </div>
319
+ ) : null}
320
+ </div>
321
+ );
322
+ }
@@ -0,0 +1,362 @@
1
+ import { Portal } from "../dom/control-flow";
2
+ import type { Child } from "../dom/render";
3
+ import type { ReadonlySignal } from "../signals";
4
+ import { createContext, useContext } from "../signals/context";
5
+ import { createControllableSignal, createId, trapFocus } from "./utils";
6
+
7
+ export interface DialogContextValue {
8
+ open: ReadonlySignal<boolean>;
9
+ setOpen: (open: boolean) => void;
10
+ titleId: string;
11
+ descriptionId: string;
12
+ contentRef: { current: HTMLElement | null };
13
+ }
14
+
15
+ export const DialogContext = createContext<DialogContextValue | null>(null);
16
+
17
+ export function useDialogContext(): DialogContextValue {
18
+ const ctx = useContext(DialogContext);
19
+ return ctx!;
20
+ }
21
+
22
+ export interface DialogProps {
23
+ open?: () => boolean | undefined;
24
+ defaultOpen?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
26
+ trigger?: Child | (() => Child);
27
+ title?: Child | (() => Child);
28
+ description?: Child | (() => Child);
29
+ footer?: Child | (() => Child);
30
+ triggerClass?: any;
31
+ overlayClass?: any;
32
+ contentClass?: any;
33
+ titleClass?: any;
34
+ descriptionClass?: any;
35
+ footerClass?: any;
36
+ portalled?: boolean;
37
+ children?: Child | (() => Child);
38
+ }
39
+
40
+ export function DialogRoot(props: DialogProps): Child {
41
+ const [open, setOpen] = createControllableSignal<boolean>({
42
+ value: props.open,
43
+ defaultValue: props.defaultOpen ?? false,
44
+ onChange: props.onOpenChange
45
+ });
46
+
47
+ const titleId = createId("dialog-title");
48
+ const descriptionId = createId("dialog-desc");
49
+ const contentRef: { current: HTMLElement | null } = { current: null };
50
+
51
+ const ctx: DialogContextValue = {
52
+ open,
53
+ setOpen,
54
+ titleId,
55
+ descriptionId,
56
+ contentRef
57
+ };
58
+
59
+ if (props.trigger !== undefined || props.title !== undefined || props.description !== undefined) {
60
+ return DialogContext.Provider({
61
+ value: ctx,
62
+ children: () => (
63
+ <>
64
+ {props.trigger ? (
65
+ <DialogTrigger as="span" class={props.triggerClass}>
66
+ {typeof props.trigger === "function" ? props.trigger() : props.trigger}
67
+ </DialogTrigger>
68
+ ) : null}
69
+ <DialogOverlay class={props.overlayClass} portalled={props.portalled} />
70
+ <DialogContent portalled={props.portalled} class={props.contentClass}>
71
+ {props.title ? (
72
+ <DialogTitle class={props.titleClass}>
73
+ {typeof props.title === "function" ? props.title() : props.title}
74
+ </DialogTitle>
75
+ ) : null}
76
+ {props.description ? (
77
+ <DialogDescription class={props.descriptionClass}>
78
+ {typeof props.description === "function" ? props.description() : props.description}
79
+ </DialogDescription>
80
+ ) : null}
81
+ {typeof props.children === "function" ? props.children() : props.children}
82
+ {props.footer ? (
83
+ <div class={props.footerClass}>
84
+ {typeof props.footer === "function" ? props.footer() : props.footer}
85
+ </div>
86
+ ) : null}
87
+ </DialogContent>
88
+ </>
89
+ )
90
+ });
91
+ }
92
+
93
+ return DialogContext.Provider({
94
+ value: ctx,
95
+ children: props.children
96
+ });
97
+ }
98
+
99
+ export interface DialogTriggerProps {
100
+ as?: string;
101
+ class?: any;
102
+ style?: any;
103
+ onClick?: (e: MouseEvent) => void;
104
+ children: Child | (() => Child);
105
+ [key: string]: any;
106
+ }
107
+
108
+ export function DialogTrigger(props: DialogTriggerProps): Child {
109
+ const ctx = useContext(DialogContext);
110
+
111
+ const handleClick = (e: MouseEvent) => {
112
+ if (props.onClick) props.onClick(e);
113
+ if (!e.defaultPrevented && ctx) {
114
+ ctx.setOpen(true);
115
+ }
116
+ };
117
+
118
+ const { as: Tag = "button", children, onClick, class: className, style, ...rest } = props;
119
+
120
+ return (
121
+ <Tag
122
+ type={Tag === "button" ? "button" : undefined}
123
+ aria-haspopup="dialog"
124
+ aria-expanded={() => ctx?.open() ?? false}
125
+ data-state={() => (ctx?.open() ? "open" : "closed")}
126
+ class={className}
127
+ style={style}
128
+ onClick={handleClick}
129
+ {...rest}
130
+ >
131
+ {children}
132
+ </Tag>
133
+ );
134
+ }
135
+
136
+ export interface DialogPortalProps {
137
+ mount?: HTMLElement | null;
138
+ children: Child | (() => Child);
139
+ }
140
+
141
+ export function DialogPortal(props: DialogPortalProps): Child {
142
+ return <Portal mount={props.mount}>{props.children}</Portal>;
143
+ }
144
+
145
+ export interface DialogOverlayProps {
146
+ as?: string;
147
+ portalled?: boolean;
148
+ mount?: HTMLElement | null;
149
+ class?: any;
150
+ style?: any;
151
+ onClick?: (e: MouseEvent) => void;
152
+ children?: Child | (() => Child);
153
+ [key: string]: any;
154
+ }
155
+
156
+ export function DialogOverlay(props: DialogOverlayProps): Child {
157
+ const ctx = useContext(DialogContext);
158
+
159
+ const handleClick = (e: MouseEvent) => {
160
+ if (props.onClick) props.onClick(e);
161
+ if (!e.defaultPrevented && ctx) {
162
+ ctx.setOpen(false);
163
+ }
164
+ };
165
+
166
+ const {
167
+ as: Tag = "div",
168
+ children,
169
+ onClick,
170
+ class: className,
171
+ style,
172
+ portalled = true,
173
+ mount,
174
+ ...rest
175
+ } = props;
176
+
177
+ const overlay = (
178
+ <Tag
179
+ data-state={() => (ctx?.open() ? "open" : "closed")}
180
+ style={() => ({
181
+ display: ctx?.open() ? undefined : "none",
182
+ ...style
183
+ })}
184
+ class={className}
185
+ onClick={handleClick}
186
+ aria-hidden="true"
187
+ {...rest}
188
+ >
189
+ {children}
190
+ </Tag>
191
+ );
192
+
193
+ return portalled ? <Portal mount={mount}>{overlay}</Portal> : overlay;
194
+ }
195
+
196
+ export interface DialogContentProps {
197
+ as?: string;
198
+ portalled?: boolean;
199
+ mount?: HTMLElement | null;
200
+ class?: any;
201
+ style?: any;
202
+ onKeyDown?: (e: KeyboardEvent) => void;
203
+ children: Child | (() => Child);
204
+ [key: string]: any;
205
+ }
206
+
207
+ export function DialogContent(props: DialogContentProps): Child {
208
+ const ctx = useContext(DialogContext);
209
+
210
+ const handleKeyDown = (e: KeyboardEvent) => {
211
+ if (props.onKeyDown) props.onKeyDown(e);
212
+ if (e.defaultPrevented) return;
213
+
214
+ if (e.key === "Escape") {
215
+ ctx?.setOpen(false);
216
+ e.stopPropagation();
217
+ } else if (e.key === "Tab" && ctx?.contentRef.current) {
218
+ trapFocus(ctx.contentRef.current, e);
219
+ }
220
+ };
221
+
222
+ const handleClick = (e: MouseEvent) => {
223
+ if (props.onClick) props.onClick(e);
224
+ if (e.defaultPrevented) return;
225
+ const target = e.target as HTMLElement | null;
226
+ if (target?.closest("[data-dialog-close]")) {
227
+ ctx?.setOpen(false);
228
+ }
229
+ };
230
+
231
+ const {
232
+ as: Tag = "div",
233
+ children,
234
+ onKeyDown,
235
+ onClick: _onClick,
236
+ class: className,
237
+ style,
238
+ portalled = true,
239
+ mount,
240
+ ...rest
241
+ } = props;
242
+
243
+ const content = (
244
+ <Tag
245
+ ref={(el: HTMLElement | null) => {
246
+ if (ctx) {
247
+ ctx.contentRef.current = el;
248
+ if (el && ctx.open()) {
249
+ const focusable = el.querySelector<HTMLElement>(
250
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
251
+ );
252
+ if (focusable) {
253
+ setTimeout(() => focusable.focus(), 0);
254
+ }
255
+ }
256
+ }
257
+ }}
258
+ role="dialog"
259
+ aria-modal="true"
260
+ aria-labelledby={ctx?.titleId}
261
+ aria-describedby={ctx?.descriptionId}
262
+ data-state={() => (ctx?.open() ? "open" : "closed")}
263
+ style={() => ({
264
+ display: ctx?.open() ? undefined : "none",
265
+ ...style
266
+ })}
267
+ class={className}
268
+ onKeyDown={handleKeyDown}
269
+ onClick={handleClick}
270
+ tabIndex={-1}
271
+ {...rest}
272
+ >
273
+ {children}
274
+ </Tag>
275
+ );
276
+
277
+ return portalled ? <Portal mount={mount}>{content}</Portal> : content;
278
+ }
279
+
280
+ export interface DialogTitleProps {
281
+ as?: string;
282
+ class?: any;
283
+ style?: any;
284
+ children: Child | (() => Child);
285
+ [key: string]: any;
286
+ }
287
+
288
+ export function DialogTitle(props: DialogTitleProps): Child {
289
+ const ctx = useContext(DialogContext);
290
+ const { as: Tag = "h2", children, class: className, style, ...rest } = props;
291
+
292
+ return (
293
+ <Tag id={ctx?.titleId} class={className} style={style} {...rest}>
294
+ {children}
295
+ </Tag>
296
+ );
297
+ }
298
+
299
+ export interface DialogDescriptionProps {
300
+ as?: string;
301
+ class?: any;
302
+ style?: any;
303
+ children: Child | (() => Child);
304
+ [key: string]: any;
305
+ }
306
+
307
+ export function DialogDescription(props: DialogDescriptionProps): Child {
308
+ const ctx = useContext(DialogContext);
309
+ const { as: Tag = "p", children, class: className, style, ...rest } = props;
310
+
311
+ return (
312
+ <Tag id={ctx?.descriptionId} class={className} style={style} {...rest}>
313
+ {children}
314
+ </Tag>
315
+ );
316
+ }
317
+
318
+ export interface DialogCloseProps {
319
+ as?: string;
320
+ class?: any;
321
+ style?: any;
322
+ onClick?: (e: MouseEvent) => void;
323
+ children: Child | (() => Child);
324
+ [key: string]: any;
325
+ }
326
+
327
+ export function DialogClose(props: DialogCloseProps): Child {
328
+ const ctx = useContext(DialogContext);
329
+
330
+ const handleClick = (e: MouseEvent) => {
331
+ if (props.onClick) props.onClick(e);
332
+ if (!e.defaultPrevented && ctx) {
333
+ ctx.setOpen(false);
334
+ }
335
+ };
336
+
337
+ const { as: Tag = "button", children, onClick, class: className, style, ...rest } = props;
338
+
339
+ return (
340
+ <Tag
341
+ type={Tag === "button" ? "button" : undefined}
342
+ data-dialog-close=""
343
+ class={className}
344
+ style={style}
345
+ onClick={handleClick}
346
+ {...rest}
347
+ >
348
+ {children}
349
+ </Tag>
350
+ );
351
+ }
352
+
353
+ export const Dialog = Object.assign(DialogRoot, {
354
+ Root: DialogRoot,
355
+ Trigger: DialogTrigger,
356
+ Portal: DialogPortal,
357
+ Overlay: DialogOverlay,
358
+ Content: DialogContent,
359
+ Title: DialogTitle,
360
+ Description: DialogDescription,
361
+ Close: DialogClose
362
+ });