@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,786 @@
1
+ /**
2
+ * The WebSocket reference's main pane for one item: a channel (with its
3
+ * try-it when a console is given), an operation, or a message (with its
4
+ * payload and, for an event, its try-it).
5
+ *
6
+ * The try-it deep-links a console the host app injects as {@link WsConsole}
7
+ * (the jobs UI passes its Events console); a standalone viewer passes none
8
+ * and no try-it renders.
9
+ */
10
+ import type { ReactNode } from "react";
11
+ import type { SpecDocument } from "../../../api/docs";
12
+ import type { PermissionProbe } from "../permissions";
13
+ import type { LimitInfo } from "./format";
14
+ import type {
15
+ WsAddressOptions,
16
+ WsChannel,
17
+ WsDoc,
18
+ WsMessage,
19
+ WsOperation,
20
+ } from "./model";
21
+ import { useState } from "react";
22
+ import { Badge } from "../../../components/Badge";
23
+ import { Card } from "../../../components/Card";
24
+ import { CopyButton } from "../../../components/CopyButton";
25
+ import { Field } from "../../../components/Field";
26
+ import { TextInput } from "../../../components/inputs";
27
+ import { JsonView } from "../../../components/JsonView";
28
+ import { KeyValue } from "../../../components/KeyValue";
29
+ import { Table } from "../../../components/Table";
30
+ import { Link } from "../../../router";
31
+ import { Prose, SchemaTree } from "../schema";
32
+ import { InlineText } from "./InlineText";
33
+ import {
34
+ channelEventTypes,
35
+ channelPermissions,
36
+ channelsCarrying,
37
+ channelSubscribable,
38
+ eventPayloadSchema,
39
+ exampleEventPayload,
40
+ fillAddress,
41
+ itemPath,
42
+ operationsOn,
43
+ operationsUsing,
44
+ parameterProblem,
45
+ parameterSchemas,
46
+ tryProblems,
47
+ } from "./model";
48
+ import { CloseCodesPanel, LimitsPanel, RefusalsPanel } from "./panels";
49
+ import { PayloadSchema } from "./PayloadSchema";
50
+
51
+ /**
52
+ * A console the try-it cards deep-link (the jobs UI's Events console). The
53
+ * link builders answer `null` when the console cannot open what was asked.
54
+ */
55
+ export interface WsConsole {
56
+ /** Whether the console can be opened at all. */
57
+ available: boolean;
58
+ /** Its name in the UI copy ("Events console"). */
59
+ label: string;
60
+ /** Why it cannot be opened, shown when `available` is false. Defaults to a generic sentence. */
61
+ unavailableReason?: ReactNode;
62
+ /** Why a filled channel still cannot open, when the link builder said no. Defaults to a generic sentence. */
63
+ noChannelReason?: (channel: string) => ReactNode;
64
+ /** The link subscribing the console to `channel`, filtered to `types` (`[]` would be all of them). */
65
+ channelLink: (channel: string, types: readonly string[]) => string | null;
66
+ /** The link opening the console on an event message's channel, filtered to its type. */
67
+ messageLink: (message: WsMessage, doc: WsDoc) => string | null;
68
+ }
69
+
70
+ /** Options of the panes, threaded from {@link WsReference}. */
71
+ export interface WsPaneOptions {
72
+ /** The console the try-it cards open, when the host has one. */
73
+ console?: WsConsole;
74
+ /** Answers "does the caller hold this permission" (the have/lack markers). */
75
+ probe?: PermissionProbe;
76
+ /** The host's contract for the try-it's address fields. */
77
+ address?: WsAddressOptions;
78
+ /** The docs section's path within the host app ("" for a standalone viewer). */
79
+ pathBase?: string;
80
+ }
81
+
82
+ /** A permission with a have/lack marker when a probe is given, plain otherwise. */
83
+ export function PermissionMarker({
84
+ action,
85
+ probe,
86
+ }: {
87
+ action: string;
88
+ probe?: PermissionProbe;
89
+ }) {
90
+ if (probe === undefined) {
91
+ return (
92
+ <span
93
+ className="ws-permission"
94
+ data-testid={`ws-permission-${action}`}
95
+ >
96
+ <code>{action}</code> <Badge tone="neutral">permission</Badge>
97
+ </span>
98
+ );
99
+ }
100
+ const held = probe(action);
101
+ return (
102
+ <span
103
+ className="ws-permission"
104
+ data-testid={`ws-permission-${action}`}
105
+ data-has={held === undefined ? "unknown" : String(held)}
106
+ >
107
+ <code>{action}</code>{" "}
108
+ {held === undefined ? (
109
+ <Badge tone="neutral">Not an action this UI knows</Badge>
110
+ ) : (
111
+ <Badge tone={held ? "success" : "danger"}>
112
+ {held ? "You have this" : "You lack this"}
113
+ </Badge>
114
+ )}
115
+ </span>
116
+ );
117
+ }
118
+
119
+ /** Links to items by slug. */
120
+ function ItemLinks({
121
+ items,
122
+ pathBase = "",
123
+ empty = "None.",
124
+ }: {
125
+ items: readonly {
126
+ /** The item's slug. */
127
+ slug: string;
128
+ /** Its label. */
129
+ label: string;
130
+ }[];
131
+ /** The docs section's path within the host app. */
132
+ pathBase?: string;
133
+ empty?: string;
134
+ }) {
135
+ if (items.length === 0) {
136
+ return <span className="muted">{empty}</span>;
137
+ }
138
+ return (
139
+ <ul className="ws-links">
140
+ {items.map((item) => (
141
+ <li key={item.slug}>
142
+ <Link to={itemPath(item.slug, "", pathBase)}>
143
+ <code>{item.label}</code>
144
+ </Link>
145
+ </li>
146
+ ))}
147
+ </ul>
148
+ );
149
+ }
150
+
151
+ /** Why "try it" is not offered, when the console is not available. */
152
+ function ConsoleUnavailable({ console: target }: { console: WsConsole }) {
153
+ return (
154
+ <p
155
+ className="muted"
156
+ data-testid="ws-try-unavailable"
157
+ >
158
+ {target.unavailableReason ?? (
159
+ <>The {target.label} is not available here.</>
160
+ )}
161
+ </p>
162
+ );
163
+ }
164
+
165
+ /** Props of {@link ChannelTryIt}. */
166
+ interface ChannelTryItProps {
167
+ /** The document. */
168
+ doc: WsDoc;
169
+ /** The channel family. */
170
+ channel: WsChannel;
171
+ /** The console to open. */
172
+ console: WsConsole;
173
+ /** The host's contract for the address fields. */
174
+ address?: WsAddressOptions;
175
+ }
176
+
177
+ /** Try a channel: fill its address, then open the console subscribed to it. */
178
+ function ChannelTryIt({
179
+ doc,
180
+ channel,
181
+ console: target,
182
+ address,
183
+ }: ChannelTryItProps) {
184
+ const [values, setValues] = useState<Record<string, string>>({});
185
+ if (!target.available) {
186
+ return (
187
+ <Card title="Try it">
188
+ <ConsoleUnavailable console={target} />
189
+ </Card>
190
+ );
191
+ }
192
+ const filled = fillAddress(
193
+ channel.address,
194
+ values,
195
+ parameterSchemas(channel.parameters),
196
+ address,
197
+ );
198
+ const types = channelEventTypes(doc, channel);
199
+ const link = filled === null ? null : target.channelLink(filled, types);
200
+ const problems = tryProblems(channel, values, address);
201
+ return (
202
+ <Card title="Try it">
203
+ <div
204
+ className="ws-try"
205
+ data-testid="ws-try-channel"
206
+ >
207
+ <p className="muted">
208
+ Opens the {target.label} subscribed to this channel
209
+ {types.length > 0
210
+ ? `, showing the ${types.length} event types it carries`
211
+ : ""}
212
+ .
213
+ </p>
214
+ {channel.parameters.length > 0 && (
215
+ <div className="ws-try-fields">
216
+ {channel.parameters.map((parameter) => {
217
+ const value = values[parameter.name] ?? "";
218
+ const problem = parameterProblem(
219
+ parameter.name,
220
+ value,
221
+ parameter.schema,
222
+ address,
223
+ );
224
+ return (
225
+ <Field
226
+ key={parameter.name}
227
+ label={parameter.name}
228
+ hint={
229
+ parameter.description && (
230
+ <InlineText text={parameter.description} />
231
+ )
232
+ }
233
+ error={problem}
234
+ >
235
+ <TextInput
236
+ value={value}
237
+ onChange={(next) =>
238
+ setValues((current) => ({
239
+ ...current,
240
+ [parameter.name]: next,
241
+ }))
242
+ }
243
+ autoComplete="off"
244
+ spellCheck={false}
245
+ />
246
+ </Field>
247
+ );
248
+ })}
249
+ </div>
250
+ )}
251
+ <p className="ws-try-channel">
252
+ Channel:{" "}
253
+ {filled === null ? (
254
+ <span className="muted">fill in every parameter</span>
255
+ ) : (
256
+ <code data-testid="ws-try-address">{filled}</code>
257
+ )}
258
+ </p>
259
+ {link === null ? (
260
+ <>
261
+ <span
262
+ className="btn btn-primary ws-disabled"
263
+ aria-disabled="true"
264
+ aria-describedby={`ws-try-reason-${channel.key}`}
265
+ data-testid="ws-try-disabled"
266
+ >
267
+ Open in the {target.label}
268
+ </span>
269
+ <ul
270
+ className="ws-try-reason muted"
271
+ id={`ws-try-reason-${channel.key}`}
272
+ data-testid="ws-try-reason"
273
+ >
274
+ {problems.length > 0 ? (
275
+ problems.map((problem) => <li key={problem}>{problem}</li>)
276
+ ) : (
277
+ <li>
278
+ {target.noChannelReason?.(filled ?? channel.address) ?? (
279
+ <>
280
+ The {target.label} cannot open{" "}
281
+ <code>{filled ?? channel.address}</code>.
282
+ </>
283
+ )}
284
+ </li>
285
+ )}
286
+ </ul>
287
+ </>
288
+ ) : (
289
+ <Link
290
+ to={link}
291
+ className="btn btn-primary"
292
+ data-testid="ws-try-link"
293
+ >
294
+ Open in the {target.label}
295
+ </Link>
296
+ )}
297
+ </div>
298
+ </Card>
299
+ );
300
+ }
301
+
302
+ /** Props of {@link ChannelPane}. */
303
+ interface ChannelPaneProps extends WsPaneOptions {
304
+ /** The document, read. */
305
+ doc: WsDoc;
306
+ /** The raw document, for `$ref`s in parameter schemas. */
307
+ root: SpecDocument;
308
+ /** The channel. */
309
+ channel: WsChannel;
310
+ /** Labels and meanings for the limits, when the embedding viewer knows them. */
311
+ limitsInfo?: Readonly<Record<string, LimitInfo>>;
312
+ }
313
+
314
+ /** A channel: address, parameters, operations, messages, permission and try-it. */
315
+ export function ChannelPane({
316
+ doc,
317
+ root,
318
+ channel,
319
+ limitsInfo,
320
+ console: target,
321
+ probe,
322
+ address,
323
+ pathBase,
324
+ }: ChannelPaneProps) {
325
+ const operations = operationsOn(doc, channel.key);
326
+ const permissions = channelPermissions(doc, channel.key);
327
+ const messages = channel.messages.map((key) => {
328
+ const message = doc.messages.find((candidate) => candidate.key === key);
329
+ return { slug: message?.slug ?? `message-${key}`, label: key };
330
+ });
331
+ return (
332
+ <div
333
+ className="ws-pane"
334
+ data-testid={`ws-pane-${channel.slug}`}
335
+ >
336
+ <Card
337
+ title={
338
+ <>
339
+ {channel.title} <Badge tone="info">channel</Badge>
340
+ </>
341
+ }
342
+ >
343
+ <KeyValue
344
+ items={[
345
+ {
346
+ label: channel.isConnection ? "Path" : "Address",
347
+ value: (
348
+ <code data-testid="ws-channel-address">{channel.address}</code>
349
+ ),
350
+ hint: channel.isConnection
351
+ ? "The socket itself: control messages travel here."
352
+ : "A logical channel: subscribe with this name over the connection.",
353
+ },
354
+ channel.bindingMethod !== undefined && {
355
+ label: "Upgrade",
356
+ value: <code>{channel.bindingMethod}</code>,
357
+ },
358
+ permissions.length > 0 && {
359
+ label: permissions.length === 1 ? "Permission" : "Permissions",
360
+ value: (
361
+ <span className="ws-permissions">
362
+ {permissions.map((action) => (
363
+ <PermissionMarker
364
+ key={action}
365
+ action={action}
366
+ probe={probe}
367
+ />
368
+ ))}
369
+ </span>
370
+ ),
371
+ },
372
+ ]}
373
+ />
374
+ {channel.description && <Prose text={channel.description} />}
375
+ </Card>
376
+ {channel.parameters.length > 0 && (
377
+ <Card title="Parameters">
378
+ <Table label="Parameters">
379
+ <thead>
380
+ <tr>
381
+ <th scope="col">Name</th>
382
+ <th scope="col">Description</th>
383
+ <th scope="col">Value</th>
384
+ </tr>
385
+ </thead>
386
+ <tbody>
387
+ {channel.parameters.map((parameter) => (
388
+ <tr key={parameter.name}>
389
+ <td>
390
+ <code>{`{${parameter.name}}`}</code>
391
+ </td>
392
+ <td>
393
+ {parameter.description && (
394
+ <InlineText text={parameter.description} />
395
+ )}
396
+ </td>
397
+ <td data-testid={`ws-parameter-schema-${parameter.name}`}>
398
+ {parameter.schema ? (
399
+ <SchemaTree
400
+ schema={parameter.schema}
401
+ root={root}
402
+ label={`Schema of {${parameter.name}}`}
403
+ />
404
+ ) : (
405
+ <span className="muted">Any string</span>
406
+ )}
407
+ </td>
408
+ </tr>
409
+ ))}
410
+ </tbody>
411
+ </Table>
412
+ </Card>
413
+ )}
414
+ <Card title="Operations">
415
+ <ItemLinks
416
+ items={operations.map((operation) => ({
417
+ slug: operation.slug,
418
+ label: `${operation.action ?? "?"} ${operation.key}`,
419
+ }))}
420
+ pathBase={pathBase}
421
+ />
422
+ </Card>
423
+ <Card title={`Messages (${messages.length})`}>
424
+ <ItemLinks
425
+ items={messages}
426
+ pathBase={pathBase}
427
+ />
428
+ </Card>
429
+ {/* Only a channel there is something to subscribe to, and only with a console: the connection has no try-it. */}
430
+ {target !== undefined && channelSubscribable(channel) && (
431
+ <ChannelTryIt
432
+ key={channel.key}
433
+ doc={doc}
434
+ channel={channel}
435
+ console={target}
436
+ address={address}
437
+ />
438
+ )}
439
+ {channel.isConnection && doc.limits && (
440
+ <LimitsPanel
441
+ limits={doc.limits}
442
+ extensionKey={doc.extensionKeys.limits}
443
+ limitsInfo={limitsInfo}
444
+ />
445
+ )}
446
+ {channel.isConnection && doc.closeCodes.length > 0 && (
447
+ <CloseCodesPanel doc={doc} />
448
+ )}
449
+ {channel.isConnection && doc.refusals.length > 0 && (
450
+ <RefusalsPanel doc={doc} />
451
+ )}
452
+ </div>
453
+ );
454
+ }
455
+
456
+ /** Message links for ids, labelled by id. */
457
+ function messageLinks(doc: WsDoc, ids: readonly string[]) {
458
+ return ids.map((key) => ({
459
+ slug:
460
+ doc.messages.find((message) => message.key === key)?.slug ??
461
+ `message-${key}`,
462
+ label: key,
463
+ }));
464
+ }
465
+
466
+ /** An operation: direction, channel, messages, reply and permission. */
467
+ export function OperationPane({
468
+ doc,
469
+ operation,
470
+ probe,
471
+ pathBase,
472
+ }: WsPaneOptions & {
473
+ doc: WsDoc;
474
+ operation: WsOperation;
475
+ }) {
476
+ const channel = doc.channels.find(
477
+ (candidate) => candidate.key === operation.channel,
478
+ );
479
+ const replyChannel = doc.channels.find(
480
+ (candidate) => candidate.key === operation.reply?.channel,
481
+ );
482
+ return (
483
+ <div
484
+ className="ws-pane"
485
+ data-testid={`ws-pane-${operation.slug}`}
486
+ >
487
+ <Card
488
+ title={
489
+ <>
490
+ {operation.title} <Badge tone="accent">operation</Badge>
491
+ </>
492
+ }
493
+ >
494
+ <KeyValue
495
+ items={[
496
+ {
497
+ label: "Direction",
498
+ value: operation.action && (
499
+ <span data-testid="ws-operation-action">
500
+ <Badge
501
+ tone={operation.action === "send" ? "info" : "success"}
502
+ >
503
+ {operation.action}
504
+ </Badge>{" "}
505
+ {operation.action === "send"
506
+ ? "client → server"
507
+ : "server → client"}
508
+ </span>
509
+ ),
510
+ },
511
+ {
512
+ label: "Channel",
513
+ value: channel && (
514
+ <Link to={itemPath(channel.slug, "", pathBase)}>
515
+ <code>{channel.address}</code>
516
+ </Link>
517
+ ),
518
+ },
519
+ operation.permission !== undefined && {
520
+ label: "Permission",
521
+ value: (
522
+ <PermissionMarker
523
+ action={operation.permission}
524
+ probe={probe}
525
+ />
526
+ ),
527
+ },
528
+ operation.summary !== undefined && {
529
+ label: "Summary",
530
+ value: <InlineText text={operation.summary} />,
531
+ },
532
+ ]}
533
+ />
534
+ </Card>
535
+ <Card title={operation.action === "send" ? "Sends" : "Receives"}>
536
+ <ItemLinks
537
+ items={messageLinks(doc, operation.messages)}
538
+ pathBase={pathBase}
539
+ />
540
+ </Card>
541
+ {operation.reply && (
542
+ <Card title="Reply">
543
+ <div data-testid="ws-operation-reply">
544
+ <p className="muted">
545
+ The server answers on{" "}
546
+ {replyChannel ? (
547
+ <Link to={itemPath(replyChannel.slug, "", pathBase)}>
548
+ <code>{replyChannel.address}</code>
549
+ </Link>
550
+ ) : (
551
+ <code>{operation.reply.channel ?? "?"}</code>
552
+ )}{" "}
553
+ with one of:
554
+ </p>
555
+ <ItemLinks
556
+ items={messageLinks(doc, operation.reply.messages)}
557
+ pathBase={pathBase}
558
+ />
559
+ </div>
560
+ </Card>
561
+ )}
562
+ </div>
563
+ );
564
+ }
565
+
566
+ /** Props of {@link MessagePane}. */
567
+ interface MessagePaneProps extends WsPaneOptions {
568
+ /** The document, read. */
569
+ doc: WsDoc;
570
+ /** The raw document, for `$ref`s. */
571
+ root: SpecDocument;
572
+ /** The message. */
573
+ message: WsMessage;
574
+ }
575
+
576
+ /** A message: name, summary, where it travels, its payload schema, examples and (for an event) try-it. */
577
+ export function MessagePane({
578
+ doc,
579
+ root,
580
+ message,
581
+ console: target,
582
+ pathBase,
583
+ }: MessagePaneProps) {
584
+ const channels = channelsCarrying(doc, message.key);
585
+ const operations = operationsUsing(doc, message.key);
586
+ const eventPayload = message.event
587
+ ? eventPayloadSchema(message.payload, root)
588
+ : undefined;
589
+ const link =
590
+ target !== undefined && message.event !== null
591
+ ? target.messageLink(message, doc)
592
+ : null;
593
+ return (
594
+ <div
595
+ className="ws-pane"
596
+ data-testid={`ws-pane-${message.slug}`}
597
+ >
598
+ <Card
599
+ title={
600
+ <>
601
+ {message.title ?? message.name}{" "}
602
+ <Badge tone={message.event ? "success" : "neutral"}>
603
+ {message.event
604
+ ? `${message.event.kind} event`
605
+ : "control message"}
606
+ </Badge>
607
+ </>
608
+ }
609
+ >
610
+ <KeyValue
611
+ items={[
612
+ {
613
+ label: "Name",
614
+ value: <code data-testid="ws-message-name">{message.name}</code>,
615
+ },
616
+ message.summary !== undefined && {
617
+ label: "Summary",
618
+ value: <InlineText text={message.summary} />,
619
+ },
620
+ message.contentType !== undefined && {
621
+ label: "Content type",
622
+ value: <code>{message.contentType}</code>,
623
+ },
624
+ {
625
+ label: "Channels",
626
+ value: (
627
+ <ItemLinks
628
+ items={channels.map((channel) => ({
629
+ slug: channel.slug,
630
+ label: channel.address,
631
+ }))}
632
+ pathBase={pathBase}
633
+ />
634
+ ),
635
+ },
636
+ {
637
+ label: "Operations",
638
+ value: (
639
+ <ItemLinks
640
+ items={operations.map((operation) => ({
641
+ slug: operation.slug,
642
+ label: operation.key,
643
+ }))}
644
+ pathBase={pathBase}
645
+ />
646
+ ),
647
+ },
648
+ ]}
649
+ />
650
+ </Card>
651
+ {message.event && eventPayload !== undefined && (
652
+ <Card
653
+ title="Event payload"
654
+ className="ws-highlight"
655
+ >
656
+ <p className="muted">
657
+ <code>event.payload</code>: what this {message.event.kind} event
658
+ says. It arrives inside the envelope below.
659
+ </p>
660
+ <div data-testid="ws-event-payload">
661
+ <PayloadSchema
662
+ schema={eventPayload}
663
+ root={root}
664
+ label={`Payload of ${message.name}`}
665
+ />
666
+ </div>
667
+ </Card>
668
+ )}
669
+ <Card title={message.event ? "Full envelope" : "Payload"}>
670
+ <div data-testid="ws-message-payload">
671
+ <PayloadSchema
672
+ schema={message.payload}
673
+ root={root}
674
+ label={`Schema of ${message.name}`}
675
+ depth={message.event ? 1 : 2}
676
+ />
677
+ </div>
678
+ </Card>
679
+ {message.examples.length > 0 && (
680
+ <Card title={message.examples.length === 1 ? "Example" : "Examples"}>
681
+ {message.examples.map((example, index) => (
682
+ <MessageExample
683
+ // eslint-disable-next-line react/no-array-index-key -- the document's examples, in its order
684
+ key={index}
685
+ example={example}
686
+ index={index}
687
+ isEvent={message.event !== null}
688
+ />
689
+ ))}
690
+ </Card>
691
+ )}
692
+ {message.event && target !== undefined && (
693
+ <Card title="Try it">
694
+ {!target.available ? (
695
+ <ConsoleUnavailable console={target} />
696
+ ) : link === null ? (
697
+ <p className="muted">No channel in this document carries it.</p>
698
+ ) : (
699
+ <div className="ws-try">
700
+ <p className="muted">
701
+ Opens the {target.label} on every {message.event.kind}, showing
702
+ only <code>{message.event.type}</code> events.
703
+ </p>
704
+ <Link
705
+ to={link}
706
+ className="btn btn-primary"
707
+ data-testid="ws-try-link"
708
+ >
709
+ Watch {message.event.type} events
710
+ </Link>
711
+ </div>
712
+ )}
713
+ </Card>
714
+ )}
715
+ </div>
716
+ );
717
+ }
718
+
719
+ /** Props of {@link MessageExample}. */
720
+ interface MessageExampleProps {
721
+ /** The example, as the document gives it. */
722
+ example: WsMessage["examples"][number];
723
+ /** Its position, naming it when it has no name. */
724
+ index: number;
725
+ /** Whether the message is an event frame (so its `event.payload` is shown first). */
726
+ isEvent: boolean;
727
+ }
728
+
729
+ /**
730
+ * One message example: its name and summary, then the frame. For an event,
731
+ * the frame's `event.payload` (what the event says) comes first, open and
732
+ * highlighted, and the whole frame below it; the copy button copies the
733
+ * whole frame, exactly as it travels.
734
+ */
735
+ function MessageExample({ example, index, isEvent }: MessageExampleProps) {
736
+ const name = example.name ?? `Example ${index + 1}`;
737
+ const eventPayload = isEvent
738
+ ? exampleEventPayload(example.payload)
739
+ : undefined;
740
+ return (
741
+ <div
742
+ className="ws-example"
743
+ data-testid="ws-example"
744
+ >
745
+ <div className="ws-example-head">
746
+ <strong data-testid="ws-example-name">{name}</strong>
747
+ <CopyButton
748
+ text={() => JSON.stringify(example.payload, null, 2)}
749
+ label="Copy JSON"
750
+ ariaLabel={`Copy example ${name} as JSON`}
751
+ />
752
+ </div>
753
+ {example.summary && (
754
+ <p
755
+ className="muted"
756
+ data-testid="ws-example-summary"
757
+ >
758
+ <InlineText text={example.summary} />
759
+ </p>
760
+ )}
761
+ {eventPayload !== undefined && (
762
+ <div
763
+ className="ws-example-event ws-highlight"
764
+ data-testid="ws-example-event-payload"
765
+ >
766
+ <p className="muted">
767
+ <code>event.payload</code>
768
+ </p>
769
+ <JsonView
770
+ value={eventPayload}
771
+ label={`event.payload of ${name}`}
772
+ expandDepth={4}
773
+ />
774
+ </div>
775
+ )}
776
+ <div data-testid="ws-example-frame">
777
+ {eventPayload !== undefined && <p className="muted">The whole frame</p>}
778
+ <JsonView
779
+ value={example.payload}
780
+ label={`Example ${name}`}
781
+ expandDepth={eventPayload !== undefined ? 1 : 2}
782
+ />
783
+ </div>
784
+ </div>
785
+ );
786
+ }