blume 1.5.0 → 1.5.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/CHANGELOG.md +32 -0
- package/README.md +16 -12
- package/dist/cli/index.js +449 -135
- package/dist/cli/index.js.map +24 -23
- package/dist/types/ai/ask-context.d.ts +78 -0
- package/dist/types/core/config-input.d.ts +54 -2
- package/dist/types/core/data.d.ts +19 -2
- package/dist/types/core/open-in-chat.d.ts +9 -0
- package/dist/types/core/schema.d.ts +48 -1
- package/dist/types/core/types.d.ts +10 -3
- package/dist/types/openapi/references.d.ts +9 -0
- package/dist/types/search/orama-index.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +11 -2
- package/docs/advanced/api-reference.mdx +67 -5
- package/docs/advanced/custom-pages.mdx +5 -1
- package/docs/configuration/ai.mdx +35 -0
- package/docs/configuration/index.mdx +14 -2
- package/docs/configuration/search.mdx +4 -4
- package/docs/configuration/theming.mdx +4 -2
- package/docs/reference/cli.mdx +2 -2
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/ask-context.ts +51 -11
- package/src/ai/mcp/data.ts +3 -2
- package/src/ai/mcp/server.ts +3 -2
- package/src/assets/icon-dark.png +0 -0
- package/src/astro/generate.ts +172 -18
- package/src/astro/templates.ts +89 -15
- package/src/components/content/AccordionItem.astro +4 -0
- package/src/components/content/Update.astro +3 -0
- package/src/components/islands/AskAI.astro +6 -0
- package/src/components/islands/ask-ai.tsx +39 -9
- package/src/components/layout/Analytics.astro +9 -1
- package/src/components/layout/Favicon.astro +29 -8
- package/src/components/layout/Fonts.astro +23 -3
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/NavSelector.astro +1 -1
- package/src/components/layout/PageActions.astro +120 -78
- package/src/components/layout/PageFeedback.astro +12 -3
- package/src/components/layout/PageLayout.astro +79 -5
- package/src/components/layout/ReferenceLayout.astro +12 -9
- package/src/components/layout/RootLayout.astro +153 -121
- package/src/components/layout/Search.astro +41 -26
- package/src/components/layout/drawer-inert.ts +10 -5
- package/src/components/layout/head-scripts.ts +34 -16
- package/src/components/layout/nav-utils.ts +34 -15
- package/src/components/layout/search/orama.ts +3 -2
- package/src/components/openapi/AsyncApiOperation.astro +22 -7
- package/src/components/openapi/MessageComposer.astro +238 -0
- package/src/components/openapi/Operation.astro +26 -12
- package/src/components/openapi/PanelTabs.astro +7 -0
- package/src/components/openapi/Playground.astro +320 -0
- package/src/components/openapi/RequestPanel.astro +1 -0
- package/src/components/openapi/async-snippets.ts +20 -7
- package/src/components/openapi/async.ts +13 -2
- package/src/components/openapi/message-composer.ts +242 -0
- package/src/components/openapi/message-model.ts +108 -0
- package/src/components/openapi/message.ts +153 -0
- package/src/components/openapi/operation-model.ts +260 -0
- package/src/components/openapi/playground-client.ts +486 -0
- package/src/components/openapi/playground-schema.ts +109 -0
- package/src/components/openapi/request.ts +287 -0
- package/src/components/openapi/security.ts +0 -56
- package/src/components/openapi/snippets.ts +23 -136
- package/src/components/openapi/validate-json.ts +144 -0
- package/src/components/openapi/ws-client.ts +194 -0
- package/src/core/config-input.ts +67 -1
- package/src/core/content-assets.ts +66 -15
- package/src/core/data.ts +16 -2
- package/src/core/last-modified.ts +76 -2
- package/src/core/links.ts +30 -4
- package/src/core/navigation.ts +26 -1
- package/src/core/open-in-chat.ts +17 -0
- package/src/core/project-graph.ts +11 -0
- package/src/core/schema.ts +60 -1
- package/src/core/server-features.ts +11 -0
- package/src/core/sources/normalize.ts +10 -2
- package/src/core/types.ts +10 -3
- package/src/deploy/vercel-negotiation.ts +34 -14
- package/src/og/card.ts +3 -1
- package/src/openapi/model.ts +7 -0
- package/src/openapi/proxy.ts +217 -0
- package/src/openapi/references.ts +8 -0
- package/src/openapi/source.ts +13 -0
- package/src/registry/eject.ts +4 -5
- package/src/search/orama-index.ts +109 -36
- package/src/theme/entry.ts +15 -2
- package/src/theme/fonts.ts +75 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRootTab, isUnderPath } from "../../core/navigation.ts";
|
|
1
2
|
import type { NavNode, NavTab } from "../../core/types.ts";
|
|
2
3
|
|
|
3
4
|
/** A flat, ordered page reference used for previous/next pagination. */
|
|
@@ -71,14 +72,6 @@ export const findBreadcrumbs = (nodes: NavNode[], route: string): Crumb[] => {
|
|
|
71
72
|
return search(nodes, []) ?? [];
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
-
* Whether `route` is the section root `base` or nested beneath it. Requires a
|
|
76
|
-
* path boundary, so `/api-reference` is not under `/api`. The root `/` spans
|
|
77
|
-
* every route.
|
|
78
|
-
*/
|
|
79
|
-
export const isUnderPath = (route: string, base: string): boolean =>
|
|
80
|
-
base === "/" || route === base || route.startsWith(`${base}/`);
|
|
81
|
-
|
|
82
75
|
/**
|
|
83
76
|
* The tab whose `path` is the longest prefix of `route`. The root tab (`/`)
|
|
84
77
|
* acts as the fallback when no more specific tab matches.
|
|
@@ -99,6 +92,28 @@ export const activeTabForRoute = (
|
|
|
99
92
|
return match;
|
|
100
93
|
};
|
|
101
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The tab to mark as the current one (`aria-current`) for `route`. Same
|
|
97
|
+
* longest-prefix match as {@link activeTabForRoute}, except inside an archived
|
|
98
|
+
* version tree: the root tab claims every archived route through its
|
|
99
|
+
* spans-everything fallback while its link points back at the current docs, so
|
|
100
|
+
* no tab is genuinely current there.
|
|
101
|
+
*/
|
|
102
|
+
export const currentTabForRoute = (
|
|
103
|
+
tabs: NavTab[],
|
|
104
|
+
route: string,
|
|
105
|
+
root = "/"
|
|
106
|
+
): NavTab | null => {
|
|
107
|
+
const tab = activeTabForRoute(tabs, route);
|
|
108
|
+
// The root tab sitting away from the tree root means the tree is a version
|
|
109
|
+
// snapshot in a different path space — the tab matched as a fallback, not
|
|
110
|
+
// because it owns the route.
|
|
111
|
+
if (tab && isRootTab(tab, root) && tab.path !== root) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return tab;
|
|
115
|
+
};
|
|
116
|
+
|
|
102
117
|
/**
|
|
103
118
|
* The children of the group whose path is `base`, searched at any depth — so a
|
|
104
119
|
* content tree wrapped in a top-level container group still resolves to the
|
|
@@ -144,7 +159,7 @@ const withoutTabSections = (
|
|
|
144
159
|
): NavNode[] => {
|
|
145
160
|
const tabPaths = new Set<string>();
|
|
146
161
|
for (const tab of tabs) {
|
|
147
|
-
if (tab
|
|
162
|
+
if (!isRootTab(tab, root)) {
|
|
148
163
|
tabPaths.add(tab.path);
|
|
149
164
|
}
|
|
150
165
|
}
|
|
@@ -181,11 +196,15 @@ const withoutTabSections = (
|
|
|
181
196
|
* the root tab), the tab-owned groups are hidden so the root sidebar shows
|
|
182
197
|
* only pages that don't belong to a tab.
|
|
183
198
|
*
|
|
184
|
-
* `root` is the tree root
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
199
|
+
* `root` is the tree root (`Navigation.root`), localized and based like tab
|
|
200
|
+
* paths — under i18n or a `basePath` the root tab sits at `/en` or `/docs`,
|
|
201
|
+
* not `/`, and a bare-`/` comparison would misread it as a section tab (a
|
|
202
|
+
* root-level `(group)` folder's path is exactly that prefix, so the sidebar
|
|
203
|
+
* collapsed to that one group or blanked entirely). In an archived version
|
|
204
|
+
* tree the root is versionized (`/v1.0`) while tab paths stay in current-docs
|
|
205
|
+
* space, so root-tab checks use {@link isRootTab} containment, not equality:
|
|
206
|
+
* the root tab owns no group in a snapshot, and misreading it as a section
|
|
207
|
+
* tab blanked the archived sidebar.
|
|
189
208
|
*
|
|
190
209
|
* When a matched tab owns no sidebar group — a standalone page like the
|
|
191
210
|
* generated changelog timeline (`/changelog`), or a tab whose source produced
|
|
@@ -201,7 +220,7 @@ export const sidebarForRoute = (
|
|
|
201
220
|
root = "/"
|
|
202
221
|
): NavNode[] => {
|
|
203
222
|
const tab = activeTabForRoute(tabs, route);
|
|
204
|
-
if (tab && tab
|
|
223
|
+
if (tab && !isRootTab(tab, root)) {
|
|
205
224
|
return sectionChildren(sidebar, tab.path) ?? [];
|
|
206
225
|
}
|
|
207
226
|
const scoped = withoutTabSections(sidebar, tabs, root);
|
|
@@ -11,8 +11,9 @@ import type { IndexedDocument, SearchFn } from "./types.ts";
|
|
|
11
11
|
* available in both dev and the production build. A generous match pool is
|
|
12
12
|
* pulled so the section pills can count across the whole result set before the
|
|
13
13
|
* active filter and display limit are applied. `locale` is the site's
|
|
14
|
-
* `i18n.defaultLocale`, baked into the generated client so
|
|
15
|
-
* (Japanese
|
|
14
|
+
* `i18n.defaultLocale`, baked into the generated client so non-Latin scripts
|
|
15
|
+
* (Japanese and Chinese, but equally Cyrillic, Greek, Hebrew, Devanagari…)
|
|
16
|
+
* get a word-segmenting tokenizer.
|
|
16
17
|
*/
|
|
17
18
|
export const createSearch = async (opts: {
|
|
18
19
|
indexUrl: string;
|
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
} from "./async.ts";
|
|
19
19
|
import type { MessageSample } from "./async-snippets.ts";
|
|
20
20
|
import { asyncSampleLanguages } from "./async-snippets.ts";
|
|
21
|
+
import { buildMessage, defaultMessageValues } from "./message.ts";
|
|
22
|
+
import { messageModel } from "./message-model.ts";
|
|
23
|
+
import MessageComposer from "./MessageComposer.astro";
|
|
21
24
|
import Authorization from "./Authorization.astro";
|
|
22
25
|
import Bindings from "./Bindings.astro";
|
|
23
26
|
import { exampleValue, type SchemaLike, toJson } from "./helpers.ts";
|
|
@@ -86,13 +89,21 @@ const messagePanels = await Promise.all(
|
|
|
86
89
|
})
|
|
87
90
|
);
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
// One derived model feeds the static default sample, the composer form, and
|
|
93
|
+
// (client-side) the live samples + WebSocket frame — they can never drift.
|
|
94
|
+
const model = ref
|
|
95
|
+
? messageModel({
|
|
91
96
|
action: ref.method === "send" ? "send" : "receive",
|
|
92
97
|
address: ref.path,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
messages,
|
|
99
|
+
parameters,
|
|
100
|
+
protocol,
|
|
101
|
+
schemas,
|
|
102
|
+
servers,
|
|
103
|
+
})
|
|
104
|
+
: null;
|
|
105
|
+
const sample: MessageSample | null = model
|
|
106
|
+
? buildMessage(model, defaultMessageValues(model))
|
|
96
107
|
: null;
|
|
97
108
|
const languages = asyncSampleLanguages(spec?.codeSamples ?? [], protocol);
|
|
98
109
|
const samplePanels = sample
|
|
@@ -104,6 +115,7 @@ const samplePanels = sample
|
|
|
104
115
|
}),
|
|
105
116
|
key: language.id,
|
|
106
117
|
label: language.label,
|
|
118
|
+
lang: language.id,
|
|
107
119
|
}))
|
|
108
120
|
)
|
|
109
121
|
: [];
|
|
@@ -223,8 +235,11 @@ const channelBindings = bindingGroups(channel?.bindings);
|
|
|
223
235
|
title="Channel bindings"
|
|
224
236
|
/>
|
|
225
237
|
</div>
|
|
226
|
-
{(
|
|
227
|
-
|
|
238
|
+
{(spec.playground.enabled ||
|
|
239
|
+
samplePanels.length > 0 ||
|
|
240
|
+
messagePanels.length > 0) && (
|
|
241
|
+
<div class="xl:sticky xl:top-24 xl:self-start" data-operation-panel>
|
|
242
|
+
{spec.playground.enabled && model && <MessageComposer model={model} />}
|
|
228
243
|
<div class="not-prose flex flex-col gap-6">
|
|
229
244
|
<PanelTabs copy heading="Example" panels={samplePanels} />
|
|
230
245
|
<PanelTabs heading="Message" panels={messagePanels} />
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
import type { MessageModel } from "./message.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The interactive "Try it" panel for one event operation. Server-rendered as a
|
|
8
|
+
* native collapsed `<details>` with plain labelled controls — keyboard
|
|
9
|
+
* operable with zero JS — and the client module loads only when the reader
|
|
10
|
+
* first opens it (loader script at the foot of this file). The message model
|
|
11
|
+
* rides along as JSON so the lazy chunk needs no data fetch.
|
|
12
|
+
*
|
|
13
|
+
* Composing and copying a sample works on every protocol; a live connection is
|
|
14
|
+
* offered only where a browser can actually make one (WebSocket bindings), and
|
|
15
|
+
* the panel says so plainly rather than presenting a dead button.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
model: MessageModel;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { model } = Astro.props;
|
|
23
|
+
|
|
24
|
+
// `</script>`-proof embedding: `<` only ever occurs inside JSON strings, where
|
|
25
|
+
// the \u escape is equivalent, so a spec description can never close the model
|
|
26
|
+
// script early and inject markup.
|
|
27
|
+
const modelJson = JSON.stringify(model).replaceAll("<", "\\u003c");
|
|
28
|
+
|
|
29
|
+
// Unique per rendered composer (a page can hold several), so the textarea's
|
|
30
|
+
// aria-describedby can point at ITS error container and no other.
|
|
31
|
+
const payloadErrorsId = `blume-payload-errors-${randomUUID()}`;
|
|
32
|
+
|
|
33
|
+
const LABEL_ROW =
|
|
34
|
+
"mb-1 flex flex-wrap items-baseline gap-x-1.5 font-medium text-foreground text-xs";
|
|
35
|
+
const FIELD =
|
|
36
|
+
"w-full rounded border border-border bg-background px-2 py-1.5 font-mono text-foreground text-xs";
|
|
37
|
+
const HINT = "mt-1 block text-muted-foreground text-xs";
|
|
38
|
+
const GROUP = "flex flex-col gap-3";
|
|
39
|
+
const GROUP_HEADING =
|
|
40
|
+
"font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide";
|
|
41
|
+
const BUTTON =
|
|
42
|
+
"cursor-pointer rounded-blume bg-accent px-4 py-2 font-medium text-accent-foreground text-sm transition hover:bg-accent/90 disabled:cursor-not-allowed disabled:opacity-50";
|
|
43
|
+
const BUTTON_QUIET =
|
|
44
|
+
"cursor-pointer rounded-blume border border-border px-4 py-2 font-medium text-foreground text-sm transition hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50";
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<blume-message-composer class="mb-6 block">
|
|
48
|
+
<script
|
|
49
|
+
data-composer-model
|
|
50
|
+
is:inline
|
|
51
|
+
set:html={modelJson}
|
|
52
|
+
type="application/json"
|
|
53
|
+
/>
|
|
54
|
+
<details
|
|
55
|
+
class="not-prose rounded-blume border border-border bg-background"
|
|
56
|
+
data-composer
|
|
57
|
+
>
|
|
58
|
+
<summary class="cursor-pointer px-4 py-3 font-semibold text-foreground text-sm">
|
|
59
|
+
Try it
|
|
60
|
+
</summary>
|
|
61
|
+
<div class="flex flex-col gap-5 border-border border-t px-4 py-4">
|
|
62
|
+
<div class={GROUP}>
|
|
63
|
+
<div class={GROUP_HEADING}>Server</div>
|
|
64
|
+
{
|
|
65
|
+
model.servers.length > 0 && (
|
|
66
|
+
<label class="block">
|
|
67
|
+
<span class={LABEL_ROW}>Server</span>
|
|
68
|
+
<select class={FIELD} data-server>
|
|
69
|
+
{model.servers.map((option, index) => (
|
|
70
|
+
<option value={String(index)}>{option.label}</option>
|
|
71
|
+
))}
|
|
72
|
+
</select>
|
|
73
|
+
</label>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
<label class="block">
|
|
77
|
+
<span class={LABEL_ROW}>Custom server URL</span>
|
|
78
|
+
<input
|
|
79
|
+
class={FIELD}
|
|
80
|
+
data-server-custom
|
|
81
|
+
placeholder="wss://events.example.com"
|
|
82
|
+
type="text"
|
|
83
|
+
/>
|
|
84
|
+
<span class={HINT}>
|
|
85
|
+
Overrides the selected server; the channel address is appended.
|
|
86
|
+
</span>
|
|
87
|
+
</label>
|
|
88
|
+
</div>
|
|
89
|
+
{
|
|
90
|
+
model.params.length > 0 && (
|
|
91
|
+
<div class={GROUP}>
|
|
92
|
+
<div class={GROUP_HEADING}>Channel parameters</div>
|
|
93
|
+
{model.params.map((param) => (
|
|
94
|
+
<label class="block">
|
|
95
|
+
<span class={LABEL_ROW}>
|
|
96
|
+
<code class="font-mono">{param.name}</code>
|
|
97
|
+
<span class="text-red-600 dark:text-red-400" title="required">
|
|
98
|
+
*
|
|
99
|
+
</span>
|
|
100
|
+
</span>
|
|
101
|
+
{param.enum ? (
|
|
102
|
+
<select
|
|
103
|
+
class={FIELD}
|
|
104
|
+
data-param={param.name}
|
|
105
|
+
title={param.description}
|
|
106
|
+
>
|
|
107
|
+
{param.enum.map((value) => (
|
|
108
|
+
<option selected={value === param.value} value={value}>
|
|
109
|
+
{value}
|
|
110
|
+
</option>
|
|
111
|
+
))}
|
|
112
|
+
</select>
|
|
113
|
+
) : (
|
|
114
|
+
<input
|
|
115
|
+
class={FIELD}
|
|
116
|
+
data-param={param.name}
|
|
117
|
+
placeholder="string"
|
|
118
|
+
title={param.description}
|
|
119
|
+
type="text"
|
|
120
|
+
value={param.value}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
{param.description && (
|
|
124
|
+
<span class={HINT} set:text={param.description} />
|
|
125
|
+
)}
|
|
126
|
+
</label>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
<div class={GROUP}>
|
|
132
|
+
<div class={GROUP_HEADING}>
|
|
133
|
+
Payload
|
|
134
|
+
{
|
|
135
|
+
model.payload.contentType && (
|
|
136
|
+
<span class="ml-1.5 font-mono normal-case">
|
|
137
|
+
{model.payload.contentType}
|
|
138
|
+
</span>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
</div>
|
|
142
|
+
<label class="block">
|
|
143
|
+
<span class={LABEL_ROW}>Message payload</span>
|
|
144
|
+
<textarea
|
|
145
|
+
aria-describedby={payloadErrorsId}
|
|
146
|
+
class={FIELD}
|
|
147
|
+
data-payload
|
|
148
|
+
rows="8"
|
|
149
|
+
spellcheck="false">{model.payload.example}</textarea
|
|
150
|
+
>
|
|
151
|
+
</label>
|
|
152
|
+
<div
|
|
153
|
+
aria-live="polite"
|
|
154
|
+
class="mt-1 flex flex-col gap-0.5 empty:hidden"
|
|
155
|
+
data-payload-errors
|
|
156
|
+
id={payloadErrorsId}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
{
|
|
160
|
+
model.connectable ? (
|
|
161
|
+
<div class={GROUP}>
|
|
162
|
+
<div class={GROUP_HEADING}>Connection</div>
|
|
163
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
164
|
+
<button class={BUTTON} data-connect type="button">
|
|
165
|
+
Connect
|
|
166
|
+
</button>
|
|
167
|
+
<button class={BUTTON_QUIET} data-disconnect disabled type="button">
|
|
168
|
+
Disconnect
|
|
169
|
+
</button>
|
|
170
|
+
{/* AsyncAPI 3 states the action from the API's side: a
|
|
171
|
+
`receive` operation is the one a reader publishes to, while a
|
|
172
|
+
`send` operation only streams frames at them. */}
|
|
173
|
+
{model.action === "receive" && (
|
|
174
|
+
<button class={BUTTON} data-send disabled type="button">
|
|
175
|
+
Send
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
<div
|
|
180
|
+
aria-live="polite"
|
|
181
|
+
class="text-muted-foreground text-xs"
|
|
182
|
+
data-status
|
|
183
|
+
>
|
|
184
|
+
{model.action === "send"
|
|
185
|
+
? "Not connected. Connect to stream messages as they arrive."
|
|
186
|
+
: "Not connected."}
|
|
187
|
+
</div>
|
|
188
|
+
<div
|
|
189
|
+
aria-live="polite"
|
|
190
|
+
class="flex flex-col gap-1 empty:hidden"
|
|
191
|
+
data-log
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
) : (
|
|
195
|
+
<p class="text-muted-foreground text-xs">
|
|
196
|
+
{model.protocol
|
|
197
|
+
? `Live sending isn't possible for ${model.protocol} from a browser tab — copy the sample above and run it against your broker.`
|
|
198
|
+
: "Live sending needs a WebSocket binding — copy the sample above and run it against your broker."}
|
|
199
|
+
</p>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
</div>
|
|
203
|
+
</details>
|
|
204
|
+
</blume-message-composer>
|
|
205
|
+
|
|
206
|
+
<script>
|
|
207
|
+
// Hydration waits for intent: a collapsed <details> can only fire its first
|
|
208
|
+
// `toggle` by opening, so the one-shot listener imports the client module —
|
|
209
|
+
// a separate chunk readers who never compose a message never download — and
|
|
210
|
+
// wires the form. The custom element lives here, not in the client module,
|
|
211
|
+
// so that module stays import-safe under the test runner.
|
|
212
|
+
class BlumeMessageComposer extends HTMLElement {
|
|
213
|
+
connectedCallback() {
|
|
214
|
+
const details = this.querySelector<HTMLDetailsElement>(
|
|
215
|
+
"details[data-composer]"
|
|
216
|
+
);
|
|
217
|
+
if (!details) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const init = async () => {
|
|
221
|
+
const { initComposer } = await import("./message-composer.ts");
|
|
222
|
+
initComposer(this);
|
|
223
|
+
};
|
|
224
|
+
// A re-attached element (bfcache restore, DOM moves) can arrive with the
|
|
225
|
+
// panel already open — its first toggle has fired, so a listener alone
|
|
226
|
+
// would leave a visible form dead.
|
|
227
|
+
if (details.open) {
|
|
228
|
+
void init();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
details.addEventListener("toggle", init, { once: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!customElements.get("blume-message-composer")) {
|
|
236
|
+
customElements.define("blume-message-composer", BlumeMessageComposer);
|
|
237
|
+
}
|
|
238
|
+
</script>
|
|
@@ -6,18 +6,20 @@ import {
|
|
|
6
6
|
resolveComponentRef,
|
|
7
7
|
type SchemaLike,
|
|
8
8
|
} from "./helpers.ts";
|
|
9
|
+
import { operationModel } from "./operation-model.ts";
|
|
10
|
+
import { buildRequest, defaultValues } from "./request.ts";
|
|
9
11
|
import {
|
|
10
12
|
effectiveSecurity,
|
|
11
13
|
resolveSecurity,
|
|
12
|
-
sampleAuth,
|
|
13
14
|
type SecurityRequirementLike,
|
|
14
15
|
type SecuritySchemeLike,
|
|
15
16
|
} from "./security.ts";
|
|
16
|
-
import {
|
|
17
|
+
import { sampleLanguages } from "./snippets.ts";
|
|
17
18
|
import AsyncApiOperation from "./AsyncApiOperation.astro";
|
|
18
19
|
import Authorization from "./Authorization.astro";
|
|
19
20
|
import MethodBadge from "./MethodBadge.astro";
|
|
20
21
|
import ParametersTable from "./ParametersTable.astro";
|
|
22
|
+
import Playground from "./Playground.astro";
|
|
21
23
|
import RequestBody from "./RequestBody.astro";
|
|
22
24
|
import RequestPanel from "./RequestPanel.astro";
|
|
23
25
|
import Responses from "./Responses.astro";
|
|
@@ -104,17 +106,21 @@ const security = resolveSecurity(
|
|
|
104
106
|
doc.components?.securitySchemes
|
|
105
107
|
);
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
// One derived model feeds the static default sample, the playground form,
|
|
110
|
+
// and (client-side) the live samples + fetch — they can never drift apart.
|
|
111
|
+
const model =
|
|
108
112
|
ref && operation
|
|
109
|
-
?
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
ref.path,
|
|
113
|
-
|
|
113
|
+
? operationModel({
|
|
114
|
+
method: ref.method,
|
|
115
|
+
parameters: params,
|
|
116
|
+
path: ref.path,
|
|
117
|
+
requestBody,
|
|
114
118
|
schemas,
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
security,
|
|
120
|
+
servers: doc.servers ?? [],
|
|
121
|
+
})
|
|
117
122
|
: null;
|
|
123
|
+
const sample = model ? buildRequest(model, defaultValues(model)) : null;
|
|
118
124
|
const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
119
125
|
---
|
|
120
126
|
{
|
|
@@ -154,8 +160,16 @@ const languages = sampleLanguages(spec?.codeSamples ?? []);
|
|
|
154
160
|
/>
|
|
155
161
|
)}
|
|
156
162
|
</div>
|
|
157
|
-
{sample && (
|
|
158
|
-
<div class="xl:sticky xl:top-24 xl:self-start">
|
|
163
|
+
{sample && model && (
|
|
164
|
+
<div class="xl:sticky xl:top-24 xl:self-start" data-operation-panel>
|
|
165
|
+
{spec.playground.enabled && (
|
|
166
|
+
<Playground
|
|
167
|
+
model={model}
|
|
168
|
+
operation={id}
|
|
169
|
+
proxy={spec.playground.proxy}
|
|
170
|
+
slug={spec.slug}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
159
173
|
<RequestPanel
|
|
160
174
|
languages={languages}
|
|
161
175
|
responses={responses}
|
|
@@ -13,6 +13,12 @@ interface Panel {
|
|
|
13
13
|
html?: string | null;
|
|
14
14
|
/** Plain-text fallback when there is nothing to highlight. */
|
|
15
15
|
text?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Code-sample language id for a request-sample panel. Tags the panel with
|
|
18
|
+
* `data-sample-lang` so the playground client can re-render it live; panels
|
|
19
|
+
* that aren't samples leave it off.
|
|
20
|
+
*/
|
|
21
|
+
lang?: string;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
interface Props {
|
|
@@ -88,6 +94,7 @@ const TAB_CLASS =
|
|
|
88
94
|
aria-labelledby={idFor("tab", panel.key)}
|
|
89
95
|
class:list={[index === 0 ? "" : "hidden"]}
|
|
90
96
|
data-panel={panel.key}
|
|
97
|
+
data-sample-lang={panel.lang}
|
|
91
98
|
id={idFor("panel", panel.key)}
|
|
92
99
|
role="tabpanel"
|
|
93
100
|
>
|