@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.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- package/package.json +198 -4
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { AnchorHTMLAttributes, MouseEvent, ReactNode } from "react";
|
|
2
|
+
import type { NavigateOptions, RouteDef } from "./routing";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { ScreenErrorBoundary } from "./components/ScreenErrorBoundary";
|
|
5
|
+
import {
|
|
6
|
+
matchPath,
|
|
7
|
+
normalizeBasePath,
|
|
8
|
+
ParamsContext,
|
|
9
|
+
readLocation,
|
|
10
|
+
RouterContext,
|
|
11
|
+
useLocation,
|
|
12
|
+
useNavigate,
|
|
13
|
+
useRouter,
|
|
14
|
+
} from "./routing";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The router's components: the provider, route matching, redirects and
|
|
18
|
+
* links. The pure path functions and the hooks live in `routing.ts`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Props of {@link RouterProvider}. */
|
|
22
|
+
export interface RouterProviderProps {
|
|
23
|
+
/** The mount path (`DocsUiConfig.basePath`). */
|
|
24
|
+
basePath: string;
|
|
25
|
+
/** The app. */
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Provides routing under `basePath`, following `window.history`. */
|
|
30
|
+
export function RouterProvider({ basePath, children }: RouterProviderProps) {
|
|
31
|
+
const base = normalizeBasePath(basePath);
|
|
32
|
+
const [location, setLocation] = useState(() => readLocation(base));
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const onPopState = () => setLocation(readLocation(base));
|
|
36
|
+
window.addEventListener("popstate", onPopState);
|
|
37
|
+
return () => window.removeEventListener("popstate", onPopState);
|
|
38
|
+
}, [base]);
|
|
39
|
+
|
|
40
|
+
const href = useCallback(
|
|
41
|
+
(to: string) => {
|
|
42
|
+
const target = to.startsWith("/") ? to : `/${to}`;
|
|
43
|
+
if (base === "") {
|
|
44
|
+
return target;
|
|
45
|
+
}
|
|
46
|
+
// `/` is the mount itself; keep the query attached without a slash.
|
|
47
|
+
return target === "/" || target.startsWith("/?")
|
|
48
|
+
? `${base}${target.slice(1)}`
|
|
49
|
+
: `${base}${target}`;
|
|
50
|
+
},
|
|
51
|
+
[base],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const navigate = useCallback(
|
|
55
|
+
(to: string, options: NavigateOptions = {}) => {
|
|
56
|
+
const url = href(to);
|
|
57
|
+
if (options.replace) {
|
|
58
|
+
window.history.replaceState(null, "", url);
|
|
59
|
+
} else {
|
|
60
|
+
window.history.pushState(null, "", url);
|
|
61
|
+
}
|
|
62
|
+
setLocation(readLocation(base));
|
|
63
|
+
},
|
|
64
|
+
[base, href],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const value = useMemo(
|
|
68
|
+
() => ({ basePath: base, location, navigate, href }),
|
|
69
|
+
[base, location, navigate, href],
|
|
70
|
+
);
|
|
71
|
+
return <RouterContext value={value}>{children}</RouterContext>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Props of {@link Routes}. */
|
|
75
|
+
export interface RoutesProps {
|
|
76
|
+
/** Routes, tried in order; the first match wins. */
|
|
77
|
+
routes: readonly RouteDef[];
|
|
78
|
+
/** Rendered when nothing matches (including a URL outside `basePath`). */
|
|
79
|
+
notFound: ReactNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Renders the first route matching the current location, inside a
|
|
84
|
+
* {@link ScreenErrorBoundary}: a screen that crashes while rendering shows a
|
|
85
|
+
* recoverable panel in its place instead of blanking the app, and moving to
|
|
86
|
+
* another location tries again.
|
|
87
|
+
*/
|
|
88
|
+
export function Routes({ routes, notFound }: RoutesProps) {
|
|
89
|
+
const { path } = useLocation();
|
|
90
|
+
return (
|
|
91
|
+
<ScreenErrorBoundary resetKey={path ?? ""}>
|
|
92
|
+
<MatchedRoute
|
|
93
|
+
routes={routes}
|
|
94
|
+
notFound={notFound}
|
|
95
|
+
path={path}
|
|
96
|
+
/>
|
|
97
|
+
</ScreenErrorBoundary>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The first route matching `path`, or `notFound`. */
|
|
102
|
+
function MatchedRoute({
|
|
103
|
+
routes,
|
|
104
|
+
notFound,
|
|
105
|
+
path,
|
|
106
|
+
}: RoutesProps & { path: string | null }) {
|
|
107
|
+
if (path !== null) {
|
|
108
|
+
for (const route of routes) {
|
|
109
|
+
const params = matchPath(route.path, path);
|
|
110
|
+
if (params) {
|
|
111
|
+
return (
|
|
112
|
+
<ParamsContext
|
|
113
|
+
key={route.path}
|
|
114
|
+
value={params}
|
|
115
|
+
>
|
|
116
|
+
{route.element}
|
|
117
|
+
</ParamsContext>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return <>{notFound}</>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Props of {@link Navigate}. */
|
|
126
|
+
export interface NavigateProps {
|
|
127
|
+
/** App path to go to. */
|
|
128
|
+
to: string;
|
|
129
|
+
/** Replace the history entry. Defaults to `true`: a redirect should not be a back-button stop. */
|
|
130
|
+
replace?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Redirects on render. */
|
|
134
|
+
export function Navigate({ to, replace = true }: NavigateProps) {
|
|
135
|
+
const navigate = useNavigate();
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
navigate(to, { replace });
|
|
138
|
+
}, [navigate, to, replace]);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Props of {@link Link}. */
|
|
143
|
+
export interface LinkProps extends Omit<
|
|
144
|
+
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
145
|
+
"href"
|
|
146
|
+
> {
|
|
147
|
+
/** App path (relative to `basePath`), may carry `?query`. */
|
|
148
|
+
to: string;
|
|
149
|
+
/** Mark the link `aria-current="page"` when the location matches: `"exact"`, or `"prefix"` for a section. Defaults to none. */
|
|
150
|
+
activeMatch?: "exact" | "prefix";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Whether a click should be left to the browser (new tab, download, modified). */
|
|
154
|
+
function isModifiedClick(event: MouseEvent<HTMLAnchorElement>): boolean {
|
|
155
|
+
return (
|
|
156
|
+
event.button !== 0 ||
|
|
157
|
+
event.metaKey ||
|
|
158
|
+
event.altKey ||
|
|
159
|
+
event.ctrlKey ||
|
|
160
|
+
event.shiftKey ||
|
|
161
|
+
event.defaultPrevented
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** An `<a>` that navigates in-app on a plain left-click. */
|
|
166
|
+
export function Link({ to, activeMatch, onClick, target, ...rest }: LinkProps) {
|
|
167
|
+
const { navigate, href, location } = useRouter();
|
|
168
|
+
const toPath = to.split("?")[0] || "/";
|
|
169
|
+
const active =
|
|
170
|
+
location.path !== null &&
|
|
171
|
+
(activeMatch === "exact"
|
|
172
|
+
? location.path === toPath
|
|
173
|
+
: activeMatch === "prefix"
|
|
174
|
+
? toPath === "/"
|
|
175
|
+
? location.path === "/"
|
|
176
|
+
: location.path === toPath || location.path.startsWith(`${toPath}/`)
|
|
177
|
+
: false);
|
|
178
|
+
return (
|
|
179
|
+
<a
|
|
180
|
+
{...rest}
|
|
181
|
+
target={target}
|
|
182
|
+
href={href(to)}
|
|
183
|
+
aria-current={active ? "page" : undefined}
|
|
184
|
+
onClick={(event) => {
|
|
185
|
+
onClick?.(event);
|
|
186
|
+
if (isModifiedClick(event) || (target && target !== "_self")) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
navigate(to);
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { createContext, use, useCallback, useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A small history router scoped under the mount's `basePath`.
|
|
6
|
+
*
|
|
7
|
+
* The app's routes are flat and few, so this replaces a router library: it
|
|
8
|
+
* tracks `location.pathname`/`search` relative to `basePath`, intercepts
|
|
9
|
+
* plain left-clicks on its {@link Link}s, matches `:param` and trailing `*`
|
|
10
|
+
* patterns, and keeps URL-addressable state in the query string. A path
|
|
11
|
+
* outside `basePath` matches nothing and renders the 404.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Where the app is, relative to `basePath`. */
|
|
15
|
+
export interface AppLocation {
|
|
16
|
+
/** Path under `basePath`, always starting with `/`; `null` when the URL is outside `basePath`. */
|
|
17
|
+
path: string | null;
|
|
18
|
+
/** The query string, with its leading `?` (or `""`). */
|
|
19
|
+
search: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Options of a navigation. */
|
|
23
|
+
export interface NavigateOptions {
|
|
24
|
+
/** Replace the current history entry instead of pushing one. Defaults to `false`. */
|
|
25
|
+
replace?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What the router provides. */
|
|
29
|
+
export interface RouterContextValue {
|
|
30
|
+
/** The mount path, no trailing slash (`""` when mounted at the root). */
|
|
31
|
+
basePath: string;
|
|
32
|
+
/** The current location. */
|
|
33
|
+
location: AppLocation;
|
|
34
|
+
/** Navigates to an app path (relative to `basePath`, may carry `?query`). */
|
|
35
|
+
navigate: (to: string, options?: NavigateOptions) => void;
|
|
36
|
+
/** The full URL path for an app path. */
|
|
37
|
+
href: (to: string) => string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Carries the router. */
|
|
41
|
+
export const RouterContext = createContext<RouterContextValue | null>(null);
|
|
42
|
+
/** Carries the params the matched route bound. */
|
|
43
|
+
export const ParamsContext = createContext<Readonly<Record<string, string>>>(
|
|
44
|
+
{},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/** Normalises a base path: leading `/`, no trailing `/`, `/` itself becomes `""`. */
|
|
48
|
+
export function normalizeBasePath(basePath: string): string {
|
|
49
|
+
const trimmed = basePath.replace(/\/+$/, "");
|
|
50
|
+
if (trimmed === "") {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The path under `basePath`, or `null` when `pathname` is outside it.
|
|
58
|
+
* `/jobs` and `/jobs/` are both `/`; `/jobsx` is outside `/jobs`.
|
|
59
|
+
*/
|
|
60
|
+
export function stripBasePath(
|
|
61
|
+
pathname: string,
|
|
62
|
+
basePath: string,
|
|
63
|
+
): string | null {
|
|
64
|
+
const base = normalizeBasePath(basePath);
|
|
65
|
+
if (base !== "" && pathname !== base && !pathname.startsWith(`${base}/`)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const rest = pathname.slice(base.length);
|
|
69
|
+
if (rest === "" || rest === "/") {
|
|
70
|
+
return "/";
|
|
71
|
+
}
|
|
72
|
+
return rest.length > 1 && rest.endsWith("/") ? rest.slice(0, -1) : rest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Matches an app path against a pattern: `/queues/:queue` binds `queue`
|
|
77
|
+
* (percent-decoded), a trailing `/*` binds the rest as `*`. Returns the
|
|
78
|
+
* params, or `null` when it does not match.
|
|
79
|
+
*/
|
|
80
|
+
export function matchPath(
|
|
81
|
+
pattern: string,
|
|
82
|
+
path: string,
|
|
83
|
+
): Record<string, string> | null {
|
|
84
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
85
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
86
|
+
const params: Record<string, string> = {};
|
|
87
|
+
for (let index = 0; index < patternParts.length; index++) {
|
|
88
|
+
const part = patternParts[index];
|
|
89
|
+
if (part === "*" && index === patternParts.length - 1) {
|
|
90
|
+
params["*"] = pathParts.slice(index).map(safeDecode).join("/");
|
|
91
|
+
return params;
|
|
92
|
+
}
|
|
93
|
+
const actual = pathParts[index];
|
|
94
|
+
if (actual === undefined) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (part.startsWith(":")) {
|
|
98
|
+
params[part.slice(1)] = safeDecode(actual);
|
|
99
|
+
} else if (part !== actual) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return pathParts.length === patternParts.length ? params : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `decodeURIComponent`, keeping a malformed segment as it is. */
|
|
107
|
+
function safeDecode(value: string): string {
|
|
108
|
+
try {
|
|
109
|
+
return decodeURIComponent(value);
|
|
110
|
+
} catch {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reads the current location from `window.location`. */
|
|
116
|
+
export function readLocation(basePath: string): AppLocation {
|
|
117
|
+
return {
|
|
118
|
+
path: stripBasePath(window.location.pathname, basePath),
|
|
119
|
+
search: window.location.search,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The router, or a clear error outside a {@link RouterProvider}. */
|
|
124
|
+
export function useRouter(): RouterContextValue {
|
|
125
|
+
const router = use(RouterContext);
|
|
126
|
+
if (!router) {
|
|
127
|
+
throw new Error("useRouter must be used inside a <RouterProvider>");
|
|
128
|
+
}
|
|
129
|
+
return router;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The current location. */
|
|
133
|
+
export function useLocation(): AppLocation {
|
|
134
|
+
return useRouter().location;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A function that navigates to an app path. */
|
|
138
|
+
export function useNavigate(): RouterContextValue["navigate"] {
|
|
139
|
+
return useRouter().navigate;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A function that turns an app path into a full URL path. */
|
|
143
|
+
export function useHref(): RouterContextValue["href"] {
|
|
144
|
+
return useRouter().href;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The params the matched route bound. */
|
|
148
|
+
export function useParams<
|
|
149
|
+
T extends Record<string, string> = Record<string, string>,
|
|
150
|
+
>(): Readonly<T> {
|
|
151
|
+
return use(ParamsContext) as Readonly<T>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The current query string, parsed. */
|
|
155
|
+
export function useSearchParams(): URLSearchParams {
|
|
156
|
+
const { search } = useLocation();
|
|
157
|
+
return useMemo(() => new URLSearchParams(search), [search]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One query-string value as state: `[value, setValue]`. Setting `""` or
|
|
162
|
+
* `null` removes the key. Updates replace the history entry, so typing in a
|
|
163
|
+
* filter does not flood the back button.
|
|
164
|
+
*/
|
|
165
|
+
export function useQueryParam(
|
|
166
|
+
key: string,
|
|
167
|
+
): [string, (value: string | null) => void] {
|
|
168
|
+
const { location, navigate } = useRouter();
|
|
169
|
+
const value = new URLSearchParams(location.search).get(key) ?? "";
|
|
170
|
+
const setValue = useCallback(
|
|
171
|
+
(next: string | null) => {
|
|
172
|
+
const params = new URLSearchParams(window.location.search);
|
|
173
|
+
if (next === null || next === "") {
|
|
174
|
+
params.delete(key);
|
|
175
|
+
} else {
|
|
176
|
+
params.set(key, next);
|
|
177
|
+
}
|
|
178
|
+
const query = params.toString();
|
|
179
|
+
navigate(`${location.path ?? "/"}${query ? `?${query}` : ""}`, {
|
|
180
|
+
replace: true,
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
[key, location.path, navigate],
|
|
184
|
+
);
|
|
185
|
+
return [value, setValue];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** One route: a pattern and what it renders. */
|
|
189
|
+
export interface RouteDef {
|
|
190
|
+
/** Pattern relative to `basePath`: `/`, `/queues/:queue`, `/docs/*`. */
|
|
191
|
+
path: string;
|
|
192
|
+
/** What the route renders; params are available through {@link useParams}. */
|
|
193
|
+
element: ReactNode;
|
|
194
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loaded document, shared by the viewer's routes: its context and hook,
|
|
3
|
+
* kept apart from `viewer.tsx` so that file exports components only (fast
|
|
4
|
+
* refresh, `react/only-export-components`).
|
|
5
|
+
*/
|
|
6
|
+
import type { DocumentKind, SpecDocument } from "../../api/docs";
|
|
7
|
+
import { createContext, use } from "react";
|
|
8
|
+
|
|
9
|
+
/** What the loaded document is. */
|
|
10
|
+
export interface LoadedDocument {
|
|
11
|
+
/** The document. */
|
|
12
|
+
doc: SpecDocument;
|
|
13
|
+
/** Its kind. */
|
|
14
|
+
kind: DocumentKind;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Carries the loaded document. */
|
|
18
|
+
export const DocumentContext = createContext<LoadedDocument | null>(null);
|
|
19
|
+
|
|
20
|
+
/** The loaded document, or a clear error outside `<DocsViewer>`. */
|
|
21
|
+
export function useDocument(): LoadedDocument {
|
|
22
|
+
const loaded = use(DocumentContext);
|
|
23
|
+
if (!loaded) {
|
|
24
|
+
throw new Error("useDocument must be used inside <DocsViewer>");
|
|
25
|
+
}
|
|
26
|
+
return loaded;
|
|
27
|
+
}
|