@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2

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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/app/docs/App.tsx +72 -0
  4. package/app/docs/api/client.ts +303 -0
  5. package/app/docs/api/contract.ts +24 -0
  6. package/app/docs/api/docs.ts +38 -0
  7. package/app/docs/api/errors.ts +144 -0
  8. package/app/docs/boot.tsx +58 -0
  9. package/app/docs/components/Badge.tsx +65 -0
  10. package/app/docs/components/Button.tsx +37 -0
  11. package/app/docs/components/Card.tsx +41 -0
  12. package/app/docs/components/ConfirmDialog.tsx +173 -0
  13. package/app/docs/components/CopyButton.tsx +68 -0
  14. package/app/docs/components/Dialog.tsx +230 -0
  15. package/app/docs/components/EmptyState.tsx +34 -0
  16. package/app/docs/components/ErrorView.tsx +70 -0
  17. package/app/docs/components/Field.tsx +93 -0
  18. package/app/docs/components/JsonEditor.tsx +151 -0
  19. package/app/docs/components/JsonView.tsx +338 -0
  20. package/app/docs/components/KeyValue.tsx +63 -0
  21. package/app/docs/components/ProblemBanner.tsx +116 -0
  22. package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
  23. package/app/docs/components/Spinner.tsx +33 -0
  24. package/app/docs/components/Table.tsx +33 -0
  25. package/app/docs/components/Tabs.tsx +226 -0
  26. package/app/docs/components/classNames.ts +4 -0
  27. package/app/docs/components/components.css +691 -0
  28. package/app/docs/components/copy.ts +89 -0
  29. package/app/docs/components/fieldContext.ts +29 -0
  30. package/app/docs/components/inputValues.ts +142 -0
  31. package/app/docs/components/inputs.tsx +387 -0
  32. package/app/docs/components/jsonParse.ts +338 -0
  33. package/app/docs/components/listKeys.ts +30 -0
  34. package/app/docs/config.ts +86 -0
  35. package/app/docs/context.ts +33 -0
  36. package/app/docs/embed.ts +74 -0
  37. package/app/docs/env.d.ts +2 -0
  38. package/app/docs/format.ts +9 -0
  39. package/app/docs/hooks/useUrlTab.ts +16 -0
  40. package/app/docs/main.tsx +11 -0
  41. package/app/docs/queryClient.ts +54 -0
  42. package/app/docs/router.tsx +194 -0
  43. package/app/docs/routing.ts +194 -0
  44. package/app/docs/screens/docs/document.ts +27 -0
  45. package/app/docs/screens/docs/http/OperationView.tsx +407 -0
  46. package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
  47. package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
  48. package/app/docs/screens/docs/http/http.css +405 -0
  49. package/app/docs/screens/docs/http/index.tsx +395 -0
  50. package/app/docs/screens/docs/http/model.ts +604 -0
  51. package/app/docs/screens/docs/http/tryIt.ts +455 -0
  52. package/app/docs/screens/docs/permissions.ts +6 -0
  53. package/app/docs/screens/docs/schema/Prose.tsx +43 -0
  54. package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
  55. package/app/docs/screens/docs/schema/index.ts +30 -0
  56. package/app/docs/screens/docs/schema/resolve.ts +466 -0
  57. package/app/docs/screens/docs/schema/schema.css +137 -0
  58. package/app/docs/screens/docs/schema/skeleton.ts +92 -0
  59. package/app/docs/screens/docs/viewer.tsx +154 -0
  60. package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
  61. package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
  62. package/app/docs/screens/docs/ws/format.ts +82 -0
  63. package/app/docs/screens/docs/ws/index.tsx +286 -0
  64. package/app/docs/screens/docs/ws/items.tsx +786 -0
  65. package/app/docs/screens/docs/ws/model.ts +1178 -0
  66. package/app/docs/screens/docs/ws/panels.tsx +440 -0
  67. package/app/docs/screens/docs/ws/ws.css +231 -0
  68. package/app/docs/shortcuts.ts +94 -0
  69. package/app/docs/styles/base.css +847 -0
  70. package/app/docs/styles/tokens.css +143 -0
  71. package/app/docs/theme.ts +56 -0
  72. package/app/kit/unauthorized.ts +222 -0
  73. package/dist/docs/assets/main-0rchxtv5.js +18 -0
  74. package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
  75. package/dist/docs/assets/main-cqgyvs86.css +1 -0
  76. package/dist/docs/manifest.json +26 -0
  77. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
  78. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
  79. package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
  80. package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
  81. package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
  82. package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
  83. package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
  84. package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
  85. package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
  86. package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
  87. package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
  88. package/dist/jobs/assets/index-02427smp.css +1 -0
  89. package/dist/jobs/assets/index-09z2cjft.js +4 -0
  90. package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
  91. package/dist/jobs/assets/index-18bn0gvy.css +1 -0
  92. package/dist/jobs/assets/index-47pxmyxn.js +4 -0
  93. package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
  94. package/dist/jobs/assets/index-4e55z986.js +4 -0
  95. package/dist/jobs/assets/index-4e55z986.js.map +11 -0
  96. package/dist/jobs/assets/index-5c933424.js +4 -0
  97. package/dist/jobs/assets/index-5c933424.js.map +18 -0
  98. package/dist/jobs/assets/index-5kbyqspm.css +1 -0
  99. package/dist/jobs/assets/index-5ng37hk6.css +1 -0
  100. package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
  101. package/dist/jobs/assets/index-71qs5qyx.css +1 -0
  102. package/dist/jobs/assets/index-btpg4r8b.css +1 -0
  103. package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
  104. package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
  105. package/dist/jobs/assets/index-e0yy2skq.css +1 -0
  106. package/dist/jobs/assets/index-hs60b25v.js +10 -0
  107. package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
  108. package/dist/jobs/assets/index-k2smrr70.js +4 -0
  109. package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
  110. package/dist/jobs/assets/index-q24jw3w9.js +4 -0
  111. package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
  112. package/dist/jobs/assets/index-qfb80xen.css +1 -0
  113. package/dist/jobs/assets/index-rtynx6wj.js +4 -0
  114. package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
  115. package/dist/jobs/assets/live-6k7s6358.js +4 -0
  116. package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
  117. package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
  118. package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
  119. package/dist/jobs/assets/main-3yj6bv90.js +5 -0
  120. package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
  121. package/dist/jobs/assets/main-4g69fn3x.js +5 -0
  122. package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
  123. package/dist/jobs/assets/main-4gte3eqq.js +5 -0
  124. package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
  125. package/dist/jobs/assets/main-4j1fcevh.js +5 -0
  126. package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
  127. package/dist/jobs/assets/main-54qwkz28.css +1 -0
  128. package/dist/jobs/assets/main-5grjvsy7.js +5 -0
  129. package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
  130. package/dist/jobs/assets/main-5zbx60gq.js +5 -0
  131. package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
  132. package/dist/jobs/assets/main-9prgjhg4.js +5 -0
  133. package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
  134. package/dist/jobs/assets/main-a250ffk7.js +17 -0
  135. package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
  136. package/dist/jobs/assets/main-d6ge7w08.js +5 -0
  137. package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
  138. package/dist/jobs/assets/main-gngzht43.js +5 -0
  139. package/dist/jobs/assets/main-gngzht43.js.map +10 -0
  140. package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
  141. package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
  142. package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
  143. package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
  144. package/dist/jobs/assets/main-jkvd92z9.js +5 -0
  145. package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
  146. package/dist/jobs/assets/main-jv5xqd13.js +5 -0
  147. package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
  148. package/dist/jobs/assets/main-jxre7w93.js +5 -0
  149. package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
  150. package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
  151. package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
  152. package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
  153. package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
  154. package/dist/jobs/assets/main-pegp0ygb.js +5 -0
  155. package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
  156. package/dist/jobs/assets/main-qr573853.js +5 -0
  157. package/dist/jobs/assets/main-qr573853.js.map +12 -0
  158. package/dist/jobs/assets/main-rq97rdr4.js +4 -0
  159. package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
  160. package/dist/jobs/assets/main-szjdmydc.js +5 -0
  161. package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
  162. package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
  163. package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
  164. package/dist/jobs/assets/main-ts19j4n0.js +5 -0
  165. package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
  166. package/dist/jobs/assets/main-w93x1dzs.js +5 -0
  167. package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
  168. package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
  169. package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
  170. package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
  171. package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
  172. package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
  173. package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
  174. package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
  175. package/dist/jobs/assets/validation-ysjfy072.js +4 -0
  176. package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
  177. package/dist/jobs/manifest.json +511 -0
  178. package/dts/docs/assets.d.ts +144 -0
  179. package/dts/docs/assets.d.ts.map +1 -0
  180. package/dts/docs/browser/config.d.ts +61 -0
  181. package/dts/docs/browser/config.d.ts.map +1 -0
  182. package/dts/docs/browser/index.d.ts +12 -0
  183. package/dts/docs/browser/index.d.ts.map +1 -0
  184. package/dts/docs/docsUi.d.ts +42 -0
  185. package/dts/docs/docsUi.d.ts.map +1 -0
  186. package/dts/docs/hosted.d.ts +22 -0
  187. package/dts/docs/hosted.d.ts.map +1 -0
  188. package/dts/docs/index.d.ts +18 -0
  189. package/dts/docs/index.d.ts.map +1 -0
  190. package/dts/docs/shell.d.ts +57 -0
  191. package/dts/docs/shell.d.ts.map +1 -0
  192. package/dts/docs/types.d.ts +137 -0
  193. package/dts/docs/types.d.ts.map +1 -0
  194. package/dts/index.d.ts +21 -0
  195. package/dts/index.d.ts.map +1 -0
  196. package/dts/jobs/assets.d.ts +157 -0
  197. package/dts/jobs/assets.d.ts.map +1 -0
  198. package/dts/jobs/browser/config.d.ts +67 -0
  199. package/dts/jobs/browser/config.d.ts.map +1 -0
  200. package/dts/jobs/browser/index.d.ts +10 -0
  201. package/dts/jobs/browser/index.d.ts.map +1 -0
  202. package/dts/jobs/browser/unauthorized.d.ts +9 -0
  203. package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
  204. package/dts/jobs/index.d.ts +24 -0
  205. package/dts/jobs/index.d.ts.map +1 -0
  206. package/dts/jobs/jobsUi.d.ts +63 -0
  207. package/dts/jobs/jobsUi.d.ts.map +1 -0
  208. package/dts/jobs/shell.d.ts +77 -0
  209. package/dts/jobs/shell.d.ts.map +1 -0
  210. package/dts/jobs/types.d.ts +146 -0
  211. package/dts/jobs/types.d.ts.map +1 -0
  212. package/dts/kit/browser/shared.d.ts +10 -0
  213. package/dts/kit/browser/shared.d.ts.map +1 -0
  214. package/dts/kit/browser/unauthorized.d.ts +74 -0
  215. package/dts/kit/browser/unauthorized.d.ts.map +1 -0
  216. package/dts/kit/unauthorized.d.ts +47 -0
  217. package/dts/kit/unauthorized.d.ts.map +1 -0
  218. package/guides/docs.md +170 -0
  219. package/guides/jobs.md +1120 -0
  220. package/lib/docs/assets.ts +427 -0
  221. package/lib/docs/browser/config.ts +66 -0
  222. package/lib/docs/browser/index.ts +24 -0
  223. package/lib/docs/docsUi.ts +623 -0
  224. package/lib/docs/hosted.ts +31 -0
  225. package/lib/docs/index.ts +37 -0
  226. package/lib/docs/shell.ts +156 -0
  227. package/lib/docs/types.ts +149 -0
  228. package/lib/index.ts +27 -0
  229. package/lib/jobs/assets.ts +448 -0
  230. package/lib/jobs/browser/config.ts +65 -0
  231. package/lib/jobs/browser/index.ts +9 -0
  232. package/lib/jobs/browser/unauthorized.ts +11 -0
  233. package/lib/jobs/index.ts +41 -0
  234. package/lib/jobs/jobsUi.ts +709 -0
  235. package/lib/jobs/shell.ts +205 -0
  236. package/lib/jobs/types.ts +162 -0
  237. package/lib/kit/browser/shared.ts +11 -0
  238. package/lib/kit/browser/unauthorized.ts +114 -0
  239. package/lib/kit/unauthorized.ts +222 -0
  240. package/package.json +198 -4
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The viewer: loads the one document the mount is configured with
3
+ * (`config.documentUrl`), reads from it whether it is an OpenAPI 3.1 or an
4
+ * AsyncAPI 3.0 document, and routes to the matching reference — `/http[…]`
5
+ * or `/ws[…]`, with `/` redirecting to it.
6
+ */
7
+ import type { ReactNode } from "react";
8
+ import { useQuery } from "@tanstack/react-query";
9
+ import { useMemo } from "react";
10
+ import { createDocsClient } from "../../api/client";
11
+ import { documentKind, docsKeys, getDocument } from "../../api/docs";
12
+ import { Card } from "../../components/Card";
13
+ import { EmptyState } from "../../components/EmptyState";
14
+ import { ErrorView } from "../../components/ErrorView";
15
+ import { Spinner } from "../../components/Spinner";
16
+ import { useDocsFetch, useDocsUiConfig } from "../../context";
17
+ import { Navigate, Link, Routes } from "../../router";
18
+ import type { RouteDef } from "../../routing";
19
+ import { DocumentContext, useDocument } from "./document";
20
+ import { HttpReference } from "./http";
21
+ import { WsReference } from "./ws";
22
+
23
+ export type { LoadedDocument } from "./document";
24
+
25
+ /** `/http[…]` for an OpenAPI document; anything else is told there is none. */
26
+ function HttpRoute() {
27
+ const { doc, kind } = useDocument();
28
+ if (kind !== "openapi") {
29
+ return (
30
+ <div className="screen">
31
+ <EmptyState
32
+ headingLevel={1}
33
+ title="No HTTP documentation"
34
+ description="This document is not an OpenAPI document."
35
+ />
36
+ </div>
37
+ );
38
+ }
39
+ return <HttpReference doc={doc} />;
40
+ }
41
+
42
+ /** `/ws[…]` for an AsyncAPI document; anything else is told there is none. */
43
+ function WsRoute() {
44
+ const { doc, kind } = useDocument();
45
+ if (kind !== "asyncapi") {
46
+ return (
47
+ <div className="screen">
48
+ <EmptyState
49
+ headingLevel={1}
50
+ title="No WebSocket documentation"
51
+ description="This document is not an AsyncAPI document."
52
+ />
53
+ </div>
54
+ );
55
+ }
56
+ return <WsReference document={doc} />;
57
+ }
58
+
59
+ /** `/`: straight to the one reference this viewer has. */
60
+ function Home() {
61
+ const { kind } = useDocument();
62
+ return <Navigate to={kind === "asyncapi" ? "/ws" : "/http"} />;
63
+ }
64
+
65
+ /** The route table: the reference's own pages under the mount. */
66
+ const ROUTES: readonly RouteDef[] = [
67
+ { path: "/", element: <Home /> },
68
+ { path: "/http", element: <HttpRoute /> },
69
+ { path: "/http/:operationId", element: <HttpRoute /> },
70
+ { path: "/ws", element: <WsRoute /> },
71
+ { path: "/ws/:item", element: <WsRoute /> },
72
+ ];
73
+
74
+ /** The viewer, once the document has loaded. */
75
+ export function DocsViewer({ children }: { children?: ReactNode }) {
76
+ const config = useDocsUiConfig();
77
+ const fetcher = useDocsFetch();
78
+ // The document URL is absolute or same-origin; the client sends it as it is.
79
+ const client = useMemo(
80
+ () =>
81
+ createDocsClient({
82
+ base: "",
83
+ csrfHeader: config.csrfHeader,
84
+ fetch: fetcher,
85
+ }),
86
+ [config.csrfHeader, fetcher],
87
+ );
88
+ const spec = useQuery({
89
+ queryKey: docsKeys.document(config.documentUrl),
90
+ queryFn: ({ signal }) => getDocument(client, config.documentUrl, signal),
91
+ staleTime: Infinity,
92
+ });
93
+
94
+ if (spec.isPending) {
95
+ return (
96
+ <div className="screen">
97
+ <Spinner
98
+ label="Loading the API document"
99
+ showLabel
100
+ />
101
+ </div>
102
+ );
103
+ }
104
+ if (spec.isError) {
105
+ return (
106
+ <div className="screen">
107
+ <ErrorView
108
+ headingLevel={1}
109
+ error={spec.error}
110
+ title="Could not load the API document"
111
+ onRetry={() => void spec.refetch()}
112
+ />
113
+ </div>
114
+ );
115
+ }
116
+ const kind = documentKind(spec.data);
117
+ if (kind === null) {
118
+ return (
119
+ <div className="screen">
120
+ <EmptyState
121
+ headingLevel={1}
122
+ title="Not a document this viewer can show"
123
+ description="The document carries neither an `openapi` nor an `asyncapi` field. This viewer shows an OpenAPI 3.1 or an AsyncAPI 3.0 document."
124
+ />
125
+ </div>
126
+ );
127
+ }
128
+ return (
129
+ <DocumentContext value={{ doc: spec.data, kind }}>
130
+ {children ?? (
131
+ <Routes
132
+ routes={ROUTES}
133
+ notFound={<DocsNotFound />}
134
+ />
135
+ )}
136
+ </DocumentContext>
137
+ );
138
+ }
139
+
140
+ /** The 404 for a client route the viewer does not have. */
141
+ export function DocsNotFound() {
142
+ return (
143
+ <div className="screen">
144
+ <Card>
145
+ <EmptyState
146
+ headingLevel={1}
147
+ title="Page not found"
148
+ description="This page is not part of the API documentation."
149
+ action={<Link to="/">Back to the documentation</Link>}
150
+ />
151
+ </Card>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,25 @@
1
+ /** Props of {@link InlineText}. */
2
+ export interface InlineTextProps {
3
+ /** One line of spec text, whose backtick spans are code. */
4
+ text: string;
5
+ }
6
+
7
+ /**
8
+ * One line of spec text with its `` `code` `` spans as `<code>`: the inline
9
+ * half of the shared `Prose`, which does not export it. No HTML is injected.
10
+ */
11
+ export function InlineText({ text }: InlineTextProps) {
12
+ const parts = text.split(/(`[^`]+`)/);
13
+ return (
14
+ <>
15
+ {parts.map((part, index) =>
16
+ part.length > 2 && part.startsWith("`") && part.endsWith("`") ? (
17
+ // eslint-disable-next-line react/no-array-index-key -- the split is fixed for a given text
18
+ <code key={index}>{part.slice(1, -1)}</code>
19
+ ) : (
20
+ part
21
+ ),
22
+ )}
23
+ </>
24
+ );
25
+ }
@@ -0,0 +1,39 @@
1
+ import type { SpecDocument } from "../../../api/docs";
2
+ import { SchemaTree } from "../schema";
3
+
4
+ /** Props of {@link PayloadSchema}. */
5
+ export interface PayloadSchemaProps {
6
+ /** The schema, as the document writes it (often a `$ref`). */
7
+ schema: unknown;
8
+ /** The whole document, for resolving `$ref`s. */
9
+ root: SpecDocument;
10
+ /** The tree's accessible name. */
11
+ label: string;
12
+ /** Levels open at first. Defaults to 2. */
13
+ depth?: number;
14
+ }
15
+
16
+ /**
17
+ * A message's payload schema, `$ref`s resolved, drawn by the docs' shared
18
+ * schema tree. The one place the WebSocket reference renders a schema.
19
+ */
20
+ export function PayloadSchema({
21
+ schema,
22
+ root,
23
+ label,
24
+ depth = 2,
25
+ }: PayloadSchemaProps) {
26
+ return (
27
+ <div
28
+ className="ws-schema"
29
+ data-testid="ws-schema"
30
+ >
31
+ <SchemaTree
32
+ schema={schema}
33
+ root={root}
34
+ depth={depth}
35
+ label={label}
36
+ />
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,82 @@
1
+ import { formatNumber } from "../../../format";
2
+
3
+ /**
4
+ * How the limits panel reads each `<prefix>-limits` entry. A limit's unit is
5
+ * read from its name's suffix (`…Bytes`, `…Ms`, `…PerSecond`); a viewer
6
+ * embedding this reference can pass a {@link LimitInfo} per name for a label
7
+ * and a meaning instead (the jobs UI does, for its own vocabulary).
8
+ */
9
+
10
+ /** A limit's unit. */
11
+ export type LimitUnit = "bytes" | "ms" | "count" | "per-second";
12
+
13
+ /** How one limit reads. */
14
+ export interface LimitInfo {
15
+ /** Its label. */
16
+ label: string;
17
+ /** What it means. */
18
+ meaning: string;
19
+ /** Its unit. */
20
+ unit: LimitUnit;
21
+ }
22
+
23
+ /** A limit's unit from its name: `maxMessageBytes` → bytes, `heartbeatMs` → ms, `messagesPerSecond` → per-second, anything else a count. */
24
+ export function limitUnit(name: string): LimitUnit {
25
+ if (/bytes$/i.test(name)) {
26
+ return "bytes";
27
+ }
28
+ if (/ms$/i.test(name)) {
29
+ return "ms";
30
+ }
31
+ if (/persecond$/i.test(name)) {
32
+ return "per-second";
33
+ }
34
+ return "count";
35
+ }
36
+
37
+ /** `1,048,576 bytes (1 MiB)`. */
38
+ export function formatBytes(value: number): string {
39
+ const units = ["KiB", "MiB", "GiB"];
40
+ let scaled = value;
41
+ let unit = "";
42
+ for (const next of units) {
43
+ if (scaled < 1024 || scaled % 1024 !== 0) {
44
+ break;
45
+ }
46
+ scaled /= 1024;
47
+ unit = next;
48
+ }
49
+ return unit === ""
50
+ ? `${formatNumber(value)} bytes`
51
+ : `${formatNumber(value)} bytes (${formatNumber(scaled)} ${unit})`;
52
+ }
53
+
54
+ /** `25,000 ms (25 s)`, `300,000 ms (5 min)`. */
55
+ export function formatMs(value: number): string {
56
+ const plain = `${formatNumber(value)} ms`;
57
+ if (value >= 60_000 && value % 60_000 === 0) {
58
+ return `${plain} (${formatNumber(value / 60_000)} min)`;
59
+ }
60
+ if (value >= 1000 && value % 1000 === 0) {
61
+ return `${plain} (${formatNumber(value / 1000)} s)`;
62
+ }
63
+ return plain;
64
+ }
65
+
66
+ /** A limit's value in its unit: `info`'s when given, else the name's suffix. */
67
+ export function formatLimit(
68
+ name: string,
69
+ value: number,
70
+ info?: LimitInfo,
71
+ ): string {
72
+ switch (info?.unit ?? limitUnit(name)) {
73
+ case "bytes":
74
+ return formatBytes(value);
75
+ case "ms":
76
+ return formatMs(value);
77
+ case "per-second":
78
+ return `${formatNumber(value)} / s`;
79
+ default:
80
+ return formatNumber(value);
81
+ }
82
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The WebSocket API reference (AsyncAPI 3.0): `/ws` and `/ws/:item` under the
3
+ * mount. The document is loaded by the viewer (`../viewer.tsx`); the pure
4
+ * reading of it is in `model.ts`, the per-item panes in `items.tsx`, the
5
+ * extension panels in `panels.tsx`.
6
+ */
7
+ import type { KeyboardEvent } from "react";
8
+ import type { SpecDocument } from "../../../api/docs";
9
+ import type { PermissionProbe } from "../permissions";
10
+ import type { LimitInfo } from "./format";
11
+ import type { WsConsole, WsPaneOptions } from "./items";
12
+ import type { WsAddressOptions, WsNavGroup } from "./model";
13
+ import { useMemo, useRef } from "react";
14
+ import { EmptyState } from "../../../components/EmptyState";
15
+ import { Field } from "../../../components/Field";
16
+ import { TextInput } from "../../../components/inputs";
17
+ import { arrowTarget } from "../../../components/listKeys";
18
+ import { Link } from "../../../router";
19
+ import { useParams, useQueryParam } from "../../../routing";
20
+ import { SEARCH_SHORTCUT } from "../../../shortcuts";
21
+ import { ChannelPane, MessagePane, OperationPane } from "./items";
22
+ import {
23
+ filterGroups,
24
+ itemPath,
25
+ navGroups,
26
+ readWsDoc,
27
+ selectItem,
28
+ } from "./model";
29
+ import {
30
+ CloseCodesPanel,
31
+ DocHeader,
32
+ LimitsPanel,
33
+ RefusalsPanel,
34
+ SecurityPanel,
35
+ ServerPanel,
36
+ } from "./panels";
37
+ import "./ws.css";
38
+
39
+ /** Props of {@link WsSidebar}. */
40
+ interface WsSidebarProps {
41
+ /** The groups, before searching. */
42
+ groups: readonly WsNavGroup[];
43
+ /** The selected slug. */
44
+ selected: string;
45
+ /** The search (`?q=`). */
46
+ query: string;
47
+ /** Sets the search. */
48
+ onQuery: (query: string) => void;
49
+ /** The docs section's path within the host app ("" for a standalone viewer). */
50
+ pathBase: string;
51
+ }
52
+
53
+ /** The sidebar: a search box, then the connection's panels, channels, operations and messages. */
54
+ function WsSidebar({
55
+ groups,
56
+ selected,
57
+ query,
58
+ onQuery,
59
+ pathBase,
60
+ }: WsSidebarProps) {
61
+ const shown = filterGroups(groups, query);
62
+ const navRef = useRef<HTMLElement>(null);
63
+ const links = () =>
64
+ Array.from(
65
+ navRef.current?.querySelectorAll<HTMLAnchorElement>("a.ws-nav-link") ??
66
+ [],
67
+ );
68
+ // Arrow keys move between the links (Home/End to the ends), and ArrowDown
69
+ // from the search goes to the first; each link stays a Tab stop too.
70
+ const onListKey = (event: KeyboardEvent<HTMLDivElement>) => {
71
+ const target = arrowTarget(event.key, links(), document.activeElement);
72
+ if (target) {
73
+ event.preventDefault();
74
+ target.focus();
75
+ }
76
+ };
77
+ return (
78
+ <nav
79
+ ref={navRef}
80
+ className="ws-sidebar"
81
+ aria-label="WebSocket reference"
82
+ >
83
+ <Field label="Search">
84
+ <TextInput
85
+ type="search"
86
+ value={query}
87
+ onChange={onQuery}
88
+ onKeyDown={(event) => {
89
+ const first = links()[0];
90
+ if (event.key === "ArrowDown" && first) {
91
+ event.preventDefault();
92
+ first.focus();
93
+ }
94
+ }}
95
+ placeholder="Channel, message, code…"
96
+ {...SEARCH_SHORTCUT}
97
+ autoComplete="off"
98
+ spellCheck={false}
99
+ />
100
+ </Field>
101
+ {shown.length === 0 && (
102
+ <p
103
+ className="muted"
104
+ data-testid="ws-search-empty"
105
+ >
106
+ Nothing matches “{query}”.
107
+ </p>
108
+ )}
109
+ <div
110
+ className="ws-sidebar-list"
111
+ onKeyDown={onListKey}
112
+ >
113
+ {shown.map((group) => (
114
+ <section
115
+ key={group.title}
116
+ className="ws-group"
117
+ data-testid={`ws-group-${group.title}`}
118
+ >
119
+ <h2 className="ws-group-title">{group.title}</h2>
120
+ <ul>
121
+ {group.items.map((item) => (
122
+ <li key={item.slug}>
123
+ <Link
124
+ to={itemPath(item.slug, query, pathBase)}
125
+ className="ws-nav-link"
126
+ activeMatch="exact"
127
+ data-selected={item.slug === selected ? "true" : undefined}
128
+ data-testid={`ws-nav-${item.slug}`}
129
+ >
130
+ <span className="ws-nav-label">{item.label}</span>
131
+ {item.hint && (
132
+ <span className="ws-nav-hint">{item.hint}</span>
133
+ )}
134
+ </Link>
135
+ </li>
136
+ ))}
137
+ </ul>
138
+ </section>
139
+ ))}
140
+ </div>
141
+ </nav>
142
+ );
143
+ }
144
+
145
+ /** Props of {@link WsReference}. */
146
+ export interface WsReferenceProps {
147
+ /** The AsyncAPI document. */
148
+ document: SpecDocument;
149
+ /** The extension prefixes to read; defaults to the configured ones. */
150
+ extensions?: readonly string[];
151
+ /** The subprotocol shown when the document states none. */
152
+ defaultSubprotocol?: string;
153
+ /** Labels and meanings for the limits, when the embedding viewer knows them. */
154
+ limitsInfo?: Readonly<Record<string, LimitInfo>>;
155
+ /** The console the try-it cards open (the jobs UI's Events console), when the host has one. */
156
+ console?: WsConsole;
157
+ /** Answers "does the caller hold this permission" (the have/lack markers). */
158
+ probe?: PermissionProbe;
159
+ /** The host's contract for the try-it's address fields. */
160
+ address?: WsAddressOptions;
161
+ /** The docs section's path within the host app ("" for a standalone viewer). */
162
+ pathBase?: string;
163
+ }
164
+
165
+ /** The reference for a loaded document. */
166
+ export function WsReference({
167
+ document: spec,
168
+ extensions,
169
+ defaultSubprotocol,
170
+ limitsInfo,
171
+ console: consoleOptions,
172
+ probe,
173
+ address,
174
+ pathBase = "",
175
+ }: WsReferenceProps) {
176
+ const paneOptions: WsPaneOptions = {
177
+ console: consoleOptions,
178
+ probe,
179
+ address,
180
+ pathBase,
181
+ };
182
+ const doc = useMemo(
183
+ () => readWsDoc(spec, { extensions, defaultSubprotocol }),
184
+ [spec, extensions, defaultSubprotocol],
185
+ );
186
+ const groups = useMemo(() => navGroups(doc), [doc]);
187
+ const { item } = useParams<{ item?: string }>();
188
+ const [query, setQuery] = useQueryParam("q");
189
+
190
+ const connection = doc.channels.find((channel) => channel.isConnection);
191
+ const slug = item ?? connection?.slug ?? doc.channels[0]?.slug ?? "";
192
+ const selection = selectItem(doc, slug);
193
+
194
+ let pane;
195
+ if (selection === null) {
196
+ pane = (
197
+ <EmptyState
198
+ title="No such item"
199
+ description={
200
+ <>
201
+ This document has nothing called <code>{slug}</code>.{" "}
202
+ <Link to={`${pathBase}/ws`}>Back to the connection</Link>.
203
+ </>
204
+ }
205
+ />
206
+ );
207
+ } else {
208
+ switch (selection.kind) {
209
+ case "channel":
210
+ pane = (
211
+ <ChannelPane
212
+ doc={doc}
213
+ root={spec}
214
+ channel={selection.channel}
215
+ limitsInfo={limitsInfo}
216
+ {...paneOptions}
217
+ />
218
+ );
219
+ break;
220
+ case "operation":
221
+ pane = (
222
+ <OperationPane
223
+ doc={doc}
224
+ operation={selection.operation}
225
+ {...paneOptions}
226
+ />
227
+ );
228
+ break;
229
+ case "message":
230
+ pane = (
231
+ <MessagePane
232
+ doc={doc}
233
+ root={spec}
234
+ message={selection.message}
235
+ {...paneOptions}
236
+ />
237
+ );
238
+ break;
239
+ case "limits":
240
+ pane = doc.limits && (
241
+ <LimitsPanel
242
+ limits={doc.limits}
243
+ extensionKey={doc.extensionKeys.limits}
244
+ limitsInfo={limitsInfo}
245
+ />
246
+ );
247
+ break;
248
+ case "close-codes":
249
+ pane = <CloseCodesPanel doc={doc} />;
250
+ break;
251
+ case "upgrade-refusals":
252
+ pane = <RefusalsPanel doc={doc} />;
253
+ break;
254
+ }
255
+ }
256
+
257
+ return (
258
+ <div
259
+ className="screen ws-docs"
260
+ data-testid="ws-docs"
261
+ >
262
+ <DocHeader doc={doc} />
263
+ <div className="ws-overview">
264
+ <ServerPanel doc={doc} />
265
+ <SecurityPanel doc={doc} />
266
+ </div>
267
+ <div className="ws-layout">
268
+ <WsSidebar
269
+ groups={groups}
270
+ selected={selection ? slug : ""}
271
+ query={query}
272
+ onQuery={setQuery}
273
+ pathBase={pathBase}
274
+ />
275
+ <section
276
+ className="ws-main"
277
+ aria-label="Selected item"
278
+ data-testid="ws-main"
279
+ data-selected={selection ? slug : ""}
280
+ >
281
+ {pane}
282
+ </section>
283
+ </div>
284
+ </div>
285
+ );
286
+ }