From 78e48dee5f68e8b18ce40a6c45983e77acedc3ff Mon Sep 17 00:00:00 2001 From: tomos Date: Fri, 14 Aug 2026 14:04:31 +0100 Subject: [PATCH 1/3] docs: add SDK language switcher --- astro.config.mjs | 3 +- plugins/remark-language-code-groups.mjs | 174 ++++++++++++++++++++++++ scripts/check-generated-markdown.mjs | 10 +- scripts/copy-md-sources.mjs | 20 ++- src/components/Head.astro | 2 + src/components/LanguageCodeSync.astro | 168 +++++++++++++++++++++++ src/styles/custom.css | 125 +++++++++++++++++ 7 files changed, 496 insertions(+), 6 deletions(-) create mode 100644 plugins/remark-language-code-groups.mjs create mode 100644 src/components/LanguageCodeSync.astro diff --git a/astro.config.mjs b/astro.config.mjs index 5a2ee6e..d5afc2b 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -5,6 +5,7 @@ import starlightClientMermaid from '@pasqal-io/starlight-client-mermaid'; import starlightLlmsTxt from 'starlight-llms-txt'; import rehypeBasePath from './plugins/rehype-base-path.mjs'; import remarkSnippet from './plugins/remark-snippet.mjs'; +import remarkLanguageCodeGroups from './plugins/remark-language-code-groups.mjs'; // https://astro.build/config export default defineConfig({ @@ -73,7 +74,7 @@ export default defineConfig({ }, base: process.env.BASE_PATH || '/', markdown: { - remarkPlugins: [remarkSnippet], + remarkPlugins: [remarkSnippet, remarkLanguageCodeGroups], rehypePlugins: [[rehypeBasePath, { base: process.env.BASE_PATH || '/' }]], }, integrations: [ diff --git a/plugins/remark-language-code-groups.mjs b/plugins/remark-language-code-groups.mjs new file mode 100644 index 0000000..105f708 --- /dev/null +++ b/plugins/remark-language-code-groups.mjs @@ -0,0 +1,174 @@ +const CODE_LANGUAGE_META = + /(?:^|\s)code-language="(javascript|react-native|rust)"/; + +const LANGUAGE_LABELS = { + javascript: 'JavaScript', + 'react-native': 'React Native', + rust: 'Rust', +}; + +const LANGUAGE_ORDER = ['javascript', 'react-native', 'rust']; + +/** + * Turn consecutive language-specific code fences into an accessible, + * synchronized language group. Every group has JavaScript and Rust, while + * individual groups may also offer React Native. + * + * Usage: + * + * ```js code-language="javascript" + * ``` + * ```js code-language="react-native" + * ``` + * ```rust code-language="rust" + * ``` + */ +export default function remarkLanguageCodeGroups() { + return (tree) => { + let groupIndex = 0; + + walk(tree, () => { + groupIndex += 1; + return groupIndex; + }); + }; +} + +function walk(parent, nextGroupIndex) { + if (!Array.isArray(parent.children)) return; + + for (let index = 0; index < parent.children.length; index += 1) { + const first = getLanguageCode(parent.children[index]); + if (!first) { + walk(parent.children[index], nextGroupIndex); + continue; + } + + const groupedCodes = []; + for (let cursor = index; cursor < parent.children.length; cursor += 1) { + const code = getLanguageCode(parent.children[cursor]); + if (!code) break; + groupedCodes.push(code); + } + + if (groupedCodes.length < 2) { + throw new Error( + `language code group: ${first.language} block must be followed by its paired language block`, + ); + } + + const blocks = new Map(); + for (const code of groupedCodes) { + if (blocks.has(code.language)) { + throw new Error( + `language code group: duplicate ${code.language} block`, + ); + } + blocks.set(code.language, code.node); + } + + if (!blocks.has('javascript') || !blocks.has('rust')) { + throw new Error( + 'language code group: each group must contain one JavaScript block and one Rust block', + ); + } + + const groupId = `language-code-${nextGroupIndex()}`; + const languages = LANGUAGE_ORDER.filter((language) => blocks.has(language)); + for (const language of languages) { + const block = blocks.get(language); + block.meta = withUnframedCodeWindow(block.meta); + } + + const replacement = [ + { + type: 'html', + value: renderGroupStart(groupId, languages), + }, + blocks.get(languages[0]), + ]; + + for (const language of languages.slice(1)) { + replacement.push( + { + type: 'html', + value: renderPanelSwitch(groupId, language), + }, + blocks.get(language), + ); + } + replacement.push({ + type: 'html', + value: '\n\n', + }); + + parent.children.splice(index, groupedCodes.length, ...replacement); + + index += replacement.length - 1; + } +} + +function getLanguageCode(node) { + if (node?.type !== 'code' || !node.meta) return null; + + const language = getCodeLanguage(node.meta); + if (!language) return null; + + return { + language, + node, + }; +} + +export function getCodeLanguage(meta) { + return meta?.match(CODE_LANGUAGE_META)?.[1] ?? null; +} + +export function stripCodeLanguage(meta) { + return meta + .replace( + /(^|\s+)code-language="(?:javascript|react-native|rust)"/, + '$1', + ) + .trim() || null; +} + +export function getCodeLanguageLabel(language) { + return LANGUAGE_LABELS[language] ?? language; +} + +function withUnframedCodeWindow(meta) { + return [stripCodeLanguage(meta), 'frame="none"'].filter(Boolean).join(' '); +} + +function renderGroupStart(groupId, languages) { + const firstLanguage = languages[0]; + const tabs = languages + .map((language, index) => { + const active = index === 0; + const activeClass = active ? ' active' : ''; + const tabIndex = active ? '' : ' tabindex="-1"'; + + return ``; + }) + .join('\n'); + + return `
+
+
+ +
+${tabs} +
+
+
`; +} + +function renderPanelSwitch(groupId, language) { + return `
+\n
\n
', - }); - - parent.children.splice(index, groupedCodes.length, ...replacement); - - index += replacement.length - 1; - } -} - -function getLanguageCode(node) { - if (node?.type !== 'code' || !node.meta) return null; - - const language = getCodeLanguage(node.meta); - if (!language) return null; - - return { - language, - node, - }; -} - -export function getCodeLanguage(meta) { - return meta?.match(CODE_LANGUAGE_META)?.[1] ?? null; -} - -export function stripCodeLanguage(meta) { - return meta - .replace( - /(^|\s+)code-language="(?:javascript|react-native|rust)"/, - '$1', - ) - .trim() || null; -} - -export function getCodeLanguageLabel(language) { - return LANGUAGE_LABELS[language] ?? language; -} - -function withUnframedCodeWindow(meta) { - return [stripCodeLanguage(meta), 'frame="none"'].filter(Boolean).join(' '); -} - -function renderGroupStart(groupId, languages) { - const firstLanguage = languages[0]; - const tabs = languages - .map((language, index) => { - const active = index === 0; - const activeClass = active ? ' active' : ''; - const tabIndex = active ? '' : ' tabindex="-1"'; - - return ``; - }) - .join('\n'); - - return `
-
-
- -
-${tabs} -
-
-
`; -} - -function renderPanelSwitch(groupId, language) { - return `
-
-
-
-
- - - -
-
- {commands.map((cmd, i) => ( - - ))} -
-
-
- {commands.map((cmd, i) => ( -
- $ - {cmd.command} - -
+
+ + {commands.map((cmd) => ( + +
+ $ + {cmd.command} + +
+
))} -
+
@@ -119,24 +103,10 @@ const installSectionBg = `--install-section-bg: url("${withBase('/images/bg-squa diff --git a/src/components/TabUrlSync.astro b/src/components/TabUrlSync.astro new file mode 100644 index 0000000..a9ebcaa --- /dev/null +++ b/src/components/TabUrlSync.astro @@ -0,0 +1,104 @@ +--- +/** + * Syncs Starlight `` selection with a URL query parameter. + * + * Usage (in .mdx): + * + * + * - On load: reads `?lang=…` and activates the matching tab. + * - On tab switch: updates the URL via `history.replaceState`. + */ +interface Props { + syncKey: string; + param: string; +} + +const { syncKey, param } = Astro.props; +--- + + + + diff --git a/src/content/docs/explore/pubky-protocol/sdk.md b/src/content/docs/explore/pubky-protocol/sdk.mdx similarity index 88% rename from src/content/docs/explore/pubky-protocol/sdk.md rename to src/content/docs/explore/pubky-protocol/sdk.mdx index bf69594..047d33b 100644 --- a/src/content/docs/explore/pubky-protocol/sdk.md +++ b/src/content/docs/explore/pubky-protocol/sdk.mdx @@ -2,6 +2,11 @@ title: "Pubky SDK: Client Libraries for Decentralized Applications" --- +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import TabUrlSync from '../../../../components/TabUrlSync.astro'; + + + :::note The SDK is now at **v0.10**. If you're upgrading from an earlier version, see the [v0.10 Migration Guide](https://github.com/pubky/pubky-homeserver/tree/main/docs/v0.10-migration). ::: @@ -48,10 +53,18 @@ For event-driven apps, **EventStreamBuilder** lets you subscribe to real-time ch Sign in and read/write data. Account creation (signup) is generally handled outside of your app. See the [Developer Guide](/explore/pubkycore/getting-started/) for testnet setup where you bootstrap accounts for development. -```js snippet="snippets/js/src/sdk.ts:js_quick_example" code-language="javascript" -``` -```rust snippet="snippets/rust/src/lib.rs:rust_quick_example" code-language="rust" -``` +
+ + + ```js snippet="snippets/js/src/sdk.ts:js_quick_example" + ``` + + + ```rust snippet="snippets/rust/src/lib.rs:rust_quick_example" + ``` + + +
## Resources diff --git a/src/styles/custom.css b/src/styles/custom.css index d9a674e..22851b2 100644 --- a/src/styles/custom.css +++ b/src/styles/custom.css @@ -34,7 +34,6 @@ --spacing-1: 4px; --spacing-2: 8px; --spacing-2-5: 10px; /* Figma half-step between spacing-2(8) and spacing-3(12) */ - --spacing-window-controls: 11px; --spacing-3: 12px; --spacing-4: 16px; --spacing-5: 20px; @@ -46,7 +45,6 @@ --spacing-20: 80px; /* Border radius */ - --radius-none: 0; --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; @@ -87,16 +85,7 @@ --alpha-white-16: rgba(255, 255, 255, 0.16); --alpha-white-10: rgba(255, 255, 255, 0.1); --alpha-white-5: rgba(255, 255, 255, 0.045); - --terminal-tab-hover: rgba(255, 255, 255, 0.05); --base-secondary-hover: #3a3a3f; - --window-control-red: #ff5f57; - --window-control-yellow: #febc2e; - --window-control-green: #28c840; - - /* Component dimensions */ - --window-control-size: 9px; - --code-switcher-width: 200px; - --code-switcher-height: 48px; /* Shadows */ --shadow-xs: 0 1px 2px rgba(5, 5, 10, 0.2); @@ -226,118 +215,74 @@ site-search button[data-open-modal] kbd { outline-offset: 2px; } -/* Language code windows copy the landing install terminal's exact - terminal-bar, dots, tabs, and tab trigger styling. */ -.language-code-group { - margin-block: var(--spacing-4); -} - -.language-code-terminal { - overflow: hidden; +/* ── Terminal-style tabs ── */ +/* Opt-in via .terminal-tabs wrapper around Starlight . + ⚠ Couples to Starlight's internal DOM: starlight-tabs, .tablist-wrapper, + .tab, [role="tab"], [role="tabpanel"]. Review after Starlight upgrades. */ +.terminal-tabs starlight-tabs { border: 1px solid var(--base-border); border-radius: var(--radius-xl); + overflow: hidden; background: var(--base-background); - display: flex; - flex-direction: column; } -.language-code-group .terminal-bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-window-controls); - padding: var(--spacing-2) var(--spacing-4); +.terminal-tabs starlight-tabs .tablist-wrapper { + overflow: visible; background: var(--base-card); + padding: var(--spacing-2) var(--spacing-4); } -.language-code-group .dots { - display: flex; - gap: var(--spacing-window-controls); -} - -.language-code-group .dot { - width: var(--window-control-size); - height: var(--window-control-size); - border-radius: var(--radius-full); -} - -.language-code-group .dot-red { - background: var(--window-control-red); -} - -.language-code-group .dot-yellow { - background: var(--window-control-yellow); -} - -.language-code-group .dot-green { - background: var(--window-control-green); -} - -.language-code-group .tabs { - display: flex; - align-items: center; - width: var(--code-switcher-width); - height: var(--code-switcher-height); +.terminal-tabs starlight-tabs [role="tablist"] { + border-bottom: none; + width: fit-content; + max-width: 100%; padding: var(--spacing-2); gap: var(--spacing-2); border-radius: var(--radius-md); } -.language-code-group .tabs[data-language-count="3"] { - width: max-content; - flex-shrink: 0; -} - -.language-code-group .tabs[data-language-count="3"] .tab { - white-space: nowrap; +.terminal-tabs starlight-tabs .tab { + flex: 1; } -.language-code-group .tab { - flex: 1; - background: transparent; - border: none; - color: var(--base-secondary-foreground); - font-family: var(--sl-font); +.terminal-tabs starlight-tabs .tab > [role="tab"] { + justify-content: center; + padding: var(--spacing-2); + border-radius: var(--radius-md); font-size: var(--text-xs-size); font-weight: 700; line-height: var(--text-xs-lh); - padding: var(--spacing-2); - border-radius: var(--radius-md); - cursor: pointer; + box-shadow: none; + color: var(--base-secondary-foreground); transition: all 0.15s ease; - text-align: center; } -.language-code-group .tab:hover { +.terminal-tabs starlight-tabs .tab > [role="tab"]:hover { color: var(--base-foreground); - background: var(--terminal-tab-hover); + background: rgba(255, 255, 255, 0.05); } -.language-code-group .tab.active { - color: var(--base-foreground); +.terminal-tabs starlight-tabs .tab > [role="tab"][aria-selected="true"] { background: var(--base-muted); box-shadow: var(--shadow-sm); + color: var(--base-foreground); } -.language-code-group .command { - display: none; -} - -.language-code-group .command.active { - display: block; +.terminal-tabs starlight-tabs > [role="tabpanel"] { + margin-top: 0; } -.language-code-panel > .expressive-code { +.terminal-tabs starlight-tabs > [role="tabpanel"] .expressive-code { margin: 0; } -.language-code-panel > .expressive-code .frame { +.terminal-tabs starlight-tabs > [role="tabpanel"] .expressive-code .frame { --code-background: var(--base-background); } -.language-code-panel > .expressive-code pre { +.terminal-tabs starlight-tabs > [role="tabpanel"] .expressive-code pre { border: none; - border-radius: var(--radius-none); + border-radius: 0; } /* Shrink FAQ per-question headings */