diff --git a/packages/pds-landing/README.md b/packages/pds-landing/README.md
index 1a9e8ad..4e6a84c 100644
--- a/packages/pds-landing/README.md
+++ b/packages/pds-landing/README.md
@@ -1,65 +1,132 @@
-# Svelte library
+# @ewanc26/pds-landing
-Everything you need to build a Svelte library, powered by [`sv`](https://npmjs.com/package/sv).
+Composable Svelte 5 components for an ATProto PDS landing page — terminal aesthetic, live status fetching, zero config to drop in.
-Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
+## Install
-## Creating a project
+```bash
+pnpm add @ewanc26/pds-landing @ewanc26/ui
+```
-If you're seeing this, you've probably already done this step. Congrats!
+## Quick start — full page
+
+```svelte
+
+
+
+
+```
-```sh
-# create a new project in the current directory
-npx sv create
+Import the PDS design tokens and base styles once in your layout:
-# create a new project in my-app
-npx sv create my-app
+```css
+/* app.css / layout.css */
+@import '@ewanc26/ui/styles/pds-tokens.css';
```
-To recreate this project with the same configuration:
+---
-```sh
-# recreate this project
-pnpm dlx sv@0.12.5 create --template library --types ts --add prettier tailwindcss="plugins:typography" sveltekit-adapter="adapter:static" --install pnpm ./packages/pds-landing
-```
+## Mix-and-match — primitives
+
+All primitives are exported individually so you can compose custom layouts.
+
+```svelte
+
-## Developing
+
+
+
-Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
+
+
-```sh
-npm run dev
+
-# or start the server and open the app in a new browser tab
-npm run dev -- --open
+
+
+
+
+
+
+
+
+
+
```
-Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
+### Use raw KV data
-## Building
+```svelte
+
-```sh
-npm pack
+
```
-To create a production version of your showcase app:
+### Fetch status yourself
-```sh
-npm run build
+```ts
+import { fetchPDSStatus } from '@ewanc26/pds-landing';
+
+const { health, description, accountCount } = await fetchPDSStatus('https://pds.example.com');
```
-You can preview the production build with `npm run preview`.
+---
-> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+## Components
-## Publishing
+| Component | Description |
+|---|---|
+| `PDSPage` | Full assembled landing page (convenience) |
+| `TerminalCard` | Terminal window shell with traffic-light dots titlebar |
+| `PromptLine` | `user@host:path $` bash prompt header |
+| `Tagline` | Dimmed subtitle beneath the prompt |
+| `SectionLabel` | Uppercase section heading |
+| `Divider` | Thin green-tinted `
` |
+| `KVGrid` | Key-value grid with ok/warn/err/loading states |
+| `StatusGrid` | Live-fetching PDS status grid (wraps `KVGrid`) |
+| `LinkList` | `→ link` list |
+| `ContactSection` | Bluesky mention + optional email |
+| `PDSFooter` | Footer with nixpkgs / atproto links |
-Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
+## Design tokens
-To publish your library to [npm](https://www.npmjs.com):
+All components consume CSS custom properties from `@ewanc26/ui/styles/pds-tokens.css`:
-```sh
-npm publish
+```
+--pds-font-mono
+--pds-color-crust / mantle / base / surface-0 / surface-1 / overlay-0
+--pds-color-text / subtext-0
+--pds-color-green / red / yellow / shadow
```
diff --git a/packages/pds-landing/package.json b/packages/pds-landing/package.json
index 6341ecd..629c381 100644
--- a/packages/pds-landing/package.json
+++ b/packages/pds-landing/package.json
@@ -1,6 +1,7 @@
{
- "name": "pds-landing",
- "version": "0.0.1",
+ "name": "@ewanc26/pds-landing",
+ "version": "2.0.0",
+ "description": "Composable Svelte components for an ATProto PDS landing page — terminal-aesthetic UI with live status fetching.",
"scripts": {
"dev": "vite dev",
"build": "vite build && npm run prepack",
@@ -26,11 +27,17 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
- "svelte": "./dist/index.js"
+ "svelte": "./dist/index.js",
+ "default": "./dist/index.js"
}
},
"peerDependencies": {
- "svelte": "^5.0.0"
+ "@sveltejs/kit": ">=2.0.0",
+ "svelte": "^5.0.0",
+ "tailwindcss": ">=4.0.0"
+ },
+ "dependencies": {
+ "@ewanc26/ui": "workspace:*"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
@@ -50,6 +57,13 @@
"vite": "^7.3.1"
},
"keywords": [
- "svelte"
- ]
+ "svelte",
+ "atproto",
+ "bluesky",
+ "pds",
+ "landing-page"
+ ],
+ "publishConfig": {
+ "access": "public"
+ }
}
diff --git a/packages/pds-landing/src/lib/components/ContactSection.svelte b/packages/pds-landing/src/lib/components/ContactSection.svelte
new file mode 100644
index 0000000..1c426a0
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/ContactSection.svelte
@@ -0,0 +1,59 @@
+
+
+{#if blueskyHandle && profileUrl}
+
+ Send a mention on Bluesky to
+ @{blueskyHandle}
+
+{/if}
+
+{#if email}
+
+ Email: {email}
+
+{/if}
+
+
diff --git a/packages/pds-landing/src/lib/components/Divider.svelte b/packages/pds-landing/src/lib/components/Divider.svelte
new file mode 100644
index 0000000..9278010
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/Divider.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/packages/pds-landing/src/lib/components/KVGrid.svelte b/packages/pds-landing/src/lib/components/KVGrid.svelte
new file mode 100644
index 0000000..b35c8d4
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/KVGrid.svelte
@@ -0,0 +1,87 @@
+
+
+
+ {#each items as item (item.key)}
+ {item.key}
+ {item.value}
+ {/each}
+
+
+
diff --git a/packages/pds-landing/src/lib/components/LinkList.svelte b/packages/pds-landing/src/lib/components/LinkList.svelte
new file mode 100644
index 0000000..6bf7290
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/LinkList.svelte
@@ -0,0 +1,60 @@
+
+
+
+
+
diff --git a/packages/pds-landing/src/lib/components/PDSFooter.svelte b/packages/pds-landing/src/lib/components/PDSFooter.svelte
new file mode 100644
index 0000000..bc5bd89
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/PDSFooter.svelte
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/packages/pds-landing/src/lib/components/PDSPage.svelte b/packages/pds-landing/src/lib/components/PDSPage.svelte
new file mode 100644
index 0000000..5d8130e
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/PDSPage.svelte
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+ {#if showStatus}
+
+
+ {/if}
+
+ {#if showEndpoints}
+
+
+
+ Most API routes are under /xrpc/
+
+
+ {/if}
+
+ {#if showLinks}
+
+
+
+ {/if}
+
+ {#if showContact}
+
+
+
+ {/if}
+
+
+ {#if showFooter}
+
+ {/if}
+
+
+
diff --git a/packages/pds-landing/src/lib/components/PromptLine.svelte b/packages/pds-landing/src/lib/components/PromptLine.svelte
new file mode 100644
index 0000000..8abf194
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/PromptLine.svelte
@@ -0,0 +1,48 @@
+
+
+
+ {user}@{host}:{path} $
+
+
+
diff --git a/packages/pds-landing/src/lib/components/SectionLabel.svelte b/packages/pds-landing/src/lib/components/SectionLabel.svelte
new file mode 100644
index 0000000..03bdc8a
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/SectionLabel.svelte
@@ -0,0 +1,25 @@
+
+
+
+ {#if children}{@render children()}{:else}{label ?? ''}{/if}
+
+
+
diff --git a/packages/pds-landing/src/lib/components/StatusGrid.svelte b/packages/pds-landing/src/lib/components/StatusGrid.svelte
new file mode 100644
index 0000000..14f7714
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/StatusGrid.svelte
@@ -0,0 +1,92 @@
+
+
+
diff --git a/packages/pds-landing/src/lib/components/Tagline.svelte b/packages/pds-landing/src/lib/components/Tagline.svelte
new file mode 100644
index 0000000..75fbe00
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/Tagline.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {#if children}{@render children()}{:else}{text ?? ''}{/if}
+
+
+
diff --git a/packages/pds-landing/src/lib/components/TerminalCard.svelte b/packages/pds-landing/src/lib/components/TerminalCard.svelte
new file mode 100644
index 0000000..fe70bdc
--- /dev/null
+++ b/packages/pds-landing/src/lib/components/TerminalCard.svelte
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+ {title}
+
+
+ {#if children}{@render children()}{/if}
+
+
+
+
diff --git a/packages/pds-landing/src/lib/index.ts b/packages/pds-landing/src/lib/index.ts
index 47d3c46..ac381c3 100644
--- a/packages/pds-landing/src/lib/index.ts
+++ b/packages/pds-landing/src/lib/index.ts
@@ -1 +1,24 @@
-// Reexport your entry components here
+// ─── Utilities ────────────────────────────────────────────────────────────────
+export { fetchPDSStatus } from './utils/fetchPDSStatus.js';
+export type { PDSHealth, PDSDescription, PDSStatusResult } from './utils/fetchPDSStatus.js';
+
+// ─── Primitive components ─────────────────────────────────────────────────────
+export { default as TerminalCard } from './components/TerminalCard.svelte';
+export { default as PromptLine } from './components/PromptLine.svelte';
+export { default as Tagline } from './components/Tagline.svelte';
+export { default as SectionLabel } from './components/SectionLabel.svelte';
+export { default as Divider } from './components/Divider.svelte';
+export { default as KVGrid } from './components/KVGrid.svelte';
+export type { KVItem } from './components/KVGrid.svelte';
+export { default as LinkList } from './components/LinkList.svelte';
+export type { LinkItem } from './components/LinkList.svelte';
+
+// ─── Smart / data-fetching components ────────────────────────────────────────
+export { default as StatusGrid } from './components/StatusGrid.svelte';
+
+// ─── Compound / section components ───────────────────────────────────────────
+export { default as ContactSection } from './components/ContactSection.svelte';
+export { default as PDSFooter } from './components/PDSFooter.svelte';
+
+// ─── Full-page convenience component ─────────────────────────────────────────
+export { default as PDSPage } from './components/PDSPage.svelte';
diff --git a/packages/pds-landing/src/lib/utils/fetchPDSStatus.ts b/packages/pds-landing/src/lib/utils/fetchPDSStatus.ts
new file mode 100644
index 0000000..715b138
--- /dev/null
+++ b/packages/pds-landing/src/lib/utils/fetchPDSStatus.ts
@@ -0,0 +1,99 @@
+/**
+ * Types and utilities for fetching live status from an ATProto PDS.
+ */
+
+export interface PDSHealth {
+ reachable: boolean;
+ /** Reported software version, or null if unreachable / not exposed. */
+ version: string | null;
+}
+
+export interface PDSDescription {
+ did: string | null;
+ inviteCodeRequired: boolean | null;
+ phoneVerificationRequired: boolean | null;
+ availableUserDomains: string[];
+ links: {
+ privacyPolicy?: string;
+ termsOfService?: string;
+ } | null;
+ contact: { email?: string } | null;
+}
+
+export interface PDSStatusResult {
+ health: PDSHealth;
+ description: PDSDescription;
+ /** Total repo count, or -1 if the fetch failed. */
+ accountCount: number;
+}
+
+async function fetchJSON(url: string): Promise {
+ const r = await fetch(url);
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ return r.json();
+}
+
+/**
+ * Fetch live status from a PDS.
+ *
+ * @param baseUrl - Origin to prepend to `/xrpc/…` paths.
+ * Defaults to `''` (same origin, works in-browser).
+ */
+export async function fetchPDSStatus(baseUrl = ''): Promise {
+ // ── health ────────────────────────────────────────────────────────────────
+ let health: PDSHealth = { reachable: false, version: null };
+ try {
+ const h = (await fetchJSON(`${baseUrl}/xrpc/_health`)) as { version?: string };
+ health = { reachable: true, version: h.version ?? null };
+ } catch {
+ // leave defaults
+ }
+
+ // ── description ───────────────────────────────────────────────────────────
+ let description: PDSDescription = {
+ did: null,
+ inviteCodeRequired: null,
+ phoneVerificationRequired: null,
+ availableUserDomains: [],
+ links: null,
+ contact: null
+ };
+ try {
+ const d = (await fetchJSON(`${baseUrl}/xrpc/com.atproto.server.describeServer`)) as {
+ did?: string;
+ inviteCodeRequired?: boolean;
+ phoneVerificationRequired?: boolean;
+ availableUserDomains?: string[];
+ links?: { privacyPolicy?: string; termsOfService?: string };
+ contact?: { email?: string };
+ };
+ description = {
+ did: d.did ?? null,
+ inviteCodeRequired: d.inviteCodeRequired ?? null,
+ phoneVerificationRequired: d.phoneVerificationRequired ?? null,
+ availableUserDomains: d.availableUserDomains ?? [],
+ links: d.links ?? null,
+ contact: d.contact ?? null
+ };
+ } catch {
+ // leave defaults
+ }
+
+ // ── account count (paginated) ─────────────────────────────────────────────
+ let accountCount = 0;
+ try {
+ let cursor: string | undefined;
+ do {
+ const url =
+ `${baseUrl}/xrpc/com.atproto.sync.listRepos?limit=1000` +
+ (cursor ? `&cursor=${encodeURIComponent(cursor)}` : '');
+ const r = (await fetchJSON(url)) as { repos?: unknown[]; cursor?: string };
+ accountCount += (r.repos ?? []).length;
+ cursor = r.cursor;
+ } while (cursor);
+ } catch {
+ accountCount = -1;
+ }
+
+ return { health, description, accountCount };
+}
diff --git a/packages/pds-landing/src/routes/+layout.ts b/packages/pds-landing/src/routes/+layout.ts
new file mode 100644
index 0000000..189f71e
--- /dev/null
+++ b/packages/pds-landing/src/routes/+layout.ts
@@ -0,0 +1 @@
+export const prerender = true;
diff --git a/packages/pds-landing/src/routes/+page.svelte b/packages/pds-landing/src/routes/+page.svelte
index 9c42926..a5ab9e3 100644
--- a/packages/pds-landing/src/routes/+page.svelte
+++ b/packages/pds-landing/src/routes/+page.svelte
@@ -1,3 +1,5 @@
-Welcome to your library project
-Create your package using @sveltejs/package and preview/showcase your work with SvelteKit
-Visit svelte.dev/docs/kit to read the documentation
+
+
+
diff --git a/packages/pds-landing/src/routes/layout.css b/packages/pds-landing/src/routes/layout.css
index 1c4d2a8..dac1868 100644
--- a/packages/pds-landing/src/routes/layout.css
+++ b/packages/pds-landing/src/routes/layout.css
@@ -1,2 +1,29 @@
@import 'tailwindcss';
-@plugin '@tailwindcss/typography';
+@import '@ewanc26/ui/styles/pds-tokens.css';
+
+/* ─── Global resets & base ───────────────────────────────────────────────── */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html,
+body {
+ min-height: 100dvh;
+ background-color: var(--pds-color-crust);
+ color: var(--pds-color-text);
+ font-family: var(--pds-font-mono);
+ font-size: clamp(13px, 3.5vw, 15px);
+ line-height: 1.6;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: clamp(1.5rem, 6vw, 4rem) clamp(1rem, 4vw, 1.5rem) clamp(1.5rem, 6vw, 3rem);
+ gap: 2rem;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1704afd..5f0b805 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -18,6 +18,10 @@ importers:
version: 5.9.3
packages/pds-landing:
+ dependencies:
+ '@ewanc26/ui':
+ specifier: workspace:*
+ version: link:../ui
devDependencies:
'@sveltejs/adapter-static':
specifier: ^3.0.10