@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,407 @@
1
+ import type { PermissionProbe } from "../permissions";
2
+ import type { SchemaRoot } from "../schema/resolve";
3
+ import type { DocOperation, DocParameter, DocResponse } from "./model";
4
+ import { Badge } from "../../../components/Badge";
5
+ import { CopyButton } from "../../../components/CopyButton";
6
+ import { useDocsUiConfig } from "../../../context";
7
+ import { Prose } from "../schema/Prose";
8
+ import { SchemaTree } from "../schema/SchemaTree";
9
+ import { methodTone } from "./model";
10
+ import { TryItPanel } from "./TryItPanel";
11
+
12
+ /** Props of {@link OperationView}. */
13
+ export interface OperationViewProps {
14
+ /** The operation shown. */
15
+ operation: DocOperation;
16
+ /** The document. */
17
+ root: SchemaRoot;
18
+ /** Opens a component schema (`#/components/schemas/X`) in the side panel. */
19
+ onRef: (ref: string, name: string) => void;
20
+ /** Answers "does the caller hold this permission" (the have/lack badge). Absent: the permission renders plainly. */
21
+ probe?: PermissionProbe;
22
+ /** Whether the API is read-only (a mutation's panel and marker say so). Defaults to `false`. */
23
+ readOnly?: boolean;
24
+ /** After a successful mutation (the host refreshes what it shows). */
25
+ onChanged?: () => void;
26
+ }
27
+
28
+ /** A method badge. */
29
+ export function MethodBadge({
30
+ method,
31
+ }: {
32
+ /** Upper-case method. */ method: string;
33
+ }) {
34
+ return (
35
+ <Badge
36
+ tone={methodTone(method)}
37
+ className="http-method"
38
+ >
39
+ {method}
40
+ </Badge>
41
+ );
42
+ }
43
+
44
+ /** How an array query parameter is sent, from `style`/`explode`. */
45
+ function styleNote(parameter: DocParameter): string | undefined {
46
+ if (parameter.in !== "query" || parameter.style === undefined) {
47
+ return undefined;
48
+ }
49
+ if (parameter.style === "form" && parameter.explode !== false) {
50
+ return "Repeat the key for each value (state=a&state=b).";
51
+ }
52
+ return `style: ${parameter.style}${parameter.explode === undefined ? "" : `, explode: ${String(parameter.explode)}`}`;
53
+ }
54
+
55
+ /** The parameters, grouped by where they go. */
56
+ function Parameters({ operation, root, onRef }: OperationViewProps) {
57
+ if (operation.parameters.length === 0) {
58
+ return null;
59
+ }
60
+ return (
61
+ <section
62
+ className="http-section"
63
+ aria-labelledby={`params-${operation.id}`}
64
+ >
65
+ <h3 id={`params-${operation.id}`}>Parameters</h3>
66
+ <ul
67
+ className="http-params"
68
+ data-testid="op-parameters"
69
+ >
70
+ {operation.parameters.map((parameter) => {
71
+ const note = styleNote(parameter);
72
+ return (
73
+ <li
74
+ key={`${parameter.in}:${parameter.name}`}
75
+ className="http-param"
76
+ data-param={parameter.name}
77
+ >
78
+ <div className="http-param-head">
79
+ <code className="http-param-name">{parameter.name}</code>
80
+ <Badge tone="neutral">{parameter.in}</Badge>
81
+ {parameter.required ? (
82
+ <span className="schema-required">required</span>
83
+ ) : (
84
+ <span className="http-muted">optional</span>
85
+ )}
86
+ {parameter.deprecated && (
87
+ <Badge tone="warning">deprecated</Badge>
88
+ )}
89
+ </div>
90
+ {parameter.description && (
91
+ <div className="http-param-description">
92
+ <Prose text={parameter.description} />
93
+ </div>
94
+ )}
95
+ {note && <p className="http-muted http-param-style">{note}</p>}
96
+ <SchemaTree
97
+ schema={parameter.schema}
98
+ root={root}
99
+ depth={0}
100
+ onRef={onRef}
101
+ label={`Schema of ${parameter.name}`}
102
+ />
103
+ </li>
104
+ );
105
+ })}
106
+ </ul>
107
+ </section>
108
+ );
109
+ }
110
+
111
+ /** The request body. */
112
+ function RequestBody({ operation, root, onRef }: OperationViewProps) {
113
+ const body = operation.requestBody;
114
+ if (!body) {
115
+ return null;
116
+ }
117
+ return (
118
+ <section
119
+ className="http-section"
120
+ aria-labelledby={`body-${operation.id}`}
121
+ data-testid="op-body"
122
+ >
123
+ <h3 id={`body-${operation.id}`}>
124
+ Request body{" "}
125
+ <span className="http-muted">
126
+ {body.required ? "(required)" : "(optional)"}
127
+ </span>
128
+ </h3>
129
+ {body.description && <Prose text={body.description} />}
130
+ {body.content.map((media) => (
131
+ <div
132
+ key={media.contentType}
133
+ className="http-media"
134
+ >
135
+ <code className="http-media-type">{media.contentType}</code>
136
+ {media.schema !== undefined && (
137
+ <SchemaTree
138
+ schema={media.schema}
139
+ root={root}
140
+ depth={2}
141
+ onRef={onRef}
142
+ label={`Request body schema (${media.contentType})`}
143
+ />
144
+ )}
145
+ </div>
146
+ ))}
147
+ </section>
148
+ );
149
+ }
150
+
151
+ /** A status badge's tone. */
152
+ function statusTone(
153
+ status: string,
154
+ ): "success" | "warning" | "danger" | "neutral" {
155
+ if (status.startsWith("2")) {
156
+ return "success";
157
+ }
158
+ if (status.startsWith("4")) {
159
+ return "warning";
160
+ }
161
+ if (status.startsWith("5")) {
162
+ return "danger";
163
+ }
164
+ return "neutral";
165
+ }
166
+
167
+ /** One response. */
168
+ function ResponseItem({
169
+ response,
170
+ root,
171
+ onRef,
172
+ }: {
173
+ /** The response. */
174
+ response: DocResponse;
175
+ /** The document. */
176
+ root: SchemaRoot;
177
+ /** Opens a component. */
178
+ onRef: OperationViewProps["onRef"];
179
+ }) {
180
+ const success = response.status.startsWith("2");
181
+ return (
182
+ <li
183
+ className="http-response"
184
+ data-status={response.status}
185
+ >
186
+ <div className="http-response-head">
187
+ <Badge tone={statusTone(response.status)}>{response.status}</Badge>
188
+ <span>{response.description}</span>
189
+ </div>
190
+ {response.codes.length > 0 && (
191
+ <ul
192
+ className="http-codes"
193
+ aria-label={`Problem codes for ${response.status}`}
194
+ >
195
+ {response.codes.map((code) => (
196
+ <li key={code}>
197
+ <code>{code}</code>
198
+ </li>
199
+ ))}
200
+ </ul>
201
+ )}
202
+ {response.content.map((media) => (
203
+ <div
204
+ key={media.contentType}
205
+ className="http-media"
206
+ >
207
+ <code className="http-media-type">{media.contentType}</code>
208
+ {media.schema !== undefined && (
209
+ <SchemaTree
210
+ schema={media.schema}
211
+ root={root}
212
+ depth={success ? 1 : 0}
213
+ onRef={onRef}
214
+ label={`Response ${response.status} schema (${media.contentType})`}
215
+ />
216
+ )}
217
+ </div>
218
+ ))}
219
+ </li>
220
+ );
221
+ }
222
+
223
+ /**
224
+ * One operation: method and path, summary and description, the permission
225
+ * it needs, whether it is a mutation, its CSRF rules and driver requirements,
226
+ * its parameters, body and responses (schemas as trees), and its try-it panel.
227
+ */
228
+ export function OperationView({
229
+ operation,
230
+ root,
231
+ onRef,
232
+ probe,
233
+ readOnly = false,
234
+ onChanged,
235
+ }: OperationViewProps) {
236
+ const config = useDocsUiConfig();
237
+ const held =
238
+ operation.action === undefined || probe === undefined
239
+ ? undefined
240
+ : probe(operation.action);
241
+ const csrf = operation.csrf;
242
+ return (
243
+ <article
244
+ className="http-operation"
245
+ aria-labelledby={`op-${operation.id}`}
246
+ data-testid="http-operation"
247
+ >
248
+ <header className="http-operation-head">
249
+ <div className="http-operation-line">
250
+ <MethodBadge method={operation.method} />
251
+ <code
252
+ className="http-operation-path"
253
+ data-testid="op-path"
254
+ >
255
+ {operation.path}
256
+ </code>
257
+ {operation.deprecated && <Badge tone="warning">deprecated</Badge>}
258
+ </div>
259
+ <h2
260
+ id={`op-${operation.id}`}
261
+ className="http-operation-title"
262
+ >
263
+ {operation.summary ?? operation.id}
264
+ </h2>
265
+ <p className="http-operation-id">
266
+ <span className="http-muted">operationId</span>{" "}
267
+ <code>{operation.id}</code>
268
+ <CopyButton
269
+ text={operation.id}
270
+ ariaLabel="Copy the operation id"
271
+ />
272
+ </p>
273
+ </header>
274
+
275
+ <div
276
+ className="http-markers"
277
+ aria-label="Access"
278
+ role="group"
279
+ >
280
+ {operation.action !== undefined &&
281
+ (probe === undefined ? (
282
+ <Badge
283
+ tone="neutral"
284
+ title="The permission the API's authorize hook is asked about."
285
+ >
286
+ <span data-testid="op-permission">
287
+ Needs <code>{operation.action}</code>
288
+ </span>
289
+ </Badge>
290
+ ) : (
291
+ <Badge
292
+ tone={held ? "success" : "danger"}
293
+ title="From the caller's permissions. authorize may still decide per target."
294
+ >
295
+ <span
296
+ data-testid="op-permission"
297
+ data-allowed={String(held ?? false)}
298
+ >
299
+ {held ? "You have " : "You lack "}
300
+ <code>{operation.action}</code>
301
+ </span>
302
+ </Badge>
303
+ ))}
304
+ <Badge
305
+ tone={operation.mutation ? "warning" : "neutral"}
306
+ title={
307
+ operation.mutation
308
+ ? "Changes state: CSRF checks apply, and a read-only API refuses it."
309
+ : "Reads only."
310
+ }
311
+ >
312
+ <span data-testid="op-mutation">
313
+ {operation.mutation ? "Mutation" : "Read"}
314
+ </span>
315
+ </Badge>
316
+ {/* Only for a stale or foreign document: a read-only API documents no mutation (see tryItGate). */}
317
+ {operation.mutation && readOnly && (
318
+ <Badge tone="danger">Refused: the API is read-only</Badge>
319
+ )}
320
+ {operation.requires.length > 0 && (
321
+ <span
322
+ className="http-requires"
323
+ data-testid="op-requires"
324
+ >
325
+ <span className="http-muted">Needs driver support:</span>{" "}
326
+ {operation.requires.map((method) => (
327
+ <code key={method}>{method}</code>
328
+ ))}
329
+ </span>
330
+ )}
331
+ </div>
332
+
333
+ {csrf && (
334
+ <div
335
+ className="http-csrf"
336
+ role="note"
337
+ data-testid="op-csrf"
338
+ >
339
+ {csrf.header !== null && (
340
+ <p>
341
+ Every mutation must carry the <code>{csrf.header}</code> header,
342
+ with any non-empty value.{" "}
343
+ {config.csrfHeader === csrf.header
344
+ ? "The app sends it."
345
+ : "This UI is not configured to send it."}
346
+ </p>
347
+ )}
348
+ {csrf.requireJson && (
349
+ <p>
350
+ Send <code>Content-Type: application/json</code>, even with no
351
+ body; any other media type is refused with 415.
352
+ </p>
353
+ )}
354
+ {csrf.header === null && !csrf.requireJson && (
355
+ <p>Cross-site requests are refused (Origin / Sec-Fetch-Site).</p>
356
+ )}
357
+ </div>
358
+ )}
359
+
360
+ {operation.description && (
361
+ <div className="http-description">
362
+ <Prose text={operation.description} />
363
+ </div>
364
+ )}
365
+
366
+ <Parameters
367
+ operation={operation}
368
+ root={root}
369
+ onRef={onRef}
370
+ />
371
+ <RequestBody
372
+ operation={operation}
373
+ root={root}
374
+ onRef={onRef}
375
+ />
376
+
377
+ <section
378
+ className="http-section"
379
+ aria-labelledby={`responses-${operation.id}`}
380
+ >
381
+ <h3 id={`responses-${operation.id}`}>Responses</h3>
382
+ <ul
383
+ className="http-responses"
384
+ data-testid="op-responses"
385
+ >
386
+ {operation.responses.map((response) => (
387
+ <ResponseItem
388
+ key={response.status}
389
+ response={response}
390
+ root={root}
391
+ onRef={onRef}
392
+ />
393
+ ))}
394
+ </ul>
395
+ </section>
396
+
397
+ <TryItPanel
398
+ key={operation.id}
399
+ operation={operation}
400
+ root={root}
401
+ readOnly={readOnly}
402
+ probe={probe}
403
+ onChanged={onChanged}
404
+ />
405
+ </article>
406
+ );
407
+ }
@@ -0,0 +1,190 @@
1
+ import type { KeyboardEvent } from "react";
2
+ import type { DocSection, DocTagGroup } from "./model";
3
+ import { useRef } from "react";
4
+ import { keyedSections } from "./model";
5
+ import { TextInput } from "../../../components/inputs";
6
+ import { arrowTarget } from "../../../components/listKeys";
7
+ import { SEARCH_SHORTCUT } from "../../../shortcuts";
8
+ import { Link } from "../../../router";
9
+ import { MethodBadge } from "./OperationView";
10
+
11
+ /** Props of {@link Sidebar}. */
12
+ export interface SidebarProps {
13
+ /** The groups to list, already filtered by the search. */
14
+ groups: readonly DocTagGroup[];
15
+ /**
16
+ * The document's sections over those groups (`x-tagGroups`), already
17
+ * filtered: each heads its tags, one level deeper. Default: none, and the
18
+ * groups are listed at the top level.
19
+ */
20
+ sections?: readonly DocSection[];
21
+ /** The search text (`?q=`). */
22
+ query: string;
23
+ /** Sets the search text. */
24
+ onQuery: (query: string) => void;
25
+ /** The link to an operation, keeping the search. */
26
+ hrefOf: (operationId: string) => string;
27
+ }
28
+
29
+ /** The links of a sidebar, in order. */
30
+ function links(root: HTMLElement | null): HTMLAnchorElement[] {
31
+ return root
32
+ ? Array.from(root.querySelectorAll<HTMLAnchorElement>("a.http-op-link"))
33
+ : [];
34
+ }
35
+
36
+ /** One tag's heading and its operations' links. */
37
+ function TagBlock({
38
+ group,
39
+ headingId,
40
+ level,
41
+ hrefOf,
42
+ }: {
43
+ /** The tag and its operations. */
44
+ group: DocTagGroup;
45
+ /** The heading's id, unique in the sidebar (a tag can be in two sections). */
46
+ headingId: string;
47
+ /** The heading's level: 2 at the top, 3 inside a section. */
48
+ level: 2 | 3;
49
+ /** The link to an operation. */
50
+ hrefOf: (operationId: string) => string;
51
+ }) {
52
+ const Heading = level === 2 ? "h2" : "h3";
53
+ return (
54
+ <section
55
+ className="http-tag"
56
+ aria-labelledby={headingId}
57
+ >
58
+ <Heading
59
+ id={headingId}
60
+ className="http-tag-name"
61
+ title={group.description}
62
+ >
63
+ {group.name}
64
+ </Heading>
65
+ <ul className="http-tag-ops">
66
+ {group.operations.map((operation) => (
67
+ <li key={operation.id}>
68
+ <Link
69
+ to={hrefOf(operation.id)}
70
+ className="http-op-link"
71
+ activeMatch="exact"
72
+ data-operation={operation.id}
73
+ >
74
+ <MethodBadge method={operation.method} />
75
+ <span className="http-op-text">
76
+ <span className="http-op-path">{operation.path}</span>
77
+ {operation.summary && (
78
+ <span className="http-op-summary">{operation.summary}</span>
79
+ )}
80
+ </span>
81
+ </Link>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+ </section>
86
+ );
87
+ }
88
+
89
+ /**
90
+ * The operations by tag (by section, then tag, when the document declares
91
+ * `x-tagGroups`), with method badges, and a search over path,
92
+ * operation id and summary. Arrow keys move between operations (Home/End to
93
+ * the ends), and ArrowDown from the search goes to the first match.
94
+ */
95
+ export function Sidebar({
96
+ groups,
97
+ sections,
98
+ query,
99
+ onQuery,
100
+ hrefOf,
101
+ }: SidebarProps) {
102
+ const listRef = useRef<HTMLDivElement>(null);
103
+ const count = groups.reduce((sum, group) => sum + group.operations.length, 0);
104
+
105
+ function onListKey(event: KeyboardEvent<HTMLDivElement>) {
106
+ const target = arrowTarget(
107
+ event.key,
108
+ links(listRef.current),
109
+ document.activeElement,
110
+ );
111
+ if (target) {
112
+ event.preventDefault();
113
+ target.focus();
114
+ }
115
+ }
116
+
117
+ return (
118
+ <nav
119
+ className="http-sidebar"
120
+ aria-label="Operations"
121
+ >
122
+ <TextInput
123
+ type="search"
124
+ value={query}
125
+ aria-label="Search operations"
126
+ placeholder="Search path, id, summary"
127
+ {...SEARCH_SHORTCUT}
128
+ onChange={(value) => onQuery(value)}
129
+ onKeyDown={(event) => {
130
+ if (event.key === "ArrowDown") {
131
+ const first = links(listRef.current)[0];
132
+ if (first) {
133
+ event.preventDefault();
134
+ first.focus();
135
+ }
136
+ }
137
+ }}
138
+ />
139
+ <p
140
+ className="http-muted http-sidebar-count"
141
+ aria-live="polite"
142
+ >
143
+ {count === 1 ? "1 operation" : `${count} operations`}
144
+ </p>
145
+ {/* Arrow keys move between the links; each link stays a tab stop too. */}
146
+ <div
147
+ ref={listRef}
148
+ className="http-sidebar-list"
149
+ onKeyDown={onListKey}
150
+ >
151
+ {sections === undefined
152
+ ? groups.map((group) => (
153
+ <TagBlock
154
+ key={group.name}
155
+ group={group}
156
+ headingId={`tag-${group.name}`}
157
+ level={2}
158
+ hrefOf={hrefOf}
159
+ />
160
+ ))
161
+ : keyedSections(sections).map(({ key, index, section }) => (
162
+ <section
163
+ key={key}
164
+ className="http-section"
165
+ aria-labelledby={`section-${index}`}
166
+ >
167
+ <h2
168
+ id={`section-${index}`}
169
+ className="http-section-name"
170
+ >
171
+ {section.name}
172
+ </h2>
173
+ {section.tags.map((group) => (
174
+ <TagBlock
175
+ key={group.name}
176
+ group={group}
177
+ headingId={`tag-${index}-${group.name}`}
178
+ level={3}
179
+ hrefOf={hrefOf}
180
+ />
181
+ ))}
182
+ </section>
183
+ ))}
184
+ {groups.length === 0 && (
185
+ <p className="http-muted">No operation matches.</p>
186
+ )}
187
+ </div>
188
+ </nav>
189
+ );
190
+ }