arcway 0.1.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 (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -0
  3. package/client/env.js +55 -0
  4. package/client/fetcher.js +50 -0
  5. package/client/graphql.js +35 -0
  6. package/client/head.js +140 -0
  7. package/client/hooks/use-api.js +80 -0
  8. package/client/hooks/use-debounce.js +12 -0
  9. package/client/hooks/use-form.js +86 -0
  10. package/client/hooks/use-graphql.js +30 -0
  11. package/client/hooks/use-interval.js +12 -0
  12. package/client/hooks/use-mutation.js +27 -0
  13. package/client/hooks/use-query.js +45 -0
  14. package/client/hooks/web/use-click-outside.js +22 -0
  15. package/client/hooks/web/use-local-storage.js +42 -0
  16. package/client/index.js +62 -0
  17. package/client/page-loader.js +155 -0
  18. package/client/provider.js +53 -0
  19. package/client/query.js +13 -0
  20. package/client/router.jsx +303 -0
  21. package/client/ui/accordion.jsx +65 -0
  22. package/client/ui/accordion.stories.jsx +48 -0
  23. package/client/ui/alert-dialog.jsx +122 -0
  24. package/client/ui/alert-dialog.stories.jsx +44 -0
  25. package/client/ui/alert.jsx +52 -0
  26. package/client/ui/alert.stories.jsx +31 -0
  27. package/client/ui/app-shell.jsx +39 -0
  28. package/client/ui/app-shell.stories.jsx +51 -0
  29. package/client/ui/aspect-ratio.jsx +6 -0
  30. package/client/ui/aspect-ratio.stories.jsx +69 -0
  31. package/client/ui/avatar.jsx +78 -0
  32. package/client/ui/avatar.stories.jsx +62 -0
  33. package/client/ui/badge.jsx +34 -0
  34. package/client/ui/badge.stories.js +32 -0
  35. package/client/ui/breadcrumb.jsx +86 -0
  36. package/client/ui/breadcrumb.stories.jsx +43 -0
  37. package/client/ui/button-group.jsx +58 -0
  38. package/client/ui/button-group.stories.jsx +67 -0
  39. package/client/ui/button.jsx +46 -0
  40. package/client/ui/button.stories.js +72 -0
  41. package/client/ui/calendar.jsx +172 -0
  42. package/client/ui/card.jsx +57 -0
  43. package/client/ui/card.stories.jsx +33 -0
  44. package/client/ui/carousel.jsx +167 -0
  45. package/client/ui/chart.jsx +244 -0
  46. package/client/ui/checkbox.jsx +24 -0
  47. package/client/ui/checkbox.stories.js +33 -0
  48. package/client/ui/collapsible.jsx +12 -0
  49. package/client/ui/collapsible.stories.jsx +42 -0
  50. package/client/ui/combobox.jsx +223 -0
  51. package/client/ui/command.jsx +128 -0
  52. package/client/ui/context-menu.jsx +170 -0
  53. package/client/ui/context-menu.stories.jsx +35 -0
  54. package/client/ui/dialog.jsx +109 -0
  55. package/client/ui/dialog.stories.jsx +37 -0
  56. package/client/ui/direction.jsx +9 -0
  57. package/client/ui/drawer.jsx +87 -0
  58. package/client/ui/dropdown-menu.jsx +172 -0
  59. package/client/ui/dropdown-menu.stories.jsx +34 -0
  60. package/client/ui/empty.jsx +76 -0
  61. package/client/ui/empty.stories.jsx +64 -0
  62. package/client/ui/field.jsx +174 -0
  63. package/client/ui/field.stories.jsx +118 -0
  64. package/client/ui/form.jsx +17 -0
  65. package/client/ui/hooks/use-mobile.js +16 -0
  66. package/client/ui/hover-card.jsx +26 -0
  67. package/client/ui/hover-card.stories.jsx +28 -0
  68. package/client/ui/index.js +649 -0
  69. package/client/ui/input-group.jsx +116 -0
  70. package/client/ui/input-group.stories.jsx +65 -0
  71. package/client/ui/input-otp.jsx +62 -0
  72. package/client/ui/input.jsx +16 -0
  73. package/client/ui/input.stories.js +27 -0
  74. package/client/ui/item.jsx +155 -0
  75. package/client/ui/item.stories.jsx +118 -0
  76. package/client/ui/kbd.jsx +24 -0
  77. package/client/ui/kbd.stories.jsx +32 -0
  78. package/client/ui/label.jsx +16 -0
  79. package/client/ui/label.stories.js +25 -0
  80. package/client/ui/lib/utils.js +6 -0
  81. package/client/ui/main-content.jsx +30 -0
  82. package/client/ui/menubar.jsx +189 -0
  83. package/client/ui/menubar.stories.jsx +43 -0
  84. package/client/ui/native-select.jsx +34 -0
  85. package/client/ui/native-select.stories.jsx +67 -0
  86. package/client/ui/navigation-menu.jsx +120 -0
  87. package/client/ui/navigation-menu.stories.jsx +45 -0
  88. package/client/ui/pagination.jsx +92 -0
  89. package/client/ui/pagination.stories.jsx +52 -0
  90. package/client/ui/panel.jsx +66 -0
  91. package/client/ui/popover.jsx +54 -0
  92. package/client/ui/popover.stories.jsx +27 -0
  93. package/client/ui/progress.jsx +19 -0
  94. package/client/ui/progress.stories.js +34 -0
  95. package/client/ui/radio-group.jsx +33 -0
  96. package/client/ui/radio-group.stories.jsx +49 -0
  97. package/client/ui/resizable.jsx +33 -0
  98. package/client/ui/scroll-area.jsx +41 -0
  99. package/client/ui/scroll-area.stories.jsx +43 -0
  100. package/client/ui/select.jsx +145 -0
  101. package/client/ui/select.stories.jsx +80 -0
  102. package/client/ui/separator.jsx +18 -0
  103. package/client/ui/separator.stories.jsx +37 -0
  104. package/client/ui/sheet.jsx +95 -0
  105. package/client/ui/sheet.stories.jsx +56 -0
  106. package/client/ui/sidebar.jsx +544 -0
  107. package/client/ui/skeleton.jsx +8 -0
  108. package/client/ui/skeleton.stories.js +23 -0
  109. package/client/ui/slider.jsx +41 -0
  110. package/client/ui/slider.stories.js +31 -0
  111. package/client/ui/sonner.jsx +37 -0
  112. package/client/ui/spinner.jsx +14 -0
  113. package/client/ui/spinner.stories.js +16 -0
  114. package/client/ui/style-mira.css +1316 -0
  115. package/client/ui/switch.jsx +22 -0
  116. package/client/ui/switch.stories.js +44 -0
  117. package/client/ui/table.jsx +33 -0
  118. package/client/ui/table.stories.jsx +42 -0
  119. package/client/ui/tabs.jsx +63 -0
  120. package/client/ui/tabs.stories.jsx +45 -0
  121. package/client/ui/textarea.jsx +15 -0
  122. package/client/ui/textarea.stories.js +33 -0
  123. package/client/ui/theme.css +459 -0
  124. package/client/ui/toggle-group.jsx +62 -0
  125. package/client/ui/toggle-group.stories.jsx +68 -0
  126. package/client/ui/toggle.jsx +34 -0
  127. package/client/ui/toggle.stories.js +46 -0
  128. package/client/ui/tooltip.jsx +37 -0
  129. package/client/ui/tooltip.stories.jsx +32 -0
  130. package/client/ui/use-transition.js +35 -0
  131. package/client/ws.js +132 -0
  132. package/package.json +134 -0
  133. package/server/bin/cli.js +42 -0
  134. package/server/bin/commands/build.js +23 -0
  135. package/server/bin/commands/dev.js +57 -0
  136. package/server/bin/commands/docs.js +30 -0
  137. package/server/bin/commands/graphql-schema.js +32 -0
  138. package/server/bin/commands/lint.js +35 -0
  139. package/server/bin/commands/mcp.js +26 -0
  140. package/server/bin/commands/migrate.js +82 -0
  141. package/server/bin/commands/schema.js +41 -0
  142. package/server/bin/commands/seed.js +36 -0
  143. package/server/bin/commands/start.js +31 -0
  144. package/server/bin/commands/test.js +20 -0
  145. package/server/bin/solo.js +4 -0
  146. package/server/boot/index.js +150 -0
  147. package/server/boot.js +2 -0
  148. package/server/build.js +23 -0
  149. package/server/cache/drivers/memory.js +23 -0
  150. package/server/cache/drivers/redis.js +28 -0
  151. package/server/cache/index.js +69 -0
  152. package/server/config/loader.js +89 -0
  153. package/server/config/modules/api.js +17 -0
  154. package/server/config/modules/build.js +9 -0
  155. package/server/config/modules/cache.js +10 -0
  156. package/server/config/modules/database.js +29 -0
  157. package/server/config/modules/events.js +15 -0
  158. package/server/config/modules/files.js +15 -0
  159. package/server/config/modules/jobs.js +20 -0
  160. package/server/config/modules/logger.js +9 -0
  161. package/server/config/modules/mail.js +11 -0
  162. package/server/config/modules/mcp.js +9 -0
  163. package/server/config/modules/pages.js +20 -0
  164. package/server/config/modules/queue.js +10 -0
  165. package/server/config/modules/redis.js +9 -0
  166. package/server/config/modules/server.js +30 -0
  167. package/server/config/modules/session.js +9 -0
  168. package/server/config/modules/websocket.js +11 -0
  169. package/server/constants.js +67 -0
  170. package/server/context.js +15 -0
  171. package/server/db/index.js +87 -0
  172. package/server/db/schema/drivers/mysql.js +28 -0
  173. package/server/db/schema/drivers/pg.js +34 -0
  174. package/server/db/schema/drivers/sqlite.js +22 -0
  175. package/server/db/schema/index.js +78 -0
  176. package/server/db/seeds.js +22 -0
  177. package/server/discovery.js +67 -0
  178. package/server/docs/openapi.js +153 -0
  179. package/server/env.js +17 -0
  180. package/server/events/drivers/memory.js +45 -0
  181. package/server/events/drivers/redis.js +64 -0
  182. package/server/events/handler.js +67 -0
  183. package/server/events/index.js +35 -0
  184. package/server/events/pattern.js +5 -0
  185. package/server/files/drivers/local.js +83 -0
  186. package/server/files/drivers/s3.js +113 -0
  187. package/server/files/index.js +57 -0
  188. package/server/filewatcher/index.js +156 -0
  189. package/server/glob.js +6 -0
  190. package/server/graphql/discovery.js +70 -0
  191. package/server/graphql/handler.js +41 -0
  192. package/server/graphql/index.js +13 -0
  193. package/server/graphql/loaders.js +19 -0
  194. package/server/graphql/merge.js +48 -0
  195. package/server/graphql/subscriptions.js +43 -0
  196. package/server/health.js +34 -0
  197. package/server/helpers.js +9 -0
  198. package/server/index.js +55 -0
  199. package/server/internals.js +139 -0
  200. package/server/jobs/cron.js +10 -0
  201. package/server/jobs/drivers/knex-queue.js +207 -0
  202. package/server/jobs/drivers/lease.js +148 -0
  203. package/server/jobs/drivers/memory-queue.js +134 -0
  204. package/server/jobs/queue.js +27 -0
  205. package/server/jobs/runner.js +197 -0
  206. package/server/jobs/throughput.js +63 -0
  207. package/server/lib/vault/encrypt.js +40 -0
  208. package/server/lib/vault/ids.js +9 -0
  209. package/server/lib/vault/index.js +14 -0
  210. package/server/lib/vault/jwt.js +55 -0
  211. package/server/lib/vault/password.js +10 -0
  212. package/server/lint/boundaries.js +77 -0
  213. package/server/logger/index.js +130 -0
  214. package/server/mail/drivers/console.js +31 -0
  215. package/server/mail/drivers/smtp.js +34 -0
  216. package/server/mail/imap.js +105 -0
  217. package/server/mail/inbound-store.js +58 -0
  218. package/server/mail/inbound.js +79 -0
  219. package/server/mail/index.js +112 -0
  220. package/server/mcp/debug-api.js +137 -0
  221. package/server/mcp/helpers.js +30 -0
  222. package/server/mcp/index.js +77 -0
  223. package/server/mcp/runtime.js +7 -0
  224. package/server/mcp/server.js +19 -0
  225. package/server/mcp/tools/debugging.js +133 -0
  226. package/server/mcp/tools/introspection.js +87 -0
  227. package/server/middlewares/cors.js +30 -0
  228. package/server/middlewares/index.js +3 -0
  229. package/server/middlewares/require-session.js +15 -0
  230. package/server/module-loader.js +9 -0
  231. package/server/pages/build-client.js +187 -0
  232. package/server/pages/build-css.js +47 -0
  233. package/server/pages/build-manifest.js +55 -0
  234. package/server/pages/build-plugins.js +75 -0
  235. package/server/pages/build-server.js +115 -0
  236. package/server/pages/build.js +116 -0
  237. package/server/pages/discovery.js +120 -0
  238. package/server/pages/fonts.js +128 -0
  239. package/server/pages/handler.js +276 -0
  240. package/server/pages/hmr.js +176 -0
  241. package/server/pages/pages-router.js +78 -0
  242. package/server/pages/ssr.js +276 -0
  243. package/server/pages/static.js +92 -0
  244. package/server/pages/watcher.js +90 -0
  245. package/server/queue/drivers/knex.js +67 -0
  246. package/server/queue/drivers/redis.js +91 -0
  247. package/server/queue/index.js +61 -0
  248. package/server/rate-limit/consume.js +21 -0
  249. package/server/rate-limit/drivers/memory.js +24 -0
  250. package/server/rate-limit/drivers/redis.js +32 -0
  251. package/server/rate-limit/index.js +33 -0
  252. package/server/redis/index.js +67 -0
  253. package/server/ring-buffer.js +44 -0
  254. package/server/route.js +4 -0
  255. package/server/router/api-router.js +317 -0
  256. package/server/router/cors.js +31 -0
  257. package/server/router/middleware.js +91 -0
  258. package/server/router/routes.js +132 -0
  259. package/server/server.js +35 -0
  260. package/server/session/helpers.js +21 -0
  261. package/server/session/index.js +89 -0
  262. package/server/static/index.js +36 -0
  263. package/server/system-jobs/index.js +50 -0
  264. package/server/system-routes/index.js +84 -0
  265. package/server/testing/index.js +263 -0
  266. package/server/validation.js +41 -0
  267. package/server/watcher.js +34 -0
  268. package/server/web-server.js +231 -0
  269. package/server/ws/discovery.js +54 -0
  270. package/server/ws/index.js +14 -0
  271. package/server/ws/realtime.js +318 -0
  272. package/server/ws/registry.js +17 -0
  273. package/server/ws/server.js +152 -0
  274. package/server/ws/ws-router.js +335 -0
package/client/env.js ADDED
@@ -0,0 +1,55 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ const ENV_PREFIX = 'SOLO_PUBLIC_';
4
+ const WINDOW_KEY = '__SOLO_ENV__';
5
+
6
+ function collectPublicEnv() {
7
+ const result = {};
8
+ for (const [key, value] of Object.entries(process.env)) {
9
+ if (key.startsWith(ENV_PREFIX) && value !== undefined) {
10
+ result[key.slice(ENV_PREFIX.length)] = value;
11
+ }
12
+ }
13
+ return result;
14
+ }
15
+
16
+ function buildEnvScriptTag(envVars) {
17
+ const keys = Object.keys(envVars);
18
+ if (keys.length === 0) return '';
19
+ const json = JSON.stringify(envVars).replace(/</g, '\\u003c');
20
+ return `<script id="__app_env">window.${WINDOW_KEY}=${json}</script>`;
21
+ }
22
+
23
+ function isServer() {
24
+ return typeof window === 'undefined';
25
+ }
26
+
27
+ function getEnvStore() {
28
+ if (isServer()) {
29
+ return collectPublicEnv();
30
+ }
31
+ return globalThis[WINDOW_KEY] ?? {};
32
+ }
33
+
34
+ function env(key) {
35
+ return getEnvStore()[key];
36
+ }
37
+
38
+ function subscribe(_onStoreChange) {
39
+ return () => {};
40
+ }
41
+
42
+ function getSnapshot() {
43
+ return getEnvStore();
44
+ }
45
+
46
+ function getServerSnapshot() {
47
+ return collectPublicEnv();
48
+ }
49
+
50
+ function useEnv(key) {
51
+ const store = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
52
+ return store[key];
53
+ }
54
+
55
+ export { buildEnvScriptTag, collectPublicEnv, env, useEnv };
@@ -0,0 +1,50 @@
1
+ class ApiError extends Error {
2
+ status;
3
+ code;
4
+ details;
5
+
6
+ constructor(status, code, message, details) {
7
+ super(message);
8
+ this.name = 'ApiError';
9
+ this.status = status;
10
+ this.code = code;
11
+ this.details = details;
12
+ }
13
+ }
14
+
15
+ async function soloFetch(url, init) {
16
+ const res = await fetch(url, init);
17
+
18
+ if (res.status === 204) {
19
+ if (!res.ok) {
20
+ throw new ApiError(res.status, 'UNKNOWN', res.statusText);
21
+ }
22
+ return void 0;
23
+ }
24
+
25
+ let json;
26
+ try {
27
+ json = await res.json();
28
+ } catch {
29
+ if (!res.ok) {
30
+ throw new ApiError(res.status, 'NETWORK_ERROR', res.statusText);
31
+ }
32
+ return void 0;
33
+ }
34
+
35
+ if (!res.ok) {
36
+ if (json?.error && typeof json.error === 'object') {
37
+ throw new ApiError(
38
+ res.status,
39
+ json.error.code || 'UNKNOWN',
40
+ json.error.message || res.statusText,
41
+ json.error.details,
42
+ );
43
+ }
44
+ throw new ApiError(res.status, 'UNKNOWN', json?.message || res.statusText);
45
+ }
46
+
47
+ return json.data;
48
+ }
49
+
50
+ export { ApiError, soloFetch };
@@ -0,0 +1,35 @@
1
+ class GraphQLError extends Error {
2
+ errors;
3
+
4
+ constructor(errors) {
5
+ super(errors[0]?.message ?? 'GraphQL Error');
6
+ this.name = 'GraphQLError';
7
+ this.errors = errors;
8
+ }
9
+ }
10
+
11
+ async function graphqlFetch(url, query, variables, headers) {
12
+ const res = await fetch(url, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ ...headers,
17
+ },
18
+ body: JSON.stringify(variables !== undefined ? { query, variables } : { query }),
19
+ });
20
+
21
+ let json;
22
+ try {
23
+ json = await res.json();
24
+ } catch {
25
+ throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
26
+ }
27
+
28
+ if (json.errors?.length) {
29
+ throw new GraphQLError(json.errors);
30
+ }
31
+
32
+ return json.data;
33
+ }
34
+
35
+ export { GraphQLError, graphqlFetch };
package/client/head.js ADDED
@@ -0,0 +1,140 @@
1
+ import { useEffect, Children } from 'react';
2
+
3
+ const SSR_HEAD_KEY = '__ssr_head_data__';
4
+
5
+ function setSSRHeadData(data) {
6
+ globalThis[SSR_HEAD_KEY] = data;
7
+ }
8
+
9
+ function getSSRHeadData() {
10
+ return globalThis[SSR_HEAD_KEY] ?? null;
11
+ }
12
+
13
+ function clearSSRHeadData() {
14
+ delete globalThis[SSR_HEAD_KEY];
15
+ }
16
+
17
+ function setAttributes(el, attrs) {
18
+ for (const [key, val] of Object.entries(attrs)) {
19
+ el.setAttribute(key, val);
20
+ }
21
+ }
22
+
23
+ function appendElement(tag, attrs) {
24
+ const el = document.createElement(tag);
25
+ setAttributes(el, attrs);
26
+ document.head.appendChild(el);
27
+ return el;
28
+ }
29
+
30
+ function Head({ title, description, children }) {
31
+ const { metas, links } = extractHeadChildren(children);
32
+ if (description) {
33
+ metas.push({ name: 'description', content: description });
34
+ }
35
+
36
+ const headData = getSSRHeadData();
37
+ if (headData) {
38
+ if (title) headData.title = title;
39
+ headData.meta.push(...metas);
40
+ headData.links.push(...links);
41
+ }
42
+
43
+ useEffect(() => {
44
+ if (title) {
45
+ document.title = title;
46
+ }
47
+
48
+ const addedElements = [];
49
+
50
+ for (const meta of metas) {
51
+ const selector = meta.name
52
+ ? `meta[name="${meta.name}"]`
53
+ : meta.property
54
+ ? `meta[property="${meta.property}"]`
55
+ : null;
56
+
57
+ if (selector) {
58
+ const existing = document.head.querySelector(selector);
59
+ if (existing) {
60
+ setAttributes(existing, meta);
61
+ continue;
62
+ }
63
+ }
64
+
65
+ addedElements.push(appendElement('meta', meta));
66
+ }
67
+
68
+ for (const link of links) {
69
+ const selector =
70
+ link.rel && link.href ? `link[rel="${link.rel}"][href="${link.href}"]` : null;
71
+ if (selector) {
72
+ const existing = document.head.querySelector(selector);
73
+ if (existing) continue;
74
+ }
75
+
76
+ addedElements.push(appendElement('link', link));
77
+ }
78
+
79
+ return () => {
80
+ for (const el of addedElements) {
81
+ el.parentNode?.removeChild(el);
82
+ }
83
+ };
84
+ }, [title, description, ...metas.flatMap(Object.values), ...links.flatMap(Object.values)]);
85
+
86
+ return null;
87
+ }
88
+
89
+ function extractHeadChildren(children) {
90
+ const metas = [];
91
+ const links = [];
92
+
93
+ Children.forEach(children, (child) => {
94
+ if (!child || typeof child !== 'object') return;
95
+ if (!child.props) return;
96
+
97
+ const { children: _, key: __, ...props } = child.props;
98
+ if (child.type === 'meta') {
99
+ metas.push(props);
100
+ } else if (child.type === 'link') {
101
+ links.push(props);
102
+ }
103
+ });
104
+
105
+ return { metas, links };
106
+ }
107
+
108
+ function escapeHtml(value) {
109
+ return value
110
+ .replace(/&/g, '&amp;')
111
+ .replace(/"/g, '&quot;')
112
+ .replace(/</g, '&lt;')
113
+ .replace(/>/g, '&gt;');
114
+ }
115
+
116
+ function renderHeadToString(headData) {
117
+ const parts = [];
118
+
119
+ if (headData.title) {
120
+ parts.push(`<title>${escapeHtml(headData.title)}</title>`);
121
+ }
122
+
123
+ for (const meta of headData.meta) {
124
+ const attrs = Object.entries(meta)
125
+ .map(([k, v]) => `${k}="${escapeHtml(v)}"`)
126
+ .join(' ');
127
+ parts.push(`<meta ${attrs} />`);
128
+ }
129
+
130
+ for (const link of headData.links) {
131
+ const attrs = Object.entries(link)
132
+ .map(([k, v]) => `${k}="${escapeHtml(v)}"`)
133
+ .join(' ');
134
+ parts.push(`<link ${attrs} />`);
135
+ }
136
+
137
+ return parts.join('\n');
138
+ }
139
+
140
+ export { Head, clearSSRHeadData, extractHeadChildren, renderHeadToString, setSSRHeadData };
@@ -0,0 +1,80 @@
1
+ import { useEffect, useCallback, useRef } from 'react';
2
+ import useSWR, { useSWRConfig } from 'swr';
3
+ import { useApiContext, useWsManager } from '../provider.js';
4
+ import { soloFetch, ApiError } from '../fetcher.js';
5
+ import { stringifyQuery } from '../query.js';
6
+
7
+ function buildUrl(base, query) {
8
+ if (!query) return base;
9
+ const qs = stringifyQuery(query);
10
+ return qs ? `${base}?${qs}` : base;
11
+ }
12
+
13
+ export default function useApi(path, query, options) {
14
+ const { pathPrefix, headers } = useApiContext();
15
+ const wsManager = useWsManager();
16
+ const { mutate: globalMutate } = useSWRConfig();
17
+
18
+ const disabled = path === null || options?.disable === true;
19
+ const wsEnabled = options?.ws !== false;
20
+ const fullPath = disabled ? null : `${pathPrefix}${path}`;
21
+ const url = disabled ? null : buildUrl(fullPath, query);
22
+
23
+ const { disable: _, ws: _ws, ...swrConfig } = options ?? {};
24
+ const result = useSWR(url, (key) => soloFetch(key, { headers }), swrConfig);
25
+
26
+ const mutateRef = useRef(result.mutate);
27
+ mutateRef.current = result.mutate;
28
+
29
+ useEffect(() => {
30
+ if (!wsManager || !fullPath || !wsEnabled) return;
31
+
32
+ const unsubscribe = wsManager.subscribe(fullPath, (msg) => {
33
+ if (msg.data !== undefined) {
34
+ mutateRef.current(msg.data, { revalidate: false });
35
+ }
36
+ });
37
+ return unsubscribe;
38
+ }, [wsManager, fullPath, wsEnabled]);
39
+
40
+ const makeMethod = useCallback(
41
+ (method) => async (body) => {
42
+ if (!fullPath) throw new Error('Cannot call mutation on a disabled useApi hook');
43
+
44
+ if (wsManager?.isConnected() && wsEnabled) {
45
+ const response = await wsManager.request(fullPath, method, body);
46
+ if (response.error) {
47
+ throw new ApiError(
48
+ response.status ?? 400,
49
+ response.error.code,
50
+ response.error.message,
51
+ response.error.details,
52
+ );
53
+ }
54
+ await globalMutate(url);
55
+ return response.data;
56
+ }
57
+
58
+ const res = await soloFetch(url, {
59
+ method,
60
+ headers: {
61
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
62
+ ...headers,
63
+ },
64
+ body: body !== undefined ? JSON.stringify(body) : undefined,
65
+ });
66
+ await globalMutate(url);
67
+ return res;
68
+ },
69
+ [fullPath, wsManager, wsEnabled, url, headers, globalMutate],
70
+ );
71
+
72
+ return {
73
+ ...result,
74
+ loading: result.isLoading,
75
+ post: makeMethod('POST'),
76
+ put: makeMethod('PUT'),
77
+ patch: makeMethod('PATCH'),
78
+ del: makeMethod('DELETE'),
79
+ };
80
+ }
@@ -0,0 +1,12 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export default function useDebounce(value, delayMs) {
4
+ const [debouncedValue, setDebouncedValue] = useState(value);
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => setDebouncedValue(value), delayMs);
8
+ return () => clearTimeout(timer);
9
+ }, [value, delayMs]);
10
+
11
+ return debouncedValue;
12
+ }
@@ -0,0 +1,86 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ import { ArkErrors } from 'arktype';
3
+
4
+ export default function useForm(options) {
5
+ const [values, setValues] = useState(options.initialValues);
6
+ const [errors, setErrors] = useState({});
7
+ const [touched, setTouched] = useState({});
8
+ const [isSubmitting, setIsSubmitting] = useState(false);
9
+
10
+ const initialRef = useRef(options.initialValues);
11
+ const schemaRef = useRef(options.schema);
12
+ schemaRef.current = options.schema;
13
+ const onSubmitRef = useRef(options.onSubmit);
14
+ onSubmitRef.current = options.onSubmit;
15
+ const valuesRef = useRef(values);
16
+ valuesRef.current = values;
17
+
18
+ const isDirty = Object.keys(initialRef.current).some(
19
+ (key) => values[key] !== initialRef.current[key],
20
+ );
21
+
22
+ function setField(name, value) {
23
+ setValues((prev) => ({ ...prev, [name]: value }));
24
+ setTouched((prev) => ({ ...prev, [name]: true }));
25
+ setErrors((prev) => {
26
+ if (!prev[name]) return prev;
27
+ const next = { ...prev };
28
+ delete next[name];
29
+ return next;
30
+ });
31
+ }
32
+
33
+ const setError = useCallback((name, message) => {
34
+ setErrors((prev) => ({ ...prev, [name]: message }));
35
+ }, []);
36
+
37
+ const handleSubmit = useCallback(async (e) => {
38
+ e?.preventDefault?.();
39
+ const currentValues = valuesRef.current;
40
+ const schema = schemaRef.current;
41
+
42
+ if (schema) {
43
+ const result = schema(currentValues);
44
+ if (result instanceof ArkErrors) {
45
+ const fieldErrors = {};
46
+ for (const err of result) {
47
+ const key = String(err.path[0]);
48
+ if (key && !fieldErrors[key]) {
49
+ fieldErrors[key] = err.message;
50
+ }
51
+ }
52
+ setErrors(fieldErrors);
53
+ return;
54
+ }
55
+ }
56
+
57
+ setErrors({});
58
+ setIsSubmitting(true);
59
+ try {
60
+ await onSubmitRef.current(currentValues);
61
+ } finally {
62
+ setIsSubmitting(false);
63
+ }
64
+ }, []);
65
+
66
+ const reset = useCallback((newValues) => {
67
+ const resetTo = newValues ?? initialRef.current;
68
+ if (newValues) initialRef.current = newValues;
69
+ setValues(resetTo);
70
+ setErrors({});
71
+ setTouched({});
72
+ setIsSubmitting(false);
73
+ }, []);
74
+
75
+ return {
76
+ values,
77
+ errors,
78
+ touched,
79
+ isDirty,
80
+ isSubmitting,
81
+ setField,
82
+ setError,
83
+ handleSubmit,
84
+ reset,
85
+ };
86
+ }
@@ -0,0 +1,30 @@
1
+ import useSWR from 'swr';
2
+ import useSWRMutation from 'swr/mutation';
3
+ import { useApiContext } from '../provider.js';
4
+ import { graphqlFetch } from '../graphql.js';
5
+
6
+ function useGraphQL(query, variables, options) {
7
+ const { pathPrefix, headers } = useApiContext();
8
+
9
+ const key = query
10
+ ? variables
11
+ ? `gql:${query}:${JSON.stringify(variables)}`
12
+ : `gql:${query}`
13
+ : null;
14
+
15
+ return useSWR(
16
+ key,
17
+ () => graphqlFetch(`${pathPrefix}/graphql`, query, variables, headers),
18
+ options,
19
+ );
20
+ }
21
+
22
+ function useGraphQLMutation(query) {
23
+ const { pathPrefix, headers } = useApiContext();
24
+
25
+ return useSWRMutation(`gql-mutation:${query}`, (_key, { arg }) =>
26
+ graphqlFetch(`${pathPrefix}/graphql`, query, arg, headers),
27
+ );
28
+ }
29
+
30
+ export { useGraphQL, useGraphQLMutation };
@@ -0,0 +1,12 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export default function useInterval(callback, delayMs) {
4
+ const callbackRef = useRef(callback);
5
+ callbackRef.current = callback;
6
+
7
+ useEffect(() => {
8
+ if (delayMs === null) return;
9
+ const id = setInterval(() => callbackRef.current(), delayMs);
10
+ return () => clearInterval(id);
11
+ }, [delayMs]);
12
+ }
@@ -0,0 +1,27 @@
1
+ import useSWRMutation from 'swr/mutation';
2
+ import { useApiContext } from '../provider.js';
3
+ import { soloFetch } from '../fetcher.js';
4
+
5
+ export default function useMutation(path, options) {
6
+ const { pathPrefix, headers } = useApiContext();
7
+ const method = options?.method ?? 'POST';
8
+ const url = `${pathPrefix}${path}`;
9
+
10
+ const result = useSWRMutation(url, (key, { arg }) =>
11
+ soloFetch(key, {
12
+ method,
13
+ headers: {
14
+ // Only set Content-Type when sending a body — Safari/WebKit rejects
15
+ // DELETE (and other bodyless) requests that include Content-Type.
16
+ ...(arg !== undefined ? { 'Content-Type': 'application/json' } : {}),
17
+ ...headers,
18
+ },
19
+ body: arg !== undefined ? JSON.stringify(arg) : undefined,
20
+ }),
21
+ );
22
+
23
+ return {
24
+ ...result,
25
+ loading: result.isMutating,
26
+ };
27
+ }
@@ -0,0 +1,45 @@
1
+ import { useCallback, useSyncExternalStore } from 'react';
2
+ import { parseQuery } from '../query.js';
3
+
4
+ function subscribe(callback) {
5
+ window.addEventListener('popstate', callback);
6
+ window.addEventListener('querychange', callback);
7
+ return () => {
8
+ window.removeEventListener('popstate', callback);
9
+ window.removeEventListener('querychange', callback);
10
+ };
11
+ }
12
+
13
+ function getSnapshot() {
14
+ return window.location.search;
15
+ }
16
+
17
+ function getServerSnapshot() {
18
+ return '';
19
+ }
20
+
21
+ export default function useQuery() {
22
+ const search = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
23
+ const query = parseQuery(search);
24
+
25
+ const setQuery = useCallback((updates, options) => {
26
+ const next = options?.replace
27
+ ? new URLSearchParams()
28
+ : new URLSearchParams(window.location.search);
29
+
30
+ for (const [key, value] of Object.entries(updates)) {
31
+ if (value == null) {
32
+ next.delete(key);
33
+ } else {
34
+ next.set(key, value);
35
+ }
36
+ }
37
+
38
+ const qs = next.toString();
39
+ const newUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
40
+ window.history.replaceState(null, '', newUrl);
41
+ window.dispatchEvent(new Event('querychange'));
42
+ }, []);
43
+
44
+ return { query, setQuery };
45
+ }
@@ -0,0 +1,22 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export default function useClickOutside(ref, handler) {
4
+ const handlerRef = useRef(handler);
5
+ handlerRef.current = handler;
6
+
7
+ useEffect(() => {
8
+ function listener(event) {
9
+ if (!ref.current || ref.current.contains(event.target)) {
10
+ return;
11
+ }
12
+ handlerRef.current();
13
+ }
14
+
15
+ document.addEventListener('mousedown', listener);
16
+ document.addEventListener('touchstart', listener);
17
+ return () => {
18
+ document.removeEventListener('mousedown', listener);
19
+ document.removeEventListener('touchstart', listener);
20
+ };
21
+ }, [ref]);
22
+ }
@@ -0,0 +1,42 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+
3
+ export default function useLocalStorage(key, initialValue) {
4
+ const [storedValue, setStoredValue] = useState(initialValue);
5
+
6
+ useEffect(() => {
7
+ try {
8
+ const item = window.localStorage.getItem(key);
9
+ if (item !== null) {
10
+ setStoredValue(JSON.parse(item));
11
+ }
12
+ } catch {}
13
+
14
+ const handleStorage = (e) => {
15
+ if (e.key === key) {
16
+ try {
17
+ setStoredValue(e.newValue !== null ? JSON.parse(e.newValue) : initialValue);
18
+ } catch {
19
+ setStoredValue(initialValue);
20
+ }
21
+ }
22
+ };
23
+
24
+ window.addEventListener('storage', handleStorage);
25
+ return () => window.removeEventListener('storage', handleStorage);
26
+ }, [key, initialValue]);
27
+
28
+ const setValue = useCallback(
29
+ (value) => {
30
+ setStoredValue((prev) => {
31
+ const nextValue = value instanceof Function ? value(prev) : value;
32
+ try {
33
+ window.localStorage.setItem(key, JSON.stringify(nextValue));
34
+ } catch {}
35
+ return nextValue;
36
+ });
37
+ },
38
+ [key],
39
+ );
40
+
41
+ return [storedValue, setValue];
42
+ }