@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,338 @@
1
+ /**
2
+ * JSON parsing that says *where* it failed. `JSON.parse` does the parsing;
3
+ * only when it throws does a small scanner walk the text to find the
4
+ * offending position, because engines disagree on whether (and how) their
5
+ * messages carry one — JavaScriptCore's carry none at all.
6
+ */
7
+
8
+ /** Where and why a JSON text failed to parse. */
9
+ export interface JsonParseError {
10
+ /** What is wrong, e.g. "Expected ',' or '}'". */
11
+ message: string;
12
+ /** 1-based line. */
13
+ line: number;
14
+ /** 1-based column (in UTF-16 code units). */
15
+ column: number;
16
+ /** 0-based offset into the text. */
17
+ offset: number;
18
+ }
19
+
20
+ /** The outcome of {@link parseJsonText}. */
21
+ export type JsonParseResult =
22
+ | {
23
+ /** It parsed. */
24
+ ok: true;
25
+ /** The parsed value. */
26
+ value: unknown;
27
+ }
28
+ | {
29
+ /** It did not parse. */
30
+ ok: false;
31
+ /** Where and why. */
32
+ error: JsonParseError;
33
+ };
34
+
35
+ /** Thrown inside the scanner with the failing offset. */
36
+ class ScanError extends Error {
37
+ /** 0-based offset. */
38
+ readonly offset: number;
39
+
40
+ constructor(message: string, offset: number) {
41
+ super(message);
42
+ this.offset = offset;
43
+ }
44
+ }
45
+
46
+ /** Describes the character at `index` for a message. */
47
+ function describe(text: string, index: number): string {
48
+ return index >= text.length
49
+ ? "end of input"
50
+ : `'${text[index] === "\n" ? "\\n" : text[index]}'`;
51
+ }
52
+
53
+ /**
54
+ * Walks `text` as JSON (RFC 8259) and throws a {@link ScanError} at the
55
+ * first position that is not valid. Returns normally for valid JSON.
56
+ */
57
+ function scan(text: string): void {
58
+ let index = 0;
59
+
60
+ const skipWhitespace = () => {
61
+ while (
62
+ index < text.length &&
63
+ (text[index] === " " ||
64
+ text[index] === "\t" ||
65
+ text[index] === "\n" ||
66
+ text[index] === "\r")
67
+ ) {
68
+ index++;
69
+ }
70
+ };
71
+
72
+ const fail = (expected: string): never => {
73
+ throw new ScanError(
74
+ `Expected ${expected} but found ${describe(text, index)}`,
75
+ index,
76
+ );
77
+ };
78
+
79
+ const literal = (word: string) => {
80
+ for (let i = 0; i < word.length; i++) {
81
+ if (text[index] !== word[i]) {
82
+ fail(`'${word}'`);
83
+ }
84
+ index++;
85
+ }
86
+ };
87
+
88
+ const string = () => {
89
+ index++; // opening quote
90
+ while (index < text.length) {
91
+ const char = text[index];
92
+ if (char === '"') {
93
+ index++;
94
+ return;
95
+ }
96
+ if (char === "\\") {
97
+ const next = text[index + 1];
98
+ if (next === "u") {
99
+ if (!/^[0-9a-f]{4}$/i.test(text.slice(index + 2, index + 6))) {
100
+ index += 2;
101
+ fail("four hex digits after \\u");
102
+ }
103
+ index += 6;
104
+ continue;
105
+ }
106
+ if (next === undefined || !'"\\/bfnrt'.includes(next)) {
107
+ index++;
108
+ fail("a valid escape");
109
+ }
110
+ index += 2;
111
+ continue;
112
+ }
113
+ if (char < " ") {
114
+ throw new ScanError(
115
+ "Control characters must be escaped in strings",
116
+ index,
117
+ );
118
+ }
119
+ index++;
120
+ }
121
+ throw new ScanError("Unterminated string", index);
122
+ };
123
+
124
+ const number = () => {
125
+ const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:e[+-]?\d+)?/i.exec(
126
+ text.slice(index, index + 400),
127
+ );
128
+ if (!match) {
129
+ fail("a number");
130
+ }
131
+ index += match![0].length;
132
+ };
133
+
134
+ const value = (): void => {
135
+ skipWhitespace();
136
+ const char = text[index];
137
+ if (char === "{") {
138
+ index++;
139
+ skipWhitespace();
140
+ if (text[index] === "}") {
141
+ index++;
142
+ return;
143
+ }
144
+ for (;;) {
145
+ skipWhitespace();
146
+ if (text[index] !== '"') {
147
+ fail("a property name in double quotes");
148
+ }
149
+ string();
150
+ skipWhitespace();
151
+ if (text[index] !== ":") {
152
+ fail("':'");
153
+ }
154
+ index++;
155
+ value();
156
+ skipWhitespace();
157
+ if (text[index] === ",") {
158
+ index++;
159
+ continue;
160
+ }
161
+ if (text[index] === "}") {
162
+ index++;
163
+ return;
164
+ }
165
+ fail("',' or '}'");
166
+ }
167
+ }
168
+ if (char === "[") {
169
+ index++;
170
+ skipWhitespace();
171
+ if (text[index] === "]") {
172
+ index++;
173
+ return;
174
+ }
175
+ for (;;) {
176
+ value();
177
+ skipWhitespace();
178
+ if (text[index] === ",") {
179
+ index++;
180
+ continue;
181
+ }
182
+ if (text[index] === "]") {
183
+ index++;
184
+ return;
185
+ }
186
+ fail("',' or ']'");
187
+ }
188
+ }
189
+ if (char === '"') {
190
+ string();
191
+ return;
192
+ }
193
+ if (char === "t") {
194
+ literal("true");
195
+ return;
196
+ }
197
+ if (char === "f") {
198
+ literal("false");
199
+ return;
200
+ }
201
+ if (char === "n") {
202
+ literal("null");
203
+ return;
204
+ }
205
+ if (char === "-" || (char !== undefined && char >= "0" && char <= "9")) {
206
+ number();
207
+ return;
208
+ }
209
+ fail("a value");
210
+ };
211
+
212
+ value();
213
+ skipWhitespace();
214
+ if (index < text.length) {
215
+ fail("end of input");
216
+ }
217
+ }
218
+
219
+ /** 1-based line and column of an offset. */
220
+ export function lineColumn(
221
+ text: string,
222
+ offset: number,
223
+ ): { line: number; column: number } {
224
+ let line = 1;
225
+ let lineStart = 0;
226
+ const end = Math.min(offset, text.length);
227
+ for (let i = 0; i < end; i++) {
228
+ if (text.charCodeAt(i) === 10) {
229
+ line++;
230
+ lineStart = i + 1;
231
+ }
232
+ }
233
+ return { line, column: end - lineStart + 1 };
234
+ }
235
+
236
+ /** Parses JSON; on failure, reports the message with its line and column. */
237
+ export function parseJsonText(text: string): JsonParseResult {
238
+ try {
239
+ return { ok: true, value: JSON.parse(text) as unknown };
240
+ } catch (caught) {
241
+ let message = caught instanceof Error ? caught.message : String(caught);
242
+ let offset = 0;
243
+ try {
244
+ scan(text);
245
+ // The scanner accepted what JSON.parse refused (should not happen);
246
+ // keep the engine's message without a position.
247
+ } catch (scanned) {
248
+ if (scanned instanceof ScanError) {
249
+ message = scanned.message;
250
+ offset = scanned.offset;
251
+ }
252
+ }
253
+ return {
254
+ ok: false,
255
+ error: { message, offset, ...lineColumn(text, offset) },
256
+ };
257
+ }
258
+ }
259
+
260
+ /** The UTF-8 size of a text, in bytes (what the API measures against its limits). */
261
+ export function byteLength(text: string): number {
262
+ return new TextEncoder().encode(text).byteLength;
263
+ }
264
+
265
+ /** Formats a byte count: `512 B`, `1.5 KiB`, `2.0 MiB`. */
266
+ export function formatBytes(bytes: number): string {
267
+ if (bytes < 1024) {
268
+ return `${bytes} B`;
269
+ }
270
+ if (bytes < 1024 * 1024) {
271
+ return `${(bytes / 1024).toFixed(1)} KiB`;
272
+ }
273
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
274
+ }
275
+
276
+ /** The editor's state, as `JsonEditor` reports it on every change. */
277
+ export interface JsonEditorState {
278
+ /** The raw text. */
279
+ text: string;
280
+ /** The parsed value; `undefined` when the text does not parse (or is empty). */
281
+ value: unknown;
282
+ /** Whether the text parses (or is empty and `allowEmpty`) and fits `maxBytes`. The one flag a submit button needs. */
283
+ valid: boolean;
284
+ /** The parse error with its line and column, or `null`. */
285
+ error: JsonParseError | null;
286
+ /** The text's UTF-8 size. */
287
+ bytes: number;
288
+ /** Whether `bytes` exceeds `maxBytes`. */
289
+ overLimit: boolean;
290
+ }
291
+
292
+ /** Options of {@link jsonEditorState}. */
293
+ export interface JsonEditorStateOptions {
294
+ /** The size limit, bytes. */
295
+ maxBytes?: number;
296
+ /** Whether empty (whitespace-only) text is valid, as `undefined`. Defaults to `false`. */
297
+ allowEmpty?: boolean;
298
+ }
299
+
300
+ /**
301
+ * Works out a {@link JsonEditorState} from text, e.g. to seed a form's
302
+ * validity before the first edit.
303
+ */
304
+ export function jsonEditorState(
305
+ text: string,
306
+ { maxBytes, allowEmpty = false }: JsonEditorStateOptions = {},
307
+ ): JsonEditorState {
308
+ const bytes = byteLength(text);
309
+ const overLimit = maxBytes !== undefined && bytes > maxBytes;
310
+ if (allowEmpty && text.trim() === "") {
311
+ return {
312
+ text,
313
+ value: undefined,
314
+ valid: !overLimit,
315
+ error: null,
316
+ bytes,
317
+ overLimit,
318
+ };
319
+ }
320
+ const parsed = parseJsonText(text);
321
+ return parsed.ok
322
+ ? {
323
+ text,
324
+ value: parsed.value,
325
+ valid: !overLimit,
326
+ error: null,
327
+ bytes,
328
+ overLimit,
329
+ }
330
+ : {
331
+ text,
332
+ value: undefined,
333
+ valid: false,
334
+ error: parsed.error,
335
+ bytes,
336
+ overLimit,
337
+ };
338
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Arrow-key movement through a vertical list of links (the API docs
3
+ * sidebars): Down/Up to the next/previous item, stopping at the ends,
4
+ * Home/End to the first/last. Each item stays a Tab stop too, so this adds
5
+ * speed without hiding anything from Tab.
6
+ */
7
+
8
+ /** The item a key moves focus to from `active`, or `null` when the key is not a movement key (or the list is empty). */
9
+ export function arrowTarget<T extends Element>(
10
+ key: string,
11
+ items: readonly T[],
12
+ active: Element | null,
13
+ ): T | null {
14
+ if (items.length === 0) {
15
+ return null;
16
+ }
17
+ const index = active ? items.indexOf(active as T) : -1;
18
+ switch (key) {
19
+ case "ArrowDown":
20
+ return items[index < 0 ? 0 : Math.min(index + 1, items.length - 1)];
21
+ case "ArrowUp":
22
+ return items[index <= 0 ? 0 : index - 1];
23
+ case "Home":
24
+ return items[0];
25
+ case "End":
26
+ return items[items.length - 1];
27
+ default:
28
+ return null;
29
+ }
30
+ }
@@ -0,0 +1,86 @@
1
+ import type { DocsUiConfig } from "../../lib/docs/browser/config.ts";
2
+ import { DOCS_UI_CONFIG_ELEMENT_ID } from "../../lib/docs/browser/config.ts";
3
+ import { isUiUnauthorized } from "../../lib/kit/browser/unauthorized.ts";
4
+
5
+ /** The injected configuration was missing or malformed. */
6
+ export class DocsUiConfigError extends Error {
7
+ constructor(message: string) {
8
+ super(message);
9
+ this.name = "DocsUiConfigError";
10
+ }
11
+ }
12
+
13
+ /** Whether a value is a plain object. */
14
+ function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ /**
19
+ * Checks the fields the viewer relies on before its first request. It does
20
+ * not re-validate everything the server resolved — only enough that a stale
21
+ * or hand-edited shell fails with a message instead of a blank page.
22
+ */
23
+ export function parseDocsUiConfig(value: unknown): DocsUiConfig {
24
+ if (!isRecord(value)) {
25
+ throw new DocsUiConfigError("The UI configuration is not an object.");
26
+ }
27
+ if (value.version !== 1) {
28
+ throw new DocsUiConfigError(
29
+ `Unsupported UI configuration version ${JSON.stringify(value.version)} (expected 1).`,
30
+ );
31
+ }
32
+ for (const key of [
33
+ "title",
34
+ "basePath",
35
+ "assetsPath",
36
+ "documentUrl",
37
+ ] as const) {
38
+ if (typeof value[key] !== "string") {
39
+ throw new DocsUiConfigError(
40
+ `The UI configuration's "${key}" is not a string.`,
41
+ );
42
+ }
43
+ }
44
+ if (value.csrfHeader !== null && typeof value.csrfHeader !== "string") {
45
+ throw new DocsUiConfigError(
46
+ `The UI configuration's "csrfHeader" must be a string or null.`,
47
+ );
48
+ }
49
+ if (
50
+ !Array.isArray(value.extensions) ||
51
+ !value.extensions.every((prefix) => typeof prefix === "string")
52
+ ) {
53
+ throw new DocsUiConfigError(
54
+ `The UI configuration's "extensions" must be an array of strings.`,
55
+ );
56
+ }
57
+ if (!["system", "light", "dark"].includes(String(value.theme))) {
58
+ throw new DocsUiConfigError(`The UI configuration's "theme" is not valid.`);
59
+ }
60
+ // Missing in a shell from before the option existed, or from a host that
61
+ // mirrors the config (bun-common's docsRouter): off.
62
+ const unauthorized = value.unauthorized ?? null;
63
+ if (unauthorized !== null && !isUiUnauthorized(unauthorized)) {
64
+ throw new DocsUiConfigError(
65
+ `The UI configuration's "unauthorized" must be null or { redirectTo, returnParam, statuses }.`,
66
+ );
67
+ }
68
+ return { ...value, unauthorized } as unknown as DocsUiConfig;
69
+ }
70
+
71
+ /** Reads the {@link DocsUiConfig} the server inlined as `<script type="application/json" id="bun-api-docs-ui-config">`. */
72
+ export function readDocsUiConfig(doc: Document = document): DocsUiConfig {
73
+ const element = doc.getElementById(DOCS_UI_CONFIG_ELEMENT_ID);
74
+ if (!element) {
75
+ throw new DocsUiConfigError(
76
+ `The page has no #${DOCS_UI_CONFIG_ELEMENT_ID} element: it was not served by docsUi().`,
77
+ );
78
+ }
79
+ let parsed: unknown;
80
+ try {
81
+ parsed = JSON.parse(element.textContent ?? "");
82
+ } catch {
83
+ throw new DocsUiConfigError("The UI configuration is not valid JSON.");
84
+ }
85
+ return parseDocsUiConfig(parsed);
86
+ }
@@ -0,0 +1,33 @@
1
+ import type { DocsUiConfig } from "../../lib/docs/browser/config.ts";
2
+ import type { DocsClient, FetchLike } from "./api/client";
3
+ import { createContext, use } from "react";
4
+
5
+ /** Carries the injected {@link DocsUiConfig}. */
6
+ export const DocsUiConfigContext = createContext<DocsUiConfig | null>(null);
7
+ /** Carries the try-it panel's client (built on the document's resolved server). */
8
+ export const DocsClientContext = createContext<DocsClient | null>(null);
9
+ /** Carries the `fetch` every client uses; tests inject one. Defaults to `globalThis.fetch`. */
10
+ export const DocsFetchContext = createContext<FetchLike | null>(null);
11
+
12
+ /** The resolved {@link DocsUiConfig} the server injected. */
13
+ export function useDocsUiConfig(): DocsUiConfig {
14
+ const config = use(DocsUiConfigContext);
15
+ if (!config) {
16
+ throw new Error("useDocsUiConfig must be used inside <DocsUiProviders>");
17
+ }
18
+ return config;
19
+ }
20
+
21
+ /** The try-it panel's client. */
22
+ export function useDocsClient(): DocsClient {
23
+ const client = use(DocsClientContext);
24
+ if (!client) {
25
+ throw new Error("useDocsClient must be used inside <DocsClientContext>");
26
+ }
27
+ return client;
28
+ }
29
+
30
+ /** The `fetch` the clients use, or `undefined` for `globalThis.fetch`. */
31
+ export function useDocsFetch(): FetchLike | undefined {
32
+ return use(DocsFetchContext) ?? undefined;
33
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The embedding surface (`@kingsleyweb/bun-ui/docs/embed`): the viewer's
3
+ * screens and models for a host app to render inside its own shell, with the
4
+ * host's router, client, permissions and console injected through the seam
5
+ * props. The jobs UI's docs section is built from these.
6
+ *
7
+ * This is browser code (React, TSX, CSS side-effect imports): it is for a host
8
+ * that bundles it (Bun.build does), never for a Node consumer, and it ships as
9
+ * source — there are no declarations, the types ARE the source. It is covered
10
+ * in-repo (the jobs app imports it by this name); the package's
11
+ * consumer check covers the server-side `lib/` surface.
12
+ *
13
+ * The two models come out as namespaces: both name a `filterGroups`, and the
14
+ * WebSocket one a `refName` that is not the schema's.
15
+ */
16
+
17
+ // The shared JSON Schema renderer.
18
+ export * from "./screens/docs/schema";
19
+
20
+ // The HTTP reference (OpenAPI 3.1).
21
+ export * as httpModel from "./screens/docs/http/model";
22
+ export * as httpTryIt from "./screens/docs/http/tryIt";
23
+ export { HttpReference } from "./screens/docs/http/index";
24
+ export type { HttpReferenceProps } from "./screens/docs/http/index";
25
+
26
+ // The WebSocket reference (AsyncAPI 3.0).
27
+ export * as wsModel from "./screens/docs/ws/model";
28
+ export * as wsFormat from "./screens/docs/ws/format";
29
+ export { InlineText } from "./screens/docs/ws/InlineText";
30
+ export { PayloadSchema } from "./screens/docs/ws/PayloadSchema";
31
+ export type { PayloadSchemaProps } from "./screens/docs/ws/PayloadSchema";
32
+ export { PermissionMarker } from "./screens/docs/ws/items";
33
+ export { WsReference } from "./screens/docs/ws/index";
34
+ export type { WsConsole, WsPaneOptions } from "./screens/docs/ws/items";
35
+ export type { WsReferenceProps } from "./screens/docs/ws/index";
36
+
37
+ // The seams.
38
+ export type { PermissionProbe } from "./screens/docs/permissions";
39
+ export type { WsAddressOptions } from "./screens/docs/ws/model";
40
+ export type { LimitInfo } from "./screens/docs/ws/format";
41
+
42
+ // The host provides the config and the try-it client through these.
43
+ export {
44
+ DocsClientContext,
45
+ DocsFetchContext,
46
+ DocsUiConfigContext,
47
+ } from "./context";
48
+
49
+ // The host's app shares the router's contexts by re-exporting these.
50
+ export * from "./routing";
51
+ export { Link, Navigate, RouterProvider } from "./router";
52
+ export type { LinkProps, RouterProviderProps } from "./router";
53
+
54
+ // The client and the document type, for the host's adapters, and the error
55
+ // class (the host re-homes its own errors into it for the try-it's banner).
56
+ export { ApiError, isApiError } from "./api/errors";
57
+ export type {
58
+ ApiErrorInit,
59
+ ApiErrorKind,
60
+ ProblemDto,
61
+ ProblemIssueDto,
62
+ } from "./api/errors";
63
+ export { createDocsClient } from "./api/client";
64
+ export type {
65
+ DocsClient,
66
+ FetchLike,
67
+ HttpMethod,
68
+ QueryParams,
69
+ QueryScalar,
70
+ QueryValue,
71
+ RawResponse,
72
+ RequestOptions,
73
+ } from "./api/client";
74
+ export type { SpecDocument } from "./api/docs";
@@ -0,0 +1,2 @@
1
+ // Stylesheets are imported for their side effect; Bun.build emits them as CSS.
2
+ declare module "*.css";
@@ -0,0 +1,9 @@
1
+ /** One shared integer formatter in the browser's locale. */
2
+ const integerFormat = new Intl.NumberFormat(undefined, {
3
+ maximumFractionDigits: 0,
4
+ });
5
+
6
+ /** Formats a count, e.g. `12,345`. */
7
+ export function formatNumber(value: number): string {
8
+ return integerFormat.format(value);
9
+ }
@@ -0,0 +1,16 @@
1
+ import { useRouter } from "../routing";
2
+
3
+ /**
4
+ * The selected tab of a `UrlTabs`, read from the URL: the value of
5
+ * `param` when it is one of `tabs`, else `defaultValue`. Use it in the
6
+ * screen to pick the data for the panel.
7
+ */
8
+ export function useUrlTab<V extends string>(
9
+ param: string,
10
+ tabs: readonly { value: V }[],
11
+ defaultValue: V,
12
+ ): V {
13
+ const { location } = useRouter();
14
+ const raw = new URLSearchParams(location.search).get(param);
15
+ return tabs.find((tab) => tab.value === raw)?.value ?? defaultValue;
16
+ }
@@ -0,0 +1,11 @@
1
+ import { boot } from "./boot";
2
+ /**
3
+ * The browser entry point `Bun.build` bundles: the stylesheets (emitted as
4
+ * CSS files) and the viewer. Everything else lives in `boot.tsx`, so tests
5
+ * can start the app without this module's import-time side effect.
6
+ */
7
+ import "./styles/tokens.css";
8
+ import "./styles/base.css";
9
+ import "./components/components.css";
10
+
11
+ boot();
@@ -0,0 +1,54 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+ import { isApiError } from "./api/errors";
3
+
4
+ /** Most retries of a failed read. */
5
+ const MAX_RETRIES = 2;
6
+
7
+ /**
8
+ * Retries network failures and 5xx, never a 4xx: a 401/403/404/400 will not
9
+ * change by asking again, and retrying a 401 delays the sign-in message.
10
+ * Nor a malformed response (kind `parse`): the host answered, and answering
11
+ * the same again would only hold the error back for the retries' backoff.
12
+ */
13
+ export function shouldRetry(failureCount: number, error: unknown): boolean {
14
+ if (
15
+ isApiError(error) &&
16
+ (error.kind === "parse" || (error.status >= 400 && error.status < 500))
17
+ ) {
18
+ return false;
19
+ }
20
+ return failureCount < MAX_RETRIES;
21
+ }
22
+
23
+ /** Options of {@link createQueryClient}. */
24
+ export interface CreateQueryClientOptions {
25
+ /** Retry failed reads (see {@link shouldRetry}). Defaults to `true`; tests pass `false`. */
26
+ retry?: boolean;
27
+ }
28
+
29
+ /** The viewer's TanStack Query client. */
30
+ export function createQueryClient(
31
+ options: CreateQueryClientOptions = {},
32
+ ): QueryClient {
33
+ return new QueryClient({
34
+ defaultOptions: {
35
+ queries: {
36
+ retry: options.retry === false ? false : shouldRetry,
37
+ staleTime: 2_000,
38
+ refetchOnWindowFocus: true,
39
+ },
40
+ mutations: { retry: false },
41
+ },
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Before "Reload this screen" (`ScreenErrorBoundary`) remounts a crashed
47
+ * screen, drops the cached answers the screen read, so it reads them again:
48
+ * a crash caused by the DATA would otherwise render the same cached answer
49
+ * and crash again at once. Only inactive queries are reset; the crashed
50
+ * screen's are inactive by then.
51
+ */
52
+ export function resetScreenQueries(queryClient: QueryClient): Promise<void> {
53
+ return queryClient.resetQueries({ type: "inactive" });
54
+ }