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
@@ -0,0 +1,62 @@
1
+ import {
2
+ ApiProvider,
3
+ Provider,
4
+ SoloProvider,
5
+ useApiContext,
6
+ useSoloContext,
7
+ useWsManager,
8
+ } from './provider.js';
9
+ import useApi from './hooks/use-api.js';
10
+ import useQuery from './hooks/use-query.js';
11
+ import useMutation from './hooks/use-mutation.js';
12
+ import useDebounce from './hooks/use-debounce.js';
13
+ import useForm from './hooks/use-form.js';
14
+ import useInterval from './hooks/use-interval.js';
15
+ import { ApiError, soloFetch } from './fetcher.js';
16
+ import { GraphQLError, graphqlFetch } from './graphql.js';
17
+ import { useGraphQL, useGraphQLMutation } from './hooks/use-graphql.js';
18
+ import { WsManager } from './ws.js';
19
+ import useLocalStorage from './hooks/web/use-local-storage.js';
20
+ import useClickOutside from './hooks/web/use-click-outside.js';
21
+ import { Link, Router, SoloRouter, useRouter, usePathname, useParams, useSearchParams } from './router.jsx';
22
+ import { Head, setSSRHeadData, clearSSRHeadData, renderHeadToString } from './head.js';
23
+ import { useEnv, env, collectPublicEnv, buildEnvScriptTag } from './env.js';
24
+
25
+ export {
26
+ ApiError,
27
+ ApiProvider,
28
+ GraphQLError,
29
+ Head,
30
+ Link,
31
+ Provider,
32
+ Router,
33
+ SoloProvider,
34
+ SoloRouter,
35
+ WsManager,
36
+ buildEnvScriptTag,
37
+ clearSSRHeadData,
38
+ collectPublicEnv,
39
+ env,
40
+ graphqlFetch,
41
+ renderHeadToString,
42
+ setSSRHeadData,
43
+ soloFetch,
44
+ useApi,
45
+ useApiContext,
46
+ useClickOutside,
47
+ useDebounce,
48
+ useEnv,
49
+ useForm,
50
+ useGraphQL,
51
+ useGraphQLMutation,
52
+ useInterval,
53
+ useLocalStorage,
54
+ useMutation,
55
+ useParams,
56
+ usePathname,
57
+ useQuery,
58
+ useRouter,
59
+ useSearchParams,
60
+ useSoloContext,
61
+ useWsManager,
62
+ };
@@ -0,0 +1,155 @@
1
+ const moduleCache = new Map();
2
+ const patternCache = new Map();
3
+
4
+ function shouldSkipPrefetch() {
5
+ if (typeof navigator === 'undefined') return true;
6
+ return navigator.connection?.saveData === true;
7
+ }
8
+
9
+ function prefetchRoute(manifest, pathname) {
10
+ if (shouldSkipPrefetch()) return false;
11
+
12
+ const matched = matchClientRoute(manifest, pathname);
13
+ if (!matched) return false;
14
+
15
+ const { route } = matched;
16
+ const allUrls = [route.clientBundle, ...route.layoutBundles, ...(route.loadingBundles ?? [])];
17
+ const uncached = allUrls.filter((u) => !moduleCache.has(u));
18
+ if (uncached.length === 0) return false;
19
+
20
+ Promise.all(uncached.map((u) => importComponent(u).catch(() => {})));
21
+ return true;
22
+ }
23
+
24
+ function readClientManifest() {
25
+ if (typeof document === 'undefined') return null;
26
+
27
+ const el = document.getElementById('__app_manifest');
28
+ if (!el?.textContent) return null;
29
+
30
+ try {
31
+ return JSON.parse(el.textContent);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function compilePattern(pattern) {
38
+ const paramNames = [];
39
+ let catchAllParam;
40
+
41
+ const regexStr = pattern.replace(
42
+ /\/:([^/]+)|\/\*(\w+)\?|\/\*(\w+)/g,
43
+ (match, singleParam, optCatchAll, reqCatchAll) => {
44
+ if (singleParam) {
45
+ paramNames.push(singleParam);
46
+ return '/([^/]+)';
47
+ }
48
+ if (optCatchAll) {
49
+ paramNames.push(optCatchAll);
50
+ catchAllParam = optCatchAll;
51
+ return '(?:/(.+))?';
52
+ }
53
+ if (reqCatchAll) {
54
+ paramNames.push(reqCatchAll);
55
+ catchAllParam = reqCatchAll;
56
+ return '/(.+)';
57
+ }
58
+ return match;
59
+ },
60
+ );
61
+
62
+ return {
63
+ regex: new RegExp(`^${regexStr}\\/?$`),
64
+ paramNames,
65
+ catchAllParam,
66
+ };
67
+ }
68
+
69
+ function matchClientRoute(manifest, pathname) {
70
+ for (const route of manifest.routes) {
71
+ let compiled = patternCache.get(route.pattern);
72
+ if (!compiled) {
73
+ compiled = compilePattern(route.pattern);
74
+ patternCache.set(route.pattern, compiled);
75
+ }
76
+ const { regex, paramNames, catchAllParam } = compiled;
77
+ const catchAll = catchAllParam || route.catchAllParam;
78
+ const match = regex.exec(pathname);
79
+ if (!match) continue;
80
+
81
+ const params = {};
82
+ try {
83
+ for (let i = 0; i < paramNames.length; i++) {
84
+ const name = paramNames[i];
85
+ const raw = match[i + 1];
86
+ if (name === catchAll) {
87
+ params[name] = raw ? raw.split('/').map(decodeURIComponent) : [];
88
+ } else {
89
+ params[name] = decodeURIComponent(raw);
90
+ }
91
+ }
92
+ } catch {
93
+ continue;
94
+ }
95
+
96
+ return { route, params };
97
+ }
98
+ return null;
99
+ }
100
+
101
+ async function importComponent(bundleUrl) {
102
+ const cached = moduleCache.get(bundleUrl);
103
+ if (cached) return cached;
104
+
105
+ const mod = await import(
106
+ /* @vite-ignore */
107
+ bundleUrl
108
+ );
109
+
110
+ const Component = mod.default;
111
+ if (!Component) {
112
+ throw new Error(`Page bundle at ${bundleUrl} has no default export`);
113
+ }
114
+
115
+ moduleCache.set(bundleUrl, Component);
116
+ return Component;
117
+ }
118
+
119
+ async function loadLoadingComponents(route) {
120
+ const loadingBundles = route.loadingBundles ?? [];
121
+ if (loadingBundles.length === 0) return [];
122
+
123
+ try {
124
+ return await Promise.all(loadingBundles.map((url) => importComponent(url)));
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+
130
+ async function loadPage(manifest, pathname) {
131
+ const matched = matchClientRoute(manifest, pathname);
132
+ if (!matched) return null;
133
+
134
+ const { route, params } = matched;
135
+ const loadingBundles = route.loadingBundles ?? [];
136
+ const allBundles = [route.clientBundle, ...route.layoutBundles, ...loadingBundles];
137
+ const components = await Promise.all(allBundles.map((url) => importComponent(url)));
138
+
139
+ return {
140
+ component: components[0],
141
+ layouts: components.slice(1, 1 + route.layoutBundles.length),
142
+ loadings: components.slice(1 + route.layoutBundles.length),
143
+ params,
144
+ pattern: route.pattern,
145
+ };
146
+ }
147
+
148
+ export {
149
+ compilePattern,
150
+ loadLoadingComponents,
151
+ loadPage,
152
+ matchClientRoute,
153
+ prefetchRoute,
154
+ readClientManifest,
155
+ };
@@ -0,0 +1,53 @@
1
+ import { createContext, useContext, createElement, useEffect, useRef, useMemo } from 'react';
2
+ import { WsManager } from './ws.js';
3
+
4
+ const SOLO_CTX_KEY = '__provider_context__';
5
+ const WS_CTX_KEY = '__ws_context__';
6
+ const DEFAULT_CONFIG = { pathPrefix: '' };
7
+
8
+ const ApiContext = (globalThis[SOLO_CTX_KEY] ??= createContext(DEFAULT_CONFIG));
9
+ const WsContext = (globalThis[WS_CTX_KEY] ??= createContext(null));
10
+
11
+ function useApiContext() {
12
+ return useContext(ApiContext);
13
+ }
14
+
15
+ function useWsManager() {
16
+ return useContext(WsContext);
17
+ }
18
+
19
+ const useSoloContext = useApiContext;
20
+
21
+ function ApiProvider({ children, pathPrefix = '', headers, wsUrl }) {
22
+ const config = useMemo(() => ({ pathPrefix, headers, wsUrl }), [pathPrefix, headers, wsUrl]);
23
+
24
+ const wsManagerRef = useRef(null);
25
+ const wsManager = useMemo(() => {
26
+ if (wsManagerRef.current) {
27
+ wsManagerRef.current.disconnect();
28
+ wsManagerRef.current = null;
29
+ }
30
+ if (!wsUrl || typeof window === 'undefined') return null;
31
+ const manager = new WsManager({ url: wsUrl });
32
+ wsManagerRef.current = manager;
33
+ return manager;
34
+ }, [wsUrl]);
35
+
36
+ useEffect(() => {
37
+ if (wsManager) {
38
+ wsManager.connect();
39
+ return () => wsManager.disconnect();
40
+ }
41
+ }, [wsManager]);
42
+
43
+ return createElement(
44
+ ApiContext.Provider,
45
+ { value: config },
46
+ createElement(WsContext.Provider, { value: wsManager }, children),
47
+ );
48
+ }
49
+
50
+ const Provider = ApiProvider;
51
+ const SoloProvider = ApiProvider;
52
+
53
+ export { ApiProvider, Provider, SoloProvider, useApiContext, useSoloContext, useWsManager };
@@ -0,0 +1,13 @@
1
+ export function parseQuery(search) {
2
+ return Object.fromEntries(new URLSearchParams(search));
3
+ }
4
+
5
+ export function stringifyQuery(params) {
6
+ const sp = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(params)) {
8
+ if (value != null) {
9
+ sp.set(key, String(value));
10
+ }
11
+ }
12
+ return sp.toString();
13
+ }
@@ -0,0 +1,303 @@
1
+ import React from 'react';
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useCallback,
8
+ useMemo,
9
+ useRef,
10
+ useTransition,
11
+ createElement,
12
+ } from 'react';
13
+ import {
14
+ readClientManifest,
15
+ loadPage,
16
+ matchClientRoute,
17
+ loadLoadingComponents,
18
+ prefetchRoute,
19
+ } from './page-loader.js';
20
+ import { parseQuery } from './query.js';
21
+
22
+ const ROUTER_CTX_KEY = '__router_context__';
23
+ const RouterContext = (globalThis[ROUTER_CTX_KEY] ??= createContext(null));
24
+
25
+ function wrapInLayouts(element, layouts) {
26
+ for (let i = layouts.length - 1; i >= 0; i--) {
27
+ element = createElement(layouts[i], null, element);
28
+ }
29
+ return element;
30
+ }
31
+
32
+ function useRouter() {
33
+ const ctx = useContext(RouterContext);
34
+ if (!ctx) {
35
+ throw new Error('useRouter must be used within a <Router> provider');
36
+ }
37
+
38
+ return useMemo(
39
+ () => ({
40
+ pathname: ctx.pathname,
41
+ params: ctx.params,
42
+ query: typeof window !== 'undefined' ? parseQuery(window.location.search) : {},
43
+ push: (to, options) => ctx.navigate(to, { ...options, replace: false }),
44
+ replace: (to, options) => ctx.navigate(to, { ...options, replace: true }),
45
+ back: () => {
46
+ if (typeof window !== 'undefined') window.history.back();
47
+ },
48
+ forward: () => {
49
+ if (typeof window !== 'undefined') window.history.forward();
50
+ },
51
+ refresh: () => {
52
+ if (typeof window !== 'undefined') window.location.reload();
53
+ },
54
+ }),
55
+ [ctx.pathname, ctx.params, ctx.navigate],
56
+ );
57
+ }
58
+
59
+ function usePathname() {
60
+ return useRouter().pathname;
61
+ }
62
+
63
+ function useParams() {
64
+ return useRouter().params;
65
+ }
66
+
67
+ function useSearchParams() {
68
+ return useRouter().query;
69
+ }
70
+
71
+ function Router({
72
+ initialPath,
73
+ initialParams,
74
+ initialComponent,
75
+ initialLayouts,
76
+ initialLoadings,
77
+ children,
78
+ }) {
79
+ const [pathname, setPathname] = useState(
80
+ () => initialPath ?? (typeof window !== 'undefined' ? window.location.pathname : '/'),
81
+ );
82
+ const [pageState, setPageState] = useState({
83
+ component: initialComponent ?? null,
84
+ layouts: initialLayouts ?? [],
85
+ loadings: initialLoadings ?? [],
86
+ params: initialParams ?? {},
87
+ });
88
+ const [isNavigating, setIsNavigating] = useState(false);
89
+ const [isPending, startTransition] = useTransition();
90
+ const manifestRef = useRef(null);
91
+
92
+ useEffect(() => {
93
+ manifestRef.current = readClientManifest();
94
+ const onManifestUpdate = () => {
95
+ manifestRef.current = readClientManifest();
96
+ };
97
+ window.addEventListener('manifest-update', onManifestUpdate);
98
+ return () => window.removeEventListener('manifest-update', onManifestUpdate);
99
+ }, []);
100
+
101
+ const applyLoaded = useCallback((loaded, newPath) => {
102
+ startTransition(() => {
103
+ setPathname(newPath);
104
+ setPageState({
105
+ component: loaded.component,
106
+ layouts: loaded.layouts,
107
+ loadings: loaded.loadings,
108
+ params: loaded.params,
109
+ });
110
+ setIsNavigating(false);
111
+ });
112
+ }, []);
113
+
114
+ const navigateToPage = useCallback(
115
+ async (to, options) => {
116
+ const scroll = options?.scroll !== false;
117
+ const replace = options?.replace === true;
118
+ if (to === pathname) return;
119
+
120
+ const manifest = manifestRef.current;
121
+ if (!manifest) {
122
+ window.location.href = to;
123
+ return;
124
+ }
125
+
126
+ const matched = matchClientRoute(manifest, to);
127
+ if (!matched) {
128
+ window.location.href = to;
129
+ return;
130
+ }
131
+
132
+ setIsNavigating(true);
133
+ if (replace) {
134
+ window.history.replaceState(null, '', to);
135
+ } else {
136
+ window.history.pushState(null, '', to);
137
+ }
138
+
139
+ const targetLoadings = await loadLoadingComponents(matched.route);
140
+ if (targetLoadings.length > 0) {
141
+ setPathname(to);
142
+ setPageState((prev) => ({
143
+ ...prev,
144
+ loadings: targetLoadings,
145
+ params: matched.params,
146
+ }));
147
+ }
148
+
149
+ try {
150
+ const loaded = await loadPage(manifest, to);
151
+ if (!loaded) {
152
+ window.location.href = to;
153
+ return;
154
+ }
155
+
156
+ applyLoaded(loaded, to);
157
+
158
+ if (scroll) {
159
+ window.scrollTo(0, 0);
160
+ }
161
+ } catch (err) {
162
+ console.error('Client navigation failed, falling back to full page load:', err);
163
+ window.location.href = to;
164
+ }
165
+ },
166
+ [pathname, applyLoaded],
167
+ );
168
+
169
+ useEffect(() => {
170
+ async function onPopState() {
171
+ const newPath = window.location.pathname;
172
+ const manifest = manifestRef.current;
173
+
174
+ if (!manifest) {
175
+ setPathname(newPath);
176
+ setPageState((prev) => ({ ...prev, params: {} }));
177
+ return;
178
+ }
179
+
180
+ try {
181
+ const loaded = await loadPage(manifest, newPath);
182
+ if (loaded) {
183
+ applyLoaded(loaded, newPath);
184
+ } else {
185
+ window.location.reload();
186
+ }
187
+ } catch {
188
+ window.location.reload();
189
+ }
190
+ }
191
+
192
+ window.addEventListener('popstate', onPopState);
193
+ return () => window.removeEventListener('popstate', onPopState);
194
+ }, [applyLoaded]);
195
+
196
+ const { component: PageComponent, layouts, loadings, params } = pageState;
197
+
198
+ let content;
199
+ if (PageComponent) {
200
+ const inner = isNavigating && loadings.length > 0
201
+ ? createElement(loadings.at(-1))
202
+ : createElement(PageComponent, params);
203
+ content = wrapInLayouts(inner, layouts);
204
+ } else {
205
+ content = children;
206
+ }
207
+
208
+ return (
209
+ <RouterContext.Provider
210
+ value={{
211
+ pathname,
212
+ params,
213
+ navigate: navigateToPage,
214
+ }}
215
+ >
216
+ {content}
217
+ {isNavigating && loadings.length === 0 && !isPending && (
218
+ <div
219
+ style={{
220
+ position: 'fixed',
221
+ top: 0,
222
+ left: 0,
223
+ width: '100%',
224
+ height: '2px',
225
+ backgroundColor: '#0070f3',
226
+ zIndex: 99999,
227
+ animation: 'nav-progress 1s ease-in-out infinite',
228
+ }}
229
+ />
230
+ )}
231
+ </RouterContext.Provider>
232
+ );
233
+ }
234
+
235
+ function Link({ href, children, onClick, scroll, replace, prefetch = 'hover', ...rest }) {
236
+ const router = useContext(RouterContext);
237
+ const linkRef = useRef(null);
238
+
239
+ const handleMouseEnter = useCallback(() => {
240
+ if (prefetch !== 'hover') return;
241
+ const manifest = readClientManifest();
242
+ if (manifest) {
243
+ prefetchRoute(manifest, href);
244
+ }
245
+ }, [href, prefetch]);
246
+
247
+ useEffect(() => {
248
+ if (prefetch !== 'viewport') return;
249
+ const el = linkRef.current;
250
+ if (!el || typeof IntersectionObserver === 'undefined') return;
251
+
252
+ const observer = new IntersectionObserver(
253
+ (entries) => {
254
+ for (const entry of entries) {
255
+ if (entry.isIntersecting) {
256
+ const manifest = readClientManifest();
257
+ if (manifest) {
258
+ prefetchRoute(manifest, href);
259
+ }
260
+ observer.unobserve(el);
261
+ break;
262
+ }
263
+ }
264
+ },
265
+ { rootMargin: '200px' },
266
+ );
267
+
268
+ observer.observe(el);
269
+ return () => observer.disconnect();
270
+ }, [href, prefetch]);
271
+
272
+ function handleClick(e) {
273
+ if (onClick) onClick(e);
274
+ if (e.defaultPrevented) return;
275
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
276
+ if (e.button !== 0) return;
277
+ if (rest.target === '_blank' || rest.download !== undefined) return;
278
+
279
+ try {
280
+ const url = new URL(href, window.location.origin);
281
+ if (url.origin !== window.location.origin) return;
282
+ } catch {
283
+ return;
284
+ }
285
+
286
+ e.preventDefault();
287
+ if (router) {
288
+ router.navigate(href, { scroll, replace });
289
+ } else {
290
+ window.history.pushState(null, '', href);
291
+ window.dispatchEvent(new PopStateEvent('popstate'));
292
+ }
293
+ }
294
+
295
+ return (
296
+ <a ref={linkRef} href={href} onClick={handleClick} onMouseEnter={handleMouseEnter} {...rest}>
297
+ {children}
298
+ </a>
299
+ );
300
+ }
301
+
302
+ const SoloRouter = Router;
303
+ export { Link, Router, SoloRouter, useParams, usePathname, useRouter, useSearchParams };
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { Accordion as AccordionPrimitive } from 'radix-ui';
3
+ import { cn } from './lib/utils.js';
4
+ import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
5
+ function Accordion({ className, ...props }) {
6
+ return (
7
+ <AccordionPrimitive.Root
8
+ data-slot="accordion"
9
+ className={cn('cn-accordion flex w-full flex-col', className)}
10
+ {...props}
11
+ />
12
+ );
13
+ }
14
+ function AccordionItem({ className, ...props }) {
15
+ return (
16
+ <AccordionPrimitive.Item
17
+ data-slot="accordion-item"
18
+ className={cn('cn-accordion-item', className)}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+ function AccordionTrigger({ className, children, ...props }) {
24
+ return (
25
+ <AccordionPrimitive.Header className="flex">
26
+ <AccordionPrimitive.Trigger
27
+ data-slot="accordion-trigger"
28
+ className={cn(
29
+ 'cn-accordion-trigger group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50',
30
+ className,
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDownIcon
36
+ data-slot="accordion-trigger-icon"
37
+ className="cn-accordion-trigger-icon pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
38
+ />
39
+ <ChevronUpIcon
40
+ data-slot="accordion-trigger-icon"
41
+ className="cn-accordion-trigger-icon pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
42
+ />
43
+ </AccordionPrimitive.Trigger>
44
+ </AccordionPrimitive.Header>
45
+ );
46
+ }
47
+ function AccordionContent({ className, children, ...props }) {
48
+ return (
49
+ <AccordionPrimitive.Content
50
+ data-slot="accordion-content"
51
+ className="cn-accordion-content overflow-hidden"
52
+ {...props}
53
+ >
54
+ <div
55
+ className={cn(
56
+ 'cn-accordion-content-inner [&_a]:hover:text-foreground h-(--radix-accordion-content-height) [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
57
+ className,
58
+ )}
59
+ >
60
+ {children}
61
+ </div>
62
+ </AccordionPrimitive.Content>
63
+ );
64
+ }
65
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import { expect, within } from 'storybook/test';
3
+ import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './accordion.jsx';
4
+ const meta = {
5
+ title: 'UI/Accordion',
6
+ component: Accordion,
7
+ };
8
+ var stdin_default = meta;
9
+ const Default = {
10
+ render: () => (
11
+ <Accordion type="single" collapsible>
12
+ <AccordionItem value="item-1">
13
+ <AccordionTrigger>Section One</AccordionTrigger>
14
+ <AccordionContent>Content for section one.</AccordionContent>
15
+ </AccordionItem>
16
+ <AccordionItem value="item-2">
17
+ <AccordionTrigger>Section Two</AccordionTrigger>
18
+ <AccordionContent>Content for section two.</AccordionContent>
19
+ </AccordionItem>
20
+ </Accordion>
21
+ ),
22
+ play: async ({ canvasElement }) => {
23
+ const canvas = within(canvasElement);
24
+ const trigger = canvas.getByText('Section One');
25
+ await expect(trigger).toBeInTheDocument();
26
+ await expect(canvas.getByText('Section Two')).toBeInTheDocument();
27
+ },
28
+ };
29
+ const Multiple = {
30
+ render: () => (
31
+ <Accordion type="multiple">
32
+ <AccordionItem value="a">
33
+ <AccordionTrigger>First</AccordionTrigger>
34
+ <AccordionContent>First content</AccordionContent>
35
+ </AccordionItem>
36
+ <AccordionItem value="b">
37
+ <AccordionTrigger>Second</AccordionTrigger>
38
+ <AccordionContent>Second content</AccordionContent>
39
+ </AccordionItem>
40
+ </Accordion>
41
+ ),
42
+ play: async ({ canvasElement }) => {
43
+ const canvas = within(canvasElement);
44
+ const triggers = canvas.getAllByRole('button');
45
+ await expect(triggers.length).toBe(2);
46
+ },
47
+ };
48
+ export { Default, Multiple, stdin_default as default };