Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 31 additions & 145 deletions src/components/InstallBlock.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { withBase } from '../utils/base-path';
import { Tabs, TabItem } from '@astrojs/starlight/components';

const commands = [
{ label: "npm", command: "npm install @synonymdev/pubky" },
Expand Down Expand Up @@ -75,68 +76,37 @@ const installSectionBg = `--install-section-bg: url("${withBase('/images/bg-squa
</div>

<div class="install-right not-content">
<div class="terminal not-content">
<div class="terminal-bar">
<div class="dots">
<span class="dot dot-red"></span>
<span class="dot dot-yellow"></span>
<span class="dot dot-green"></span>
</div>
<div class="tabs" role="tablist" aria-label="Package manager">
{commands.map((cmd, i) => (
<button
class:list={["tab", { active: i === 0 }]}
data-index={i}
role="tab"
aria-selected={i === 0 ? "true" : "false"}
>
{cmd.label}
</button>
))}
</div>
</div>
<div class="terminal-body">
{commands.map((cmd, i) => (
<div class:list={["command", { active: i === 0 }]} data-index={i}>
<span class="prompt">$</span>
<span class="cmd-text">{cmd.command}</span>
<button class="copy-btn" data-command={cmd.command} aria-label="Copy to clipboard" aria-live="polite">
<svg class="copy-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="check-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</button>
</div>
<div class="install-terminal terminal-tabs">
<Tabs>
{commands.map((cmd) => (
<TabItem label={cmd.label}>
<div class="install-command">
<span class="prompt">$</span>
<span class="cmd-text">{cmd.command}</span>
<button class="copy-btn" data-command={cmd.command} aria-label="Copy to clipboard" aria-live="polite">
<svg class="copy-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="check-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</button>
</div>
</TabItem>
))}
</div>
</Tabs>
</div>
</div>
</div>
</div>

<script>
document.querySelectorAll('.install-block').forEach((block) => {
const tabs = block.querySelectorAll('.tab');
const commands = block.querySelectorAll('.command');

tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const index = tab.getAttribute('data-index');
tabs.forEach((t) => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
commands.forEach((c) => c.classList.remove('active'));
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
block.querySelector(`.command[data-index="${index}"]`)?.classList.add('active');
});
});

block.querySelectorAll('.copy-prompt-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const prompt = btn.getAttribute('data-prompt') || '';
navigator.clipboard.writeText(prompt);
navigator.clipboard.writeText(prompt).catch(() => {});
btn.classList.add('copied');
btn.setAttribute('data-tooltip', 'Copied!');
btn.setAttribute('aria-label', 'Copied!');
Expand All @@ -151,7 +121,7 @@ const installSectionBg = `--install-section-bg: url("${withBase('/images/bg-squa
block.querySelectorAll('.copy-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const command = btn.getAttribute('data-command') || '';
navigator.clipboard.writeText(command);
navigator.clipboard.writeText(command).catch(() => {});
btn.classList.add('copied');
btn.setAttribute('aria-label', 'Copied!');
setTimeout(() => { btn.classList.remove('copied'); btn.setAttribute('aria-label', 'Copy to clipboard'); }, 1500);
Expand Down Expand Up @@ -518,108 +488,25 @@ const installSectionBg = `--install-section-bg: url("${withBase('/images/bg-squa
height: 500px;
}

/* ── Terminal ── */
/* Figma: 500x140, border 1px base/border, radius rounded-xl(16), bg base/background, overflow clip */
.terminal {
/* ── Install terminal (Starlight Tabs) ── */
/* Figma: 500x140 terminal */
.install-terminal {
width: 500px;
height: 140px;
border: 1px solid var(--base-border);
border-radius: var(--radius-xl);
overflow: hidden;
background: var(--base-background);
display: flex;
flex-direction: column;
}

/* Figma: Dots bar = px spacing-4(16), py 8, bg base/card(#1d1d20), gap 11.
11px is hardcoded — Figma-specific value, doesn't fit 4px spacing scale */
.terminal-bar {
display: flex;
align-items: center;
gap: 11px;
padding: var(--spacing-2) var(--spacing-4);
background: var(--base-card);
}

/* Figma: dots = 9px, gap 11px (same oddball spacing as terminal-bar) */
.dots {
display: flex;
gap: 11px;
}

.dot {
width: 9px;
height: 9px;
border-radius: 50%;
}

.dot-red { background: #ff5f57; }
.dot-yellow { background: #febc2e; }
.dot-green { background: #28c840; }

/* Figma: Tabs = w 200, h 48, p spacing-2(8), gap spacing-2(8), radius rounded-md(8) */
.tabs {
display: flex;
align-items: center;
width: 200px;
height: 48px;
padding: var(--spacing-2);
gap: var(--spacing-2);
border-radius: var(--radius-md);
}

/* Figma: Tab trigger = flex-1, p spacing-2(8), radius rounded-md(8), text-xs(12/16) bold */
.tab {
flex: 1;
background: transparent;
border: none;
color: var(--base-secondary-foreground);
font-family: var(--sl-font);
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;
transition: all 0.15s ease;
text-align: center;
}

.tab:hover {
color: var(--base-foreground);
background: rgba(255, 255, 255, 0.05);
}

/* Figma: Active tab = bg base/muted(#303034), text foreground, shadow sm */
.tab.active {
color: var(--base-foreground);
background: var(--base-muted);
box-shadow: var(--shadow-sm);
}

/* Figma: Terminal body = flex-1, p spacing-4(16), gap spacing-4(16), items center */
.terminal-body {
display: flex;
flex: 1;
align-items: center;
gap: var(--spacing-4);
.install-terminal starlight-tabs > [role="tabpanel"] {
padding: var(--spacing-4);
}

/* Figma: Large typography = text-lg(18/28) semibold, items-center inherited from .terminal-body */
.command {
display: none;
/* Figma: Large typography = text-lg(18/28) semibold */
.install-command {
display: flex;
align-items: center;
gap: var(--spacing-4);
font-family: var(--sl-font);
font-size: var(--text-lg-size);
font-weight: 600;
line-height: var(--text-lg-lh);
width: 100%;
}

.command.active {
display: flex;
}

/* Figma: $ prompt = muted-foreground */
Expand Down Expand Up @@ -762,12 +649,11 @@ const installSectionBg = `--install-section-bg: url("${withBase('/images/bg-squa
display: none;
}

.terminal {
.install-terminal {
width: 100%;
height: auto;
}

.command {
.install-command {
font-size: var(--text-sm-size);
line-height: var(--text-sm-lh);
}
Expand Down
104 changes: 104 additions & 0 deletions src/components/TabUrlSync.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
/**
* Syncs Starlight `<Tabs syncKey>` selection with a URL query parameter.
*
* Usage (in .mdx):
* <TabUrlSync syncKey="sdk-lang" param="lang" />
*
* - 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;
---

<tab-url-sync data-sync-key={syncKey} data-param={param}></tab-url-sync>

<script>
// One instance per syncKey — avoids duplicate listeners when multiple
// <TabUrlSync> elements share the same key (e.g. via layout composition).
const activeSyncKeys = new Set<string>();

class TabUrlSync extends HTMLElement {
#syncKey: string;
#param: string;

constructor() {
super();
this.#syncKey = this.dataset.syncKey ?? "";
this.#param = this.dataset.param ?? "";
}

connectedCallback() {
if (activeSyncKeys.has(this.#syncKey)) return;
activeSyncKeys.add(this.#syncKey);

// Defer so Starlight's <starlight-tabs> has time to upgrade and
// attach its own click handlers before we programmatically click.
requestAnimationFrame(() => this.#applyUrlSelection());

// Global listener because the target <starlight-tabs> may not exist
// yet at connectedCallback time (custom-element upgrade order).
document.addEventListener("click", this.#handleTabClick);
}

disconnectedCallback() {
activeSyncKeys.delete(this.#syncKey);
document.removeEventListener("click", this.#handleTabClick);
}

#getTabs(): HTMLElement[] {
const container = document.querySelector(
`starlight-tabs[data-sync-key="${CSS.escape(this.#syncKey)}"]`,
);
if (!container) return [];
return [...container.querySelectorAll<HTMLElement>('[role="tab"]')];
}

// Normalises a tab label to a URL-safe param value.
// Collisions are possible (e.g. "C Sharp" vs "c-sharp") — keep
// tab labels distinct after lowercasing + whitespace replacement.
#labelToParam(label: string): string {
return label.toLowerCase().replace(/\s+/g, "-");
}

#applyUrlSelection = (): void => {
const value = new URL(window.location.href).searchParams.get(this.#param);
if (!value) return;

const tabs = this.#getTabs();
const match = tabs.find(
(tab) => this.#labelToParam(tab.textContent?.trim() ?? "") === value,
);
if (match && match.getAttribute("aria-selected") !== "true") {
match.click();
}
};

#handleTabClick = (event: Event): void => {
const tab = (event.target as Element)?.closest?.('[role="tab"]');
if (!tab) return;

const container = tab.closest("starlight-tabs");
if (container?.dataset.syncKey !== this.#syncKey) return;

const label = tab.textContent?.trim();
if (!label) return;

const url = new URL(window.location.href);
const paramValue = this.#labelToParam(label);
if (url.searchParams.get(this.#param) === paramValue) return;

url.searchParams.set(this.#param, paramValue);
window.history.replaceState(window.history.state, "", url);
};
}

if (!customElements.get("tab-url-sync")) {
customElements.define("tab-url-sync", TabUrlSync);
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -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';

<TabUrlSync syncKey="sdk-lang" param="lang" />

:::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).
:::
Expand Down Expand Up @@ -48,13 +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.

**Rust:**
```rust snippet="snippets/rust/src/lib.rs:rust_quick_example"
```

**JavaScript:**
```javascript snippet="snippets/js/src/sdk.ts:js_quick_example"
```
<div class="terminal-tabs">
<Tabs syncKey="sdk-lang">
<TabItem label="JavaScript">
```js snippet="snippets/js/src/sdk.ts:js_quick_example"
```
</TabItem>
<TabItem label="Rust">
```rust snippet="snippets/rust/src/lib.rs:rust_quick_example"
```
</TabItem>
</Tabs>
</div>

## Resources

Expand Down
Loading
Loading