blume 1.4.2 → 1.5.0
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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +116 -15
- package/dist/types/core/sources/types.d.ts +11 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +13 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +8 -5
- package/src/components/layout/search/types.ts +45 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +74 -4
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
package/src/cli/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { previewCommand } from "./commands/preview.ts";
|
|
|
15
15
|
import { syncCommand } from "./commands/sync.ts";
|
|
16
16
|
import { translateCommand } from "./commands/translate.ts";
|
|
17
17
|
import { validateCommand } from "./commands/validate.ts";
|
|
18
|
+
import { versionCommand } from "./commands/version.ts";
|
|
18
19
|
import { loadEnvFiles } from "./env.ts";
|
|
19
20
|
import { reportInternalError } from "./internal-error.ts";
|
|
20
21
|
|
|
@@ -39,6 +40,7 @@ const main = defineCommand({
|
|
|
39
40
|
sync: syncCommand,
|
|
40
41
|
translate: translateCommand,
|
|
41
42
|
validate: validateCommand,
|
|
43
|
+
version: versionCommand,
|
|
42
44
|
},
|
|
43
45
|
});
|
|
44
46
|
|
package/src/cli/init/scaffold.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
+
import { detect } from "package-manager-detector/detect";
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative } from "pathe";
|
|
5
6
|
|
|
6
7
|
import { blumePackageJson, toPackageName } from "../../core/package-json.ts";
|
|
@@ -53,7 +54,7 @@ interface Starter {
|
|
|
53
54
|
const page = (title: string, description: string, body: string): string =>
|
|
54
55
|
`---\ntitle: ${title}\ndescription: ${description}\n---\n\n${body}\n`;
|
|
55
56
|
|
|
56
|
-
export const STARTERS
|
|
57
|
+
export const STARTERS = {
|
|
57
58
|
api: {
|
|
58
59
|
configExtra: `
|
|
59
60
|
openapi: {
|
|
@@ -134,16 +135,14 @@ export const STARTERS: Record<Template, Starter> = {
|
|
|
134
135
|
},
|
|
135
136
|
],
|
|
136
137
|
},
|
|
137
|
-
}
|
|
138
|
+
} satisfies Record<Template, Starter>;
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* Install + dev commands to print for the chosen package manager, plus the
|
|
141
142
|
* prefix that runs a locally installed bin (`exec`, e.g. `npx blume eject`) —
|
|
142
143
|
* dependency bins aren't on PATH, so a bare `blume …` hint would not run.
|
|
143
144
|
*/
|
|
144
|
-
export const commandsFor = (
|
|
145
|
-
pm: PackageManager
|
|
146
|
-
): { build: string; dev: string; exec: string; install: string } => ({
|
|
145
|
+
export const commandsFor = (pm: PackageManager) => ({
|
|
147
146
|
// `bun build` invokes Bun's bundler, not the package.json `build` script —
|
|
148
147
|
// unlike `bun dev`, the script name is shadowed by a builtin subcommand.
|
|
149
148
|
build: pm === "npm" || pm === "bun" ? `${pm} run build` : `${pm} build`,
|
|
@@ -152,13 +151,36 @@ export const commandsFor = (
|
|
|
152
151
|
install: `${pm} install`,
|
|
153
152
|
});
|
|
154
153
|
|
|
154
|
+
const isPackageManager = (value: string): value is PackageManager =>
|
|
155
|
+
PACKAGE_MANAGERS.some((pm) => pm === value);
|
|
156
|
+
|
|
155
157
|
/**
|
|
156
158
|
* Derive the package manager from an npm user-agent string (the first
|
|
157
159
|
* `name/version` token of `npm_config_user_agent`), falling back to npm.
|
|
160
|
+
* Right for `init`, where the project doesn't exist yet and the invoking
|
|
161
|
+
* runner is the only signal.
|
|
158
162
|
*/
|
|
159
163
|
export const detectPackageManager = (userAgent?: string): PackageManager => {
|
|
160
|
-
const name = userAgent?.split("/")[0]
|
|
161
|
-
return name !== undefined &&
|
|
164
|
+
const name = userAgent?.split("/")[0];
|
|
165
|
+
return name !== undefined && isPackageManager(name) ? name : "npm";
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Detect an existing project's package manager from its lockfile /
|
|
170
|
+
* `packageManager` field (package-manager-detector), falling back to the
|
|
171
|
+
* user agent. Right for `eject`: the CLI is often run directly (`npx blume
|
|
172
|
+
* eject`, a bare `blume eject`), where `npm_config_user_agent` is absent or
|
|
173
|
+
* names the runner rather than the project's manager, and the old
|
|
174
|
+
* user-agent-only detection silently printed npm hints for a pnpm project.
|
|
175
|
+
*/
|
|
176
|
+
export const detectProjectPackageManager = async (
|
|
177
|
+
root: string
|
|
178
|
+
): Promise<PackageManager> => {
|
|
179
|
+
const detected = await detect({ cwd: root });
|
|
180
|
+
const name = detected?.name;
|
|
181
|
+
return name !== undefined && isPackageManager(name)
|
|
182
|
+
? name
|
|
183
|
+
: detectPackageManager(process.env.npm_config_user_agent);
|
|
162
184
|
};
|
|
163
185
|
|
|
164
186
|
/**
|
|
@@ -195,7 +217,7 @@ const hasRemoteSource = (sources: SourceKind[]): boolean =>
|
|
|
195
217
|
* Config snippets for each remote source kind, with placeholder values to
|
|
196
218
|
* replace and comments naming the env var each source authenticates with.
|
|
197
219
|
*/
|
|
198
|
-
const SOURCE_SNIPPETS
|
|
220
|
+
const SOURCE_SNIPPETS = {
|
|
199
221
|
"github-releases": ` // Changelog entries from GitHub Releases. Private repos read
|
|
200
222
|
// GITHUB_TOKEN from the environment.
|
|
201
223
|
{
|
|
@@ -226,7 +248,7 @@ const SOURCE_SNIPPETS: Record<Exclude<SourceKind, "filesystem">, string> = {
|
|
|
226
248
|
query: \`*[_type == "doc"]\`,
|
|
227
249
|
prefix: "sanity",
|
|
228
250
|
},`,
|
|
229
|
-
}
|
|
251
|
+
} satisfies Record<Exclude<SourceKind, "filesystem">, string>;
|
|
230
252
|
|
|
231
253
|
/**
|
|
232
254
|
* The `content` block for the generated config, or an empty string when the
|
|
@@ -272,10 +294,16 @@ export default defineConfig({
|
|
|
272
294
|
`;
|
|
273
295
|
|
|
274
296
|
/** SDK dependencies required by the selected remote sources. */
|
|
275
|
-
const extraDepsFor = (sources: SourceKind[])
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
const extraDepsFor = (sources: SourceKind[]) => {
|
|
298
|
+
const deps: Record<string, string> = {};
|
|
299
|
+
if (sources.includes("notion")) {
|
|
300
|
+
deps["@notionhq/client"] = "^2.2.15";
|
|
301
|
+
}
|
|
302
|
+
if (sources.includes("sanity")) {
|
|
303
|
+
deps["@sanity/client"] = "^7.25.0";
|
|
304
|
+
}
|
|
305
|
+
return deps;
|
|
306
|
+
};
|
|
279
307
|
|
|
280
308
|
/** Every file `init` should write for the given answers, package.json first. */
|
|
281
309
|
export const buildPlan = (
|
|
@@ -33,6 +33,7 @@ export const remapBlumeStack = (stack: string): string =>
|
|
|
33
33
|
* message, a trimmed stack, and an environment dump for bug reports. Callers
|
|
34
34
|
* exit after this — it doesn't exit itself, so it's testable.
|
|
35
35
|
*/
|
|
36
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- last-resort handler for whatever a catch clause caught; anything narrower would force casts at every call site
|
|
36
37
|
export const reportInternalError = (error: unknown): void => {
|
|
37
38
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
38
39
|
const lines = [
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number formatting shared by the translate and eval report renderers. The
|
|
3
|
+
* `1.5s` / `4m 12s` shapes are asserted in both suites — change them in both
|
|
4
|
+
* minds at once.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Milliseconds as `1.5s`. */
|
|
8
|
+
export const seconds = (ms: number): string => `${(ms / 1000).toFixed(1)}s`;
|
|
9
|
+
|
|
10
|
+
/** A dollar cost as `$1.23`, or nothing when the run reported none. */
|
|
11
|
+
export const money = (cost: number | undefined): string =>
|
|
12
|
+
cost === undefined ? "" : `$${cost.toFixed(2)}`;
|
|
13
|
+
|
|
14
|
+
/** Milliseconds as `12.3s` under a minute, `4m 12s` from there up. */
|
|
15
|
+
export const duration = (ms: number): string => {
|
|
16
|
+
if (ms < 60_000) {
|
|
17
|
+
return seconds(ms);
|
|
18
|
+
}
|
|
19
|
+
const minutes = Math.floor(ms / 60_000);
|
|
20
|
+
const rest = Math.round((ms % 60_000) / 1000);
|
|
21
|
+
return `${minutes}m ${rest}s`;
|
|
22
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from "../Icon.astro";
|
|
3
|
+
import { componentSlug } from "../slug.ts";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
title: string;
|
|
@@ -10,15 +11,7 @@ interface Props {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const { defaultOpen = false, description, icon, id, title } = Astro.props;
|
|
13
|
-
const
|
|
14
|
-
value
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.trim()
|
|
17
|
-
.replaceAll(/[^\w\s-]/gu, "")
|
|
18
|
-
.replaceAll(/[\s_]+/gu, "-")
|
|
19
|
-
.replaceAll(/-+/gu, "-")
|
|
20
|
-
.replaceAll(/^-|-$/gu, "");
|
|
21
|
-
const accordionId = id ?? slugify(title);
|
|
14
|
+
const accordionId = id ?? componentSlug(title);
|
|
22
15
|
---
|
|
23
16
|
|
|
24
17
|
<details
|
|
@@ -53,6 +53,8 @@ const displayValue =
|
|
|
53
53
|
</style>
|
|
54
54
|
|
|
55
55
|
<script>
|
|
56
|
+
import { copyText, flashLabel } from "../copy-feedback.ts";
|
|
57
|
+
|
|
56
58
|
const state = window as Window & { __blumeColorCopy?: boolean };
|
|
57
59
|
if (!state.__blumeColorCopy) {
|
|
58
60
|
state.__blumeColorCopy = true;
|
|
@@ -67,24 +69,14 @@ const displayValue =
|
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
await navigator.clipboard.writeText(button.dataset.blumeColorCopy ?? "");
|
|
72
|
-
} catch {
|
|
72
|
+
if (!(await copyText(button.dataset.blumeColorCopy ?? ""))) {
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const status = button.querySelector<HTMLElement>("[data-blume-color-status]");
|
|
77
|
-
if (
|
|
78
|
-
|
|
77
|
+
if (status) {
|
|
78
|
+
flashLabel(status, "Copied");
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
// Remember the real value once — capturing at click time would capture
|
|
82
|
-
// "Copied" on a double-click and stick until reload.
|
|
83
|
-
status.dataset.blumeLabel ??= status.textContent ?? "";
|
|
84
|
-
status.textContent = "Copied";
|
|
85
|
-
window.setTimeout(() => {
|
|
86
|
-
status.textContent = status.dataset.blumeLabel ?? "";
|
|
87
|
-
}, 1500);
|
|
88
80
|
});
|
|
89
81
|
}
|
|
90
82
|
</script>
|
|
@@ -165,12 +165,16 @@ const paneClass = "motion-safe:transition-[height] motion-safe:duration-200";
|
|
|
165
165
|
|
|
166
166
|
// A pane capped by a small viewport would otherwise stay small after the
|
|
167
167
|
// window grows: the frame's content stopped changing size, so its observer
|
|
168
|
-
// has nothing new to report.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
// has nothing new to report. rAF-coalesced so a live resize drag runs the
|
|
169
|
+
// read-then-write pass once per frame, not once per event.
|
|
170
|
+
window.addEventListener(
|
|
171
|
+
"resize",
|
|
172
|
+
rafThrottle(() => {
|
|
173
|
+
for (const frame of document.querySelectorAll<HTMLIFrameElement>(
|
|
174
|
+
"iframe[data-blume-example-frame][data-blume-reported-height]"
|
|
175
|
+
)) {
|
|
176
|
+
applyMeasuredHeight(frame);
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
);
|
|
176
180
|
</script>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { createSatteriMarkdownProcessor } from "@astrojs/markdown-satteri";
|
|
3
3
|
|
|
4
|
+
import { escapeRawHtml, unwrapParagraph } from "./inline-markdown.ts";
|
|
5
|
+
|
|
4
6
|
interface Props {
|
|
5
7
|
caption?: string;
|
|
6
8
|
hint?: string;
|
|
@@ -8,18 +10,6 @@ interface Props {
|
|
|
8
10
|
|
|
9
11
|
const { caption, hint } = Astro.props;
|
|
10
12
|
|
|
11
|
-
const escapeRawHtml = (value: string): string =>
|
|
12
|
-
value.replaceAll("<", "<").replaceAll(">", ">");
|
|
13
|
-
|
|
14
|
-
const unwrapParagraph = (html: string): string => {
|
|
15
|
-
const trimmed = html.trim();
|
|
16
|
-
// Only a *single* paragraph is unwrapped: the content must not contain its
|
|
17
|
-
// own `</p>`, or `<p>a</p>\n<p>b</p>` would "unwrap" to `a</p>\n<p>b` —
|
|
18
|
-
// unbalanced HTML injected via set:html.
|
|
19
|
-
const match = trimmed.match(/^<p>(?<content>(?:(?!<\/p>)[\s\S])*)<\/p>$/u);
|
|
20
|
-
return match?.groups?.content ?? trimmed;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
13
|
const markdownProcessor = await createSatteriMarkdownProcessor();
|
|
24
14
|
const captionHtml = caption
|
|
25
15
|
? unwrapParagraph((await markdownProcessor.render(escapeRawHtml(caption))).code)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { createSatteriMarkdownProcessor } from "@astrojs/markdown-satteri";
|
|
3
3
|
|
|
4
|
+
import { escapeRawHtml, unwrapParagraph } from "./inline-markdown.ts";
|
|
5
|
+
|
|
4
6
|
interface Props {
|
|
5
7
|
actions?: boolean | string | string[];
|
|
6
8
|
description: string;
|
|
@@ -8,18 +10,6 @@ interface Props {
|
|
|
8
10
|
|
|
9
11
|
const { actions = ["copy"], description } = Astro.props;
|
|
10
12
|
|
|
11
|
-
const escapeRawHtml = (value: string): string =>
|
|
12
|
-
value.replaceAll("<", "<").replaceAll(">", ">");
|
|
13
|
-
|
|
14
|
-
const unwrapParagraph = (html: string): string => {
|
|
15
|
-
const trimmed = html.trim();
|
|
16
|
-
// Only a *single* paragraph is unwrapped: the content must not contain its
|
|
17
|
-
// own `</p>`, or `<p>a</p>\n<p>b</p>` would "unwrap" to `a</p>\n<p>b` —
|
|
18
|
-
// unbalanced HTML injected via set:html.
|
|
19
|
-
const match = trimmed.match(/^<p>(?<content>(?:(?!<\/p>)[\s\S])*)<\/p>$/u);
|
|
20
|
-
return match?.groups?.content ?? trimmed;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
13
|
const normalizeActions = (value: Props["actions"]): string[] => {
|
|
24
14
|
if (Array.isArray(value)) {
|
|
25
15
|
return value;
|
|
@@ -93,6 +83,8 @@ const secondaryButton =
|
|
|
93
83
|
</blume-prompt>
|
|
94
84
|
|
|
95
85
|
<script>
|
|
86
|
+
import { copyText, createCopyFlash } from "../copy-feedback.ts";
|
|
87
|
+
|
|
96
88
|
class BlumePrompt extends HTMLElement {
|
|
97
89
|
connectedCallback() {
|
|
98
90
|
const content = this.querySelector<HTMLElement>(
|
|
@@ -116,28 +108,17 @@ const secondaryButton =
|
|
|
116
108
|
const done = copy?.querySelector<HTMLElement>(
|
|
117
109
|
"[data-blume-prompt-copy-done]"
|
|
118
110
|
);
|
|
119
|
-
|
|
111
|
+
// Toggle visibility rather than swapping text: both labels occupy the
|
|
112
|
+
// same grid cell, so the button width never changes.
|
|
113
|
+
const flash = createCopyFlash((copied) => {
|
|
114
|
+
idle?.classList.toggle("invisible", copied);
|
|
115
|
+
done?.classList.toggle("invisible", !copied);
|
|
116
|
+
}, "Copied");
|
|
120
117
|
copy?.addEventListener("click", async () => {
|
|
121
118
|
const text = promptText();
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
await navigator.clipboard.writeText(text);
|
|
127
|
-
} catch {
|
|
128
|
-
return;
|
|
119
|
+
if (text && (await copyText(text))) {
|
|
120
|
+
flash();
|
|
129
121
|
}
|
|
130
|
-
|
|
131
|
-
// Toggle visibility rather than swapping text: both labels occupy the
|
|
132
|
-
// same grid cell, so the button width never changes. Clearing the timer
|
|
133
|
-
// keeps the "Copied" state 1.5s past the last of several rapid clicks.
|
|
134
|
-
clearTimeout(resetTimer);
|
|
135
|
-
idle?.classList.add("invisible");
|
|
136
|
-
done?.classList.remove("invisible");
|
|
137
|
-
resetTimer = setTimeout(() => {
|
|
138
|
-
idle?.classList.remove("invisible");
|
|
139
|
-
done?.classList.add("invisible");
|
|
140
|
-
}, 1500);
|
|
141
122
|
});
|
|
142
123
|
}
|
|
143
124
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from "../Icon.astro";
|
|
3
|
+
import { componentSlug } from "../slug.ts";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
class?: string;
|
|
@@ -10,15 +11,7 @@ interface Props {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const { class: className, icon, id, style, title } = Astro.props;
|
|
13
|
-
const
|
|
14
|
-
value
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.trim()
|
|
17
|
-
.replaceAll(/[^\w\s-]/gu, "")
|
|
18
|
-
.replaceAll(/[\s_]+/gu, "-")
|
|
19
|
-
.replaceAll(/-+/gu, "-")
|
|
20
|
-
.replaceAll(/^-|-$/gu, "");
|
|
21
|
-
const tabId = id ?? slugify(title);
|
|
14
|
+
const tabId = id ?? componentSlug(title);
|
|
22
15
|
---
|
|
23
16
|
|
|
24
17
|
{/*
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
|
|
4
4
|
import { contentHref } from "./base-href.ts";
|
|
5
|
+
import { unwrapParagraph } from "./inline-markdown.ts";
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
cta?: string;
|
|
@@ -14,15 +15,6 @@ const { cta, headline, href, tip } = Astro.props;
|
|
|
14
15
|
const tooltipId = `blume-tooltip-${randomUUID()}`;
|
|
15
16
|
const external = href?.startsWith("http");
|
|
16
17
|
|
|
17
|
-
const unwrapParagraph = (html: string): string => {
|
|
18
|
-
const trimmed = html.trim();
|
|
19
|
-
// Only a *single* paragraph is unwrapped: the content must not contain its
|
|
20
|
-
// own `</p>`, or `<p>a</p>\n<p>b</p>` would "unwrap" to `a</p>\n<p>b` —
|
|
21
|
-
// unbalanced HTML injected via set:html.
|
|
22
|
-
const match = trimmed.match(/^<p>(?<content>(?:(?!<\/p>)[\s\S])*)<\/p>$/u);
|
|
23
|
-
return match?.groups?.content ?? trimmed;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
18
|
const labelHtml = Astro.slots.has("default")
|
|
27
19
|
? unwrapParagraph(await Astro.slots.render("default"))
|
|
28
20
|
: "";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { componentSlug } from "../slug.ts";
|
|
2
3
|
import { contentHref } from "./base-href.ts";
|
|
3
4
|
|
|
4
5
|
interface RssMetadata {
|
|
@@ -31,16 +32,8 @@ const {
|
|
|
31
32
|
tags,
|
|
32
33
|
title,
|
|
33
34
|
} = Astro.props;
|
|
34
|
-
const slugify = (text: string): string =>
|
|
35
|
-
text
|
|
36
|
-
.toLowerCase()
|
|
37
|
-
.trim()
|
|
38
|
-
.replaceAll(/[^\w\s-]/gu, "")
|
|
39
|
-
.replaceAll(/[\s_]+/gu, "-")
|
|
40
|
-
.replaceAll(/-+/gu, "-")
|
|
41
|
-
.replaceAll(/^-|-$/gu, "");
|
|
42
35
|
const updateLabel = label ?? title ?? "Update";
|
|
43
|
-
const id = providedId ?? (
|
|
36
|
+
const id = providedId ?? (componentSlug(updateLabel) || "update");
|
|
44
37
|
const tagList = Array.isArray(tags) ? tags : tags ? [tags] : [];
|
|
45
38
|
---
|
|
46
39
|
|
|
@@ -75,6 +75,9 @@ export const extractTypeTable = async (
|
|
|
75
75
|
): Promise<TypeTableProperty[]> => {
|
|
76
76
|
const { name, path, root = process.cwd(), source } = options;
|
|
77
77
|
const tsModule = await import("typescript");
|
|
78
|
+
// SAFETY: CJS/ESM interop — the typescript package exposes its API namespace
|
|
79
|
+
// either directly or under `default` depending on the loader; both are the
|
|
80
|
+
// same object, which TypeScriptApi models structurally.
|
|
78
81
|
const ts = (tsModule.default ?? tsModule) as TypeScriptApi;
|
|
79
82
|
|
|
80
83
|
const compilerOptions: CompilerOptions = {
|
|
@@ -62,8 +62,11 @@ const registeredDiffThemeNames = new Set<string>();
|
|
|
62
62
|
* shared between both modes must not hand light mode the dark-typed
|
|
63
63
|
* registration.
|
|
64
64
|
*/
|
|
65
|
+
const isThemeName = (theme: CodeTheme): theme is string =>
|
|
66
|
+
typeof theme === "string";
|
|
67
|
+
|
|
65
68
|
const diffThemeName = (theme: CodeTheme, mode: "dark" | "light"): string => {
|
|
66
|
-
if (
|
|
69
|
+
if (isThemeName(theme)) {
|
|
67
70
|
return theme;
|
|
68
71
|
}
|
|
69
72
|
const type = theme.type ?? mode;
|
|
@@ -90,7 +93,7 @@ const diffThemeName = (theme: CodeTheme, mode: "dark" | "light"): string => {
|
|
|
90
93
|
return name;
|
|
91
94
|
};
|
|
92
95
|
|
|
93
|
-
const diffThemes = (themes: CodeThemes)
|
|
96
|
+
const diffThemes = (themes: CodeThemes) => ({
|
|
94
97
|
dark: diffThemeName(themes.dark, "dark"),
|
|
95
98
|
light: diffThemeName(themes.light, "light"),
|
|
96
99
|
});
|
|
@@ -113,11 +116,12 @@ export const renderDiff = async (options: DiffOptions): Promise<string> => {
|
|
|
113
116
|
theme = DEFAULT_CODE_THEMES,
|
|
114
117
|
} = options;
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
const patchText =
|
|
120
|
+
patch ?? (src === undefined ? undefined : await readText(src, root));
|
|
121
|
+
if (patchText !== undefined) {
|
|
118
122
|
const result = await preloadPatchDiff({
|
|
119
123
|
options: { theme: diffThemes(theme) },
|
|
120
|
-
patch:
|
|
124
|
+
patch: patchText,
|
|
121
125
|
});
|
|
122
126
|
return result.prerenderedHTML;
|
|
123
127
|
}
|
|
@@ -41,6 +41,8 @@ const load = async (
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// SAFETY: GitHub's `GET /repos/{owner}/{repo}` contract carries these three
|
|
45
|
+
// fields on every 2xx response; a malformed body rejects into `loadSafe`.
|
|
44
46
|
const data = (await response.json()) as {
|
|
45
47
|
description: string | null;
|
|
46
48
|
forks_count: number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for components that render a short Markdown prop (a caption,
|
|
3
|
+
* a description, a tooltip label) into inline HTML injected via `set:html`.
|
|
4
|
+
* Previously copied verbatim into Prompt, Frame, and Tooltip.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Neutralize raw HTML in a Markdown-rendered text prop by escaping only
|
|
9
|
+
* `<`/`>`. `&` deliberately stays: CommonMark already renders a bare `&` as
|
|
10
|
+
* `&` and resolves real entity references, so leaving it alone keeps
|
|
11
|
+
* `©`-style authoring working — a full HTML escape would render it as
|
|
12
|
+
* the literal text `©`.
|
|
13
|
+
*/
|
|
14
|
+
export const escapeRawHtml = (value: string): string =>
|
|
15
|
+
value.replaceAll("<", "<").replaceAll(">", ">");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unwrap the `<p>` a block-level Markdown render wraps around single-line
|
|
19
|
+
* content, so it can sit inside inline markup. Only a *single* paragraph is
|
|
20
|
+
* unwrapped: the content must not contain its own `</p>`, or
|
|
21
|
+
* `<p>a</p>\n<p>b</p>` would "unwrap" to `a</p>\n<p>b` — unbalanced HTML
|
|
22
|
+
* injected via `set:html`.
|
|
23
|
+
*/
|
|
24
|
+
export const unwrapParagraph = (html: string): string => {
|
|
25
|
+
const trimmed = html.trim();
|
|
26
|
+
const match = trimmed.match(/^<p>(?<content>(?:(?!<\/p>)[\s\S])*)<\/p>$/u);
|
|
27
|
+
return match?.groups?.content ?? trimmed;
|
|
28
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared clipboard + "Copied" feedback used by every copy affordance (code
|
|
3
|
+
* blocks, page actions, color swatches, prompts, API panels, Ask AI). One
|
|
4
|
+
* implementation owns the invariants each site used to hand-roll:
|
|
5
|
+
*
|
|
6
|
+
* - the clipboard write is guarded, and nothing flashes on failure — a
|
|
7
|
+
* confirmation must never lie;
|
|
8
|
+
* - repeat copies restart the hold instead of stacking timers, so the copied
|
|
9
|
+
* state never reverts early after a double-click;
|
|
10
|
+
* - every successful copy is announced to a shared polite live region, so the
|
|
11
|
+
* confirmation is audible, not just visual (previously only the code-block
|
|
12
|
+
* button announced).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** How long the copied confirmation holds before reverting. */
|
|
16
|
+
const HOLD_MS = 1500;
|
|
17
|
+
|
|
18
|
+
/** The shared visually-hidden live region, created on first announcement. */
|
|
19
|
+
let region: HTMLElement | null = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Announce `message` to screen readers. The region is re-created if a swap
|
|
23
|
+
* (view transition, client router) disconnected it.
|
|
24
|
+
*/
|
|
25
|
+
export const announceCopied = (message: string): void => {
|
|
26
|
+
if (!region?.isConnected) {
|
|
27
|
+
region = document.createElement("div");
|
|
28
|
+
region.setAttribute("role", "status");
|
|
29
|
+
region.className = "sr-only";
|
|
30
|
+
document.body.append(region);
|
|
31
|
+
}
|
|
32
|
+
// Clear first so repeating the same message is re-announced.
|
|
33
|
+
region.textContent = "";
|
|
34
|
+
region.textContent = message;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Copy `text` to the clipboard. Returns whether the write succeeded; failures
|
|
39
|
+
* (insecure context, permissions) are swallowed so callers can simply skip
|
|
40
|
+
* their confirmation.
|
|
41
|
+
*/
|
|
42
|
+
export const copyText = async (text: string): Promise<boolean> => {
|
|
43
|
+
try {
|
|
44
|
+
await navigator.clipboard.writeText(text);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A per-affordance flash: `apply(true)` paints the copied state, `apply(false)`
|
|
53
|
+
* reverts it after the hold. Calling the returned function again restarts the
|
|
54
|
+
* hold. `announce` (the localized "Copied" label) is spoken on each flash.
|
|
55
|
+
*/
|
|
56
|
+
export const createCopyFlash = (
|
|
57
|
+
apply: (copied: boolean) => void,
|
|
58
|
+
announce?: string,
|
|
59
|
+
holdMs: number = HOLD_MS
|
|
60
|
+
): (() => void) => {
|
|
61
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
return () => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
apply(true);
|
|
65
|
+
if (announce) {
|
|
66
|
+
announceCopied(announce);
|
|
67
|
+
}
|
|
68
|
+
timer = setTimeout(() => apply(false), holdMs);
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Flash timers for {@link flashLabel}, keyed per element. */
|
|
73
|
+
const labelTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The text-swap flash: show `message` in `el`, then restore its own (possibly
|
|
77
|
+
* localized) label after the hold. The original label is captured once, on the
|
|
78
|
+
* first flash — capturing at click time would capture the flash message itself
|
|
79
|
+
* on a double-click and stick until reload.
|
|
80
|
+
*/
|
|
81
|
+
export const flashLabel = (
|
|
82
|
+
el: HTMLElement,
|
|
83
|
+
message: string,
|
|
84
|
+
holdMs: number = HOLD_MS
|
|
85
|
+
): void => {
|
|
86
|
+
el.dataset.blumeLabel ??= el.textContent ?? "";
|
|
87
|
+
el.textContent = message;
|
|
88
|
+
announceCopied(message);
|
|
89
|
+
clearTimeout(labelTimers.get(el));
|
|
90
|
+
labelTimers.set(
|
|
91
|
+
el,
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
el.textContent = el.dataset.blumeLabel ?? "";
|
|
94
|
+
}, holdMs)
|
|
95
|
+
);
|
|
96
|
+
};
|