diff --git a/_site/customizations/kapa-init.js b/_site/customizations/kapa-init.js index 4974b29db..d8258ffff 100644 --- a/_site/customizations/kapa-init.js +++ b/_site/customizations/kapa-init.js @@ -54,7 +54,7 @@ 'data-website-id': '5df6cc2b-732e-44e4-b789-aec81b70fe46', 'data-project-name': 'ClickHouse', 'data-project-color': '#151515', - 'data-project-logo': 'https://avatars.githubusercontent.com/u/54801242?s=200&v=4', + 'data-project-logo': '/docs/images/logo.svg', 'data-modal-disclaimer': 'This is a custom LLM for ClickHouse with access to all developer documentation, open GitHub Issues, YouTube videos, and resolved StackOverflow posts. Please note that answers are generated by AI and may not be fully accurate, so please use your best judgement.', 'data-modal-example-questions': 'How to speed up queries?,How to use materialized views?', // Disable the MCP install dropdown/tab in the Kapa widget header. @@ -80,4 +80,4 @@ console.log('An error occurred while trying to load the Kapa.ai widget:', e); } } -})(); \ No newline at end of file +})(); diff --git a/_site/styles.css b/_site/styles.css index 84e2a46b3..846a7bb1b 100644 --- a/_site/styles.css +++ b/_site/styles.css @@ -1715,11 +1715,6 @@ button.rounded-xl { ClickPipes Page Styling ============================================ */ -/* Dark mode image swap for ClickPipes icons */ -:is(.dark) .product-item img[src*="clickpipes-light.svg"] { - content: url("https://ch-docs-product-selector.s3.eu-central-1.amazonaws.com/clickpipes-dark.svg") !important; -} - /* Kafka nav-group icon is a solid black SVG (fill:#000); invert it to white in dark mode so it doesn't disappear against the dark sidebar. Single asset, no light/dark swap. */ diff --git a/ar/index.mdx b/ar/index.mdx index 87bacf92f..58dcc6a27 100644 --- a/ar/index.mdx +++ b/ar/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/bin/gen-homepage-react.ts b/bin/gen-homepage-react.ts index 0f7de0c7b..910be0f0c 100644 --- a/bin/gen-homepage-react.ts +++ b/bin/gen-homepage-react.ts @@ -9,10 +9,13 @@ */ import fs from "node:fs"; import path from "node:path"; +import { inlinePublicIcon } from "../src/lib/build-icon.ts"; const root = process.cwd(); const locales = ["en", "ar", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh"]; const outputDir = path.join(root, "src", "generated", "homepage"); +const mcpIcon = inlinePublicIcon("/images/icons/icon-mcp.svg"); +const mcpMask = //g; function sourcePath(locale: string): string { return locale === "en" ? path.join(root, "index.mdx") : path.join(root, locale, "index.mdx"); @@ -56,7 +59,18 @@ function render(locale: string): string { .replaceAll( "typeof window !== 'undefined' && window.location.pathname.startsWith('/docs') ? '/docs' : ''", assetBaseExpression, + ) + .replace( + mcpMask, + ``, + ) + .replace( + /(export const McpLink = \(\{ children \}\) => \{\r?\n)\s+const assetBase = [^\r\n]+;\r?\n/, + "$1", ); + if (rewrittenDefinitions.includes("ch-mcp-icon")) { + throw new Error(`Could not replace the homepage MCP icon in ${file}`); + } return `// Generated from ${path.relative(root, file)}; do not edit.\n` + `import * as React from "react";\n` + `import { useEffect, useRef, useState } from "react";\n\n` diff --git a/bin/gen-sidebar.ts b/bin/gen-sidebar.ts index 57a04494c..6d5056d13 100644 --- a/bin/gen-sidebar.ts +++ b/bin/gen-sidebar.ts @@ -64,8 +64,8 @@ const tabs = (lang.tabs ?? []) as Obj[]; // ---------------------------------------------------------------- labels type NavBadge = { text: string; variant: "success" | "info" | "warning" | "danger" | "note" }; -const labelCache = new Map(); -function pageInfo(page: string): { label: string; exists: boolean; badge?: NavBadge } { +const labelCache = new Map(); +function pageInfo(page: string): { label: string; exists: boolean; badge?: NavBadge; icon?: string; hidden?: boolean } { const cached = labelCache.get(page); if (cached) return cached; // Locale nav references are `es/...`; strip the locale so files resolve. @@ -81,6 +81,8 @@ function pageInfo(page: string): { label: string; exists: boolean; badge?: NavBa const file = localizedFile ?? englishFile; let label = rel.split("/").pop() ?? rel; let badge: NavBadge | undefined; + let icon: string | undefined; + let hidden = false; if (file) { const src = fs.readFileSync(file, "utf8"); const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---/); @@ -89,6 +91,8 @@ function pageInfo(page: string): { label: string; exists: boolean; badge?: NavBa const fm = (parseYaml(m[1]) ?? {}) as Record; const st = fm.sidebarTitle ?? fm.title; if (typeof st === "string" && st.trim()) label = st.trim(); + if (typeof fm.icon === "string" && fm.icon.trim()) icon = fm.icon.trim(); + hidden = fm.hidden === true; if (typeof fm.openapi === "string") { const method = fm.openapi.match(/\b(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\//i)?.[1]?.toLowerCase() as HttpMethod | undefined; if (method) badge = { text: method.toUpperCase(), variant: apiMethodVariant(method) }; @@ -98,7 +102,7 @@ function pageInfo(page: string): { label: string; exists: boolean; badge?: NavBa } } } - const info = { label, exists: Boolean(file), badge }; + const info = { label, exists: Boolean(file), badge, icon, hidden }; labelCache.set(page, info); return info; } @@ -196,7 +200,7 @@ function noteOrder(groupPath: string, pages: string[]) { // ---------------------------------------------------------------- conversion type NimbusItem = - | { label: string; link: string; badge?: NavBadge } + | { label: string; link: string; badge?: NavBadge; icon?: string; hidden?: boolean } | { label: string; items: NimbusItem[]; collapsed?: boolean; segment?: string; landing?: string; icon?: string }; function convertPages(pages: Json[], groupPath: string): NimbusItem[] { @@ -223,7 +227,7 @@ function convertPages(pages: Json[], groupPath: string): NimbusItem[] { const info = pageInfo(p); if (!info.exists) missing.push(`${groupPath}: ${p}`); seenPages.add(p); - out.push({ label: info.label, link: pageLink(p), badge: info.badge }); + out.push({ label: info.label, link: pageLink(p), badge: info.badge, icon: info.icon, hidden: info.hidden || undefined }); continue; } if (p && typeof p === "object") { diff --git a/bin/prepare-public.ts b/bin/prepare-public.ts index 70c0481d6..2a436b9c8 100644 --- a/bin/prepare-public.ts +++ b/bin/prepare-public.ts @@ -111,11 +111,15 @@ fs.mkdirSync(output, { recursive: true }); if (scope.remotePreview) { materialize(path.join(root, "public", "favicon.svg"), path.join(output, "favicon.svg")); + materialize(path.join(root, "images", "logo.svg"), path.join(output, "images", "logo.svg")); materialize(path.join(root, "images", "icons"), path.join(output, "images", "icons")); } else { for (const entry of fs.readdirSync(path.join(root, "public"))) { + if (entry === "images" || entry === "img") continue; materialize(path.join(root, "public", entry), path.join(output, entry)); } + materialize(path.join(root, "images"), path.join(output, "images")); + materialize(path.join(root, "img"), path.join(output, "img")); } for (const remote of manifest.remotes) { diff --git a/es/index.mdx b/es/index.mdx index 5bb8698f2..66caf8fcc 100644 --- a/es/index.mdx +++ b/es/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/fr/index.mdx b/fr/index.mdx index b50ff8434..bb8f3ddbf 100644 --- a/fr/index.mdx +++ b/fr/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/index.mdx b/index.mdx index ac84b9236..2a88f500e 100644 --- a/index.mdx +++ b/index.mdx @@ -14,8 +14,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/ja/index.mdx b/ja/index.mdx index b174f7f57..0b9378f07 100644 --- a/ja/index.mdx +++ b/ja/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/ko/index.mdx b/ko/index.mdx index 6ff6edc3e..9b03088eb 100644 --- a/ko/index.mdx +++ b/ko/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/package.json b/package.json index 7acc0948d..d9f546bd1 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,14 @@ "@cloudflare/nimbus-docs": "0.13.1", "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@iconify-json/fa6-brands": "^1.2.6", "@iconify-json/fa6-solid": "^1.2.4", "@iconify-json/ph": "^1.2.0", "@inkeep/cxkit-react": "^0.5.119", "@readme/httpsnippet": "11.4.0", "@scalar/openapi-parser": "0.28.12", "@vercel/connect": "^2.0.2", + "@vercel/speed-insights": "^2.0.0", "astro": "~7.3.2", "clsx": "^2.1.1", "mermaid": "^11.17.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b84dacb24..a65efa3b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@fontsource-variable/jetbrains-mono': specifier: ^5.2.8 version: 5.3.0 + '@iconify-json/fa6-brands': + specifier: ^1.2.6 + version: 1.2.6 '@iconify-json/fa6-solid': specifier: ^1.2.4 version: 1.2.4 @@ -49,6 +52,9 @@ importers: '@vercel/connect': specifier: ^2.0.2 version: 2.0.2 + '@vercel/speed-insights': + specifier: ^2.0.0 + version: 2.0.0(react@19.2.8) astro: specifier: ~7.3.2 version: 7.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0) @@ -858,6 +864,9 @@ packages: '@fontsource-variable/jetbrains-mono@5.3.0': resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==} + '@iconify-json/fa6-brands@1.2.6': + resolution: {integrity: sha512-twL3X4KWcxAhbc1vz/mIDsVr+CAItk1/EIfxKUVQtpv6O4eydk5KNYqTZWdvJNHGInUgd6vKg21aWfVgb5DXEg==} + '@iconify-json/fa6-solid@1.2.4': resolution: {integrity: sha512-LmDNNdJVyvF5mPm1yxWvL8KjCc/E8LzoqnF1LNTVpyY2ZJRUlGOWuPIThdbuFBF2IovgttkIyumhyqfmlHdwKg==} @@ -2473,6 +2482,32 @@ packages: resolution: {integrity: sha512-RwXYtnt6za+5UO4IaLywN/6B95AlLqynPRUWRJxeJ/qufwkcLUbZNUxYtzT0uMpuraWhlNcGqPNGkTnZr4BGBw==} engines: {node: '>= 20'} + '@vercel/speed-insights@2.0.0': + resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==} + peerDependencies: + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + nuxt: '>= 3' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + next: + optional: true + nuxt: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + '@vitejs/plugin-react@5.2.0': resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5703,6 +5738,10 @@ snapshots: '@fontsource-variable/jetbrains-mono@5.3.0': {} + '@iconify-json/fa6-brands@1.2.6': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/fa6-solid@1.2.4': dependencies: '@iconify/types': 2.0.0 @@ -7217,6 +7256,10 @@ snapshots: '@vercel/cli-exec': 1.0.1 jose: 5.10.0 + '@vercel/speed-insights@2.0.0(react@19.2.8)': + optionalDependencies: + react: 19.2.8 + '@vitejs/plugin-react@5.2.0(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 diff --git a/pt-BR/index.mdx b/pt-BR/index.mdx index a4b0ac8d4..6c90ba552 100644 --- a/pt-BR/index.mdx +++ b/pt-BR/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/ru/index.mdx b/ru/index.mdx index 92809303f..6057594ff 100644 --- a/ru/index.mdx +++ b/ru/index.mdx @@ -15,8 +15,9 @@ export const localizeHref = (href) => { export const withDocsBase = (href) => { if (!href || !href.startsWith('/')) return href; + const canonicalHref = href.replace(/\/index(?=[?#]|$)/, ''); const base = typeof window === 'undefined' || window.location.pathname.startsWith('/docs') ? '/docs' : ''; - return base + href; + return base + canonicalHref; }; export const navigateTo = (event, href) => { diff --git a/snippets/components/RunnableCode/RunnableCode.jsx b/snippets/components/RunnableCode/RunnableCode.jsx index d4fc3aec6..0e2a5ae7b 100644 --- a/snippets/components/RunnableCode/RunnableCode.jsx +++ b/snippets/components/RunnableCode/RunnableCode.jsx @@ -35,7 +35,7 @@ export const RunnableCode = ({ children, run = false, showStats = true }) => { useEffect(() => { if (codeRef.current) { - const block = codeRef.current.querySelector('.code-block'); + const block = codeRef.current.querySelector('.code-block, .nb-code-figure'); if (block) { block.style.marginBottom = '0'; block.style.marginTop = '0'; @@ -222,26 +222,16 @@ export const RunnableCode = ({ children, run = false, showStats = true }) => { }; return ( -
+
{/* Code display + action bar */}
-
+
{children}
{/* Action bar */} -
+
{results && (
); -}; \ No newline at end of file +}; diff --git a/src/components/compat/Accordion.astro b/src/components/compat/Accordion.astro index 1e320c925..abd448273 100644 --- a/src/components/compat/Accordion.astro +++ b/src/components/compat/Accordion.astro @@ -6,8 +6,114 @@ const { title, description, defaultOpen } = Astro.props;
- {title} - {description && {description}} + + + {title} + {description && {description}} +
+ + diff --git a/src/components/compat/AccordionGroup.astro b/src/components/compat/AccordionGroup.astro index e438e20ef..5c7b63d74 100644 --- a/src/components/compat/AccordionGroup.astro +++ b/src/components/compat/AccordionGroup.astro @@ -2,4 +2,4 @@ /** Mintlify ``: visual grouping of accordions. */ --- -
+
diff --git a/src/components/compat/Card.astro b/src/components/compat/Card.astro index 9f3990866..4804989da 100644 --- a/src/components/compat/Card.astro +++ b/src/components/compat/Card.astro @@ -2,26 +2,33 @@ /** * Mintlify ``. Renders Nimbus's * Card, wrapped in a link when `href` is set. Mintlify's named icons are - * rendered with the same Font Awesome masks as the source site. + * resolved from local Iconify collections and inlined during the build. */ import Icon from "@cloudflare/nimbus-docs/components/Icon.astro"; -import { withBase } from "@/lib/base"; +import { inlinePublicIcon } from "@/lib/build-icon"; interface Props { title?: string; icon?: string; href?: string; horizontal?: boolean; img?: string; cta?: string; arrow?: boolean; color?: string } const { title = "", href, img, cta, icon, horizontal: _h, arrow: _a, color: _c, ...rest } = Astro.props; +if (icon?.startsWith("http://") || icon?.startsWith("https://")) { + throw new Error(`External card icon URLs are not supported: ${icon}`); +} const isNamedMintlifyIcon = Boolean(icon && !icon.startsWith("/") && !icon.startsWith("http") && !icon.includes(":")); -const iconName = isNamedMintlifyIcon ? undefined : icon; -const iconIsImage = Boolean(iconName?.startsWith("/") || iconName?.startsWith("http")); -const resolvedIconName = iconIsImage && iconName?.startsWith("/") ? withBase(iconName) : iconName; -const mintlifyIconStyle = isNamedMintlifyIcon - ? `--ch-card-icon-mask:url("https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/${icon}.svg")` - : undefined; +const brandIcons = new Set(["docker", "linux", "redhat", "ubuntu"]); +const phosphorIcons = new Set(["browser", "function"]); +const iconName = isNamedMintlifyIcon + ? brandIcons.has(icon!) + ? `fa6-brands:${icon}` + : phosphorIcons.has(icon!) + ? `ph:${icon}` + : `fa6-solid:${icon}` + : icon; +const iconIsImage = Boolean(iconName?.startsWith("/") || iconName?.startsWith("data:")); +const resolvedIconName = iconIsImage && iconName ? inlinePublicIcon(iconName) : iconName; --- {href ? (
{img && } - {isNamedMintlifyIcon && } {resolvedIconName && (iconIsImage ? : )}

{title}

@@ -31,7 +38,6 @@ const mintlifyIconStyle = isNamedMintlifyIcon ) : (
{img && } - {isNamedMintlifyIcon && } {resolvedIconName && (iconIsImage ? : )}

{title}

diff --git a/src/components/compat/accordion-group.client.ts b/src/components/compat/accordion-group.client.ts new file mode 100644 index 000000000..a256dcd7e --- /dev/null +++ b/src/components/compat/accordion-group.client.ts @@ -0,0 +1,40 @@ +import { mount } from "@cloudflare/nimbus-docs/client"; + +let groupSequence = 0; + +function initAccordionGroup(root: HTMLElement): () => void { + const accordions = Array.from(root.querySelectorAll("details.ch-accordion")) + .filter((accordion) => accordion.closest("[data-ch-accordion-group]") === root); + + if (accordions.length < 2) return () => {}; + + const groupName = `ch-accordion-group-${++groupSequence}`; + accordions.forEach((accordion) => accordion.setAttribute("name", groupName)); + + // `name` gives modern browsers native exclusive disclosure behavior. Keep + // the toggle handler as a small compatibility layer for older engines. + const onToggle = (event: Event) => { + const opened = event.target; + if (!(opened instanceof HTMLDetailsElement) || !opened.open || !accordions.includes(opened)) return; + + accordions.forEach((accordion) => { + if (accordion !== opened) accordion.open = false; + }); + }; + + root.addEventListener("toggle", onToggle, true); + + // Invalid authored state should still settle on a single expanded item. + accordions.filter((accordion) => accordion.open).slice(1).forEach((accordion) => { + accordion.open = false; + }); + + return () => { + root.removeEventListener("toggle", onToggle, true); + accordions.forEach((accordion) => { + if (accordion.getAttribute("name") === groupName) accordion.removeAttribute("name"); + }); + }; +} + +mount("[data-ch-accordion-group]", initAccordionGroup); diff --git a/src/components/ui/page-actions/PageActions.astro b/src/components/ui/page-actions/PageActions.astro index 8b42f4b68..3af5bde10 100644 --- a/src/components/ui/page-actions/PageActions.astro +++ b/src/components/ui/page-actions/PageActions.astro @@ -2,6 +2,7 @@ import Icon from "@cloudflare/nimbus-docs/components/Icon.astro"; import { cn } from "@/lib/cn"; import { withBase } from "@/lib/base"; +import { inlinePublicIcon } from "@/lib/build-icon"; import type { HTMLAttributes } from "astro/types"; interface Props extends HTMLAttributes<"div"> { @@ -22,6 +23,7 @@ const issueUrl = new URL("https://github.com/ClickHouse/ClickHouse/issues/new"); issueUrl.searchParams.set("template", "60_documentation-issue.yaml"); issueUrl.searchParams.set("page", Astro.url.pathname); const documentationMcpHref = withBase("/resources/support-center/knowledge-base/setup-installation/set-up-clickhouse-documentation-mcp-server"); +const documentationMcpIcon = inlinePublicIcon("/images/icons/icon-mcp.svg"); ---
{ const { items, persist = false, class: className, ...attrs } = Astro.props; const hash = sidebarHash(items); type LazyGroup = Extract & { _lazyKey?: string }; +type IconItem = { icon?: string }; ---
& { _lazyKey?: string } ) : item.type === "external" ? (
  • - +
  • ) : (
  • - +
  • ), )} diff --git a/src/components/ui/sidebar/SidebarChildren.astro b/src/components/ui/sidebar/SidebarChildren.astro index 6612c2a95..399e4b27c 100644 --- a/src/components/ui/sidebar/SidebarChildren.astro +++ b/src/components/ui/sidebar/SidebarChildren.astro @@ -13,6 +13,7 @@ interface Props { const { items } = Astro.props; type LazyGroup = Extract & { _lazyKey?: string }; +type IconItem = { icon?: string }; ---